ryanelian / FluentValidation.Blazor

Fluent Validation-powered Blazor component for validating standard <EditForm> :milky_way: :white_check_mark:
https://www.nuget.org/packages/Accelist.FluentValidation.Blazor
MIT License
235 stars 27 forks source link
asp-net-core blazor component dependency-injection lambda linq validation

FluentValidation.Blazor

Fluent Validation-powered Blazor component for validating standard \ :cyclone: :white_check_mark:

GitHub Actions NuGet

Install

dotnet add package FluentValidation
dotnet add package Accelist.FluentValidation.Blazor

Getting Started

This library is a direct replacement to the default Blazor <DataAnnotationValidator> with zero configuration required :zap: in the application code base:

<EditForm Model="Form">

    <FluentValidator></FluentValidator>

    <div class="form-group">
        <label for="email">Email</label>
        <InputText id="email" type="email" class="form-control" @bind-Value="Form.Email"></InputText>
        <ValidationMessage For="() => Form.Email"></ValidationMessage>
    </div>
    <div class="form-group">
        <button type="submit" class="btn btn-primary">
            <i class="fas fa-chevron-up"></i>
            Submit
        </button>
    </div>
</EditForm>
@code {
    FormModel Form = new FormModel();
}
public class FormModel
{
    public string Email { set; get; }
}

public class FormModelValidator : AbstractValidator<FormModel>
{
    public FormModelValidator()
    {
        RuleFor(Q => Q.Email).NotEmpty().EmailAddress().MaximumLength(255);
    }
}

The <FluentValidator> component automatically detects the Model data type used by the parent <EditForm> then attempts to acquire the corresponding FluentValidation.IValidator<T> for that model data type.

For this reason, in addition to coding the usual FluentValidation.AbstractValidator<T> Fluent Validation implementation, you are required to register the FluentValidation.IValidator<T> implementation in the Startup.cs Service Provider (Dependency Injection):

services.AddTransient<IValidator<CreateAccountFormModel>, CreateAccountFormModelValidator>();
// Alternatively, use FluentValidation.DependencyInjectionExtensions package (read further down below...)

This effectively allows you, dear programmer, to inject required services to your validation implementations for writing amazing custom validation methods! :fire:

public class FormModelValidator : AbstractValidator<FormModel>
{
    readonly AppDbContext DB;
    readonly IServiceProvider SP;

    public FormModelValidator(AppDbContext db, IServiceProvider sp)
    {
        this.DB = db;
        this.SP = sp;

        RuleFor(Q => Q.Email).NotEmpty().EmailAddress().MaximumLength(255)
            .Must(BeUniqueEmail).WithMessage("Email address is already registered.");
    }

    bool BeUniqueEmail(string email)
    {
        var exist = DB.Account.Where(Q => Q.Email == email).Any();
        return (exist == false);
    }
}

Inlined Validator

Validator parameter may also be passed directly to the component to inline the AbstractValidator implementation instead of relying on .NET Core DI:

<FluentValidator Validator="Validator"></FluentValidator>
@code {
    FormModelValidator Validator = new FormModelValidator();
}

FluentValidation.DependencyInjectionExtensions

dotnet add package FluentValidation.DependencyInjectionExtensions
services.AddValidatorsFromAssemblyContaining<Program>();

Can be used to auto-populate all validators from current application / other project automatically!

Nested Objects & Arrays Validation

FluentValidation offers SetValidator method for validating nested objects and arrays. Combined with Dependency Injection capability of this component library, a complex form can be validated easily:

public class RootValidator : AbstractValidator<Root>
{
    public RootValidator(IValidator<Child> childValidator, IValidator<Item> itemValidator)
    {
        RuleFor(Q => Q.Child).SetValidator(childValidator);
        RuleForEach(Q => Q.ArrayOfItems).SetValidator(itemValidator); // Array, List, IList, ...
    }
}

The validators used MUST be registered in the ASP.NET Core service provider!

If for some reason Dependency Injection is not possible, the parameter ChildValidators (inlined Dictionary<Type, IValidator> defining validators used for each children model types) MUST be passed into the component due to technical reasons.

Repeated Remote Call Warning :warning:

By default, Fluent Validation does NOT short-circuit the validation chain on first error.

This may cause performance issues when a validation logic hits the database / remote service multiple times in short burst due to validation triggers on field change!

To reduce the impact of repeated custom validation calls, use:

Why Not Just Use <DataAnnotationValidator>?

  1. <DataAnnotationValidator> cannot use DI services at the moment...

  2. ... but <DataAnnotationValidator> also cannot do AddModelError() like Razor Pages. Which rules out validation AFTER form submission!

  3. [ValidationAttribute] top-level IsValid() method cannot be async!