Quantcast
Channel: Andrew Lock | .NET Escapades
Viewing all articles
Browse latest Browse all 743

Injecting services into ValidationAttributes in ASP.NET Core

$
0
0

I was battling the other day writing a custom DataAnnotations ValidationAttribute, where I needed access to a service class to perform the validation. The documentation on creating custom attributes is excellent, covering both server side and client side validation, but it doesn't mention this, presumably relatively common, requirement. This post describes how to use dependency injection with ValidationAttributes in ASP.NET Core, and the process I took in trying to figure out how!

Injecting services into attributes in general has always been somewhat problematic as you can't use constructor injection for anything that's not a constant. This often leads to implementations requiring some sort of service locator pattern when external services are required, or a factory pattern to create the attributes.

tl;dr; ValidationAttribute.IsValid() provides a ValidationContext parameter you can use to retrieve services from the DI container by calling GetService().

Injecting services into ActionFilters

In ASP.NET Core MVC, as well having simple 'normal' IFilter attributes that can be used to decorate your actions, there are ServiceFilter and TypeFilter attributes. These implement the IFilterFactory interface, which, as the name suggests, acts as a factory for IFilters!

These two filter types allow you to use classes with constructor dependencies as attributes. For example, we can create an IFilter implementation that has external dependencies:

public class FilterClass : ActionFilterAttribute
{
  public FilterClass(IDependency1 dependency1, IDependency2 dependency2)
  {
    // ...use dependencies
  }
}

We can then decorate our controller actions to use FilterClass by using the ServiceFilter or TypeFilter:


public class HomeController: Controller
{
    [TypeFilter(typeof(FilterClass))]
    [ServiceFilter(typeof(FilterClass))]
    public IActionResult Index()
    {
        return View();
    }
}

Both of these attributes will return an instance of the FilterClass to the MVC Pipeline when requested, as though the FilterClass was an attribute applied directly to the Action. The difference between them lies in how they create an instance of the FilterClass.

The ServiceFilter will attempt to resolve an instance of FilterClass directly from the IoC container, so the FilterClass and its dependencies must be registered with the IoC container.

The Typefilter attribute also creates an instance of the FilterClass but only its dependencies are resolved from the IoC Container, rather than FilterClass.

For more details on using TypeFilter and ServiceFilter see the documentation or this post.

How ValidationAttributes are resolved

For my CustomValidationAttribute I needed access to an external service to perform the validation:


public class CustomValidationAttribute: ValidationAttribute
{
  protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        // ... need access to external service here
    }
}

In my first attempt to inject a service I thought I would have to take a similar approach to the ServiceFilter and TypeFilter attributes. Optimistically, I created a TypeFilter, passed in my CustomValidationAttribute, applied it to the model property and crossed my fingers.

It didn't work.

The mechanism by which DataAnnotation ValidationAttributes are applied to your model is completely different to the IFilter and IFilterFactory attributes used by the MVC infrastructure to build a pipeline.

The default implementation of IModelValidatorProvider used by the Microsoft.AspNetCore.Mvc.DataAnnotations library (cunningly called DataAnnotationsModelValidatorProvider) is responsible for creating the IModelValidator instances in the method CreateValidators. The IModelValidator is responsible for performing the actual validation of a decorated property.

I thought about creating a custom IModelValidatorProvider and creating the validators myself using an ObjectFactory, similar to the way the ServiceFilter and TypeFilter work.

Inside the DataAnnotationsModelValidatorProvider.CreateValidators method is this section of code, which creates a DataAnnotationsModelValidator object from a ValidationAttribute (see here for the full code):

var attribute = validatorItem.ValidatorMetadata as ValidationAttribute;
if (attribute == null)
{
    continue;
}

var validator = new DataAnnotationsModelValidator(
    _validationAttributeAdapterProvider,
    attribute,
    stringLocalizer);

As you can see, the attributes are already created at this point, and exist as ValidatorMetadata on the ModelValidatorProviderContext passed to the function. In order to be able to use a TypeFilter-like approach, we would have to hook in much further up the stack.

At this point I decided that I must be missing something, as it couldn't possibly be this difficult…

The solution

Sure enough, the final answer was simple!

When creating a custom validation attribute you need to override the Validate method:

public class CustomValidationAttribute : ValidationAttribute
{
    protected override ValidationResult IsValid(object value, ValidationContext validationContext)
    {
        // ... validation logic
    }
}

As you can see, you are provided a ValidationContext as part of the method call. The context object contains a number of properties related to the object currently being validated, and also this handy number:

public object GetService(Type serviceType);

This hooks into the IoC IServiceProvider to allow retrieving services in your ValidationAttributes:

protected override ValidationResult IsValid(object value, ValidationContext validationContext)
{
    var service = (IExternalService) validationContext
                         .GetService(typeof(IExternalService));
    // use service
}

So in the end, nice and easy, no need for the complex re-implementations route I was eyeing up.

Happy validating!

  • https://docs.asp.net/en/latest/mvc/models/validation.html#custom-validation
  • https://docs.asp.net/en/latest/mvc/controllers/filters.html#dependency-injection
  • http://www.strathweb.com/2015/06/action-filters-service-filters-type-filters-asp-net-5-mvc-6/
  • https://msdn.microsoft.com/en-us/magazine/mt767699.aspx?f=255&MSPPError=-2147217396

Viewing all articles
Browse latest Browse all 743

Trending Articles