Soe*_*ren 3 c# validation dependency-injection .net-core blazor
我们使用 .NET Core 3.1.5,这是一个 Blazor 服务器应用程序。
我们有一个 ValidationAttribute 并且需要访问外部服务来验证对象。
ValidationAttribute 具有 IsValid 方法:
protected override ValidationResult IsValid(object value, ValidationContext validationContext) ValidationContext 有一个委托给 ServiceProvider 实例的 GetService 方法。不幸的是,服务提供者字段从未被初始化,因此我们无法检索任何服务。
这在 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 后来在堆栈中,服务提供者也从未设置。我犹豫要不要打开一个错误(但可以这样做),但这对我来说似乎是错误的(或者至少应该记录下来)。
任何 Google 搜索最终都会在这篇博客文章中结束,但正如我刚刚提到的,这不起作用。
所以我们的问题是:将服务注入 ValidationAttribute 的正确方法是什么,或者更一般地说,验证需要调用外部服务的模型字段的正确方法是什么?
在statup.cs:
services.AddTransient<IMarktTypDaten, MarktTypDaten>();
Run Code Online (Sandbox Code Playgroud)
我们尝试注入服务并应用验证的类。
public class MarktTypNameValidation : ValidationAttribute {
protected override ValidationResult IsValid(object value, ValidationContext validationContext) {
var service = (IMarktTypDaten) validationContext.GetRequiredService(typeof(IMarktTypDaten));
...some code...
return ValidationResult.Success;
}
}
Run Code Online (Sandbox Code Playgroud)
ExceptionMessage when calling GetRequiredService: 'No service for type 'DataAccessLibrary.Interfaces.IMarktTypDaten' has been registered.
It's also posted on Github: https://github.com/dotnet/aspnetcore/discussions/23305
Also: I'm using C#/.NET for the first time in 15 or so years, please be gentle ;-)
正如史蒂文在评论部分所建议的,你不应该这样做。相反,您可以按照以下代码片段中的描述执行此操作,其中部分代码只是指出您需要执行的操作的伪代码...它不应该按原样工作。
您可以为此重写 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
}
}
Run Code Online (Sandbox Code Playgroud)
希望这可以帮助...
我的团队在我们的自定义验证代码上投入了大量资金,该代码下面使用 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));
Run Code Online (Sandbox Code Playgroud)
这是我的代码,略有编辑,但以下内容应该是开箱即用的。
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;
}
}
Run Code Online (Sandbox Code Playgroud)
添加此类型后,所需要做的就是在 blazor/razor 文件中使用它而不是 DataAnnotationsValidator。
所以而不是这个:
<DataAnnotationsValidator />
Run Code Online (Sandbox Code Playgroud)
做这个:
<CustomValidator />
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
1136 次 |
| 最近记录: |