我正在尝试找出将数据验证放在新应用程序中的位置。
该应用程序由 Blazor UI(主要由简单的表单组成)和一些控制器组成。 可以通过 Blazor UI 和控制器完成相同的操作,它们都调用相同的服务。
它们不共享相同的模型类。 在控制器端,我期望一个“xxxDto”类,在 blazor 端,我使用“xxxModel”类,它们都自动映射到“xxxInput”类,这是我的服务期望的类。
DTO 和 Model 略有不同(模型有默认值,部分值被拆分以便于输入)
我的问题是知道我应该将数据验证放在哪里(使用 FluentValidation)。 我认为在 Dto 和 Model 类上重复验证不是一个好主意。 在 Input 类上执行验证,并在 Model 和 Dto 类上添加一些“代理”验证是一个好习惯吗? (我正在考虑做一个验证器,它将值自动映射到输入类,然后使用该验证器。它会起作用吗?)
现在,我只有模型验证,我正在尝试添加控制器层。
当您有不同的类(Model 和 Dto)但需要在将相同的数据传递给服务之前对其进行验证时,最好集中验证逻辑以避免重复。使用输入类进行验证然后映射到模型和 Dto 的方法听起来很合理。这是建议的方法:
第1步:创建输入类: 定义一个单独的输入类,其中包含您的服务所需的数据的组合和扁平表示。这将是您在将其传递给服务之前验证的类。
public class MyEntityInput
{
public string Name { get; set; }
// Other properties with flattened structure
}
第 2 步:在输入类上使用 FluentValidation: 验证器将包含您的验证规则。
public class MyEntityInputValidator : AbstractValidator<MyEntityInput>
{
public MyEntityInputValidator()
{
RuleFor(x => x.Name).NotEmpty().MaximumLength(50);
// Other validation rules
}
}
第3步:模型和Dto的代理验证: 创建一个方法,该方法采用 Model 或 Dto 的实例,将其映射到 Input 类,然后使用 MyEntityInputValidator 进行验证。您可以在 Blazor UI 和控制器中执行操作之前使用此方法。
public class MyValidationService
{
private readonly IValidator<MyEntityInput> _inputValidator;
public MyValidationService(IValidator<MyEntityInput> inputValidator)
{
_inputValidator = inputValidator;
}
public ValidationResult ValidateAndMapToInput(MyEntityModel model)
{
var input = Mapper.Map<MyEntityInput>(model);
return _inputValidator.Validate(input);
}
public ValidationResult ValidateAndMapToInput(MyEntityDto dto)
{
var input = Mapper.Map<MyEntityInput>(dto);
return _inputValidator.Validate(input);
}
}
第 4 步:Blazor UI 和控制器中的使用: 在 Blazor UI 和控制器中调用服务之前,请使用“ValidateAndMapToInput”方法验证数据。
public class MyBlazorComponent : ComponentBase
{
[Inject]
public MyValidationService ValidationService { get; set; }
public async Task HandleSubmit(MyEntityModel model)
{
var validationResult = ValidationService.ValidateAndMapToInput(model);
if (validationResult.IsValid)
{
// Call the service
}
else
{
// Handle validation errors in the UI
}
}
}
[ApiController]
[Route("api/[controller]")]
public class MyController : ControllerBase
{
private readonly MyValidationService _validationService;
public MyController(MyValidationService validationService)
{
_validationService = validationService;
}
[HttpPost]
public IActionResult Post([FromBody] MyEntityDto dto)
{
var validationResult = _validationService.ValidateAndMapToInput(dto);
if (validationResult.IsValid)
{
// Call the service
}
else
{
// Return validation errors
return BadRequest(validationResult.Errors);
}
}
}
在上述所有步骤中,您将验证逻辑集中在输入类中并避免重复。 MyValidationService 封装了验证和映射逻辑,使其易于在 Blazor UI 和控制器中使用。
我也想要一个 AbstractValidator(来自 FluentValidator)来验证我的 DTO/模型。这是最困难的部分,我最终得到了这个(我不建议盲目复制/粘贴,但它适用于我的情况)
它为 DTO/模型创建一个验证器,其中包含对输入验证器的引用。验证 DTO/模型的工作方式如下:
我还使用了抽象方法,因此实现可以将输入中的字段名称映射到 DTO/模型
public abstract class MappedValidator<T, TMappedValidator, TMapped> : AbstractValidator<T>
where TMappedValidator : AbstractValidator<TMapped>
{
private readonly IMapper _mapper;
private readonly TMappedValidator _chainedValidator;
public MappedValidator(IMapper mapper, TMappedValidator chainedValidator)
{
_mapper = mapper;
_chainedValidator = chainedValidator;
}
public abstract string MapPropertyName(string propertyName);
public override ValidationResult Validate(ValidationContext<T> context)
{
// Validate against the current validator
var validationResult = base.Validate(context);
if (!validationResult.IsValid)
{
return validationResult;
}
TMapped? mappedModel = _mapper.Map<TMapped>(context.InstanceToValidate);
if (context.Selector is MemberNameValidatorSelector selector)
{
// If we are valid against the current type, make sure the linked validator is also correct
string[] memberNames = selector.MemberNames.Select(MapPropertyName).ToArray();
validationResult = _chainedValidator.Validate(ValidationContext<TMapped>.CreateWithOptions(mappedModel, x => x.IncludeProperties(memberNames)));
validationResult.Errors.ForEach(e => { e.PropertyName = memberNames.FirstOrDefault(); });
}
else
{
validationResult = _chainedValidator.Validate(mappedModel);
}
return validationResult;
}
public override async Task<ValidationResult> ValidateAsync(ValidationContext<T> context, CancellationToken cancellation = default)
{
// Validate against the current validator
var validationResult = await base.ValidateAsync(context, cancellation);
if (!validationResult.IsValid)
{
return validationResult;
}
TMapped? mappedModel = _mapper.Map<TMapped>(context.InstanceToValidate);
if (context.Selector is MemberNameValidatorSelector selector)
{
// If we are valid against the current type, make sure the linked validator is also correct
string[] memberNames = selector.MemberNames.Select(MapPropertyName).ToArray();
validationResult = await _chainedValidator.ValidateAsync(ValidationContext<TMapped>.CreateWithOptions(mappedModel, x => x.IncludeProperties(memberNames)), cancellation);
validationResult.Errors.ForEach(e => { e.PropertyName = memberNames.FirstOrDefault(); });
}
else
{
validationResult = await _chainedValidator.ValidateAsync(mappedModel, cancellation);
}
return validationResult;
}
}
实现如下所示(在本例中,属性 StartDateUtc 映射到自动映射器配置文件中的属性 StartDate)
public class Validator : MappedValidator<GenerationModel, GenerationInputValidator, GenerationInput>
{
public Validator(IMapper mapper, GenerationInputValidator baseValidator)
: base(mapper, baseValidator)
{
}
public override string MapPropertyName(string propertyName)
=> propertyName switch
{
nameof(StartDateUtc) => nameof(GenerationInput.StartDate),
nameof(EndDateUtc) => nameof(GenerationInput.EndDate),
_ => propertyName
};
}
我希望这可以帮助其他人