【问题标题】:How to get/inject services in custom ValidationAttributes如何在自定义验证属性中获取/注入服务
【发布时间】:2020-06-24 16:31:50
【问题描述】:

我们使用的是 .NET Core 3.1.5,这是一个 Blazor 服务器应用程序。

我们有一个 ValidationAttribute 并且需要访问外部服务来验证对象。

ValidationAttribute 有 IsValid 方法:

protected override ValidationResult IsValid(object value, ValidationContext validationContext) ValidationContext 有一个 GetService 方法,它委托给 ServiceProvider 的一个实例。 不幸的是,服务提供者字段从未初始化,因此我们无法检索任何服务。

这是在 Mvc 中提出(并修复)的:aspnet/Mvc#6346 但是我们的 Validator 是通过以下两种方法之一调用的:

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

我们尝试注入服务并应用验证的类。

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,请轻点 ;-)

【问题讨论】:

  • 我尝试了同样的错误。按照你的问题。我在服务器端和 wasm 上都试过了(blazorfiddle.com/s/gk50cc8v
  • “我们有一个 ValidationAttribute,需要访问外部服务来验证对象。”你不应该。属性应该是passive,或者最多包含不做任何不纯的逻辑(例如I/O)。相反,将该逻辑移至单独的验证服务。
  • @Steven,我知道这是一种反模式,我感谢您对正确方法的评论,但问题是:文档说我们可以访问 DI 服务,但我们不能。您知道为什么我们无法按预期访问服务吗?问候。
  • @daniherrera 感谢您在服务器和 wasm 中进行尝试,并感谢 Steven 的解释,这很有帮助。我希望文档会更清晰。但我同意 Dani 的观点,这看起来仍然像一个错误。
  • @LarsFrancke,我看到了您指向 MS 问题的链接,干得好!你好心在这里发布问题的答案吗?

标签: c# validation .net-core dependency-injection blazor


【解决方案1】:

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

【讨论】:

    【解决方案2】:

    正如 Steven 在 cmets 部分所建议的那样,您不应该那样做。相反,您可以按照以下代码 sn-p 中的说明进行操作,其中一部分只是伪代码,用于指出您需要做什么......它不应该按原样工作。

    您可以为此覆盖 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
           
        }
    
    }
    

    希望这会有所帮助...

    【讨论】:

    • 感谢您的回答。非常有帮助!我现在在这里找到了一些文档docs.microsoft.com/en-us/aspnet/core/blazor/…,但它没有明确说明你在这里所说的内容。这将是一个有用的补充。
    • @LarsFrancke 我想鼓励您向 .Net guzs 提供有关文档的反馈。他们将扩展它/修复它。谢谢! :)
    • 谢谢@SayusiAndo。我在这里提出了一个关于“原始”问题的问题github.com/dotnet/aspnetcore/issues/23380
    • Lars Francke,DataAnnotations 不支持 DI。您可以改为使用 FluentValidation。这真的很容易。这是 Chris Sainty 的一个很好的示例的链接:chrissainty.com/…,然后您可以在当前上下文中看到 DI:github.com/Blazored/FluentValidation。请注意,无论如何,我的回答中的建议仍然相关,流程必须相似,并且必须使用 EditContext.OnFieldChanged,无论您使用什么验证器(DataAnnotations 或 FluentValidation)。
    猜你喜欢
    • 2017-07-27
    • 1970-01-01
    • 2016-05-16
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-07-22
    相关资源
    最近更新 更多