C# Encapsulating FluentValidation, but still seeing AbstractValidator everywhere, really can't stand it

C# Encapsulating FluentValidation, but still seeing AbstractValidator everywhere, really can't stand it

FluentValidation is a very powerful .NET framework for building strongly-typed validation rules

Last updated 6/9/2022 10:12 PM
黑哥聊dotNet
5 min read
Category
.NET
Tags
.NET C# FluentValidation

Storytelling

A few days ago, I noticed a new project in the company using FluentValidation. As everyone knows, FluentValidation is a very powerful .NET framework for building strongly-typed validation rules. It helps programmers solve tedious validation problems and is very enjoyable to use. However, I still encountered something quite unpleasant, as shown in the code below:

public class UserInformationValidator : AbstractValidator<UserInformation>
{
 public UserInformationValidator()
 {
     RuleFor(o => o.UserName).Length(2, 20).WithMessage("Name length input error");
     RuleFor(o => o.Sex).Must(o=>o=="Male"||o=="Female").WithMessage("Sex input error");
     RuleFor(o => o.Age).ExclusiveBetween(0, 200).WithMessage("Age input error");
     RuleFor(o => o.Email).EmailAddress().WithMessage("Email input error");
  }
}


static void Main(string[] args)
{

    UserInformation userInformation = new UserInformation();
    userInformation.UserName = "";
    userInformation.Sex = "Neutral";
    userInformation.Age = 2200;
    userInformation.Email = "xxxxx";
    UserInformationValidator validationRules = new UserInformationValidator();
    var result=   validationRules.Validate(userInformation);
    if (!result.IsValid)
    {
      Console.WriteLine( string.Join(Environment.NewLine, result.Errors.Select(x => x.ErrorMessage).ToArray()));
    }

}

Every time we validate an object, we have to create a new type of validator, like UserInformationValidator above. Although this logic is fine, I have a bit of OCD. So let's try to encapsulate it — hehe, do more with less code.

Installation

Before creating any validator, you need to add a reference to FluentValidation.dll in your project. The easiest way is to use the NuGet Package Manager or dotnet CLI.

Exploring Code Encapsulation with Templates

Extract Template Code into a Base Class

Looking at the code above, you'll notice that every time we create a new validator, we must create a class inheriting from AbstractValidator<T>, where T is the type of the class you want to validate. Let's encapsulate a base validator class:

public class CommonVaildator<T> : AbstractValidator<T>
{

}

Add Validation Rules

The actual business logic is written inside the UserInformationValidator validator. The only thing we need there is RuleFor; everything else can be uniformly encapsulated in the base class, right? Following this idea, let's encapsulate a Length Validation Rule.

First, let's look at the prototype of RuleFor:

public IRuleBuilderInitial<T, TProperty> RuleFor<TProperty>(Expression<Func<T, TProperty>> expression)

Its parameter is a Func delegate. So what is Expression?

Expression is an expression tree!

An expression tree is code that allows lambda expressions to be represented as a tree-like data structure instead of executable logic.

In C#, it's defined using Expression, which is a kind of syntax tree or data structure. It is mainly used to store structures that need to be computed or evaluated — it only provides storage and does not perform computations. Usually, Expression is used together with Lambda. I won't go into too much detail here!

Now we can easily encapsulate a length validation rule:

public void LengthVaildator(Expression<Func<T, string>> expression, int min, int max, string Message)
{
    RuleFor(expression).Length(min, max).WithMessage(Message);
}

Similarly, we can also encapsulate Predicate Validation Rules, Email Validation Rules, etc.

public void MustVaildator(Expression<Func<T, string>> expression ,Func<T,string, bool> expression2, string Message)
{
    RuleFor(expression).Must(expression2).WithMessage(Message);
}
public void EmailAddressVaildator(Expression<Func<T, string>> expression, string Message)
{
    RuleFor(expression).EmailAddress().WithMessage(Message);
}

Encapsulate Validation Method

Now that we've encapsulated the validators, encapsulating the validation method var result = validationRules.Validate(userInformation); is a piece of cake. Here's the code:

public static string ModelValidator<T>(T source, AbstractValidator<T> sourceValidator) where T : class
{
    var results = sourceValidator.Validate(source);
    if (!results.IsValid)
        return string.Join(Environment.NewLine, results.Errors.Select(x => x.ErrorMessage).ToArray());
    else
        return "";

}

Test the Encapsulated Code

CommonVaildator<UserInformation> commonUserInformation = new CommonVaildator<UserInformation>();
commonUserInformation.LengthVaildator(o => o.UserName, 2, 30, "Name length input error");
commonUserInformation.MustVaildator(o => o.Sex, (user, _) => user.Sex =="Male"||user.Sex=="Female" , "Sex input error");
commonUserInformation.ExclusiveBetweenVaildator(o=>o.Age,0, 200, "Age input error");
commonUserInformation.EmailAddressVaildator(o => o.Email, "Email input error");
string msg= VaildatorHelper.ModelValidator(userInformation, commonUserInformation);
Console.WriteLine(msg);

Now the code looks much cleaner, doesn't it? I've only encapsulated four types of validation rules here; I won't bother with the rest.

Summary

This article comes from the bits and pieces of work, and it's my spontaneous encapsulation. If you have better ways to encapsulate, please feel free to share. Sharing joy makes it greater. That's all for now — hope it helps you.

Keep Exploring

Related Reading

More Articles