Fluent-ish Validations – Part 2 [Solution]
Part 1 is here
I started looking at the Validator source code to see how it works internally. If you didn’t know ASP.NET MVC and Entity Framework uses the Validator to perform their validations. The Validator uses the TypeDescriptor that’s good news as we can add attributes to objects and types at runtime with the TypeDescriptor.
Side Note: I was getting excited thinking about being able to add validation to specific object and not their type. For conditional validations but the Validator will always look at the object type to get the attributes and not the object itself.
Even though this is mainly about Validation in the end we are just adding attributes so I decided to not just called it Validation and name most things with the prefix of Attributes. Really once we are done we could add any attribute to a property. To start we are going to need to create a CustomTypeDescriptor.
To give a shout out I got the gist from this stackoverflow answer but I implemented it a bit different to make it more generic to what I want to do.
To make it simple we will take in the constructor an Enumerable of attributes to add to the class level and an Enumerable of string/attributes to add to properties. I called this class AttributeTypeDescriptor. Need to add a TypeDescriptionProvider to make sure of this CustomeTypeDescriptor. Nothing to fancy just tell the TypeDescriptor about it and then have a method to add an Attribute to the class and one to add an Attribute to a property. I called this class AttributeProvider.
Here’s where we start to get into the fluent-ish part. I wanted an easy way to configure attributes so I made an interface called IAttributeConfiguration. This interface allows adding property level attributes either by expression or by string if using nameof() and attributes to the class itself. Since it’s generic it already knows what type to add them to. I created the implementation of this called AttributeConfiguration which has a default constructor and one that allows you to replace the provider if you’d like.
Now that we are adding attributes at runtime we are no longer restricted by things needs to be constants when declaring the Attribute. Following the IValidatableObject pattern I was using where each validation was it’s own method I’m going to create a ValidationAttribute that will take a Func
One of the common things I run into with validations is skipping validation if another property isn’t valid. With that in mind I create a new Validation Attribute called ValidateIfAttribute. This attributes takes a dictionary that the key is the string name of the property and a func of how to get the value of the property and the ValidationAttribute that should be executed if all the other properties are valid.
Word of warning with this you can create a cyclical validations. For Example if FirstName is requiring LastName valid and LastName is requiring FirstName valid. But as a developer I would prefer to have the power with knowledge of the short comings and deal with it then not have the option at all.
In the Property validation there is a setter for if you need to set some properties for the validation attribute that is being added but you also noticed that the setter needs to return an attribute. For the most part returning the attribute that came in back out is what you will want to do but I had it return the attribute for this case. I want to wrap the incoming attribute in the ValidateIfAttribute so that the incoming attribute will only fire if the other properties are valid.
I create an extension method called WhenValid that would return the Func to use to wrap the setter method in. By passing in an Expression in we can get both the name of the property and compile the expression to get the func needed to access the property. I still feel this part is a bit awkward at the moment and would like to see if it can become more fluent.
Now lets go back to the original PO issue. With this solution we can create a class for the validation
public class SalesOrderLineItemsValidations
{
private readonly IAttributeConfiguration<SalesOrderLineItems> _attributeConfiguration;
private readonly IDataService _dataService;
public SalesOrderLineItemsValidations(IAttributeConfiguration attributeConfiguration, IDataService dataService)
{
_attributeConfiguration = attributeConfiguration;
_dataService = dataService;
}
public async Task Configure()
{
var itemValidations = _attributeConfiguration.ValidationsFor(x => x.Item);
itemValidations.Add<RequiredAttribute>();
var itemLength = await _dataService.ItemLengthAsync();
itemValidations.Add(() => new MaxLengthAttribute(itemLength));
var qtyValidations = _attributeConfiguration.ValidationsFor(x => x.Qty);
var whenValid = _attributeConfiguration.WhenValid(x => x.Item);
qtyValidations.Add(QtyGreaterThanZero, whenValid);
}
protected virtual ValidationResult QtyGreaterThanZero(decimal qty)
{
if (qty > 0)
{
return ValidationResult.Success;
}
return new ValidationResult("Quantity must be greater than zero");
}
}
The Configure method is where the “Fluent-ish” validation happens at. Since I already have a BootStrap class and now I just need to have it add in the validation config classes and execute them.
Last bit of warning/thoughts.
The Validator does cache the attributes on a type the first time it uses the class. Which means if adding validation after the Validator has been called it will not pick up the new validations. Even if there is a way for them to know to refresh it *cough cough*. I don’t see this as a big deal as 99% of the time the validations would be added when the application starts up. I also thought that adding the validation in a different class then the original class might confuse someone coming after me but I feel that’s a training issue and frameworks like Entity Framework have their own configurations and they do allow adding some basic validations so it’s not unheard of. If I need to add a class validation I don’t know if it’s better to just add the IValidatableObject interface or keep all the validation in one class and have the fluent add the validation. I think that’s something the team should decide the pros and cons on.
All the code is hosted in GitHub and is licensed with the MIT license