Fluent-ish Validations – Part One (The problem)

A little background. I write software that is then sold to VARs (Value-Added Resellers). They can take our code and enhancement it or change it based on their exact customer needs. We try to write code that allows them to change anything they might need. Now all software does have some basic data it needs to function properly but sometimes we put in ValidationAttribute to just make programming simple. Who doesn’t like ASP.NET MVC or Entity Framework to auto call our validation. But when you add an attribute to a class it’s really hard to take that attribute away or to change it. So typically I’d shy away from ValidationAttribute and instead implement the IValidatableObject Interface. But this leads sometimes to some ugly code.

Let take an arbitrarily contrived example. Maybe not too contrived but it’s simplified down from a real requirement.

User requirement: Create a screen for a user to buy items and needs the ability to enter the item, quantity and date requested.

Rules: The item needs to be filled in and the length of the item is configurable. Quantity must be positive. Date can be empty or any valid date.

Seems simple enough let’s create a class

public class SalesOrderLineItems
{
    public string Item { get; set; }
    public decimal Qty { get; set; }
    public DateTime RequestedBy { get; set; }
}

The business rule of item required is simple we can just slap on the RequiredAttribute but we can’t slap on the MaxLengthAttribute because the length is configurable. Ok we will just handle that with IValidatableObject interface.

To get the item’s length we have a DataService

public interface IDataService
{
    Task ItemLengthAsync();
    int ItemLength();
    Task QuantityPercisionAsync();
    int QuantityPercision();
}code>

Let's write up the validation for max length in the IValidatableObject


public class SalesOrderLineItems : IValidatableObject
{
    [Required]
    public string Item { get; set; }
    public decimal Qty { get; set; }
    public DateTime RequestedBy { get; set; }

    public IEnumerable Validate(ValidationContext validationContext)
    {
        yield return ValidateItem(validationContext);
    }

    /// This method just wraps the MaxLengthAttribute so we can set the length at runtime
    protected virtual ValidationResult ValidateItem(ValidationContext validationContext)
    {
        var dataService = (IDataService)validationContext.ServiceContainer.GetService(typeof(IDataService));
        var maxValidation = new MaxLengthAttribute(dataService.ItemLength());
        var context = new ValidationContext(this, validationContext.ServiceContainer, validationContext.Items)
        {
            MemberName = nameof(Item)
        };
        return maxValidation.GetValidationResult(Item , context);
    }
}

I'm a big fan of constructor injection but not for my models, they should have a default constructor. so we have to go back to using the Service Container (yuck) but it's possible. I also implement each validation in it's own method so the VARs can override a specific validation. I could have made it's own class and using the service locator grab the validation class and call those methods. In that class I could have injected the dataservice but still to get the validation class I need to use the Service Locator pattern. For this example we will leave as methods. For the Qty requirement let's slap on the RangeAttribute [Range(typeof(double),".01","9999999999999999")]

Now we demo to our Product Owner. First complaint is they don't like the range validation error message. Ok we can fix that with a Resource file to change the message. Second complaint is the number of decimals is configured and right now it's hard coded that .01 is the min. Product Owner likes it's trying to verify the minimum quantity that can be entered and not just greater than zero. Hmmm we were thinking of creating a MinAttribute but if they want it to be the min enterable amount based on the dataservice that is out. Product Owner also says they like that the quantity isn't validated if the item hasn't been filled in yet but....they would also like to skip the quantity validation if the item is too long because we know it's not valid. Ok we going to have to create another validation method.

Here's the code that will make our Product Owner happy

public class SalesOrderLineItems : IValidatableObject
{
    [Required]
    public string Item { get; set; }
    public decimal Qty { get; set; }
    public DateTime RequestedBy { get; set; }

    public IEnumerable Validate(ValidationContext validationContext)
    {
        var result =  ValidateItem(validationContext);
        // don't run any other validation if item didn't pass
        if (result == ValidationResult.Success)
        {
            yield return ValidateQty(validationContext);
        }
        else
        {
            yield return result;
        }
    }

    protected virtual ValidationResult ValidateItem(ValidationContext validationContext)
    {
        var dataService = (IDataService)validationContext.ServiceContainer.GetService(typeof(IDataService));
        var maxValidation = new MaxLengthAttribute(dataService.ItemLength());
        var context = new ValidationContext(this, validationContext.ServiceContainer, validationContext.Items)
        {
            MemberName = nameof(Qty)
        };
        return maxValidation.GetValidationResult(Qty, context);
    }

    protected virtual ValidationResult ValidateQty(ValidationContext validationContext)
    {
        var dataService = (IDataService)validationContext.ServiceContainer.GetService(typeof(IDataService));
        var percision = dataService.QuantityPercision();
        var minvalue = Convert.ToDecimal(1/Math.Pow(10, percision));
        if (Qty > minvalue)
        {
            return ValidationResult.Success;
        }
        return new ValidationResult("Quantity must be greater than zero");
    }
}

All that for a simple business requirements the code just keeps expanding if you have more requirements and can quickly get out of hand. We will look at Part 2 how to tame this beast.

Friday, October 27th, 2017 Validations