There’s some discussion around the ASP.NET Core repository on the best way to give Minimal API users access to validation as a programmatic concept inside of ASP.NET Core. It’s a lot of positive feedback with some good ideas, so I thought I’d try to implement some of the features using my favorite validation library, FluentValidation, because I can’t wait! I mostly find it fun to try things out, and validation is an exciting topic. If you’re exploring Minimal APIs, you’ll want to check this post out and apply the code to your projects. Let’s get started.
FluentValidation Infrastructure
You need to register FluentValidation, and its validators need to as part of ASP.NET Core’s services collection. However, before you can do that, you’ll need to add the package FluentValidation.DependencyInjectionExtensions
. This package holds the extension methods that allow you to register all validators in a Minimal APIs web application. Once installed, you can call the method AddValidatorsFromAssemblyContaining
on the services collection.
using FluentValidation;
using FluentValidationExample;
using static Microsoft.AspNetCore.Http.Results;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
var app = builder.Build();
The setup method scans the current assembly for all implementations of IValidator
and registers it into the services collection. We’ll be using a very simple validator in our example that validates the properties of the Person
record.
public record Person(string? Name, int? Age);
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(m => m.Name).NotEmpty();
RuleFor(m => m.Age).NotEmpty().GreaterThan(0);
}
}
Next, let’s implement the Validated<T>
class. This class will be both our model binder and validator.
public class Validated<T>
{
private ValidationResult Validation { get; }
private Validated(T value, ValidationResult validation)
{
Value = value;
Validation = validation;
}
public T Value { get; }
public bool IsValid => Validation.IsValid;
public IDictionary<string, string[]> Errors =>
Validation
.Errors
.GroupBy(x => x.PropertyName)
.ToDictionary(x => x.Key, x => x.Select(e => e.ErrorMessage).ToArray());
public void Deconstruct(out bool isValid, out T value)
{
isValid = IsValid;
value = Value;
}
// ReSharper disable once UnusedMember.Global
public static async ValueTask<Validated<T>> BindAsync(HttpContext context, ParameterInfo parameter)
{
// only JSON is supported right now, no complex model binding
var value = await context.Request.ReadFromJsonAsync<T>();
var validator = context.RequestServices.GetRequiredService<IValidator<T>>();
if (value is null) {
throw new ArgumentException(parameter.Name);
}
var results = await validator.ValidateAsync(value);
return new Validated<T>(value, results);
}
}
There’s a lot to unpack in our Validated<T>
implementation, and the essential logic occurs in the BindAsync
method.
- We read the body of our request and deserialize JSON into our model.
- We retrieve the appropriate validator from the services collection.
- Lastly, we validate and create an instance of our
Validated
type with the value and our validation result.
We will also include a Deconstruct
method on our Validated<T>
type. We’ll use this to make our endpoint code more readable, but it’s entirely optional for use.
Now we’re ready to write a Minimal API endpoint.
Validating Input on a Minimal API Endpoint
The first step is to change our inputs to be of type Validated<T>
. In our case, we’ll be expecting a Validated<Person>
.
app.MapPost("/person", (Validated<Person> req) =>
Next, let’s implement our endpoint body. Note, I’m statically importing the Results
static class, which you’ll see later in this post.
app.MapPost("/person", (Validated<Person> req) =>
{
// deconstruct to bool & Person
var (isValid, value) = req;
return isValid
? Ok(value)
: ValidationProblem(req.Errors);
});
And there you have it! We’ve used FluentValidation to validate an incoming request with some nice-to-have functionality. Here’s the entire program for clarity.
using FluentValidation;
using FluentValidationExample;
using static Microsoft.AspNetCore.Http.Results;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddValidatorsFromAssemblyContaining<Program>();
var app = builder.Build();
app.MapGet("/", () => "Hello World!");
app.MapPost("/person", (Validated<Person> req) =>
{
// deconstruct to bool & Person
var (isValid, value) = req;
return isValid
? Ok(value)
: ValidationProblem(req.Errors);
});
app.Run();
public record Person(string? Name, int? Age);
// ReSharper disable once UnusedType.Global
public class PersonValidator : AbstractValidator<Person>
{
public PersonValidator()
{
RuleFor(m => m.Name).NotEmpty();
RuleFor(m => m.Age).NotEmpty().GreaterThan(0);
}
}
Conclusion
What are your thoughts? Do you like this approach, and would you do anything differently? Try this code out in your Minimal APIs and follow me on Twitter @buhakmeh so we can chat about your Minimal API uses. As always, thank you for reading.