We came across a situation where we needed some additional data to perform validation on the payload being sent via an HTTP post. Essentially it involved calling up some setup rules from a database and comparing the payload passed in against those rules to ensure the payload was valid. We had the following requirements:
Retrieve data for use by validation logic using async/await api
Leverage existing validation pipeline. i.e. no custom validation logic in controller
Remain transparent to users of the validation attributes
The problem
Out of the box the System.Component model validation attribute provides a way to access a service locator using the validationContext.GetService(typeof()) api however this is not an async api. Executing an async operation here would cause us to wait synchronously which was a show stopper.
A great post by Andrew Lock got us most of the way there however this method does not allow for asynchronous operations and prevented us meeting the first requirement.
We invesitgated using Filters however the filter pipeline is executed after model binding and validation takes place. This would prevent us from meeting requirement 2 and 3.
We dug into the AspNetCore code base and found that intercepting the model binding step was possible and would give us what we needed.
Custom Model Binders
We found that we could intercept the model binding by implementing a custom IModelBinder. In AspNetCore, model binding happens asynchronoulsy so this gave us the async hook to go and fetch the additional data required for validation.
Writing a full model binder for complex objects is non-trivial and there was no way I was going to take that on. Instead I figured we could intercept and proxy the call to the original model binder. This will require some additional logic to wire it all up.
Custom Model Binding
IModelBinder
The custom model binder implementation is straight forward.
IModelBinderProvider
We need to tell the Mvc framework how to create an instance of our custom model binder. To do this we need to implement an IModelBinderProvider. This too is straight forward:
Hooking it up
To hook this up to the Mvc framework we can create an extension method to be called by the Startup.cs class.
This is called in Startup.cs.
Providing context
We still need to get the additional data to the validation attributes. To achieve this we make use of the ability to resolve services within the attribute using validationContext.GetService. The key is to preload the data (or valiadtion context) asynchrously and provide a way for the attribute to get hold of the validation context synchronously. We can create a provider mechanism to achieve this.
For this example, when given a model which contains a name and a list of items, we want to be sure that those values are contained within some pre-defined data stored away in a database or service.
Given a model which appears as follows
We will need data which represents the valid choices for name and items. The context may appear as follows:
Validation Context Provider
The provider is simply a class which provides access to the validation context instance. It could be implemented as follows.
Fetching the data
We need a way to fetch the data for our custom validation. This can be done using a DbContext or a service call. For this example we have created a simple validation context factory for brevity. This does nothing but return some sample data using an immediately fulfilled task.
Register the services
We need to make these services available to the IoC container. We can do this as follows.
This is called in Startup.cs
Wire up validation logic
So to make the data avaible to the custom validators we need to complete the IModelBinder implementation. Using service location we can get hold of our custom context factory and provider to create an instance of CustomValidationContext and register it with the provider.
Since we have access to the original HttpRequest we could use parameters from that request when creating the CustomValidationContext which can be usefull!
The implementation appears as follows:
Custom Validation Attributes
For convenience we can create a base class which is responsible for locating the CustomValidationContextProvider to get hold of the CustomValidationContext instance and make it available within the IsValid method of the validation attribute.
Custom Validators
And finally… we are able to implement our custom validation logic using the additonal validation context to do so.
Given a model and action as follows:
We can implement the custom validation attributes [IsValidName] and [ContainsValidItems] respectively.
Conclusion
We had to jump through a number of hoops to get this right and it feels like it should have been easier. Being required to create the validation context asynchronously is what threw the spanner in the works for us. If you know of a simpler solution to this problem I would love to hear from you!
Here is a gift for reading all the way to the end.
Inevitably when building enterprise software you will be required to implement audit trail and/or versioning functionality. At its core, ...… Continue reading
Leave a comment