我们在.NET Core 3.1.5上,这是一个Blazor Server应用程序。
我们有一个验证属性,需要访问外部服务来验证对象。
ValidationAttribute 具有 IsValid 方法:
受保护的覆盖 ValidationResult IsValid(对象值,ValidationContext validationContext( ValidationContext 有一个 GetService 方法,该方法委托给 ServiceProvider 的实例。 不幸的是,服务提供商字段从未初始化,因此我们无法检索任何服务。
这在 Mvc 中被提出(并修复(:aspnet/Mvc#6346 但是我们的验证器是通过以下两个之一调用的:
https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L47 https://github.com/dotnet/aspnetcore/blob/master/src/Components/Forms/src/EditContextDataAnnotationsExtensions.cs#L75 后来在堆栈中也永远不会设置服务提供商。 我犹豫要不要打开一个错误(但可以这样做(,但这对我来说似乎是错误的(或者至少应该记录下来(。
任何谷歌搜索最终都会在这篇博客文章中结束,但正如我刚刚提到的,这是行不通的。
所以我们的问题是:将服务注入 ValidationAttribute 的正确方法是什么,或者更一般地说,验证需要调用外部服务的模型字段的正确方法是什么?
在statup.cs
:
services.AddTransient<IMarktTypDaten, MarktTypDaten>();
我们尝试注入服务并应用验证的类。
public class MarktTypNameValidation : ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var service = (IMarktTypDaten) validationContext.GetRequiredService(typeof(IMarktTypDaten));
...some code...
return ValidationResult.Success;
}
}
调用GetRequiredService
时异常消息:'No service for type 'DataAccessLibrary.Interfaces.IMarktTypDaten' has been registered.
它也发布在Github上:https://github.com/dotnet/aspnetcore/discussions/23305
另外:我在15年左右的时间里第一次使用C#/.NET,请保持温和;-(
我的团队在自定义验证代码上投入了大量资金,该代码下面使用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
真正的变化实际上是位于此处的 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 />
这样做:
<CustomValidator />
正如史蒂文在评论部分所建议的那样,你不应该那样做。相反,您可以按照以下代码片段中的说明执行此操作,其中一部分只是伪代码,以指出您需要做什么......它不应该按原样工作。
您可以为此重写 EditContext 的 FieldChanged 方法。
假设您有此表单,其中包含电子邮件地址的输入字段,并且您想检查此电子邮件是否已被其他用户使用...要检查 输入的电子邮件地址的可用性 您必须对数据存储执行调用并验证这一点。请注意,FieldChanged 方法中描述的某些操作可以移动到单独的验证服务...
<EditForm EditContext="@EditContext"
OnValidSubmit="HandleValidSubmit">
<DataAnnotationsValidator />
<div class="form-group">
<label for="name">Name: </label>
<InputText Id="name" Class="form-control" @bind-
Value="@Model.Name"></InputText>
<ValidationMessage For="@(() => Model.Name)" />
</div>
<div class="form-group">
<label for="body">Text: </label>
<InputText Id="body" Class="form-control" @bind-Value="@Model.Text"></InputText>
<ValidationMessage For="@(() => Model.Text)" />
</div>
<div class="form-group">
<label for="body">Email: </label>
<InputText Id="body" Class="form-control" @bind-Value="@Model.EmailAddress"></InputText>
<ValidationMessage For="@(() => Model.EmailAddress)" />
</div>
<p>
<button type="submit">Save</button>
</p>
</EditForm>
@code
{
private EditContext EditContext;
private Comment Model = new Comment();
ValidationMessageStore messages;
protected override void OnInitialized()
{
EditContext = new EditContext(Model);
EditContext.OnFieldChanged += EditContext_OnFieldChanged;
messages = new ValidationMessageStore(EditContext);
base.OnInitialized();
}
// Note: The OnFieldChanged event is raised for each field in the
// model. Here you should validate the email address
private void EditContext_OnFieldChanged(object sender,
FieldChangedEventArgs e)
{
// Call your database to check if the email address is
// available
// Retrieve the value of the input field for email
// Pseudocode...
var email = "enet.xxxx@gmail.com";
var exists = VerifyEmail(email);
messages.Clear();
// If exists is true, form a message about this, and add it
// to the messages object so that it is displayed in the
// ValidationMessage component for email
}
}
希望这有帮助...