7

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.

1 Answer 1

4

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 });
    }
}
Sign up to request clarification or add additional context in comments.

8 Comments

Thanks Steve. This looks like exactly what I need. But if I may ask, isn't it possible the AddEmptyRuleFor method can automatically run for all the properties of the class since I can get them all from validationSetting.ValidationEntityProperties collection? For now, what I need the generic validator to do is to validate required fields only by checking if the settings in the database if it is set to true or false? I hope you understand what I'm saying.
Yes, it's a bit more complicated and the messages will not include the spaces in your property names, but I have updated the answer with a possible solution for this. I have commented out the original code that is not required for this.
Wow! Thanks Steve. This might take me a while to understand. But I am eager to try it out and see how it works. I'm very grateful.
Hi, Please can you explain when and where this method AutoApplyEmptyRules() is called. I noticed it is neither called by any class or method in the code. My guess is, the method will be called in the constructor of the EntityValidator implementing BaseValidator. Is that right? Thanks again for your assistance.
@er-sho No idea. It was in OPs question
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.