Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Validator for Model T Using Fluent Validation?

Tags:

I just got introduced to Fluent Validation yesterday and I think it's pretty cool. I have tried it and it works. But my application currently has several models and I must admit it is stressful to write Validators for each model. Is it possible to have it written in Generics and find a way to get each model validated with it?

This is how my Validator is currently written. But I don't know how to write it in generics.

EmployeeValidator.cs

public class EmployeeValidator : AbstractValidator<EmployeeViewModel>
{
    private readonly ValidationEntitySettingServices _validationEntitySettingService;

    public EmployeeValidator(ValidationEntitySettingServices validationEntitySettingService)
    {
        _validationEntitySettingService = validationEntitySettingService;

        RuleFor(x => x.LastName).NotEmpty().When(x => IsPropertyRequired(x.LastName)).WithMessage("Last Name is a required field!");
        RuleFor(x => x.FirstName).NotEmpty().When(x => IsPropertyRequired(x.FirstName)).WithMessage("First Name is a required field!");
        RuleFor(x => x.MiddleName).NotEmpty().When(x => IsPropertyRequired(x.MiddleName)).WithMessage("Middle Name is a required field!");
    }

    public bool IsPropertyRequired(string propertyName)
    {
        var empValidationSetting = _validationEntitySettingService.GetIncluding("Employee");
        if (empValidationSetting != null)
        {
            return empValidationSetting.ValidationEntityProperties.Any(p => p.PropertyName.Equals(propertyName) && p.IsRequired);
        }
        else
            return false;
    }
}

Thanks in advance.

like image 519
Kacey Ezerioha Avatar asked Mar 05 '18 10:03

Kacey Ezerioha


People also ask

How do you validate fluent validation?

To run the validator, instantiate the validator object and call the Validate method, passing in the object to validate. Customer customer = new Customer(); CustomerValidator validator = new CustomerValidator(); ValidationResult result = validator. Validate(customer);

Is fluent validation good?

FluentValidation provides a great alternative to Data Annotations in order to validate models. It gives better control of validation rules and makes validation rules easy to read, easy to test, and enable great separation of concerns.

Is fluent validation free?

FluentValidation is developed for free by @JeremySkinner in his spare time and financial sponsorship helps keep the project going. Please sponsor the project via either GitHub sponsors or OpenCollective.

What are fluent validations?

FluentValidation is a .NET library for building strongly-typed validation rules. It Uses a fluent interface and lambda expressions for building validation rules. It helps clean up your domain code and make it more cohesive, as well as giving you a single place to look for validation logic.


1 Answers

I don't think it would really make sense to make the validation generic as all of your models are likely to have different properties and property types that need to be validated differently. However, you could make what you have here more generic by adding a base validation class such as:

public abstract class BaseValidator<T> : AbstractValidator<T>
{
    private readonly ValidationEntitySettingServices _validationEntitySettingService;

    public BaseValidator(ValidationEntitySettingServices validationEntitySettingService)
    {
        _validationEntitySettingService = validationEntitySettingService;
        AutoApplyEmptyRules();
    }

    private string ViewModelName
    {
        get { return GetType().Name.Replace("Validator", string.Empty); }
    }

    // no longer needed
    //public bool IsPropertyRequired(string propertyName)
    //{
    //    var validationSetting = _validationEntitySettingService.GetIncluding(ViewModelName);
    //    if (validationSetting != null)
    //    {
    //        return validationSetting.ValidationEntityProperties.Any(p => p.PropertyName.Equals(propertyName) && p.IsRequired);
    //    }
    //    else
    //        return false;
    //}

    protected void AddEmptyRuleFor<TProperty>(Expression<Func<T, TProperty>> expression, string message)
    {
        //RuleFor(expression).NotEmpty().When(x => IsPropertyRequired(((MemberExpression)expression.Body).Name)).WithMessage(message);
        RuleFor(expression).NotEmpty().WithMessage(message);
    }

    private void AddEmptyRuleForProperty(PropertyInfo property)
    {
        MethodInfo methodInfo = GetType().GetMethod("AddEmptyRuleFor");
        Type[] argumentTypes = new Type[] { typeof(T), property.PropertyType };
        MethodInfo genericMethod = methodInfo.MakeGenericMethod(argumentTypes);
        object propertyExpression = ExpressionHelper.CreateMemberExpressionForProperty<T>(property);
        genericMethod.Invoke(this, new object[] { propertyExpression, $"{propertyInfo.Name} is a required field!" });
    }

    private PropertyInfo[] GetRequiredProperties()
    {
        var validationSetting = _validationEntitySettingService.GetIncluding(ViewModelName);
        if (validationSetting != null)
        {
            return validationSetting.ValidationEntityProperties.Where(p => p.IsRequired);
        }
        else
            return null;
    }

    private void AutoApplyEmptyRules()
    {
        PropertyInfo[] properties = GetRequiredProperties();
        if (properties == null)
            return;
        foreach (PropertyInfo propertyInfo in properties)
        {
            AddEmptyRuleForProperty(property);
        }
    }
}

The main requirement here is the AddEmptyRuleForProperty method which will call the generic AddEmptyRuleFor method by constructing the method based on the PropertyType.

Then you can inherit this class instead and apply rules using the generic method:

public class EmployeeValidator : BaseValidator<EmployeeViewModel>
{
    public EmployeeValidator(ValidationEntitySettingServices validationEntitySettingService) : base(validationEntitySettingService)
    {
        // no longer needed
        //AddEmptyRuleFor(x => x.LastName, "Last Name is a required field!");
        //AddEmptyRuleFor(x => x.FirstName, "First Name is a required field!");
        //AddEmptyRuleFor(x => x.MiddleName, "Middle Name is a required field!");
    }
}

This is the ExpressionHelper class which provides a method to create a generic member expression as an object that can be passed in when invoking the AddEmptyRuleFor method above:

public static class ExpressionHelper
{
    public static Expression<Func<TModel, TProperty>> CreateMemberExpression<TModel, TProperty>(PropertyInfo propertyInfo)
    {
        if (propertyInfo == null)
            throw new ArgumentException("Argument cannot be null", "propertyInfo");

        ParameterExpression entityParam = Expression.Parameter(typeof(TModel), "x");
        Expression columnExpr = Expression.Property(entityParam, propertyInfo);

        if (propertyInfo.PropertyType != typeof(T))
            columnExpr = Expression.Convert(columnExpr, typeof(T));

        return Expression.Lambda<Func<TModel, TProperty>>(columnExpr, entityParam);
    }

    public static object CreateMemberExpressionForProperty<TModel>(PropertyInfo property)
    {
        MethodInfo methodInfo = typeof(ExpressionHelper).GetMethod("CreateMemberExpression", BindingFlags.Static | BindingFlags.Public);
        Type[] argumentTypes = new Type[] { typeof(TModel), property.PropertyType };
        MethodInfo genericMethod = methodInfo.MakeGenericMethod(argumentTypes);
        return genericMethod.Invoke(null, new object[] { property });
    }
}
like image 152
Steve Harris Avatar answered Sep 21 '22 12:09

Steve Harris