An Interest In:
Web News this Week
- April 2, 2024
- April 1, 2024
- March 31, 2024
- March 30, 2024
- March 29, 2024
- March 28, 2024
- March 27, 2024
.NET 7: Minimal APIs y FluentValidation
Introduccin
Minimal APIs sigue siendo un tema nuevo dentro de la comunidad .NET y creo que an son pocos los que han usado Minimal APIs en un proyecto en produccin.
Yo sin duda lo he estado usando, pero no en nada grande por ahora. Pero he estado explorando todas sus funcionalidades y por ahora lo que ms me gusta es que realmente puedes estructurar tus proyectos de la forma que gustes.
Tengo un repositorio (In Progress ) donde estoy haciendo varios tipos de proyectos con Minimal APIs (Source: GitHub (isaacOjeda/MinimalApiExperiments)) donde exploro un Clean Architecture, un Vertical Slice architecture y uno Plain (sin MediatR, puro Minimal API).
Nota : El cdigo fuente de este post lo puedes encontrar aqu
El primer "pero" que encontr al estar haciendo la versin "Plain" (sin mis queridos decoradores de MediatR) fue que no haba una forma "incluida" de cmo hacer validaciones sin tener que repetir el mismo cdigo una y otra vez.
El ModelState
tal cual no existe en Minimal API (o eso creo) y el ModelState.IsValid
que solamos usar en MVC pues ya no est disponible.
Damian Edwards (de los jefes de asp.net) hizo esta librera MiniValidator) el cual tienes que hacer esto para validar:
app.MapPost("/widgets/custom-validation", (WidgetWithCustomValidation widget) => !MiniValidator.TryValidate(widget, out var errors) ? Results.ValidationProblem(errors) : Results.Created($"/widgets/{widget.Name}", widget));
MiniValidator.TryValidate(widget, out var errors)
ejecuta la validacin utilizando DataAttributes (como el [Required]
) y regresa un bool
indicando el resultado de la validacin y con el parmetro de salida regresa los errores en caso de que existan.
Hablando de performance, esto seguro es muy rpido, recuerda que las cosas "mgicas" (o sea, las cosas que usan reflection) pueden terminar siendo lentas, si lo que necesitas es high performance, debes de evitarte las formas "mgicas" de hacer las cosas (pero high performance de verdad), si no, eres como el 90% del resto como nosotros que usamos reflection al por mayor sin ningn problema.
Personalmente esto no me gusta, tener que validar en cada endpoint, ya que siento que es un "retroceso" comparado con Web API y lo que se agreg con [ApiController]
en versiones pasadas. Por lo que he estado explorando como hacer esto sin repetir cdigo y mejor an, utilizando FluentValidation.
Fluent Validation
FluentValidation ya es una librera muy popular, implementada en muchas plantillas que nos podemos encontrar en GitHub.
Esta librera me gusta usarla porque la validacin se separa del modelo y puedes sin agregar "ruido" crear validaciones muy complejas.
Utilizaremos el paquete FluentValidation.DependencyInjectionExtensions
que cuenta con extensiones para registrar los validadores en el contenedor de dependencias, no es necesario agregar FluentValidation
, ya viene incluida en ese paquete.
Con dotnet
o editando el csproj agregamos el paquete NuGet:
<PackageReferenceInclude="FluentValidation.DependencyInjectionExtensions"Version="11.3.0"/>
Validando con Endpoint Filters
Los endpoints filters bsicamente son decoradores, que nos permiten agregar comportamiento sobre el endpoint que se ejecutar. Es lo mismo que tenemos con los Action Filters de MVC.
En .NET 7 se agregaron varias funcionalidades que nos permitirn hacer esta tarea ms fcil (Endpoint filters y Endpoint Groups).
Nota : Los Endpoint filters son un poco diferentes, la razn es porque son menos "mgicos" y favorecen el rendimiento, al igual que todo en Minimal APIs (en esencia, buscan ser AOT friendly).
Para poder hacer un mecanismo "automtico" de validacin, vamos a apoyarnos con un atributo que indicar cuando un parmetro de un Endpoint debe de ser validado.
namespaceMinimalAPIFluentValidation.Common.Attributes;[AttributeUsage(AttributeTargets.Parameter,AllowMultiple=false)]publicclassValidateAttribute:Attribute{}
Esta clase solo ser un "identificador", no tendr implementado nada.
Posteriormente crearemos un Endpoint Filter Factory, donde "crearemos" un filter segn se necesite (segn el parmetro a validar).
usingFluentValidation;usingMinimalAPIFluentValidation.Common.Attributes;usingSystem.Net;usingSystem.Reflection;namespaceMinimalAPIFluentValidation.Common;publicstaticclassValidationFilter{///<summary>///FilterFactory//////SienelEndpointactualexiste[Validator]yAbstractValidatorasociados,///secrear un delegate con el"EndpointFilter"///</summary>///<paramname="context"></param>///<paramname="next"></param>///<returns></returns>publicstaticEndpointFilterDelegateValidationFilterFactory(EndpointFilterFactoryContextcontext,EndpointFilterDelegatenext){IEnumerable<ValidationDescriptor>validationDescriptors=GetValidators(context.MethodInfo,context.ApplicationServices);if(validationDescriptors.Any()){returninvocationContext=>Validate(validationDescriptors,invocationContext,next);}//dejarpasarreturninvocationContext=>next(invocationContext);}///<summary>///EndpointFilterquevalidacualquierobjetocon[Validate]ysusAbstractValidator///</summary>///<paramname="validationDescriptors"></param>///<paramname="invocationContext"></param>///<paramname="next"></param>///<returns></returns>privatestaticasyncValueTask<object?>Validate(IEnumerable<ValidationDescriptor>validationDescriptors,EndpointFilterInvocationContextinvocationContext,EndpointFilterDelegatenext){foreach(ValidationDescriptordescriptorinvalidationDescriptors){varargument=invocationContext.Arguments[descriptor.ArgumentIndex];if(argumentisnotnull){varvalidationResult=awaitdescriptor.Validator.ValidateAsync(newValidationContext<object>(argument));if(!validationResult.IsValid){returnResults.ValidationProblem(validationResult.ToDictionary(),statusCode:(int)HttpStatusCode.UnprocessableEntity);}}}returnawaitnext.Invoke(invocationContext);}///<summary>///Buscalosvalidadoresdecualquierclaseenlosparmetros///quetengaelatributo[Validate]///</summary>///<paramname="methodInfo"></param>///<paramname="serviceProvider"></param>///<returns></returns>staticIEnumerable<ValidationDescriptor>GetValidators(MethodInfomethodInfo,IServiceProviderserviceProvider){ParameterInfo[]parameters=methodInfo.GetParameters();for(inti=0;i<parameters.Length;i++){ParameterInfoparameter=parameters[i];if(parameter.GetCustomAttribute<ValidateAttribute>()isnotnull){TypevalidatorType=typeof(IValidator<>).MakeGenericType(parameter.ParameterType);//NotethatFluentValidationvalidatorsneedstoberegisteredassingletonIValidator?validator=serviceProvider.GetService(validatorType)asIValidator;if(validatorisnotnull){yieldreturnnewValidationDescriptor{ArgumentIndex=i,Validator=validator};}}}}privateclassValidationDescriptor{publicrequiredintArgumentIndex{get;init;}publicrequiredIValidatorValidator{get;init;}}}
Lo que sucede aqu:
ValidationFilterFactory
: Este mtodo es el que se asociar con cada endpoint, en esencia, con la presencia de un validador, crear un Endpoint Filter que ejecuta la validacin. Si no hay validadores, sigue con la ejecucin del endpoint sin afectar en nada.Validate
: Segn los validadores que se encontraron (si se encontraron), ejecutar la validacin utilizando FluentValidation. Si existe un error de validacin, regresar unValidationProblem
.GetValidators
: El Filter Factory nos da una descripcin del mtodo (AKA el endpoint) que se va a ejecutar junto con sus parmetros.- Con
GetParameters
se consiguen todos los parmetros del endpoint y buscamos que alguno de estos tenga el atributo[Validate]
, por lo cual significa que tendr unAbstractValidator
asociado, por lo que se buscar validar en el Filter. - Al confirmar que el parmetro tiene el atributo
[Validate]
procedemos a buscar los validadores asociados (registrados Singleton, ya que el Filter Factory se ejecuta de esta forma). - En la presencia de un validador, lo regresamos indicando el ndice de este parmetro (lo necesitaremos ms adelante)
- Con
Nota : Necesitamos el ArgumentIndex ya que la forma de acceder al "DTO" a validar, necesitamos indicar el ndice en donde est colocado en nuestra funcin endpoint, esto es raro, pero por loa misma razn de performance (quiero pensar) se tuvo que hacer as.
Y listo, es lo que necesitamos, ya podemos empezar a validar DTOs o modelos que sean recibidos en los endpoints.
Cmo usar el Factory Filter
Hagamos un ejemplo de validacin, utilizando el ejemplo de siempre. Creacin de un producto:
usingFluentValidation;usingMicrosoft.AspNetCore.Http.HttpResults;usingMinimalAPIFluentValidation.Common.Attributes;namespaceMinimalAPIFluentValidation.Features;publicclassCreateProductCommand{publicdoublePrice{get;set;}publicstringDescription{get;set;}=default!;publicintCategoryId{get;set;}}publicstaticclassCreateProductHandler{publicstaticOkHandler([Validate]CreateProductCommandrequest,ILogger<CreateProductCommand>logger){//TODO:SaveEntity...logger.LogInformation("Saving{0}",request.Description);returnTypedResults.Ok();}}publicclassCreateProductValidator:AbstractValidator<CreateProductCommand>{publicCreateProductValidator(){RuleFor(r=>r.Description).NotEmpty();RuleFor(r=>r.Price).GreaterThan(0);RuleFor(r=>r.CategoryId).GreaterThan(0);}}
Contamos con tres cosas aqu:
- Un DTO (AKA, Comando)
- El Handler del endpoint
- El Validador
El Handler bsicamente es el endpoint, por lo que aqu indicamos que el CreateProductCommand
se tiene que validar.
Nota : Estamos utilizando TypedResults, agregados recientemente en .NET 7
La intencin es crear los Queries y Comandos que necesitemos para este feature "Product" (Revisa el ejemplo Plain de Minimal API Experiments para una mejor referencia) y agregarlos a un Endpoint Group.
Esto para decirle al grupo de endpoints que utilice el Factory Filter que acabamos de crear (entre otras cosas de Swagger).
usingMinimalAPIFluentValidation.Common;namespaceMinimalAPIFluentValidation.Features.Products;publicstaticclassProductsEndpoints{publicstaticRouteGroupBuilderMapProducts(thisWebApplicationapp){vargroup=app.MapGroup("api/products");group.MapPost("/",CreateProductHandler.Handler).WithName("CreateProduct");//otherendpointshere...group.WithTags(newstring[]{"Products"});group.WithOpenApi();group.AddEndpointFilterFactory(ValidationFilter.ValidationFilterFactory);returngroup;}}
La idea de crearlo as, es para no agregar los detalles de cada endpoint, simplemente es el registro de un grupo de endpoints relacionados.
Lo importante es la llamada AddEndpointFilterFactory
, ya que esto ocurre en el grupo de endpoints, esto se aplicar a todo el grupo.
Nota : Ya s ya s, esto se convirti en una especie controller
Nota 2 : La idea de Minimal APIs, es organizarlo como tu gustes, ya que KISS (keep it simple stupid) y YAGNI (You aren't gonna need it).
Nota 3 : No es importante que lo hagas as, lo importante es el uso de FluentValidation combinado con Endpoint Filters, la organizacin de esta forma sigue siendo opinin mia.
Por ltimo, hacemos uso del grupo de endpoints en Program.cs y registramos todos los validadores que puedan existir en el proyecto:
usingFluentValidation;usingMinimalAPIFluentValidation.Features.Products;varbuilder=WebApplication.CreateBuilder(args);builder.Services.AddEndpointsApiExplorer();builder.Services.AddSwaggerGen();builder.Services.AddValidatorsFromAssemblyContaining<Program>(ServiceLifetime.Singleton); // <-- FluentValidationvarapp=builder.Build();if(app.Environment.IsDevelopment()){app.UseSwagger();app.UseSwaggerUI();}app.UseHttpsRedirection();app.MapProducts(); // <--- Groupapp.Run();
Probando la validacin con Swagger
Si corremos ahora, se abrir Swagger:
Y ya puedes confirmar que la validacin est entrando en vigor:
Y con validacin positiva:
Conclusin
Esto puede ser una opcin para validar de forma fcil tus endpoints en Minimal APIs. Me gustara que fuera algo que ya viniera incluido, en realidad no s si hay planes de agregar algo similar en versiones futuras de .NET, pero por ahora, esto puede ser una forma de hacerlo.
De igual forma, en el repositorio anteriormente mencionado, existen modos de validacin utilizando decoradores de MediatR y Fluent Validation, si te interesa eres libre de ver el cdigo y explorar por tu cuenta.
Por ltimo, este tipo de cosas, son de esas que se arreglan descargando algn paquete de NuGet, ya que realmente es algo que ocurre "detrs de cmaras" y no nos afecta en el propsito de la aplicacin que estamos realizando, pero es un feature tcnico que se necesita.
Referencias
- Minimal API Validation with FluentValidation | Khalid Abuhakmeh
- Filters in Minimal API apps | Microsoft Learn
- DamianEdwards/MiniValidation: A minimalist validation library for .NET built atop the existing features in
System.ComponentModel.DataAnnotations
namespace (github.com) - Installation FluentValidation documentation
Original Link: https://dev.to/isaacojeda/net-7-minimal-apis-y-fluentvalidation-18a9
Dev To
An online community for sharing and discovering great ideas, having debates, and making friendsMore About this Source Visit Dev To