FluentValidation Validation Tutorial Based on .NET

FluentValidation Validation Tutorial Based on .NET

FluentValidation is a validation framework based on .NET development. It is open-source, free, and elegant, supporting chained operations, easy to understand, feature-complete, and can be deeply integrated with MVC5, WebApi2, and ASP.NET Core. The component provides over a dozen commonly used validators, good scalability, support for custom validators, and support for localization and multilingual.

Last updated 1/19/2024 5:41 AM
零度
29 min read
Category
.NET
Tags
.NET C# ASP.NET Core Open Source Web API

FluentValidation is an open-source, free, and elegant validation framework based on .NET. It supports chaining operations, is easy to understand, feature-rich, and can be deeply integrated with MVC5, WebApi2, and ASP.NET CORE. The framework provides over a dozen commonly used validators, is highly extensible, supports custom validators, and supports localization for multiple languages.

Although FluentValidation is a very powerful validation framework, Chinese resources about it are not comprehensive. During his learning process, Ling Du translated the official documentation, resulting in this article for reference.

To use the validation framework, you need to add a reference to FluentValidation.dll in your project. It supports the netstandard2.0 library and .NET4.5 platform, as well as the .NET Core platform. The simplest way is to use NuGet Package Manager to reference the component.

Install-Package FluentValidation

To use FluentValidation extensions in ASP.NET Core, reference the following package:

Install-Package FluentValidation.AspNetCore

To integrate with ASP.NET MVC 5 or WebApi 2 projects, you can use the FluentValidation.Mvc5 and FluentValidation.WebApi packages respectively.

Install-Package FluentValidation.Mvc5
Install-Package FluentValidation.WebApi

Creating the First Validator

To define a set of validation rules for a specific object, you need to create a class that inherits from AbstractValidator<T>, where the generic type parameter T is the type of the class to be validated. Suppose you have a customer class:

public class Customer {
  public int Id { get; set; }
  public string Surname { get; set; }
  public string Forename { get; set; }
  public decimal Discount { get; set; }
  public string Address { get; set; }
}

Next, customize a validator by inheriting from the generic AbstractValidator class, then write RuleFor validation rules in the constructor using LINQ expressions.

using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Surname).NotNull();
  }
}

To execute the validation, simply pass the entity class Customer to the defined CustomerValidator.

Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();

ValidationResult result = validator.Validate(customer);

The Validate method returns a ValidationResult object containing the validation result. ValidationResult has two properties: IsValid is a boolean indicating whether validation succeeded, and Errors contains detailed information about validation failures.

The following code demonstrates outputting detailed validation failure information to the console:

Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();

ValidationResult results = validator.Validate(customer);

if(! results.IsValid) {
  foreach(var failure in results.Errors) {
    Console.WriteLine("Property " + failure.PropertyName + " Error was: " + failure.ErrorMessage);
  }
}

Chaining Rules

You can chain multiple validators for the same property. The following code validates that the Surname property is not null and is not equal to the string "foo".

using FluentValidation;

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Surname).NotNull().NotEqual("foo");
  }
}

Throwing Exceptions

If you want to throw an exception directly instead of returning a ValidationResult object upon validation failure, you can use the ValidateAndThrow method of the validator.

Customer customer = new Customer();
CustomerValidator validator = new CustomerValidator();

validator.ValidateAndThrow(customer);

If validation fails, an exception of type ValidationException will be thrown, which can be caught by higher-level code to retrieve detailed error information.

Collections

When validating a collection, you only need to define rules for the type of items in the collection. The following rule will run a NotNull check on each element in the collection.

public class Person {
  public List<string> AddressLines {get;set;} = new List<string>();
}
public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
    RuleForEach(x => x.AddressLines).NotNull();
  }
}

Complex Properties

Validators can be used for complex properties. Suppose you have two classes: Customer and Address.

public class Customer {
  public string Name { get; set; }
  public Address Address { get; set; }
}

public class Address {
  public string Line1 { get; set; }
  public string Line2 { get; set; }
  public string Town { get; set; }
  public string County { get; set; }
  public string Postcode { get; set; }
}

Then define an AddressValidator based on the Address class:

public class AddressValidator : AbstractValidator<Address> {
  public AddressValidator() {
    RuleFor(address => address.Postcode).NotNull();
    //etc
  }
}

Then define a CustomerValidator based on the Customer class, using the address validator to validate the address.

public class CustomerValidator : AbstractValidator<Customer> {
  public CustomerValidator() {
    RuleFor(customer => customer.Name).NotNull();
    RuleFor(customer => customer.Address).SetValidator(new AddressValidator());
  }
}

Additionally, validators can be used on collection properties. Suppose the Customer object contains an Orders collection property:

public class Customer {
   public IList<Order> Orders { get; set; }
}

public class Order {
  public string ProductName { get; set; }
  public decimal? Cost { get; set; }
}

var customer = new Customer();
customer.Orders = new List<Order> {
  new Order { ProductName = "Foo" },
  new Order { Cost = 5 }
};

Define an OrderValidator:

public class OrderValidator : AbstractValidator<Order> {
    public OrderValidator() {
        RuleFor(x => x.ProductName).NotNull();
        RuleFor(x => x.Cost).GreaterThan(0);
    }
}

This validator can be used in CustomerValidator via the SetCollectionValidator method:

public class CustomerValidator : AbstractValidator<Customer> {
    public CustomerValidator() {
        RuleFor(x => x.Orders).SetCollectionValidator(new OrderValidator());
    }
}

var validator = new CustomerValidator();
var results = validator.Validate(customer);

When the validation is executed, the validation results can be used to output detailed error information for each property of the orders:

foreach(var result in results.Errors) {
   Console.WriteLine("Property name: " + result.PropertyName);
   Console.WriteLine("Error: " + result.ErrorMessage);
   Console.WriteLine("");
}
Property name: Orders[0].Cost
Error: 'Cost' must be greater than '0'.

Property name: Orders[1].ProductName
Error: 'Product Name' must not be empty.

When writing validation rules, you can use the Where keyword to exclude or filter objects that do not need validation.

RuleFor(x => x.Orders).SetCollectionValidator(new OrderValidator())
        .Where(x => x.Cost != null);

Rule Sets

Rule sets allow you to group validation rules together to be executed as a group, while ignoring other rules. For example, the Person class has three properties: Id, Surname, and Forename. We validate Id separately, and Surname and Forename together as a group called "Names".

public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {
     RuleSet("Names", () => {
        RuleFor(x => x.Surname).NotNull();
        RuleFor(x => x.Forename).NotNull();
     });

     RuleFor(x => x.Id).NotEqual(0);
  }
}

Then we can use the extension method provided by the validator to execute validation for a specific rule set. The following code will not validate the Id property.

var validator = new PersonValidator();
var person = new Person();
var result = validator.Validate(person, ruleSet: "Names");

To execute multiple rule sets, use a comma-separated string list.

validator.Validate(person, ruleSet: "Names,MyRuleSet,SomeOtherRuleSet")

Matching all rules with the * character will enforce all rules, regardless of whether they are in a rule set:

validator.Validate(person, ruleSet: "*")

Configuration Methods

You can override the default validation error message of a validator by calling the WithMessage method on the validator:

RuleFor(customer => customer.Surname).NotNull().WithMessage("Name cannot be empty");

In the error message, you can use the {PropertyName} placeholder to replace the property name:

RuleFor(customer => customer.Surname).NotNull().WithMessage("{PropertyName} cannot be empty");

Besides {PropertyName}, the framework also includes placeholders like {PropertyValue}, {ComparisonValue}, {MinLength}, {MaxLength}, and {TotalLength}. For more built-in placeholders, refer to the official documentation.

By default, the error message outputs the property name. The following rule will output "Surname cannot be empty" on validation error.

RuleFor(customer => customer.Surname).NotNull();

The validator supports specifying an alias for the property using the WithName method. The following code outputs "Name cannot be empty".

RuleFor(customer => customer.Surname).NotNull().WithName("Name");

FluentValidation also supports custom alias retrieval logic via DisplayNameResolver:

ValidatorOptions.DisplayNameResolver = (type, member) => {
  if(member != null) {
     return member.Name + "Foo";
  }
  return null;
};

You can also use the DisplayName attribute to specify the property alias.

public class Person {
  [Display(Name="Last name")]
  public string Surname { get; set; }
}

Under certain conditions, you can use the When method to set validation rules conditionally. The following code: the GreaterThan rule only executes when IsPreferredCustomer is true.

RuleFor(customer => customer.CustomerDiscount).GreaterThan(0).When(customer => customer.IsPreferredCustomer);

If you need to specify the same condition for multiple rules, you can call it at the method top level instead of at the end of each rule:

When(customer => customer.IsPreferred, () => {
   RuleFor(customer => customer.CustomerDiscount).GreaterThan(0);
   RuleFor(customer => customer.CreditCardNumber).NotNull();
});

By default, when writing multiple chained validation rules, the subsequent rule will execute regardless of whether the previous rule failed. The following code checks if Surname is not null, then checks if Surname is not equal to "Ling Du". If NotNull validation fails, NotEqual validation will still be called.

RuleFor(x => x.Surname).NotNull().NotEqual("Ling Du");

We can set the cascade mode using the StopOnFirstFailure method.

RuleFor(x => x.Surname).Cascade(CascadeMode.StopOnFirstFailure).NotNull().NotEqual("foo");

Now, if the NotNull validator fails, the NotEqual validator will not execute.

CascadeMode is an enum: Continue means always call all validators defined in the rule; StopOnFirstFailure means stop executing subsequent rules once a validator fails.

To set the cascade mode globally, you can modify the CascadeMode property of the ValidatorOptions type at application startup:

ValidatorOptions.CascadeMode = CascadeMode.StopOnFirstFailure;

To set the cascade mode for a single validator class, write it like this:

public class PersonValidator : AbstractValidator<Person> {
  public PersonValidator() {

    // First set the cascade mode
    CascadeMode = CascadeMode.StopOnFirstFailure;

    // Rule definitions follow
    RuleFor(...)
    RuleFor(...)

   }
}

By default, all rules in FluentValidation run independently without affecting each other, which helps asynchronous validation for performance. However, in some cases, you may want certain rules to execute only after another rule has been validated. Use DependentRules to specify rule dependencies; when the dependency rule completes validation, the current rule will execute.

RuleFor(x => x.Surname).NotNull().DependentRules(() => {
  RuleFor(x => x.Forename).NotNull();
});

You can pass data to the validator via the RootContextData dictionary of the ValidationContext.

var instanceToValidate = new Person();
var context = new ValidationContext(person);
context.RootContextData["MyCustomData"] = "Test";
var validator = new PersonValidator();
validator.Validate(context);

Then, you can access the RootContextData dictionary in any custom property validator by calling the Custom method.

RuleFor(x => x.Surname).Custom((x, context) => {
  if(context.ParentContext.RootContextData.ContainsKey("MyCustomData")) {
    context.AddFailure("My error message");
  }
});

If you need to run specific code before each call to the validator, you can override the PreValidate method to intercept the validation. This method returns true to continue validation, false to stop validation.

public class MyValidator : AbstractValidator<Person> {
  public MyValidator() {
    RuleFor(x => x.Name).NotNull();
  }

  protected override bool PreValidate(ValidationContext<Person> context, ValidationResult result) {
    if (context.InstanceToValidate == null) {
      result.Errors.Add(new ValidationFailure("", "Please ensure a model was supplied."));
      return false;
    }
    return true;
  }
}

Built-in Validators

FluentValidation has several built-in validators. Their error messages can use specific placeholders.

NotNull Validator

Description: Ensures the specified property is not null.

RuleFor(customer => customer.Surname).NotNull();

Available format parameter placeholders: = Name of the property being validated = Current value of the property

NotEmpty Validator

Description: Ensures the specified property is not null, empty string, or whitespace (or the default value for value types, e.g., 0 for int).

RuleFor(customer => customer.Surname).NotEmpty();

Available format parameter placeholders: = Name of the property being validated = Current value of the property

NotEqual Validator

Description: Ensures the value of the specified property is not equal to a specific value (or not equal to another property's value).

//Not equal to a particular value
RuleFor(customer => customer.Surname).NotEqual("Foo");

//Not equal to another property
RuleFor(customer => customer.Surname).NotEqual(customer => customer.Forename);

Available format parameter placeholders: = Name of the property being validated = Value the property should not equal

Equal Validator

Description: Ensures the value of the specified property equals a specific value (or equals another property's value).

//Equal to a particular value
RuleFor(customer => customer.Surname).Equal("Foo");

//Equal to another property
RuleFor(customer => customer.Password).Equal(customer => customer.PasswordConfirmation);

Available format parameter placeholders: = Name of the property being validated = Value the property should equal = Current value of the property

Length Validator

Ensures the length of a specific string property is within a specified range. However, it does not ensure the string property is not null.

RuleFor(customer => customer.Surname).Length(1, 250); //must be between 1 and 250 chars (inclusive)

Available format parameter placeholders:

= Name of the property being validated = Minimum length = Maximum length = Number of characters entered = Current value of the property

MaxLength Validator

Description: Ensures the length of a specific string property does not exceed a specified value.

RuleFor(customer => customer.Surname).MaximumLength(250); //must be 250 chars or fewer

Available format parameter placeholders: = Name of the property being validated = Maximum length = Number of characters entered = Current value of the property

MinLength Validator

Description: Ensures the length of a specific string property is not less than a specified value.

RuleFor(customer => customer.Surname).MinimumLength(10); //must be 10 chars or more

Available format parameter placeholders: = Name of the property being validated = Minimum length = Number of characters entered = Current value of the property

LessThan Validator

Description: Ensures the value of the specified property is less than a specific value (or less than another property's value).

//Less than a particular value
RuleFor(customer => customer.CreditLimit).LessThan(100);

//Less than another property
RuleFor(customer => customer.CreditLimit).LessThan(customer => customer.MaxCreditLimit);

Available format parameter placeholders: = Name of the property being validated = Value the property is compared to = Current value of the property

LessThanOrEqualTo Validator

Description: Ensures the value of the specified property is less than or equal to a specific value (or less than or equal to another property's value).

//Less than a particular value
RuleFor(customer => customer.CreditLimit).LessThanOrEqualTo(100);

//Less than another property
RuleFor(customer => customer.CreditLimit).LessThanOrEqualTo(customer => customer.MaxCreditLimit);

Available format parameter placeholders: = Name of the property being validated = Value the property is compared to = Current value of the property

GreaterThan Validator

Description: Ensures the value of the specified property is greater than a specific value (or greater than another property's value).

//Greater than a particular value
RuleFor(customer => customer.CreditLimit).GreaterThan(0);

//Greater than another property
RuleFor(customer => customer.CreditLimit).GreaterThan(customer => customer.MinimumCreditLimit);

Available format parameter placeholders: = Name of the property being validated = Value the property is compared to = Current value of the property

GreaterThanOrEqualTo Validator

Description: Ensures the value of the specified property is greater than or equal to a specific value (or greater than or equal to another property's value).

//Greater than a particular value
RuleFor(customer => customer.CreditLimit).GreaterThanOrEqualTo(1);

//Greater than another property
RuleFor(customer => customer.CreditLimit).GreaterThanOrEqualTo(customer => customer.MinimumCreditLimit);

Available format parameter placeholders: = Name of the property being validated = Value the property is compared to = Current value of the property

Must Validator

Description: Passes the value of the specified property to a delegate where you can perform custom validation logic.

RuleFor(customer => customer.Surname).Must(surname => surname == "Foo");

Available format parameter placeholders: = Name of the property being validated = Current value of the property

Note that the delegate parameter can also accept the object being validated:

RuleFor(customer => customer.Surname).Must((customer, surname) => surname != customer.Forename)

Regular Expression Validator

Description: Ensures the value of the specified property matches a given regular expression. For regular expressions, refer to the article Regular Expression Tutorial (Note: link is dead).

RuleFor(customer => customer.Surname).Matches("some regex here");

Available format parameter placeholders: = Name of the property being validated = Current value of the property

Email Validator

Description: Ensures the value of the specified property is a valid email address format.

RuleFor(customer => customer.Email).EmailAddress();

Available format parameter placeholders: = Name of the property being validated = Current value of the property

Custom Validators

Must Validator

The simplest way to implement a custom validator is using the Must method. Suppose we have the following class:

public class Person {
  public IList<Person> Pets {get;set;} = new List<Person>();
}

To ensure the list contains at least 10 elements, we can do:

public class PersonValidator:AbstractValidator<Person> {
  public PersonValidator() {
   RuleFor(x => x.Pets).Must(list => list.Count <= 10).WithMessage("The list must contain fewer than 10 items");
  }
}

To make this logic reusable, we can encapsulate it into an extension method.

public static class MyCustomValidators {
  public static IRuleBuilderOptions<T, IList<TElement>> ListMustContainFewerThan<T, TElement>(this IRuleBuilder<T, IList<TElement>> ruleBuilder, int num) {
	return ruleBuilder.Must(list => list.Count < num).WithMessage("The list contains too many items");
  }
}

Here, we create an extension method for IRuleBuilder to implement reusable logic. Usage is simple.

RuleFor(x => x.Pets).ListMustContainFewerThan(10);

Writing Custom Validators

If you want flexible control over reusable validators, you can use the Must method to write custom rules. This method allows you to manually create instances associated with validation errors.

public class PersonValidator:AbstractValidator<Person> {
  public PersonValidator() {
   RuleFor(x => x.Pets).Custom((list, context) => {
     if(list.Count > 10) {
       context.AddFailure("The list must contain 10 items or fewer");
     }
   });
  }
}

The advantage of this method is that it allows you to return multiple errors for the same rule.

context.AddFailure("SomeOtherProperty", "The list must contain 10 items or fewer");
// Or you can instantiate the ValidationFailure directly:
context.AddFailure(new ValidationFailure("SomeOtherProperty", "The list must contain 10 items or fewer");

Custom Property Validators

In some cases, the validation logic for certain properties is very complex, and you may want to move the property-specific custom logic to a separate class. This can be done by overriding the PropertyValidator class.

using System.Collections.Generic;
using FluentValidation.Validators;
public class ListCountValidator<T> : PropertyValidator {
        private int _max;

	public ListCountValidator(int max)
		: base("{PropertyName} must contain fewer than {MaxElements} items.") {
		_max = max;
	}

	protected override bool IsValid(PropertyValidatorContext context) {
		var list = context.PropertyValue as IList<T>;

		if(list != null && list.Count >= _max) {
			context.MessageFormatter.AppendArgument("MaxElements", _max);
			return false;
		}

		return true;
	}
}

When inheriting from PropertyValidator, you must override the IsValid method. This method accepts an object and returns a boolean indicating whether validation succeeded. It can access properties via PropertyValidatorContext: Instance - the object being validated PropertyDescription - the name of the property (or a custom alias set by WithName) PropertyValue - the value of the property being validated Member - the MemberInfo describing the property being validated

To use a custom property validator, call it when defining a validation rule:

public class PersonValidator : AbstractValidator<Person> {
    public PersonValidator() {
       RuleFor(person => person.Pets).SetValidator(new ListCountValidator<Pet>(10));
    }
}

Localization and Multiple Languages

FluentValidation provides translations for default validation messages in several languages. By default, it selects the language based on the current thread's CurrentUICulture. You can also specify error messages using WithMessage and WithLocalizedMessage.

WithMessage

If you use Visual Studio's built-in resx resource files, you can localize error messages by calling WithMessage.

RuleFor(x => x.Surname).NotNull().WithMessage(x => MyLocalizedMessages.SurnameRequired);

Of course, you can store multiple languages in a database and read multilingual messages via lambda expressions.

WithLocalizedMessage

You can use the WithLocalizedMessage method, passing the resource type and resource name, to support localized multilingual error messages.

RuleFor(x => x.Surname).NotNull().WithLocalizedMessage(typeof(MyLocalizedMessages), "SurnameRequired");

Default Error Messages

If you want to replace FluentValidation's default error messages, you can implement the ILanguageManager interface, which supports localization for multiple languages.

public class CustomLanguageManager : FluentValidation.Resources.LanguageManager {
  public CustomLanguageManager() {
    AddTranslation("en", "NotNullValidator", "'{PropertyName}' is required.");
  }
}

The above code customizes the English error message for the NotNullValidator. You can also implement more languages. After defining LanguageManager, you need to configure it at startup.

ValidatorOptions.LanguageManager = new CustomLanguageManager();

Disabling Localization

You can completely disable FluentValidation's localization support, forcing the default English language regardless of the thread's CurrentUICulture. This can be done via the static class ValidatorOptions in the application's startup routine.

ValidatorOptions.LanguageManager.Enabled = false;

You can also force a specific default language to always be displayed:

ValidatorOptions.LanguageManager.Culture = new CultureInfo("fr");

ASP.NET Core Integration

FluentValidation can be integrated with ASP.NET Core. To enable MVC integration, you need to add a reference to the FluentValidation.AspNetCore assembly via NuGet:

Install-Package FluentValidation.AspNetCore

After installation, you need to reference the namespace using FluentValidation.AspNetCore, then configure it in the application's startup class Startup by calling the extension method AddFluentValidation on services.

public void ConfigureServices(IServiceCollection services) {
    services.AddMvc(setup => {
      //...mvc setup...
    }).AddFluentValidation();
}

To let ASP.NET Core discover your validators, you must register them in the service collection container.

public void ConfigureServices(IServiceCollection services) {
    services.AddMvc(setup => {
      //...mvc setup...
    }).AddFluentValidation();

    services.AddTransient<IValidator<Person>, PersonValidator>();
    //etc
}

Alternatively, use the AddFromAssemblyContaining method to automatically register all validators in a specific assembly.

services.AddMvc()
  .AddFluentValidation(fv => fv.RegisterValidatorsFromAssemblyContaining<PersonValidator>());

This example defines a PersonValidator for validating the Person type.

public class Person {
	public int Id { get; set; }
	public string Name { get; set; }
	public string Email { get; set; }
	public int Age { get; set; }
}

public class PersonValidator : AbstractValidator<Person> {
	public PersonValidator() {
		RuleFor(x => x.Id).NotNull();
		RuleFor(x => x.Name).Length(0, 10);
		RuleFor(x => x.Email).EmailAddress();
		RuleFor(x => x.Age).InclusiveBetween(18, 60);
	}
}

Define a controller PeopleController for creating and saving Person instances, using ModelState.IsValid to check if user input passes validation.

public class PeopleController : Controller {
	public ActionResult Create() {
		return View();
	}

	[HttpPost]
	public IActionResult Create(Person person) {

		if(! ModelState.IsValid) { // re-render the view when validation failed.
			return View("Create", person);
		}

		Save(person); //Save the person to the database, or some other logic

		TempData["notice"] = "Person successfully created";
		return RedirectToAction("Index");

	}
}

Here is the view code for creating a Person:

@model Person

<div asp-validation-summary="ModelOnly"></div>

<form asp-action="Create">
  Id: <input asp-for="Id" /> <span asp-validation-for="Id"></span>
  <br />
  Name: <input asp-for="Name" /> <span asp-validation-for="Name"></span>
  <br />
  Email: <input asp-for="Email" /> <span asp-validation-for="Email"></span>
  <br />
  Age: <input asp-for="Age" /> <span asp-validation-for="Age"></span>

  <br /><br />
  <input type="submit" value="submtit" />
</form>

Now, when you submit the form, MVC's model binding system will use PersonValidator to validate the input and save the validation result to ModelState for checking.

Compatibility with ASP.NET Core Built-in Validation

By default, after FluentValidation validation executes, MVC's native DataAnnotations validation may also execute. This means you can mix FluentValidation with DataAnnotations validation. To disable this behavior and make FluentValidation the only validation library, set the RunDefaultMvcValidationAfterFluentValidationExecutes property to false:

services.AddMvc().AddFluentValidation(fv => {
 fv.RunDefaultMvcValidationAfterFluentValidationExecutes = false;
});

Implicit vs Explicit Child Property Validation

When validating complex objects, by default, you must manually specify a sub-validator for complex properties using the SetValidator method. When running an ASP.NET MVC application, you can optionally enable implicit validation for child properties. When enabled, MVC's validation infrastructure will automatically look for validators for each property without needing to specify sub-validators. This can be done by setting ImplicitlyValidateChildProperties to true:

services.AddMvc().AddFluentValidation(fv => {
 fv.ImplicitlyValidateChildProperties = true;
});

Note that if you enable this behavior, you should not manually specify sub-property validators via SetValidator, otherwise the validators will execute twice.

Client-Side Validation

FluentValidation is a server-side validation framework and does not directly provide any client-side validation. However, it can generate HTML element metadata supported by the jQuery validation framework for automatic validation with jquery.validate.

Manual Validation

Sometimes you may need to manually validate objects in an MVC project. In this case, you can copy the validation result to the MVC ModelState dictionary, which can then be used for front-end error display.

public ActionResult DoSomething() {
  var customer = new Customer();
  var validator = new CustomerValidator();
  var results = validator.Validate(customer);

  results.AddToModelState(ModelState, null);
  return View();
}

The AddToModelState method is implemented as an extension method and requires referencing the FluentValidation namespace. Note that the second parameter is an optional model name prefix that can set the prefix for object properties in the ModelState dictionary.

Validator Customization

You can use CustomizeValidatorAttribute to specify a validator for a model, and also support specifying a rule set for the validator.

public ActionResult Save([CustomizeValidator(RuleSet="MyRuleset")] Customer cust) {
  // ...
}

This is equivalent to specifying a rule set for validation, similar to passing the rule set to the validator:

var validator = new CustomerValidator();
var customer = new Customer();
var result = validator.Validate(customer, ruleSet: "MyRuleset");

This attribute can also be used to invoke validation of individual properties:

public ActionResult Save([CustomizeValidator(Properties="Surname,Forename")] Customer cust) {
  // ...
}

This is equivalent to specifying specific properties for the validator; other properties will not be validated:

var validator = new CustomerValidator();
var customer = new Customer();
var result = validator.Validate(customer, properties: new[] { "Surname", "Forename" });

You can also use the CustomizeValidatorAttribute to skip validation for certain types.

public ActionResult Save([CustomizeValidator(Skip=true)] Customer cust) {
  // ...
}

Validator Interceptors

You can use interceptors to further customize the validation process. Interceptors must implement the IValidatorInterceptor interface in the FluentValidation.Mvc namespace:

public interface IValidatorInterceptor	{
  ValidationContext BeforeMvcValidation(ControllerContext controllerContext, ValidationContext validationContext);
  ValidationResult AfterMvcValidation(ControllerContext controllerContext, ValidationContext validationContext, ValidationResult result);
}

This interface has two methods: BeforeMvcValidation and AfterMvcValidation, which intercept the pre-validation and post-validation processes respectively. Besides implementing this interface directly in the validator class, you can also implement it externally and specify the interceptor via the CustomizeValidatorAttribute:

public ActionResult Save([CustomizeValidator(Interceptor=typeof(MyCustomerInterceptor))] Customer cust) {
 //...
}

In this case, the interceptor must be a class that implements the IValidatorInterceptor interface and has a public parameterless constructor. Note that interceptors are an advanced scenario; in most cases, you may not need to use them, but they are available if needed.

Specifying Rule Sets for Client-Side

By default, FluentValidation does not generate client-side validation code based on rule sets. However, you can specify a rule set for client-side using the RuleSetForClientSideMessagesAttribute.

[RuleSetForClientSideMessages("MyRuleset")]
public ActionResult Index() {
   return View(new PersonViewModel());
}

You can also use the SetRulesetForClientsideMessages extension method (requires referencing the FluentValidation namespace) in a controller to specify a rule set for client-side.

public ActionResult Index() {
   ControllerContext.SetRulesetForClientsideMessages("MyRuleset");
   return View(new PersonViewModel());
}

ASP.NET MVC 5 Integration

FluentValidation can also be integrated with older ASP.NET MVC 5 projects. You need to add a reference to the FluentValidation.Mvc5 assembly via NuGet, then reference the FluentValidation.Mvc namespace:

Install-Package FluentValidation.Mvc5

After installation, you need to configure the Application_Start event in the application's global file (Global.asax) to initialize FluentValidation.

protected void Application_Start() {
    AreaRegistration.RegisterAllAreas();

    RegisterGlobalFilters(GlobalFilters.Filters);
    RegisterRoutes(RouteTable.Routes);

    FluentValidationModelValidatorProvider.Configure();
}

Inside MVC, FluentValidation uses a validator factory to create validators for specific types. By default, it uses the AttributedValidatorFactory factory, which allows you to set a validator for a specified model via an attribute:

[Validator(typeof(PersonValidator))]
public class Person {
	public int Id { get; set; }
	public string Name { get; set; }
	public string Email { get; set; }
	public int Age { get; set; }
}

public class PersonValidator : AbstractValidator<Person> {
	public PersonValidator() {
		RuleFor(x => x.Id).NotNull();
		RuleFor(x => x.Name).Length(0, 10);
		RuleFor(x => x.Email).EmailAddress();
		RuleFor(x => x.Age).InclusiveBetween(18, 60);
	}
}

You can also implement a custom validator factory using MVC's built-in IoC dependency injection container instead of using the attribute marking method.

FluentValidationModelValidatorProvider.Configure(provider => {
  provider.ValidatorFactory = new MyCustomValidatorFactory();
});

Finally, we can create a controller and its associated view:

public class PeopleController : Controller {
	public ActionResult Create() {
		return View();
	}

	[HttpPost]
	public ActionResult Create(Person person) {

		if(! ModelState.IsValid) { // re-render the view when validation failed.
			return View("Create", person);
		}

		TempData["notice"] = "Person successfully created";
		return RedirectToAction("Index");

	}
}

This is the corresponding view code:

@Html.ValidationSummary() @using (Html.BeginForm()) { Id: @Html.TextBoxFor(x =>
x.Id) @Html.ValidationMessageFor(x => x.Id)
<br />
Name: @Html.TextBoxFor(x => x.Name) @Html.ValidationMessageFor(x => x.Name)
<br />
Email: @Html.TextBoxFor(x => x.Email) @Html.ValidationMessageFor(x => x.Email)
<br />
Age: @Html.TextBoxFor(x => x.Age) @Html.ValidationMessageFor(x => x.Age)

<br /><br />

<input type="submit" value="submit" />
}

Now, when you submit the form, MVC will use the FluentValidation framework to validate the model.

Special Notes

Using the FluentValidation framework in ASP.NET MVC 5 is essentially the same as in ASP.NET Core. Therefore, client-side validation, manual validation, validator customization, validator interceptors, and client-side rule sets are all similar to the above. Due to the length of the article, they will not be repeated here.

ASP.NET WebApi 2 Integration

The integration of FluentValidation with WebApi is the same as with MVC 5 (above), but you need to reference the FluentValidation.WebApi package via NuGet.

Source: https://www.xcode.me/post/5849 (Note: link is dead; a reposted article can be viewed at cnblogs: https://www.cnblogs.com/mq0036/p/14548370.html)

Keep Exploring

Related Reading

More Articles
Same category / Same tag 6/20/2024

CodeWF.EventBus: Lightweight Event Bus for Smoother Communication

CodeWF.EventBus is a flexible event bus library that enables decoupled communication between modules. It supports various .NET project types such as WPF, WinForms, ASP.NET Core, etc. With a clean design, it easily implements command publishing and subscribing, as well as requests and responses. Through orderly event handling, it ensures events are properly processed. Simplify your code and improve system maintainability.

Continue Reading