Para mi aplicación de servicio de trabajador .Net 5, quiero validar las opciones implementando la interfaz IValidateOptions pero no quiero escribir mis propios mensajes de error. Es por eso que quiero hacer uso del paquete FluentValidation.AspNetCore .
Dado el modelo
namespace App.Models { public class MyOptions { public string Text { get; set; } } }Agregué reglas de validación.
namespace App.ModelValidators { public class MyOptionsValidator : AbstractValidator<MyOptions> { public MyOptionsValidator() { RuleFor(model => model.Text).NotEmpty(); } } }A continuación, estoy usando este validador dentro de la validación.
namespace App.OptionsValidators { public class MyOptionsValidator : IValidateOptions<MyOptions> { private readonly IValidator<MyOptions> _validator; public MyOptionsValidator(IValidator<MyOptions> validator) { _validator = validator; } public ValidateOptionsResult Validate(string name, MyOptions options) { var validationResult = _validator.Validate(options); if (validationResult.IsValid) { return ValidateOptionsResult.Success; } return ValidateOptionsResult.Fail(validationResult.Errors.Select(validationFailure => validationFailure.ErrorMessage)); } } }Por último, configuré el contenedor DI
services.AddTransient<IValidator<MyOptions>, ModelValidators.MyOptionsValidator>(); services.AddSingleton<IValidateOptions<MyOptions>, OptionsValidators.MyOptionsValidator>(); services.Configure<MyOptions>(configuration.GetSection("My"));Quisiera saber si esto se puede simplificar.
¿Tal vez pueda implementar la interfaz IValidateOptions , evitar el AbstractValidator y escribir reglas fluidas dentro del método .Validate() ?
Ejemplo de código de lo que quiero lograr
namespace App.OptionsValidators { public class MyOptionsValidator : IValidateOptions<MyOptions> { public ValidateOptionsResult Validate(string name, MyOptions options) { var validationResult = options.Text.Should.Not.Be.Empty(); if (validationResult.IsValid) { return ValidateOptionsResult.Success; } return ValidateOptionsResult.Fail(validationResult.ErrorMessage); } } } así que ya no necesito AbstractValidator<MyOptions> .
Mi primer acercamiento no estoy seguro
En lugar de usar FluentValidation, estoy usando DataAnnotations.
[Required] a la propiedad TextMyOptionsValidator : AbstractValidator<MyOptions>.
services.AddSingleton<IValidateOptions<MyOptions>, OptionsValidators.MyOptionsValidator>(); services.Configure<MyOptions>(configuration.GetSection("My")); Dentro MyOptionsValidator valido las opciones así
public ValidateOptionsResult Validate(string name, MyOptions options) { var validationResults = new List<ValidationResult>(); if (!Validator.TryValidateObject(options, new ValidationContext(options), validationResults, true)) { return ValidateOptionsResult.Fail(validationResults.Select(validationResult => validationResult.ErrorMessage)); } return ValidateOptionsResult.Success; }pero tal vez todavía hay mejores maneras :)
Puedo simplificar usando System.ComponentModel.DataAnnotations
public class MyOptions { [Required] public string Text { get; set; } }luego puede personalizar su BadRequest a través de la canalización
services.AddControllers() .ConfigureApiBehaviorOptions(opts => { opts.InvalidModelStateResponseFactory = context => { var problemDetails = new ViolationProblemDetails() { Instance = context.HttpContext.Request.Path, Status = StatusCodes.Status400BadRequest, Detail = "Please refer to the errors property for additional details." }; problemDetails.Violations = new List<Violation>(); foreach (var modelState in context.ModelState) { problemDetails.Violations.Add(new Violation() { Field = modelState.Key, Message = string.Join(",",modelState.Value.Errors.Select(a => a.ErrorMessage)) }); } return new BadRequestObjectResult(problemDetails); }; })más información https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0
Encontré el siguiente código en este artículo https://dejanstojanovic.net/aspnet/2020/april/validate-configurations-with-fluentvalidation-in-aspnet-core/
public void ConfigureServices(IServiceCollection services) { services.Configure<EndpointsConfiguration>(Configuration.GetSection(nameof(EndpointsConfiguration))); services.AddSingleton<AbstractValidator<EndpointsConfiguration>, EndpointsConfigurationValidator>(); services.AddSingleton<EndpointsConfiguration>(container => { var config = container.GetService<IOptions<EndpointsConfiguration>>().Value; var validator = container.GetService<AbstractValidator<EndpointsConfiguration>>(); validator.Validate(config); return config; }); services.AddControllers(); } De esta manera, puede tener toda su validación en la implementación de AbstractValidator y será más simple que su implementación actual. Al usar esto, no es necesario implementar IOptionsValidator , supongo. El artículo tiene una buena explicación, así que consulte para obtener más información.
Tengo una fuerte preferencia por utilizar el mismo enfoque para la validación en toda la pila y, en mi caso, esto es a través de FluentValidation. El siguiente sería mi enfoque.
Cree un nuevo validador base para sus validadores de opciones/configuraciones:
public abstract class AbstractOptionsValidator<T> : AbstractValidator<T>, IValidateOptions<T> where T : class { public virtual ValidateOptionsResult Validate(string name, T options) { var validateResult = this.Validate(options); return validateResult.IsValid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(validateResult.Errors.Select(x => x.ErrorMessage)); } } Esto amplía FluentValidation AbstractValidator<T> para admitir IValidateOptions<T> . Ahora tiene una base que puede usar para todos sus validadores de opciones/configuraciones. Para los siguientes ajustes:
public class FooSettings { public string Bar { get; set; } }terminas con un validador típico:
public class FooSettingsValidator : AbstractOptionsValidator<FooSettings>, IFooSettingsValidator { public FooSettingsValidator() { RuleFor(x => x.Bar).NotEmpty(); } }Hágale saber al contenedor DI:
serviceCollection.AddSingleton<IValidateOptions<FooSettings>, FooSettingsValidator>();Si no hay nada incorporado para hacer lo anterior, miraría a Scrutor para convertir esto en un proceso automático.
Todo lo anterior me brinda todos los beneficios de usar FluentValidation, mientras utilizo el soporte de validación de opciones de primera clase que Microsoft nos ha brindado.
Ejemplo de trabajo de LINQPad:
using static FluentAssertions.FluentActions; void Main() { var fixture = new Fixture(); var validator = new FooSettingsValidator(); validator.Validate(fixture.Build<FooSettings>().Without(x => x.Bar).Create()).Errors.Select(x => x.ErrorMessage).Should().BeEquivalentTo(new string[] { "'Bar' must not be empty." }); validator.Validate(fixture.Build<FooSettings>().Create()).Errors.Select(x => x.ErrorMessage).Should().BeEquivalentTo(new string[] { }); using (var scope = ServiceProvider.Create(bar: null).CreateScope()) { Invoking(() => scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FooSettings>>().Value).Should().Throw<OptionsValidationException>(); } using (var scope = ServiceProvider.Create(bar: "asdf").CreateScope()) { scope.ServiceProvider.GetRequiredService<IOptionsSnapshot<FooSettings>>().Value.Bar.Should().Be("asdf"); } } // You can define other methods, fields, classes and namespaces here public class FooSettings { public string Bar { get; set; } } public interface IFooSettingsValidator : IValidator { } public class FooSettingsValidator : AbstractOptionsValidator<FooSettings>, IFooSettingsValidator { public FooSettingsValidator() { RuleFor(x => x.Bar).NotEmpty(); } } public abstract class AbstractOptionsValidator<T> : AbstractValidator<T>, IValidateOptions<T> where T : class { public virtual ValidateOptionsResult Validate(string name, T options) { var validateResult = this.Validate(options); return validateResult.IsValid ? ValidateOptionsResult.Success : ValidateOptionsResult.Fail(validateResult.Errors.Select(x => x.ErrorMessage)); } } public class ServiceProvider { public static IServiceProvider Create(string bar) { var serviceCollection = new ServiceCollection(); var config = new ConfigurationBuilder() .AddInMemoryCollection( new List<KeyValuePair<string, string>> { new KeyValuePair<string, string>("Foo:Bar", bar) }) .Build(); serviceCollection.AddSingleton<IConfiguration>(config); serviceCollection.AddOptions(); //serviceCollection.Configure<FooSettings>(config.GetSection("Foo")); serviceCollection.AddOptions<FooSettings>() .Bind(config.GetSection("Foo")); serviceCollection.AddSingleton<IValidateOptions<FooSettings>, FooSettingsValidator>(); serviceCollection.AddSingleton<IFooSettingsValidator, FooSettingsValidator>(); return serviceCollection.BuildServiceProvider(); } }