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 aValidationContext
parameter you can use to retrieve services from the DI container by callingGetService()
.
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 IFilter
s!
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
ValidationAttribute
s 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 ValidationAttribute
s:
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!
Additional Links
- 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