【问题标题】:How to write fluent validation rules inside IValidateOptions using FluentValidation?如何使用 FluentValidation 在 IValidateOptions 中编写流畅的验证规则?
【发布时间】:2022-01-21 05:35:57
【问题描述】:

对于我的 .Net 5 workerservice 应用,我想通过实现 IValidateOptions 接口来验证选项,但不想编写自己的错误消息。这就是我想使用 FluentValidation.AspNetCore 包的原因。

给定模型

namespace App.Models
{
    public class MyOptions
    {
        public string Text { get; set; }
    }
}

我添加了验证规则

namespace App.ModelValidators
{
    public class MyOptionsValidator : AbstractValidator<MyOptions>
    {
        public MyOptionsValidator()
        {
            RuleFor(model => model.Text).NotEmpty();
        }
    }
}

接下来我在验证中使用这个验证器

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));
        }
    }
}

最后我设置了 DI 容器

services.AddTransient<IValidator<MyOptions>, ModelValidators.MyOptionsValidator>();
services.AddSingleton<IValidateOptions<MyOptions>, OptionsValidators.MyOptionsValidator>();
services.Configure<MyOptions>(configuration.GetSection("My"));

我想知道这是否可以简化?

也许我可以只实现IValidateOptions 接口,避免AbstractValidator 并在.Validate() 方法中编写流畅的规则?

我想要实现的示例代码

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);
        }
    }
}

所以我不再需要AbstractValidator&lt;MyOptions&gt;


我不确定我的第一种方法

我没有使用 FluentValidation,而是使用 DataAnnotations。

  • 我将[Required] 属性添加到Text 属性中
  • 我完全删除了课程MyOptionsValidator : AbstractValidator&lt;MyOptions&gt;
  • 我只在 DI 设置中注册这个

.

services.AddSingleton<IValidateOptions<MyOptions>, OptionsValidators.MyOptionsValidator>();
services.Configure<MyOptions>(configuration.GetSection("My"));

MyOptionsValidator 内部,我像这样验证选项

    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;
    }

但也许还有更好的方法:)

【问题讨论】:

  • 当然可以,但是会破坏目的:“不想写我自己的错误信息”,不仅仅是信息,整个验证
  • @Eldar 请问您将如何解决这个问题?
  • 我会坚持你当前的实现。删除验证器会给您带来验证模型和创建验证结果的负担。
  • @Eldar 你觉得我的方法怎么样?这将消除一些东西
  • 我仍然采用第一种方法。您删除了一个验证器,而现在您正在使用另一个框架进行验证。随着应用程序变得越来越复杂,使用 2 个验证框架可能会变得很麻烦。

标签: c# .net .net-core .net-5 fluentvalidation


【解决方案1】:

我非常喜欢在整个堆栈中使用相同的方法进行验证,在我的例子中,这是通过 FluentValidation。以下是我的方法。

为您的选项/设置验证器创建一个新的基础验证器:

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));
    }
}

这扩展了 FluentValidation AbstractValidator&lt;T&gt; 以支持 IValidateOptions&lt;T&gt;。你现在有了一个可以用于所有选项/设置验证器的基础。对于以下设置:

public class FooSettings
{
    public string Bar { get; set; }
}

你最终得到了一个典型的验证器:

public class FooSettingsValidator : AbstractOptionsValidator<FooSettings>, IFooSettingsValidator
{
    public FooSettingsValidator()
    {
        RuleFor(x => x.Bar).NotEmpty();
    }
}

让 DI 容器知道:

serviceCollection.AddSingleton<IValidateOptions<FooSettings>, FooSettingsValidator>();

如果没有任何内置功能可以执行上述操作,我希望 Scrutor 将其转变为自动过程。

以上所有内容为我提供了使用 FluentValidation 的所有好处,同时利用了 Microsoft 为我们提供的一流选项验证支持。

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();
    }
}

【讨论】:

    【解决方案2】:

    我在这篇文章中找到了下面的代码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();  
    }  
    

    这样您就可以在AbstractValidator 的实现中进行所有验证,并且它会比您当前的实现更简单。使用这个你不需要实现IOptionsValidator 我猜。这篇文章有很好的解释,所以请参考以获得更多见解。

    【讨论】:

    • 谢谢。但是如果.Validate() 有一些错误怎么办?我不太了解默认行为
    • 同一篇文章这样说,Once application is started, validation is triggered on the pipeline initialization and ValidationException is thrown from validation rule(in case validation rule fails) configured in EndpointsConfigurationValidator class.
    【解决方案3】:

    我可以简化使用System.ComponentModel.DataAnnotations

     public class MyOptions
     {
          [Required]
          public string Text { get; set; }
     }
    

    然后您可以通过管道自定义您的 BadRequest

     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);
                        };
                    })
    

    更多信息 https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation?view=aspnetcore-5.0

    【讨论】:

    • 1.对不起,这不是一个 Web API 项目,2. 改用.ValidateDataAnnotations() 不是更方便吗?
    猜你喜欢
    • 1970-01-01
    • 2021-02-08
    • 1970-01-01
    • 2018-06-18
    • 2019-12-24
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多