[PostSharp] [FluentValidation] Automatic entity validation with IDataErrorInfo
By Michael DELVA on Thursday 15 July 2010, 15:49 - C# - Permalink
TweetThis post is an english translation of this french post.
One way to manage data validation when using WPF is to implement the interface IDataErrorInfo on the classes to validate, and to modify the XAML file so that WPF automatically uses the functions of this interface to validate the value of the properties (See here for an example).
The problem with this, is that you have to alter your entities to add some logic inside them, which makes them become more than entities. One way to avoid that is provided by FluentValidation, which allows you to deport the validation logic outside of your entities, inside another class, where you can define some validation rules for your entity.
But as we always want our entities to continue to implement IDataErrorInfo, we have to operate a mix with FluentValidation. You can find a way to achieve that in this thread.
This thread was the starting point of the solution I'm going to propose to you, to fix this problematic in an easy way...
When looking at this thread, and before considering to use PostSharp, I told myself that in order to implement the mix between IDataErrorInfo and FluentValidation, I should begin to put the validation logic in the base class of all my entities, namely BaseEntity:
public abstract class BaseEntity<TKey, TEntity> : IDataErrorInfo
where TEntity : BaseEntity<TKey, TEntity>
{
}
I would just have to add a generic argument named TEntityValidator in the class declaration, to eventually have something like:
public abstract class BaseEntity<TKey, TEntity, TEntityValidator> : IDataErrorInfo
where TEntity : BaseEntity<TKey, TEntity, TEntityValidator>
where TEntityValidator : IValidator<TEntity>, new()
{
public virtual string Error
{
get
{
IValidator<TEntity> validator = new TEntityValidator();
var result = validator.Validate(this);
var errors = new StringBuilder();
foreach (var validationFailure in result.Errors)
{
errors.Append(validationFailure.ErrorMessage);
errors.Append(Environment.NewLine);
}
return errors.ToString();
}
}
public virtual string this[string columnName]
{
get
{
IValidator<TEntity> validator = new TEntityValidator();
var result = validator.Validate(this);
var columnResult = result.Errors.FirstOrDefault<ValidationFailure>(x => string.Compare(x.PropertyName, columnName, true) == 0);
return columnResult != null ? columnResult.ErrorMessage : string.Empty;
}
}
}
The problem is that this alteration compelled me to modify the declarations of a lot of classes :
- The entities, of course
- The repositories
- The Code Contracts classes
This was a lot of changes.
This is how came to me the idea to use PostSharp to automate this, using the same principle as the example given on their website, with the implementation of INotifyPropertyChanged, which I have already talked about in another article.
What we are going to do, is to create an aspect which will implement the interface IDataErrorInfo on all of the entities which inherit from BaseEntity, and the aspect will use the fluent validation classes to validate these entities.
To begin, here is the skeleton of our aspect:
[Serializable]
[IntroduceInterface(typeof(IDataErrorInfo), OverrideAction = InterfaceOverrideAction.Ignore)]
[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)]
public sealed class DataErrorInfoAttribute : InstanceLevelAspect, IDataErrorInfo
{
[IntroduceMember(Visibility = Visibility.Public, IsVirtual = true, OverrideAction = MemberOverrideAction.OverrideOrFail)]
public string this[string columnName]
{
get { }
}
[IntroduceMember(Visibility = Visibility.Public, IsVirtual = true, OverrideAction = MemberOverrideAction.OverrideOrFail)]
public string Error
{
get { }
}
}
Let's begin by enumerating the attributes decorating our class:
- Like all aspects, it must be serializable.
- The interface IDataErrorInfo will be introduced (implemented). I guess this attribute is dispensable, as BaseEntity will anyway need to implement this interface, because the view-models will have to directly use in the code, the property Error.
- All derived classes of the one decorated with the attribute DataErrorInfo will implement the aspect too.
We now have to implement the call to the validation class peculiar to the entity. To allow the aspect to know what class it is, we have to give it the type of the validation class, so it can instantiate it, and use it to validate the instance of the entity on which the aspect has been applied.
In order to do that, we are going to add a constructor to the attribute, with one single parameter, which is the type of the validation class:
[Serializable]
[IntroduceInterface(typeof(IDataErrorInfo), OverrideAction = InterfaceOverrideAction.Ignore)]
[MulticastAttributeUsage(MulticastTargets.Class, Inheritance = MulticastInheritance.Strict)]
public sealed class DataErrorInfoAttribute : InstanceLevelAspect, IDataErrorInfo
{
private readonly Type validatorType;
public DataErrorInfoAttribute(Type validatorType)
{
this.validatorType = validatorType;
}
}
To retrieve the result of the validation, we will create a function ValidateInstanceProperties, which will dynamically create a new instance of the validation class, and call the Validate function, giving it the instance of the entity as a parameter, which we will get through the property InstanceLevelAspect.Instance:
private ValidationResult ValidateInstanceProperties()
{
return ((IValidator) Activator.CreateInstance(validatorType)).Validate(Instance);
}
Then, we just have to call this function in the property Error and in the indexer, using more or less what has been done earlier:
[IntroduceMember(Visibility = Visibility.Public, IsVirtual = true, OverrideAction = MemberOverrideAction.OverrideOrFail)]
public string this[string columnName]
{
get
{
var validationResults = ValidateInstanceProperties();
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;
}
}
[IntroduceMember(Visibility = Visibility.Public, IsVirtual = true, OverrideAction = MemberOverrideAction.OverrideOrFail)]
public string Error
{
get { return ErrorsToString(ValidateInstanceProperties()); }
}
The function ErrorsToString allows to format the validation errors list.
To use this aspect, we just have to decorate each of our entities with the attribute DataErrorInfo, giving to the constructor the type of the class which validates this entity:
[DataErrorInfo(typeof(PlayerValidator))]
public class Player : BaseEntity<int, Player>
{
//...
}
I hope it will help!
See you soon...