如何在自定义 ValidationAttributes 中获取/注入服务

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 ;-)

Isa*_*aac 5

正如史蒂文在评论部分所建议的,你不应该这样做。相反,您可以按照以下代码片段中的描述执行此操作,其中部分代码只是指出您需要执行的操作的伪代码...它不应该按原样工作。

您可以为此重写 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)

希望这可以帮助...

  • 谢谢你的回答。很有帮助!我现在在这里找到了一些文档 https://learn.microsoft.com/en-us/aspnet/core/blazor/forms-validation?view=aspnetcore-3.1 但它没有明确说明你在这里所说的内容。这将是一个有用的补充。 (2认同)

Gar*_*ton 5

我的团队在我们的自定义验证代码上投入了大量资金,该代码下面使用 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)