C# 自定义验证属性的依赖项注入

C# 自定义验证属性的依赖项注入,c#,.net-core,asp.net-core-webapi,data-annotations,C#,.net Core,Asp.net Core Webapi,Data Annotations,我创建了一个自定义验证属性,希望用于API控制器DTO。此属性需要来自已配置选项的值,这就是我将它们注入构造函数的原因,以便稍后在IsValid和FormatErrorMessage方法中使用选项服务 internal class MyValidationAttribute : ValidationAttribute { private readonly IOptionsMonitor<MyOptions> myOptionsMonitor; public MyVa

我创建了一个自定义验证属性,希望用于API控制器DTO。此属性需要来自已配置选项的值,这就是我将它们注入构造函数的原因,以便稍后在
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;
    }
}
我收到了错误信息

没有给出与所需的形式化参数相对应的参数 的参数“MyOptionMonitor” 'MyValidationAttribute.MyValidationAttribute(IOptionsMonitor)'

有没有一种方法可以将依赖项注入用于验证属性?我知道我可以像这样使用
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");
    }
}

属性不是为此目的而设计的。但是您可以使用动作过滤器

让我们尽可能地简化属性,我们不需要任何验证逻辑

[AttributeUsage(AttributeTargets.Property)]
public class CustomValidationAttribute : Attribute
{ }
在我的示例中,我创建了我们将要注入的服务

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";
}
现在我们终于可以创建动作过滤器来验证我们的属性了。在下面的代码段中,我们将在控制器操作执行之前挂接到ASP.NET核心管道以执行代码。这里我获取操作参数,并尝试在任何属性上查找
CustomValidationAttribute
。如果存在,则从属性中获取值,转换为type(我只需调用
.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));
});
更新:

若您严格希望在属性内部使用依赖项注入,那个么可以使用服务定位器反模式。为此,我们需要模拟ASP.NET MVC中的
DependencyResolver.Current

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

属性将元数据信息添加到类型中,为什么您认为可以向其中注入某些内容?我认为依赖注入可以在任何地方使用。你知道如何注入服务吗?比如,你能在静态构造函数中使用DI吗?对不起,你是什么意思?请不要在你的问题中编辑你的解决方案。相反,将其作为问题的自我答案发布。嗯,是的,不幸的是,这似乎是唯一的解决方案。我用自己的解决方案更新了我的问题,你怎么看?忘记了
ValidationContext.GetService
,我试图在第二种方法中做类似的事情。公平地说,我会将验证逻辑分开,或者尽可能地将其最小化,以确保您可以在运行时将验证服务注入其他地方。提供更多的代码重用性
services.AddMvc(x =>
{
    x.Filters.Add(typeof(CustomValidationActionFilter));
});
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;
    }
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    AppDependencyResolver.Init(app.ApplicationServices);

    // other code
}