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!