Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
December 30, 2022 11:39 pm GMT

.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 un ValidationProblem.
  • 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 un AbstractValidator 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)

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:

Image description

Y ya puedes confirmar que la validacin est entrando en vigor:

Image description

Y con validacin positiva:

Image description

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


Original Link: https://dev.to/isaacojeda/net-7-minimal-apis-y-fluentvalidation-18a9

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To