如何在 FluentValidation 中重用数据
How to reuse data in FluentValidation
例如我有两个验证规则的验证器:
// Rule 1
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) != 0)
.WithMessage("User with provided Email was not found in database!");
// Rule 2
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with provided Email in database!");
如您所见,有两次使用相同方法调用数据库。如何调用一次并为其他规则重复使用数据?
显示错误消息时的另一个问题:
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with following Email '{0}' in database!",
(model, email) => { return email; });
有没有更好的方法来显示错误消息,而不是总是编写那些 lambda 表达式来检索 属性?就像在某处保存模型然后稍后使用它。
简单易行的方案就好了!
第 1 部分
您想将数据库调用从 2 次减少到 1 次,因此您需要使用字段来保存数据库调用结果,因为验证器规则 code 实际上在 中起作用"runtime"。
验证者class:
public class MyValidator : Validator<UserAccount>
{
private int? _countOfExistingMails;
private string _currentEmail;
private object locker = new object();
public MyValidator()
{
CallEmailValidations();
// other rules...
}
}
这里是邮件验证调用的单独方法。至于 Must
将表达式作为参数,你可以传递方法名称及其参数:
public void CallEmailValidations()
{
RuleFor(o => o.Email).Must(x => EmailValidation(x, 0))
.WithMessage("User with provided Email was not found in database!");
RuleFor(o => o.Email).Must(x => EmailValidation(x, 1))
.WithMessage("There are multiple users with provided Email in database!");
}
和验证方法的主体本身:
public bool EmailValidation(string email, int requiredCount)
{
var isValid = false;
lock(locker)
{
if (email != _currentEmail || _currentEmail == null)
{
_currentEmail = email;
_countOfExistingMails = (int)GetDataDataFromDB(email);
}
if (requiredCount == 0)
{
isValid = _countOfExistingMails != 0; // Rule 1
}
else if (requiredCount == 1)
{
isValid = _countOfExistingMails <= 1; // Rule 2
}
}
// Rule N...
return isValid;
}
更新:
此代码有效,但更好的方法是在数据访问层方法中实现缓存。
第 2 部分
这里是重写的规则:
RuleFor(o => o.Email).Must((email) => GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with following Email '{0}' in database!", m => m.Email)
When the lambda expression only needs a single parameter, and that
parameter can be implicitly typed, C# 3 allows you to omit the
parentheses, so it now has this form
陷阱:
不要显式传递 this
到 lambda 表达式。据我所知,它可能会导致性能问题。没有理由创建额外的闭包。
我想你在 GetDataDataFromDB
方法中以某种形式使用了 DataContext
。所以你必须控制上下文的生命周期,因为验证器对象实例化为单例。
对于#1,恐怕没有办法做到这一点。验证器被设计为无状态的,因此它们可以跨线程重用(事实上,强烈建议您将验证器实例创建为单例,因为它们的实例化成本非常高。MVC 集成默认情况下会这样做)。不要乱用静态字段,因为您会 运行 陷入线程问题。
(编辑:在这种特殊的简单情况下,您可以将规则组合到对 Must 的单个调用中,但通常您不能在规则之间共享状态)
对于 #2,这取决于您使用的 属性 验证器。大多数 属性 验证器实际上允许您使用 {PropertyValue} 占位符,该值将自动插入。但是,在这种情况下,您使用的是不支持占位符的 "Must" 验证器 (PredicateValidator)。
我在此处列出了哪些验证器支持自定义占位符:https://github.com/JeremySkinner/FluentValidation/wiki/c.-Built-In-Validators
在寻找更好的方法时遇到这个问题;)
另一种方法是重写 ValidateAsync
和 Validate
方法并将结果存储在本地字段中,该字段可以按如下规则访问:
public class MyValidator : AbstractValidator<MyCommand>
{
User _user = User.Empty;
public MyValidator()
{
RuleFor(o => o.Email)
.Must((_) => !_user.IsEmpty)
.WithMessage("User with provided Email was not found in database!");
// Rule 2
//other rules which can check _user
}
public override async Task<ValidationResult> ValidateAsync(ValidationContext<MyCommand> context, CancellationToken cancellation = default)
{
var cmd = context.InstanceToValidate;
// you could wrap in a try block if this throws, here I'm assuming empty user
_user = await _repository.GetUser(cmd.Email);
return await base.ValidateAsync(context, cancellation);
}
public override ValidationResult Validate(ValidationContext<SubmitDecisionCommand> context) => ValidateAsync(context).Result;
}
你可以做的是使用 WhenAsync。我创建了一个扩展方法来简化事情。
public static class ValidatorExtensions
{
public static void ResolveDataAsync<TEntity, TData>(
this AbstractValidator<TEntity> validator,
Func<TEntity, CancellationToken, Task<TData>> resolver,
Action<ValueAccessor<TData>> continuation)
{
TData data = default;
var isInitialized = false;
var valueAccessor = new ValueAccessor<TData>(() =>
{
if (!isInitialized)
{
throw new InvalidOperationException("Value is not initialized at this point.");
}
return data;
});
validator.WhenAsync(async (entity, token) =>
{
data = await resolver(entity, token);
return isInitialized = true;
},
() => continuation(valueAccessor));
}
}
public class ValueAccessor<T>
{
private readonly Func<T> _accessor;
public ValueAccessor([NotNull] Func<T> accessor)
{
_accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
}
public T Value => _accessor();
}
用法:
public class ItemCreateCommandValidator : AbstractValidator<ItemCreateCommand>
{
private readonly ICategoryRepository _categoryRepository;
public ItemCreateCommandValidator(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
this.ResolveDataAsync(CategoryResolver, data =>
{
RuleFor(x => x.CategoryIds)
.NotEmpty()
.ForEach(subcategoryRule => subcategoryRule
.Must(x => data.Value.ContainsKey(x))
.WithMessage((_, id) => $"Category with id {id} not found."));
});
}
private Func<ItemCreateCommand, CancellationToken, Task<Dictionary<int, Category>>> CategoryResolver =>
async (command, token) =>
{
var categories = await _categoryRepository.GetByIdsAsync(command.SubcategoryIds, token);
return categories.ToDictionary(x => x.Id);
};
}
对我来说工作正常,但有一些 GOTCHAS:
验证器通常必须定义为 Scoped 或 Transient(Scoped 的性能更好)以便与其依赖项的生命周期兼容(例如在构造函数中传递的存储库)。
您无法在 ResolveDataAsync 回调 中访问 data.Value
。这是因为到那时该值尚未初始化。此时验证器处于创建阶段并且未调用 ValidateAsync 方法 => 无需验证 => 无法访问值。
它只能在 AbstractValidator 方法中使用:
this.ResolveDataAsync(CategoryResolver, data =>
{
var value = data.Value; // Throws InvalidOperationException
RuleFor(x => x.CategoryIds)
.NotEmpty()
.ForEach(subcategoryRule => subcategoryRule
.Must(data.Value.ContainsKey) // Also throws
.WithMessage((_, id) => $"Category with id {id} not found."));
});
这些陷阱也会出现在其他方法中,例如重写 ValidateAsync 方法,您对此无能为力。
您还可以在使用 WhenAsync
、UnlessAsync
时根据条件使用不同的解析器调用 ResolveDataAsync。这将帮助您不要每次都加载在所有情况下都不需要的数据:
WhenAsync(myCondition1, () => this.ResolveDataAsync(myResolver1, data => { ... }))
UnlessAsync(myCondition2, () => this.ResolveDataAsync(myResolver2, data => { ... }))
例如我有两个验证规则的验证器:
// Rule 1
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) != 0)
.WithMessage("User with provided Email was not found in database!");
// Rule 2
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with provided Email in database!");
如您所见,有两次使用相同方法调用数据库。如何调用一次并为其他规则重复使用数据?
显示错误消息时的另一个问题:
RuleFor(o => o.Email).Must((email) => this.GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with following Email '{0}' in database!",
(model, email) => { return email; });
有没有更好的方法来显示错误消息,而不是总是编写那些 lambda 表达式来检索 属性?就像在某处保存模型然后稍后使用它。
简单易行的方案就好了!
第 1 部分
您想将数据库调用从 2 次减少到 1 次,因此您需要使用字段来保存数据库调用结果,因为验证器规则 code 实际上在 中起作用"runtime"。
验证者class:
public class MyValidator : Validator<UserAccount>
{
private int? _countOfExistingMails;
private string _currentEmail;
private object locker = new object();
public MyValidator()
{
CallEmailValidations();
// other rules...
}
}
这里是邮件验证调用的单独方法。至于 Must
将表达式作为参数,你可以传递方法名称及其参数:
public void CallEmailValidations()
{
RuleFor(o => o.Email).Must(x => EmailValidation(x, 0))
.WithMessage("User with provided Email was not found in database!");
RuleFor(o => o.Email).Must(x => EmailValidation(x, 1))
.WithMessage("There are multiple users with provided Email in database!");
}
和验证方法的主体本身:
public bool EmailValidation(string email, int requiredCount)
{
var isValid = false;
lock(locker)
{
if (email != _currentEmail || _currentEmail == null)
{
_currentEmail = email;
_countOfExistingMails = (int)GetDataDataFromDB(email);
}
if (requiredCount == 0)
{
isValid = _countOfExistingMails != 0; // Rule 1
}
else if (requiredCount == 1)
{
isValid = _countOfExistingMails <= 1; // Rule 2
}
}
// Rule N...
return isValid;
}
更新: 此代码有效,但更好的方法是在数据访问层方法中实现缓存。
第 2 部分
这里是重写的规则:
RuleFor(o => o.Email).Must((email) => GetDataDataFromDB(email) >= 1)
.WithMessage("There are multiple users with following Email '{0}' in database!", m => m.Email)
When the lambda expression only needs a single parameter, and that parameter can be implicitly typed, C# 3 allows you to omit the parentheses, so it now has this form
陷阱:
不要显式传递
this
到 lambda 表达式。据我所知,它可能会导致性能问题。没有理由创建额外的闭包。我想你在
GetDataDataFromDB
方法中以某种形式使用了DataContext
。所以你必须控制上下文的生命周期,因为验证器对象实例化为单例。
对于#1,恐怕没有办法做到这一点。验证器被设计为无状态的,因此它们可以跨线程重用(事实上,强烈建议您将验证器实例创建为单例,因为它们的实例化成本非常高。MVC 集成默认情况下会这样做)。不要乱用静态字段,因为您会 运行 陷入线程问题。
(编辑:在这种特殊的简单情况下,您可以将规则组合到对 Must 的单个调用中,但通常您不能在规则之间共享状态)
对于 #2,这取决于您使用的 属性 验证器。大多数 属性 验证器实际上允许您使用 {PropertyValue} 占位符,该值将自动插入。但是,在这种情况下,您使用的是不支持占位符的 "Must" 验证器 (PredicateValidator)。
我在此处列出了哪些验证器支持自定义占位符:https://github.com/JeremySkinner/FluentValidation/wiki/c.-Built-In-Validators
在寻找更好的方法时遇到这个问题;)
另一种方法是重写 ValidateAsync
和 Validate
方法并将结果存储在本地字段中,该字段可以按如下规则访问:
public class MyValidator : AbstractValidator<MyCommand>
{
User _user = User.Empty;
public MyValidator()
{
RuleFor(o => o.Email)
.Must((_) => !_user.IsEmpty)
.WithMessage("User with provided Email was not found in database!");
// Rule 2
//other rules which can check _user
}
public override async Task<ValidationResult> ValidateAsync(ValidationContext<MyCommand> context, CancellationToken cancellation = default)
{
var cmd = context.InstanceToValidate;
// you could wrap in a try block if this throws, here I'm assuming empty user
_user = await _repository.GetUser(cmd.Email);
return await base.ValidateAsync(context, cancellation);
}
public override ValidationResult Validate(ValidationContext<SubmitDecisionCommand> context) => ValidateAsync(context).Result;
}
你可以做的是使用 WhenAsync。我创建了一个扩展方法来简化事情。
public static class ValidatorExtensions
{
public static void ResolveDataAsync<TEntity, TData>(
this AbstractValidator<TEntity> validator,
Func<TEntity, CancellationToken, Task<TData>> resolver,
Action<ValueAccessor<TData>> continuation)
{
TData data = default;
var isInitialized = false;
var valueAccessor = new ValueAccessor<TData>(() =>
{
if (!isInitialized)
{
throw new InvalidOperationException("Value is not initialized at this point.");
}
return data;
});
validator.WhenAsync(async (entity, token) =>
{
data = await resolver(entity, token);
return isInitialized = true;
},
() => continuation(valueAccessor));
}
}
public class ValueAccessor<T>
{
private readonly Func<T> _accessor;
public ValueAccessor([NotNull] Func<T> accessor)
{
_accessor = accessor ?? throw new ArgumentNullException(nameof(accessor));
}
public T Value => _accessor();
}
用法:
public class ItemCreateCommandValidator : AbstractValidator<ItemCreateCommand>
{
private readonly ICategoryRepository _categoryRepository;
public ItemCreateCommandValidator(ICategoryRepository categoryRepository)
{
_categoryRepository = categoryRepository;
this.ResolveDataAsync(CategoryResolver, data =>
{
RuleFor(x => x.CategoryIds)
.NotEmpty()
.ForEach(subcategoryRule => subcategoryRule
.Must(x => data.Value.ContainsKey(x))
.WithMessage((_, id) => $"Category with id {id} not found."));
});
}
private Func<ItemCreateCommand, CancellationToken, Task<Dictionary<int, Category>>> CategoryResolver =>
async (command, token) =>
{
var categories = await _categoryRepository.GetByIdsAsync(command.SubcategoryIds, token);
return categories.ToDictionary(x => x.Id);
};
}
对我来说工作正常,但有一些 GOTCHAS:
验证器通常必须定义为 Scoped 或 Transient(Scoped 的性能更好)以便与其依赖项的生命周期兼容(例如在构造函数中传递的存储库)。
您无法在 ResolveDataAsync 回调 中访问
data.Value
。这是因为到那时该值尚未初始化。此时验证器处于创建阶段并且未调用 ValidateAsync 方法 => 无需验证 => 无法访问值。
它只能在 AbstractValidator 方法中使用:
this.ResolveDataAsync(CategoryResolver, data =>
{
var value = data.Value; // Throws InvalidOperationException
RuleFor(x => x.CategoryIds)
.NotEmpty()
.ForEach(subcategoryRule => subcategoryRule
.Must(data.Value.ContainsKey) // Also throws
.WithMessage((_, id) => $"Category with id {id} not found."));
});
这些陷阱也会出现在其他方法中,例如重写 ValidateAsync 方法,您对此无能为力。
您还可以在使用 WhenAsync
、UnlessAsync
时根据条件使用不同的解析器调用 ResolveDataAsync。这将帮助您不要每次都加载在所有情况下都不需要的数据:
WhenAsync(myCondition1, () => this.ResolveDataAsync(myResolver1, data => { ... }))
UnlessAsync(myCondition2, () => this.ResolveDataAsync(myResolver2, data => { ... }))