简体   繁体   中英

Dependency Injection for custom validation attributes

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM