I created a custom validation attribute that I want to use for my API controller DTOs. This attribute needs values from the configured options, that's why I'm injecting them in the constructor, so that I can use the options service later on in the IsValid
and FormatErrorMessage
method.
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;
}
}
Unfortunately when I want to use this as an attribute in my DTO
internal class MyDTO
{
[MyValidationAttribute]
public string Foo { get; set; }
}
I get the error message
There is no argument given that corresponds to the required formal parameter 'myOptionsMonitor' of 'MyValidationAttribute.MyValidationAttribute(IOptionsMonitor)'
Is there a way I can use dependency injection for validation attributes? I know that I can use the ValidationContext
like so
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");
}
}
But I want to use the FormatErrorMessage
method from the base class and this has no access to the options service.
My current solution
For now, this is the code I'm using
[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);
}
}
Attributes are not designed for this purpose. But you can use action filters instead.
Let`s make your attribute as simple as it can be, we don't need any validation logic there.
[AttributeUsage(AttributeTargets.Property)]
public class CustomValidationAttribute : Attribute
{ }
For my example I created service that we are going to inject
public class SomeService
{
public bool IsValid(string str)
{
return str == "Valid";
}
}
and a class that we are going to validate
public class ClassToValidate
{
[CustomValidation]
public string ValidStr { get; set; } = "Valid";
[CustomValidation]
public string InvalidStr { get; set; } = "Invalid";
}
Now we can finally create action filter to validate our properties. In the snippet below, we hook into ASP.NET Core pipeline to execute code just before our controller action executes. Here I get action arguments and try to find CustomValidationAttribute
on any property. If it is there, grab the value from the property, cast to type (I simply invoke .ToString()
) and pass to your service. Based on value that is returned from service, we continue execution or add error to ModelState
dictionary.
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);
}
}
Don't forget to add your filter to the pipeline in Startup.cs
!
services.AddMvc(x =>
{
x.Filters.Add(typeof(CustomValidationActionFilter));
});
Update:
If you strictly want to use dependency injection inside attribute, you could use service locator anti-pattern. For that we need to emulate DependencyResolver.Current
from 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;
}
}
It should be initialized in Startup.cs
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
AppDependencyResolver.Init(app.ApplicationServices);
// other code
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.