如何使用 FluentValidation 在 IValidateOptions 中编写流畅的验证规则?
How to write fluent validation rules inside IValidateOptions using FluentValidation?
对于我的 .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<MyOptions>
。
我不确定的第一种方法
我没有使用 FluentValidation,而是使用了 DataAnnotations。
- 我将
[Required]
属性添加到 Text
属性
- 我完全删除了 class
MyOptionsValidator : AbstractValidator<MyOptions>
- 我只在 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;
}
但也许还有更好的方法:)
我可以使用 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
我在本文中找到了以下代码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
我猜。这篇文章有很好的解释,所以请参考以获得更多见解。
我非常喜欢在整个堆栈中使用相同的方法进行验证,在我的例子中,这是通过 FluentValidation。以下是我的方法。
为您的 options/settings 个验证器创建一个新的基础验证器:
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<T>
以支持 IValidateOptions<T>
。您现在已经有了一个可用于所有 options/settings 验证器的基础。对于以下设置:
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 为我们提供的第一个 class 选项验证支持。
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();
}
}
对于我的 .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<MyOptions>
。
我不确定的第一种方法
我没有使用 FluentValidation,而是使用了 DataAnnotations。
- 我将
[Required]
属性添加到Text
属性 - 我完全删除了 class
MyOptionsValidator : AbstractValidator<MyOptions>
- 我只在 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;
}
但也许还有更好的方法:)
我可以使用 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
我在本文中找到了以下代码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
我猜。这篇文章有很好的解释,所以请参考以获得更多见解。
我非常喜欢在整个堆栈中使用相同的方法进行验证,在我的例子中,这是通过 FluentValidation。以下是我的方法。
为您的 options/settings 个验证器创建一个新的基础验证器:
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<T>
以支持 IValidateOptions<T>
。您现在已经有了一个可用于所有 options/settings 验证器的基础。对于以下设置:
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 为我们提供的第一个 class 选项验证支持。
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();
}
}