Fluent Validation Rules with ASP.NET Core

When it comes to model validation in ASP.NET, we've always have had the option of using Data Annotations to declaratively define validation rules with attributes. The situation may arise where using attributes just might not be the thing for you. It might be the case that you don't have access to the code to apply attributes classes you want to have validated. It could also be that your validation logic is fairly complex and you prefer to have it separated from the model. Maybe you just have a personal distaste for using attributes in general.

In this post, we'll take a look at using the FluentValidation library as an alternative to Data Annotations.

Introducing FluentValidation

FluentValidation is an open source validation library for .NET. It supports a fluent API, and leverages lambda expressions to build validation rules. The current stable version (6.4.1) supports both .NET Framework 4.5+ and .NET Standard 1.0. In addition to that, it provides integrations for ASP.NET MVC 5, ASP.NET Web API and ASP.NET Core MVC.

Creating Validators

We're going to use the dotnet CLI to create a simple MVC application that we can use.

$ dotnet new mvc --auth None --framework netcoreapp1.1

The first thing we'll need is a model to validate. Here's a simple class that we can start off with.

public class RegistrationViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string Email { get; set; }
}

Next, using the CLI, we'll add the FluentValidation NuGet package for ASP.NET Core. If you're using Visual Studio, you can use the Package Manager if you'd like. Either option works equally well.

$ dotnet add package FluentValidation.AspNetCore

To create a validator for RegistrationViewModel, will need to create a class that inherits from AbstractValidator<T>. The validation rules are will be defined in the constructor of our newly created validator starting off with the RuleFor method.

public class RegistrationViewModelValidator : AbstractValidator<RegistrationViewModel>
{
    public RegistrationViewModelValidator()
    {
        RuleFor(reg => reg.FirstName).NotEmpty();
        RuleFor(reg => reg.LastName).NotEmpty();
        RuleFor(reg => reg.Email).NotEmpty();
    }
}

You can pass RuleFor a lambda expression that it will use to determine the property that the proceeding rules will be associated with. Then, we can make use of some of built-in validators like NotEmpty, NotEqual, Matches and Must.

Adding the FluentValidation services

To wire up FluentValidation with ASP.NET Core MVC, within the ConfigureServices method inside of Startup.cs, we will use the AddFluentValidation extension method.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
        .AddFluentValidation();
}

Calling AddFluentValidation will add the FluentValidation services to the default container in ASP.NET Core. This includes a custom IObjectModelValidator that allows FluentValidation to plug into ASP.NET Core's validation system. However, it does not register the custom RegistrationViewModelValidator validator that we created. To automatically register your validators with the container, you have the option to use either the AddFromAssemblyContaining or RegisterValidatorsFromAssembly methods.

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc()
            .AddFluentValidation(fvc =>
                fvc.RegisterValidatorsFromAssemblyContaining<Startup>());
}

With that in place, all we need is a simple Razor view to test out our setup.

@model RegistrationViewModel

<div class="panel panel-primary">
    <div class="panel-heading">
        <h4>Registration Form</h4>
    </div>
    <div class="panel-body">
        <form asp-controller="Home" asp-action="FormValidation" method="POST">
            <div class="form-group">
                <label asp-for="FirstName" class="control-label"></label>
                <div>
                    <input asp-for="FirstName" class="form-control" />
                    <span asp-validation-for="FirstName" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group" style="margin-top:10px">
                <label asp-for="LastName" class="control-label"></label>
                <div>
                    <input asp-for="LastName" class="form-control" />
                    <span asp-validation-for="LastName" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group" style="margin-top:10px">
                <label asp-for="Email" class="control-label"></label>
                <div>
                    <input asp-for="Email" class="form-control" />
                    <span asp-validation-for="Email" class="text-danger"></span>
                </div>
            </div>
            <div class="form-group" style="margin-top:10px">
                <button asp- class="btn btn-primary">Submit</button>
            </div>
        </form>
    </div>
</div>

Now the validation logic for our model is being handled by FluentValidation, and we can check the ModelState inside of our MVC Controllers just as we always have.

[HttpPost]
public IActionResult FormValidation(RegistrationViewModel model)
{
    if (this.ModelState.IsValid) {
        ViewBag.SuccessMessage = "Great!";
    }
    return View();
}

Chaining Validation Rules

So far we've setup some basic validation rules. One way we can create more complex rules is by chaining on validations using the fluent syntax. Let's take the Email field on our model for example. Right now the email validation rule only checks for an non-empty string. What if we wanted to check that the property actually contains a valid email? In that case we can tack on another rule like this.

public RegistrationViewModelValidator()
{
    RuleFor(reg => reg.FirstName).NotEmpty();
    RuleFor(reg => reg.LastName).NotEmpty();
    RuleFor(reg => reg.Email).NotEmpty().EmailAddress();
}

Now what if we wanted to take it a little further. What if we only wanted to accept emails from a certain domain? A rule like that is a little more specific to our application, so one thing we can make use of is the Must validation rule and supply it with a predicate.

public RegistrationViewModelValidator()
{
    RuleFor(reg => reg.FirstName).NotEmpty();
    RuleFor(reg => reg.LastName).NotEmpty();
    RuleFor(reg => reg.Email).NotEmpty()
                      .EmailAddress()
                      .Must(email => email.EndsWith("microsoft.com"));
}
Creating Custom Property Validators

The built-in validation rules will usually be more than enough for most of your needs. If not, FluentValidation allows you to create custom property validators by deriving from PropertyValidator and implementing it's IsValid method.

public class EmailFromDomainValidator : PropertyValidator
{
    private readonly string _domain;
    public EmailFromDomainValidator(string domain)
           : base("Email address {PropertyValue} is not from domain {domain}")
    {
        _domain = domain;
    }
    protected override bool IsValid(PropertyValidatorContext context)
    {
        if (context.PropertyValue == null) return false;
        var split = (context.PropertyValue as string).Split('@');
        if (split.Length == 2 && split[1].ToLower().Equals(_domain)) return true;
        return false;
    }
}

IsValid gets passed an instance of PropertyValidatorContext. This gives you access to information such as the name of the property being validated, value of the property and a reference to the model itself.

To use this custom PropertyValidator to extend FluentValidation's fluent DSL, we can create an extension method for IRuleBuilder and use the SetValidator to include the EmailFromDomainValidator that we created..

public static class CustomValidatorExtensions
{
    public static IRuleBuilderOptions<T, string> EmailAddressFromDomain<T>(
        this IRuleBuilder<T, string> ruleBuilder, string domain)
    {
        return ruleBuilder.SetValidator(new EmailFromDomainValidator(domain));
    }
}

Now we can update the rule definition to use the EmailFromDomainValidator like this.

public RegistrationViewModelValidator()
{
    RuleFor(reg => reg.FirstName).NotEmpty();
    RuleFor(reg => reg.LastName).NotEmpty();
    RuleFor(reg => reg.Email).EmailAddressFromDomain("microsoft.com");
}
Conclusion

FluenValidation is a useful library for not only creating validation rules but also allows you to separate your validation logic from your model. In this post, we've only covered a few of its features. Definitely check out the project on Github to see some of the other configuration options at your disposal.