Validation of input data in Minimal API .NET filters, plain and simple

Validation of input data in Minimal API .NET filters, plain and simple

I’ll start with a rhetorical question – what could be more interesting than the process of learning a new technology, when understanding occurs “on the fly”, and brain cells perceive new knowledge as something familiar, but a little forgotten?

The answer is yes, in general, a lot! Although the technology, which is easy to master, is definitely positive.

As already obvious from the name, we will quickly understand how you can easily and effectively implement a quality check of input data using filters Minimal API. Of course, we will not become like the inventors of the wheel and use existing developments, for example, a package FlatValidator. It is not our task to delve into the details of the package, we will focus primarily on integration.

A step into the past. Minimal API was first announced in the release .NET 6it is a flexible software technique designed to be maintained HTTP-routing Appearance Minimal API there was a feeling in the air that the controllers’ time was running out inexorably. And, as always, new times create new calls.

If you haven’t heard anything about it, it’s not the point, the further discussion will bring some clarity, just mark the boundaries of the researched question.

End point filters are known mainly in the art Minimal APIalthough they are now available even for MVCand for Razor Pages.

In my opinion, filters are a cross between the request handler itself and the endpoint middleware. Term ‘middleware‘ difficult to translate into Russian, let’s say this – it is a software module that is built into the processing chain HTTP-request If there are several such modules, they are executed sequentially one after the other. Why am I telling this? Previously, it was often used for data validation middleware. Now a more convenient tool has appeared – filters, as an implementation of the ` interfaceIEndpointFilter`.

The simplest implementation code `IEndpointFilterlooks something like this:

public class MyFilter: IEndpointFilter  
{  
    public async ValueTask<object?> InvokeAsync(  
        EndpointFilterInvocationContext context,  
        EndpointFilterDelegate next)  
    {        
        var result = await next(context);  
        if (result is string s)
        {
            result = s.Replace("vodka", "pineapple juice");
        }
        return result;  
    }
}

What is going on here? The filter skips processing the request to the endpoint (by calling `next()`) and at the output replaces v response all fragments `vodka“on”pineapple juice`. As you can see, everything is very simple.

Let’s connect this filter to the program Minimal API.

var builder = WebApplication.CreateBuilder(args);  
  
var app = builder.Build();  

var group = app  
    .MapGroup(string.Empty)  
    .AddEndpointFilter<MyFilter>();  // <==
  
group.MapGet("/", () => "Hello World");  
group.MapGet("/hi", () => "I like to drink vodka!");  
  
app.Run(); 

Obviously, the result of our filter will be the replacement of the phrase at the outputAnd how to drink vodka!“on”I like to drink pineapple juice!`. This is a surprise for an amateur `vodka`. “And what to do? A drunken fight.”

So, now that the filters for the Minimal API have been dealt with a bit, let’s move on to a more real-world example. By NuGet install the main package FlatValidatorit will help to check our data professionally.

❯ dotnet add package FlatValidator

In the documentation for the package, it is written that the use of the validator is provided in two tentatively named modes inline and derived.

IN inline– mode of verification rules are set directly at the point of validation. This can be convenient, as it leaves it possible to see the validation logic “in place”.

On the other hand, in solid projects, this approach will most likely not be consistent with the conceptual requirements, usually the business logic is still separated and placed in strictly designated places. Then it’s easier to use the classic approach with class inheritanceFlatValidator`.

Let me take a slightly modified example from the documentation, here inline-regime:

// use asynchronous version
var result = await FlatValidator.ValidateAsync(model, v => 
{
    // m == model, IsEmail() is one of funcs for typical data formats
    v.ValidIf(m => m.Email.IsEmail(), "Invalid email", m => m.Email);

    // involve custom userService for specific logic
    v.ErrorIf(async m => await userService.IsUserExistAsync(m.Email),
              "User already registered", m => m.Email);
});
if (!result) // check the validation result
    return TypedResults.ValidationProblem(result.ToDictionary());

Functions `ValidIf` and “ErrorIf” allow setting validation rules. There can be any number of them in one validator. `TypedResults.ValidationProblem` is a part .NET 6+which simplifies the return of errors in HTTP-response.

The program itself is in concept inline-mode we could write as follows:

var builder = WebApplication.CreateBuilder(args);  
var app = builder.Build();  
  
// Endpoint aka https://localhost:5000/todos/
app.MapPost("/todos", (Todo todo) => 
{
    var result = FlatValidator.Validate(todo, v => 
    {
        v.ErrorIf(m => m.Title.IsEmpty(), "Title can not be empty.", m => m.Title);
    });
    if (!result)
        return TypedResults.ValidationProblem(result.ToDictionary()) 

    // ....
    return Results.Ok();
});  

app.Run(); 

// Model to test validation functionality
public record Todo(string Title, bool IsComplete = false);

If inline-The style does not suit you, use the version with inheritance.

var builder = WebApplication.CreateBuilder(args);  
var app = builder.Build();  
  
app.MapPost("/todos", (Todo todo) => 
{
    if (new TodoValidator().Validate(todo) == false)
        return TypedResults.ValidationProblem(result.ToDictionary()) 

    // ....
    return Results.Ok();
});  

app.Run(); 

// Model to test validation functionality
public record Todo(string Title, bool IsComplete = false);

// Implement custom validator for the model Todo
public class TodoValidator : FlatValidator<Todo>
{
    public TodoValidator()
    {
        v.ErrorIf(m => m.Title.IsEmpty(), "Title can not be empty.", m => m.Title);
    }
}

This entry looks neater. It is clear that classesTodo`and’TodoValidatoreveryone should be in their own file.

Well, we are one step away from implementing the validation filter stated in the title of the article. We will move the logic of calling the validator directly inside the filter.

app.MapPost("/todos", (Todo todo) => 
{
    return Results.Ok();

}).AddEndpointFilter<ValidationFilter<Todo>>();  // <==


public class ValidationFilter<T>(IServiceProvider serviceProvider) : IEndpointFilter
{
    public async ValueTask<object?> InvokeAsync(
        EndpointFilterInvocationContext context, 
        EndpointFilterDelegate next)
    {
        var validators = serviceProvider.GetServices<IFlatValidator<T>>();
        foreach (var validator in validators)
        {
            if (context.Arguments.FirstOrDefault(x => 
                    x?.GetType() == typeof(T)) is not T model)
            {
                return TypedResults.Problem(
                            detail: "No approptiate parameter.", 
                            statusCode: StatusCodes.Status500InternalServerError);
            }

            if (!await validator.ValidateAsync(model))
            {
                return TypedResults.ValidationProblem(result.ToDictionary());
            }
        }

        // call next filter in the chain
        return await next(context);
    }
}

Did you notice? The body of the endpoint.MapPost(“/todos”)got rid of any checks at all, the logic is now in the filter. But let’s not forget `.AddEndpointFilter>`. And because we have a filter genericit is suitable for any type of model, it is enough to implement the validator class itself and register itIServiceCollection`.

// register a validator for the Todo model
builder.Services.AddScoped<IFlatValidator<Todo>, TodoValidator>();

However, there are no problems with this either. If you want to automate the registration of all your validators, use the FlatValidator.DependencyInjection helper package.

❯ dotnet add package FlatValidator.DependencyInjection
var builder = WebApplication.CreateBuilder(args);  

builder.Services.AddFlatValidatorsFromAssembly(Assembly.GetExecutingAssembly());

In general, in order to get to know the capabilities of the package better FlatValidatorI recommend first of all to look at the project page, there you will find documentation and quite suitable examples.

It’s no secret that writing productive code is quite difficult. It is no less difficult to read it later and understand what is written, because productivity and code comprehensibility are sometimes two completely different concepts. However, the package managed to get fantastic performance with completely readable code.

And finally, as promised, a couple of slides about the comparative performance of the package. The benchmarks themselves are also on the project page, they can be downloaded and tested.

Performance comparison of FlatValidator and FluentValidation

Related posts