我的团队在自定义验证代码上投入了大量资金,该代码下面使用 DataAnnotations 进行验证。具体来说,我们的自定义验证器(通过大量抽象)取决于 ValidationAttribute.IsValid 方法以及传递给它的 ValidationContext 参数本身就是一个 IServiceProvider 的事实。这对我们在 MVC 中很有用。
我们目前正在将服务器端 Blazor 集成到现有的 MVC 应用程序中,该应用程序已经通过我们的自定义验证实现了许多验证器(全部基于 DataAnnotations),并且我们希望在 Blazor 验证中利用这些验证器。尽管“你不应该这样做”的论点可能是有效的,但如果不进行重大重构,我们就远远超出了该选择。
因此,我进行了更深入的研究,发现我们可以对位于此处的 Microsoft DataAnnotationsValidator.cs 类型进行相对较小的更改。https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/DataAnnotationsValidator.cs https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/DataAnnotationsValidator.cs
真正的变化实际上是位于此处的 EditContextDataAnnotationsExtensions.cs 类型:https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs
具体来说,EditContextDataAnnotationsExtensions 方法实际上创建一个新的 ValidationContext 对象,但不会初始化服务提供者。我创建了一个 CustomValidator 组件来替换 DataAnnotationsValidator 组件并复制了大部分流程(我更改了代码以更适合我们的风格,但流程是相同的)。
在我们的 CustomValidator 中,我包含了 ValidationContext 服务提供者的初始化。
var validationContext = new ValidationContext(editContext.Model);
validationContext.InitializeServiceProvider(type => this.serviceProvider.GetService(type));
这是我的代码,稍作编辑,但以下内容应该可以开箱即用。
public class CustomValidator : ComponentBase, IDisposable
{
private static readonly ConcurrentDictionary<(Type ModelType, string FieldName), PropertyInfo> PropertyInfoCache = new ConcurrentDictionary<(Type, string), PropertyInfo>();
[CascadingParameter] EditContext CurrentEditContext { get; set; }
[Inject] private IServiceProvider serviceProvider { get; set; }
private ValidationMessageStore messages;
protected override void OnInitialized()
{
if (CurrentEditContext == null)
{
throw new InvalidOperationException($"{nameof(CustomValidator)} requires a cascading " +
$"parameter of type {nameof(EditContext)}. For example, you can use {nameof(CustomValidator)} " + "inside an EditForm.");
}
this.messages = new ValidationMessageStore(CurrentEditContext);
// Perform object-level validation on request
CurrentEditContext.OnValidationRequested += validateModel;
// Perform per-field validation on each field edit
CurrentEditContext.OnFieldChanged += validateField;
}
private void validateModel(object sender, ValidationRequestedEventArgs e)
{
var editContext = (EditContext) sender;
var validationContext = new ValidationContext(editContext.Model);
validationContext.InitializeServiceProvider(type => this.serviceProvider.GetService(type));
var validationResults = new List<ValidationResult>();
Validator.TryValidateObject(editContext.Model, validationContext, validationResults, true);
// Transfer results to the ValidationMessageStore
messages.Clear();
foreach (var validationResult in validationResults)
{
if (!validationResult.MemberNames.Any())
{
messages.Add(new FieldIdentifier(editContext.Model, fieldName: string.Empty), validationResult.ErrorMessage);
continue;
}
foreach (var memberName in validationResult.MemberNames)
{
messages.Add(editContext.Field(memberName), validationResult.ErrorMessage);
}
}
editContext.NotifyValidationStateChanged();
}
private void validateField(object? sender, FieldChangedEventArgs e)
{
if (!TryGetValidatableProperty(e.FieldIdentifier, out var propertyInfo)) return;
var propertyValue = propertyInfo.GetValue(e.FieldIdentifier.Model);
var validationContext = new ValidationContext(CurrentEditContext.Model) {MemberName = propertyInfo.Name};
validationContext.InitializeServiceProvider(type => this.serviceProvider.GetService(type));
var results = new List<ValidationResult>();
Validator.TryValidateProperty(propertyValue, validationContext, results);
messages.Clear(e.FieldIdentifier);
messages.Add(e.FieldIdentifier, results.Select(result => result.ErrorMessage));
// We have to notify even if there were no messages before and are still no messages now,
// because the "state" that changed might be the completion of some async validation task
CurrentEditContext.NotifyValidationStateChanged();
}
private static bool TryGetValidatableProperty(in FieldIdentifier fieldIdentifier, [NotNullWhen(true)] out PropertyInfo propertyInfo)
{
var cacheKey = (ModelType: fieldIdentifier.Model.GetType(), fieldIdentifier.FieldName);
if (PropertyInfoCache.TryGetValue(cacheKey, out propertyInfo)) return true;
// DataAnnotations only validates public properties, so that's all we'll look for
// If we can't find it, cache 'null' so we don't have to try again next time
propertyInfo = cacheKey.ModelType.GetProperty(cacheKey.FieldName);
// No need to lock, because it doesn't matter if we write the same value twice
PropertyInfoCache[cacheKey] = propertyInfo;
return propertyInfo != null;
}
public void Dispose()
{
if (CurrentEditContext == null) return;
CurrentEditContext.OnValidationRequested -= validateModel;
CurrentEditContext.OnFieldChanged -= validateField;
}
}
添加此类型后所需要做的就是在 blazor/razor 文件中使用它而不是 DataAnnotationsValidator。
所以代替这个:
<DataAnnotationsValidator />
do this:
<CustomValidator />