ASP.NET Core 8: Model Validations in Minimal APIs using Endpoint Filters
Minimal API is the great features of ASP.NET Core. This feature provides a better mechanism to create APIs with less complex code than the traditional controllers' approach. Minimal APIs provides simple and clean code to define HTTP Endpoints. In the case of Minimal APIs, we directly focus on the domain implementation rather than controllers and its actions methods. Minimal APIs offers readability, Testability, as well as they offer an easy approach for building APIs for new professionals.
While implementing APIs, it is important to implement the Model Data Validations to make sure that the data received is valid before it is considered for processing. While using the Minimal APIs, we need to use the Fluent Validations to validate the model data. In this article, we will perform the Model Validation using the FluentValidation package as well as using the Endpoint Filter.
The Figure 1 will provide an idea of the Validation in ASP.NET Core Minimal API.
Figure 1: The Validation
Step 1: Open Visual Studio 2022 and create a new API Project targeted to .NET 8 and name this project as Minimal_APIValidators. Make sure that when you create the project the User Controller checkbox is unchecked. In this project, add the NuGet Package named FluentValidation.
Step 2: In this project, add a new folder named Models. In this folder add a new class file named ProductInfo.cs. In this class file, add the ProductInfo class as shown in Listing 1.
namespace Minimal_APIValidators.Models { public class ProductInfo { public int ProductId { get; set; } public string? ProductName { get; set; } public string? CategoryName { get; set; } public string? Manufacturer { get; set; } public string? Description { get; set; } public int Price { get; set; } } }
Listing 1: The ProductInfo class
Step 2: In the project, add a new folder named Validators. In this folder, add a new class file named ProductValidatorInfo.cs. In this file, let's add the code for applying validation rules for the ProductInfo class as shown in the Listing 2.
using FluentValidation; using Minimal_APIValidators.Models; namespace Minimal_APIValidators.Validators { public class ProductInfoValidator : AbstractValidator<ProductInfo> { public ProductInfoValidator() { RuleFor(p => p.ProductId).GreaterThan(0); RuleFor(p=>p.ProductName).NotEmpty(); RuleFor(p => p.CategoryName).NotEmpty(); RuleFor(p => p.Manufacturer).NotEmpty(); RuleFor(p => p.Description).NotEmpty(); RuleFor(p => p.Price).GreaterThan(0); } } }
Listing 2: The Validator
As shown in Listing 2, the ProductInfoValidator class is derived from the AbstractValidator class. The AbstractValidator is a generic class which is typed to the ProductInfo class. The AbstarctValidator class has the Validate() method to validate the Model based on the rules for validation defined in the ProductInfoValidator constructor.
Step 3: In the Program.cs file, add the Post Endpoint for accepting POST request. This Endpoint will accept the ProductInfo class. The POST Endpoint will validate the ProductInfo Model by using the ProductInfoValidator as shown in the Listing 3.
app.MapPost("/api/product", (ProductInfo product) => { var validator = new ProductInfoValidator(); var validationresult = validator.Validate(product); if (validationresult.IsValid) { return Results.Ok("Data is Valid"); } return Results.BadRequest(validationresult.Errors.Select(e=>e.ErrorMessage).ToList()); });
Listing 3: The POST Endpoint
As shown in Listing 3, the POST Endpoint uses the ProductInfoValidator class and invokes its Validate() method. The received model class is passed to the Validate() method. The Validate() method validate the ProductInfo model class and if the received data is validated based on the rules defined in the ProductInfoValidator class, then the IsValid property will be true else it will be false and based on it the response will be send back. Run the application and Test the Post HTTP request, if the data is invalid then the response will be sent as shown in Figure 2
Figure 2: The Error Response
Validation using the Endpoint Filter
In Minimal API we can even implement validations using the Endpoint Filter. The ASP.NET Core provides the IEndpointFilter interface. This interface is used for implementing a filter targeting to the route handler. This means that we can create a filter and configure it to the Endpoint. This filter will be executed when the request is received on the Endpoint where this filter is used. As shown in the Figure 1, the Endpoint uses the filter, and this filter uses the AbstractValidator class to validate the data from the HTTP request body. Let's see the implementation.
Step 4: In the project, add a new folder named CustomFilters. In this folder, add a new class file named ModelValiationFilter.cs. In this file, we will add code for the ModelValidationFilter class. The code for the class is shown in Listing 4.
using FluentValidation; using System.Text.Json; namespace Minimal_APIValidators.CustomFilters { public class ModelValidationFilter<TModel> : IEndpointFilter where TModel : class { private readonly IValidator<TModel> _validator; public ModelValidationFilter(IValidator<TModel> validator) { _validator = validator; } async ValueTask<object?> IEndpointFilter.InvokeAsync(EndpointFilterInvocationContext context, EndpointFilterDelegate next) { try { // The Stream Data Stream bodyData = context.HttpContext.Request.Body; if (bodyData.CanSeek) { bodyData.Seek(0, System.IO.SeekOrigin.Begin); } // Deserialize the Data var request = await JsonSerializer.DeserializeAsync<TModel>(bodyData); // Validate var validationResult = await _validator.ValidateAsync(request); if (!validationResult.IsValid) { // If Invalid then Read Error Messages var errors = validationResult.Errors.ToDictionary(e => e.PropertyName, e => e.ErrorMessage); context.HttpContext.Response.StatusCode = StatusCodes.Status400BadRequest; await context.HttpContext.Response.WriteAsJsonAsync(errors); } } catch (Exception ex) { throw ex; } return await next(context); } } }
Listing 4: The ModelValidationFilter
The code in Listing 4 shows the important implementation of the Endpoint filter. The ModelValidationFilter is created as a generic class. The generic parameter for this class will be the Model class which will be validated. The ModelValidationFilter class implements IEndointFilter interface. This interface is used to implement the filter that is targeted to the route handler that will be executed when the request is received by the API Endpoint. This interface has the InvokeAsync() method. This method accepts EndpointFilterInvocationContext and EndpointFilterDelegate parameters. The EndpointFilterInvocationContext is used to retrieve the HttpContext object so that we can read data received from the HTTP request body. The EndpointFilterDelegate object is used to handle the filter execution in the HTTP Pipeline. As shown in the Listing 4, the method InvokeAsync() method reads the data form the HTTP request body, further to that the method deserialize the data from the body to the generic model parameter and then this deserialized object is passed to the ValidateAsync() method to check for the data validation. If this data is valid then the execution will be continued else the validation errors will be responded back to the request.
The ModelValidationFilter class is constructor injected with the IValidator interface. This is the generic interface, here we will be using the ProductInfo as a type for the generic parameter so that the class will be validated. The IValidator interface is implemented by the AbstractValidator class, the class that is used to define validation rules on the model class ProductInfo. Once the filter is implemented, we will be using it on the Endpoint.
Step 5: We need to modify the Program.cs file by adding code to register the ProductInfoValidator class in dependency container so that the IValidator injection is resolved. Since we will be reading the HTTP request body, we need to add the middleware for the same. Finally, we will be adding an Endpoint that will be used to accept the HTTP request and we will be applying filter on this Endpoint. The code is shown in Listing 5
using FluentValidation; using Microsoft.AspNetCore.Identity; using Minimal_APIValidators.CustomFilters; using Minimal_APIValidators.Models; using Minimal_APIValidators.Validators; // For JSON Seriaization Policy using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions; var builder = WebApplication.CreateBuilder(args); // The registration of the ProductInfoValidator to resolve the ProductInfoValidator class builder.Services.AddScoped<IValidator<ProductInfo>, ProductInfoValidator>(); // Add services to the container. // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.Configure<JsonOptions>(options => { options.SerializerOptions.PropertyNamingPolicy = null; }); var app = builder.Build(); // Middleware to Read the HTTP request body data app.Use((context, next) => { context.Request.EnableBuffering(); return next(); }); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapPost("/api/product", (ProductInfo product) => { var validator = new ProductInfoValidator(); var validationresult = validator.Validate(product); if (validationresult.IsValid) { return Results.Ok("Data is Valid"); } return Results.BadRequest(validationresult.Errors.Select(e=>e.ErrorMessage).ToList()); }); // Endpoint with the ModelValidationFilter Applied on it app.MapPost("/api/filter/product", (ProductInfo product) => { return Results.Ok("Data is Valid"); }).AddEndpointFilter<ModelValidationFilter<ProductInfo>>(); app.Run();
Listing 5: Program.cs modifications
The filter is configured using the AddEndpointFilter() method.
Run the application and make the POST request with invalid data the error message will be responded as shown Figure 2.