自定义验证属性的依赖注入
Dependency Injection for custom validation attributes
我创建了一个要用于我的 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;
}
}
不幸的是,当我想将其用作我的 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)'
有没有一种方法可以使用依赖注入来验证属性?我知道我可以像这样使用 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");
}
}
但我想使用基础 class 中的 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
{ }
对于我的示例,我创建了我们要注入的服务
public class SomeService
{
public bool IsValid(string str)
{
return str == "Valid";
}
}
和我们要验证的 class
public class ClassToValidate
{
[CustomValidation]
public string ValidStr { get; set; } = "Valid";
[CustomValidation]
public string InvalidStr { get; set; } = "Invalid";
}
现在我们终于可以创建动作过滤器来验证我们的属性了。在下面的代码片段中,我们挂接到 ASP.NET 核心管道以在我们的控制器操作执行之前执行代码。在这里,我获取操作参数并尝试在任何 属性 上找到 CustomValidationAttribute
。如果存在,从 属性 中获取值,转换为类型(我只是调用 .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));
});
更新:
如果你确实想在属性内部使用依赖注入,你可以使用服务定位器anti-pattern。为此,我们需要从 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;
}
}
应该在Startup.cs
初始化
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
AppDependencyResolver.Init(app.ApplicationServices);
// other code
}
我创建了一个要用于我的 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;
}
}
不幸的是,当我想将其用作我的 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)'
有没有一种方法可以使用依赖注入来验证属性?我知道我可以像这样使用 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");
}
}
但我想使用基础 class 中的 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
{ }
对于我的示例,我创建了我们要注入的服务
public class SomeService
{
public bool IsValid(string str)
{
return str == "Valid";
}
}
和我们要验证的 class
public class ClassToValidate
{
[CustomValidation]
public string ValidStr { get; set; } = "Valid";
[CustomValidation]
public string InvalidStr { get; set; } = "Invalid";
}
现在我们终于可以创建动作过滤器来验证我们的属性了。在下面的代码片段中,我们挂接到 ASP.NET 核心管道以在我们的控制器操作执行之前执行代码。在这里,我获取操作参数并尝试在任何 属性 上找到 CustomValidationAttribute
。如果存在,从 属性 中获取值,转换为类型(我只是调用 .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));
});
更新:
如果你确实想在属性内部使用依赖注入,你可以使用服务定位器anti-pattern。为此,我们需要从 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;
}
}
应该在Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
AppDependencyResolver.Init(app.ApplicationServices);
// other code
}