c# - 自定义验证属性的依赖注入(inject)

标签 c# .net-core asp.net-core-webapi data-annotations

我创建了一个自定义验证属性,我想将其用于我的 API Controller DTO。该属性需要来自配置选项的值,这就是我将它们注入(inject)构造函数的原因,以便稍后我可以在 IsValid 中使用选项服务。和 FormatErrorMessage方法。

internal class MyValidationAttribute : ValidationAttribute
{
    private readonly IOptionsMonitor<MyOptions> myOptionsMonitor;

    public MyValidationAttribute(IOptionsMonitor<MyOptions> myOptionsMonitor)
    {
        this.myOptionsMonitor = myOptionsMonitor;
    }

    public override bool IsValid(object value)
    {
        // ... use myOptionsMonitor here ...

        return false;
    }

    public override string FormatErrorMessage(string name)
    {
        // ... use myOptionsMonitor here ...

        return string.Empty;
    }
}
不幸的是,当我想将其用作 DTO 中的属性时
internal class MyDTO
{
    [MyValidationAttribute]
    public string Foo { get; set; }
}
我收到错误消息

There is no argument given that corresponds to the required formal parameter 'myOptionsMonitor' of 'MyValidationAttribute.MyValidationAttribute(IOptionsMonitor)'


有没有办法可以将依赖注入(inject)用于验证属性?我知道我可以使用 ValidationContext像这样
internal class MyValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        if (value != null)
        {
            IOptionsMonitor<MyOptions> myOptionsMonitor = validationContext.GetService<IOptionsMonitor<MyOptions>>();

            // ...

            return ValidationResult.Success;
        }

        return new ValidationResult("Something failed");
    }
}
但我想使用 FormatErrorMessage来自基类的方法,并且无法访问选项服务。

我目前的解决方案
现在,这是我正在使用的代码
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
internal class CustomValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        IOptionsMonitor<MyOptions> myOptionsMonitor = validationContext.GetService<IOptionsMonitor<MyOptions>>();
        Dictionary<string, string> myMap = myOptionsMonitor.CurrentValue.MyMap;
        string key = value.ToString() ?? string.Empty;

        if (myMap.ContainsKey(key))
            return ValidationResult.Success;

        string[] formattedKeys = myMap.Keys.Select(key => $"'{key}'").ToArray();
        string keysText = string.Join(" / ", formattedKeys);
        string errorMessage = $"Invalid value. Valid ones are {keysText}";

        return new ValidationResult(errorMessage);
    }
}

最佳答案

属性不是为此目的而设计的。但是您可以改用操作过滤器。
让您的属性尽可能简单,我们不需要任何验证逻辑。

[AttributeUsage(AttributeTargets.Property)]
public class CustomValidationAttribute : Attribute
{ }
对于我的示例,我创建了我们将要注入(inject)的服务
public class SomeService
{
    public bool IsValid(string str)
    {
        return str == "Valid";
    }
}
和一个我们要验证的类
public class ClassToValidate
{
    [CustomValidation]
    public string ValidStr { get; set; } = "Valid";
    
    [CustomValidation]
    public string InvalidStr { get; set; } = "Invalid";
}
现在我们终于可以创建 Action 过滤器来验证我们的属性了。在下面的代码段中,我们 Hook 到 ASP.NET Core 管道以在我们的 Controller 操作执行之前执行代码。在这里我得到 Action 参数并尝试找到 CustomValidationAttribute在任何属性(property)上。如果它在那里,从属性中获取值,转换为类型(我只是调用 .ToString() )并传递给您的服务。根据服务返回的值,我们继续执行或向 ModelState 添加错误字典。
public class CustomValidationActionFilter : ActionFilterAttribute
{
    private readonly SomeService someService;

    public CustomValidationActionFilter(SomeService someService)
    {
        this.someService = someService;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var actionArguments = context.ActionArguments;

        foreach (var actionArgument in actionArguments)
        {
            var propertiesWithAttributes = actionArgument.Value
                .GetType()
                .GetProperties()
                .Where(x => x.GetCustomAttributes(true).Any(y => y.GetType() == typeof(CustomValidationAttribute)))
                .ToList();

            foreach (var property in propertiesWithAttributes)
            {
                var value = property.GetValue(actionArgument.Value).ToString();

                if (someService.IsValid(value))
                    continue;
                else
                    context.ModelState.AddModelError(property.Name, "ModelState is invalid!!!");
            }
        }

        base.OnActionExecuting(context);
    }
}
不要忘记在 Startup.cs 中将过滤器添加到管道中!
services.AddMvc(x =>
{
    x.Filters.Add(typeof(CustomValidationActionFilter));
});
更新:
如果您严格想在属性内使用依赖注入(inject),则可以使用服务定位器反模式。为此,我们需要模拟 DependencyResolver.Current来自 ASP.NET MVC
public class CustomValidationAttribute : ValidationAttribute
{
    private IServiceProvider serviceProvider;

    public CustomValidationAttribute()
    {
        serviceProvider = AppDependencyResolver.Current.GetService<IServiceProvider>();
    }

    public override bool IsValid(object value)
    {
        // scope is required for scoped services
        using (var scope = serviceProvider.CreateScope())
        {
            var service = scope.ServiceProvider.GetService<SomeService>();

            return base.IsValid(value);
        }
    }
}


public class AppDependencyResolver
{
    private static AppDependencyResolver _resolver;

    public static AppDependencyResolver Current
    {
        get
        {
            if (_resolver == null)
                throw new Exception("AppDependencyResolver not initialized. You should initialize it in Startup class");
            return _resolver;
        }
    }

    public static void Init(IServiceProvider services)
    {
        _resolver = new AppDependencyResolver(services);
    }

    private readonly IServiceProvider _serviceProvider;

    public object GetService(Type serviceType)
    {
        return _serviceProvider.GetService(serviceType);
    }

    public T GetService<T>()
    {
        return (T)_serviceProvider.GetService(typeof(T));
    }

    private AppDependencyResolver(IServiceProvider serviceProvider)
    {
        _serviceProvider = serviceProvider;
    }
}
它应该在 Startup.cs 中初始化
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    AppDependencyResolver.Init(app.ApplicationServices);

    // other code
}

关于c# - 自定义验证属性的依赖注入(inject),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/64385660/

相关文章:

asp.net-core - 如何在启动时将数据放入 MemoryCache?

azure - DotNet Core Web 应用程序在本地工作,但在部署到 Azure 时无法工作

azure - 我们可以在 azure 托管的 Web api 中拥有类对象的内存限制吗?

c# - 如何将此 SQL Order By 语句转换为 LINQ-To-Object?

asp.net-core - .NET Core 锁定文件

c# - 在 Windows 8.1 应用程序中使用使用 Visual C++ 2012 构建的 Windows 8.0 扩展 SDK 组件

c# - 单击一次不将 appsettings 安装到用户计算机

c# - 如何让我的 .NET Core 3 单文件应用程序找到 appsettings.json 文件?

c# - 通过引用传递不起作用

c# - HTMLAgilityPack Asp.net C#错误处理