MVVM / NHibernate - How do I dynamically validate a model?

I am creating a C # WPF application using the MVVM pattern. I have repository classes that use NHibernate to save my domain model.

My model consists of a larger tree structure ( Recipe containing Operation containing Phase s). Operations and steps contain a dynamic list of key value mappings as IDictionary<string, string> . The corresponding NHibernate mapping for Operation is

 <class name="Operation" table="operations"> <id column="id" type="int" generator="native" /> <property name="Name" column="name" /> <map name="Parameters" table="operation_params"> <key column="operation" /> <index column="param" type="string" /> <element column="value" type="string" /> </map> <list name="Phases" cascade="all-delete-orphan"> <key column="operation" /> <index column="`index`" /> <one-to-many class="Phase" /> </list> </class> 

Now this part is simple and works quite well. The Operation class is currently a POCO with almost no logic inside, a simple data container.


My problem: I have to check the parameters according to the external scheme that my application reads from the XML file. The scheme contains restrictions for individual parameters (range, acceptable values, etc.), as well as dependencies between several parameters (that is, actual values ​​vary depending on another parameter value).

What is the best way to integrate my validation logic? I read a lot of the last days and so far, I have come across the following alternatives:

  • Add validation logic to the model class itself.

    To do this, I do not know how to correctly enter the verification scheme into objects created by NHibernate. I do not need a validation function all the time, only when the user edits the parameters or when I import the operation (for example, from a backup). Maybe I could implement the actual validation logic in the model class and introduce validation rules using the property whenever I really need validation? Is it good practice to add this functionality to the model classes that I store using NHibernate, or if the model classes remain "dumb"?

  • Create a decorator class for the validation logic that wraps around my Operation objects.

    That way, I would use a wrapper every time I need validation and a model class when I need to render it. My problem here is that my ViewModel classes are already wrappers, so I would get another wrapper layer. In addition, since the Operation class is part of a larger tree structure (Recipe / Operation / Phase), I will need to create wrappers for the collections and change the collection of maps back to the basic collections, which can be a daunting task.

  • Create a service class that I call, passing the operation whenever I want to check it.

    The problem that I see here is that the service has no state and therefore needs to re-check the entire list of parameters every time the user changes one parameter. This does not seem to be the best approach, especially when I want to trigger some kind of change event for the user interface when the validation status for the parameter changes.

What is the general approach to my problem? Is there a sample that I have not yet found that I absolutely need? I mean, there are many implementations that rely on external schema definitions for validation (read: XML / XSD and similar document structures), there just have to be some geniuses who have already found the perfect solution for my problem ;-) Help me, SO!

+6
source share
1 answer
  • Add validation logic to the model class itself. β€œNot the best approach, because you will explode POCO with logic, and in the end you will get an ActiveRecord template that will not be so clean.”
  • Create a decorator class for validation logic that wraps the objects of my activity. . Think this is the best approach, with the difference that you have to wrap existing wrappers, and youll also end up blowing levels of abstractions, so it’s also not recommended.
  • Create a service class that I call, passing the operation whenever I want to check it. - It is probably not your case to do such things (if I correctly understood that you are talking about a web service or some other remote service), this solution is more suitable if you have restrictions on these validation rules, which should be , for example, centralized for multiple customers, and not for a specific application.

I would prefer the following solution :

Add the validator project to your solution, which will contain:

  • Logic that checks the parameters for an external schema that your application reads from an XML file.

  • Validation rules for each POCO object that you use in your project, and this requires verification (or you can apply these rules at a higher level, not POCO, but some Wrapper in POCO, if you already have such an implementation, but in as a best practice, try to apply the rules directly to POCO - a cleaner and more correct approach)

So

1 - Your POCO will contain properties and a simple SelfValidate () check:

 namespace Core.Domain { public class Operation : ValidatableDomainObject { #region Properties public virtual String Name { get; set; } public virtual ISet Phases { get; set; } #endregion Properties #region Validation public override ValidationResult SelfValidate() { return ValidationHelper.Validate(this); } #endregion Validation } }
namespace Core.Domain { public class Operation : ValidatableDomainObject { #region Properties public virtual String Name { get; set; } public virtual ISet Phases { get; set; } #endregion Properties #region Validation public override ValidationResult SelfValidate() { return ValidationHelper.Validate(this); } #endregion Validation } } 

2 - Your POCO Validator will contain the rules that should be applied to validate POCO based on your XML file:

 #region Usings using System.Linq; using FluentValidation; using FluentValidation.Results; #endregion Usings namespace Core.Validation { public class OperationValidator : AbstractValidator { #region .Ctors /// /// .Ctor used for operation purpose /// public OperationValidator() { Validate(); } #endregion .Ctors /// /// Validation rules for Operation /// private void Validate() { //here you may get validations rules from you xml file and structure the following code after your requirements //Name RuleFor(x => x.Name).Length(2, 20).WithMessage("Operation name should have length between 2 and 20 symbols"); //ApplicationFormsWrapper Custom(entity => { foreach (var item in entity.Phases) if (item.PhaseState == null) return new ValidationFailure("Phases", "First Phase is missing"); return null; }); } } }
#region Usings using System.Linq; using FluentValidation; using FluentValidation.Results; #endregion Usings namespace Core.Validation { public class OperationValidator : AbstractValidator { #region .Ctors /// /// .Ctor used for operation purpose /// public OperationValidator() { Validate(); } #endregion .Ctors /// /// Validation rules for Operation /// private void Validate() { //here you may get validations rules from you xml file and structure the following code after your requirements //Name RuleFor(x => x.Name).Length(2, 20).WithMessage("Operation name should have length between 2 and 20 symbols"); //ApplicationFormsWrapper Custom(entity => { foreach (var item in entity.Phases) if (item.PhaseState == null) return new ValidationFailure("Phases", "First Phase is missing"); return null; }); } } } 

3 - Add the ValidatableDomainObject class, it implements System.ComponentModel.IDataErrorInfo (provides functionality for providing user error information that the user interface can bind):

 #region Usings using System.ComponentModel; using System.Linq; using FluentValidation.Results; using Core.Validation.Helpers; #endregion Usings namespace Core.Domain.Base { public abstract class ValidatableDomainObject : DomainObject, IDataErrorInfo { public abstract ValidationResult SelfValidate(); public bool IsValid { get { return SelfValidate().IsValid; } } public string Error { get { return ValidationHelper.GetError(SelfValidate()); } } public string this[string columnName] { get { var validationResults = SelfValidate(); if (validationResults == null) return string.Empty; var columnResults = validationResults.Errors.FirstOrDefault(x => string.Compare(x.PropertyName, columnName, true) == 0); return columnResults != null ? columnResults.ErrorMessage : string.Empty; } } } }
#region Usings using System.ComponentModel; using System.Linq; using FluentValidation.Results; using Core.Validation.Helpers; #endregion Usings namespace Core.Domain.Base { public abstract class ValidatableDomainObject : DomainObject, IDataErrorInfo { public abstract ValidationResult SelfValidate(); public bool IsValid { get { return SelfValidate().IsValid; } } public string Error { get { return ValidationHelper.GetError(SelfValidate()); } } public string this[string columnName] { get { var validationResults = SelfValidate(); if (validationResults == null) return string.Empty; var columnResults = validationResults.Errors.FirstOrDefault(x => string.Compare(x.PropertyName, columnName, true) == 0); return columnResults != null ? columnResults.ErrorMessage : string.Empty; } } } } 

4 - Add the following ValidationHelper:

 #region Usings using System; using System.Text; using FluentValidation; using FluentValidation.Results; #endregion Usings namespace Core.Validation.Helpers { public class ValidationHelper { public static ValidationResult Validate(TK entity) where T : IValidator, new() where TK : class { IValidator validator = new T(); return validator.Validate(entity); } public static string GetError(ValidationResult result) { var validationErrors = new StringBuilder(); foreach (var validationFailure in result.Errors) { validationErrors.Append(validationFailure.ErrorMessage); validationErrors.Append(Environment.NewLine); } return validationErrors.ToString(); } } }
#region Usings using System; using System.Text; using FluentValidation; using FluentValidation.Results; #endregion Usings namespace Core.Validation.Helpers { public class ValidationHelper { public static ValidationResult Validate(TK entity) where T : IValidator, new() where TK : class { IValidator validator = new T(); return validator.Validate(entity); } public static string GetError(ValidationResult result) { var validationErrors = new StringBuilder(); foreach (var validationFailure in result.Errors) { validationErrors.Append(validationFailure.ErrorMessage); validationErrors.Append(Environment.NewLine); } return validationErrors.ToString(); } } } 

This will allow you to do the following in your application code :

  • At the service level or viewmodel, you can do this to get validation errors:

  var operation = new Operation () {Name = "A"};
 var validationResults = operation.SelfValidate ();

  1. At the presentation level, you can write something like this (in this case, if validation errors appear, they are directly obtained from the OperationValidator class):
<TextBox Text = "{Binding CurrentOperation.Name, Mode = TwoWay, UpdateSourceTrigger = PropertyChanged, ValidatesOnDataErrors = True}">

NOTE : the implementation is based on FluentValidation (a small .NET authentication library that uses a free interface and lambda expressions), see http://fluentvalidation.codeplex.com/ , but of course you can use one more, hopefully that I have succeeded in describing the mechanism for linking validation to a domain object.

+6
source

Source: https://habr.com/ru/post/926703/


All Articles