[PostSharp] [FluentValidation] Validation de classes grâce à IDataErrorInfo
By Michael DELVA on Monday 28 June 2010, 16:11 - C# - Permalink
TweetUne manière de gérer la validation des données lorsqu'on utilise WPF est de faire implémenter l'interface IDataErrorInfo par les classes à valider, et de modifier votre fichier XAML pour que WPF utilise automatiquement les fonctions de cette interface pour valider la valeur des propriétés. (Un exemple en action ici).
Le souci avec cette méthode est que vos entités deviennent un peu plus que des entités de base. Et vous vous dites que finalement, ça ne serait pas mal d'externaliser la validation de ces entités dans une autre classe. Et pourquoi pas de définir des règles que votre entité se doit de respecter pour être valide. Pour cela, il existe FluentValidation.
Mais pour continuer à utiliser IDataErrorInfo, on doit maintenant utiliser FluentValidation au sein des fonctions définies par IDataErrorInfo. C'est ce que propose ce thread dans le forum de FluentValidation.
C'est en partant de cet article que je vais vous montrer le cheminement qui m'a amené à utiliser PostSharp pour régler en beauté (j'espère en tout cas) cette problématique.
En regardant cet article, je me suis dit que pour implémenter ce concept de manière générique, j'allais tout mettre dans ma classe BaseEntity, de laquelle dérivent mes entités:
public abstract class BaseEntity<TKey, TEntity> : IDataErrorInfo
where TEntity : BaseEntity<TKey, TEntity>
{
}
Il suffisait de rajouter un argument générique TEntityValidator dans la déclaration, et au final j'avais quelque chose comme:
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;
}
}
}
Le souci est qu'il fallait modifier la déclaration des mes entités, mais également des repositories, et aussi des classes servant à générer des contrats pour mes classes, sans oublier de mettre les contraintes de type générique à chaque fois.
Ça faisait donc beaucoup de modifications, et des déclarations de classes pas possibles.
M'est donc venue l'idée d'automatiser tout ça grâce à PostSharp, en utilisant le même principe que l'exemple donné sur leur site, avec l'implémentation de INotifyPropertyChanged, et dont j'ai déjà parlé dans un autre post.
Nous allons donc créer un aspect qui implémentera l'interface IDataErrorInfo à toutes nos entités dérivant de BaseEntity, et nous utiliserons les classes définies grâce à FluentValidation pour les valider.
Pour commencer, voici le squelette de notre 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 { }
}
}
Commençons par détailler les attributs décorant notre classe:
- Comme tous les aspects, on déclare l'attribut comme étant serialisable.
- On va implémenter l'interface IDataErrorInfo. Je pense que cet attribut est dispensable, vu que BaseEntity va devoir implémenter cette interface malgré tout, pour permettre aux view models d'appeler directement la propriété Error.
- On va appliquer cet aspect à toutes les classes dérivant de celle qu'on a décoré de l'attribut DataErrorInfo.
Il nous faut maintenant implémenter l'appel à la classe de validation. Et comme chaque entité aura une classe de validation propre, il nous faut passer le type de cette classe de validation à l'aspect, afin qu'il puisse l'instancier, et valider l'instance courante de l'entité.
Pour cela, nous allons créer un constructeur à notre attribut, qui prendra comme paramètre le type de la classe de validation:
[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;
}
}
Pour récupérer le résultat de la validation, nous allons créer une fonction ValidateInstanceProperties, qui va dynamiquement créer une nouvelle instance de la classe de validation, et appeler la fonction Validate, en lui passant en paramètre l'instance de l'entité, accessible via la propriété InstanceLevelAspect.Instance:
private ValidationResult ValidateInstanceProperties()
{
return ((IValidator) Activator.CreateInstance(validatorType)).Validate(Instance);
}
Il ne nous reste plus qu'à appeler cette fonction dans la propriété Error et dans l'indexeur, en reprenant plus ou moins ce qui avait été fait plus haut:
[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()); }
}
La fonction ErrorsToString permet juste de formater la liste des erreurs de la validation.
Pour utiliser cet aspect, il suffit de décorer avec l'attribut DataErrorInfo chacune des entités, en donnant comme paramètre au constructeur le type de la classe de validation de cette entité:
[DataErrorInfo(typeof(PlayerValidator))]
public class Player : BaseEntity<int, Player>
{
//...
}
Et voilà le travail!
Comments
Merci pour ce billet interessant. Je n'avais jamais pense auparavant a ce cas.
Je crois qu'il vaut mieux ne pas introduire IDataErrorInfo.Item[this] de maniere explicite, de sorte que ce membre d'interface sera introduit implicitement. Les concepteurs de IDataErrorInfo ont sans doute fait une erreur en choisissant d'exposer cette fonctionalite comme un accesseur de collection; ils auraient du exposer IDataErrorInfo.GetError(string).
C'est à dire?
Cet indexeur doit être public afin que les mécanismes de validation du DataBinding de WPF puissent fonctionner.