F# Validation Framework

If you're looking for help with C#, .NET, Azure, Architecture, or would simply value an independent opinion then please get in touch here or over on Twitter.

If you follow me on Twitter you’ll know I’ve been working on a first class F# interface for Function Monkey. I’d got to the point where it made sense to put together a nice little exemplar of how its used and I went looking for a simple, ideally declarative, validation framework. Something akin to Fluent Validation but for F#. As I’d been working with computation expressions to build the Function Monkey DSL they were foremost in my mind as an approach to a clean validation framework.

I had a hunt around but to my surprise couldn’t really find anything. Closest I came was a blog post that built out a validation framework and left things speculating if it would be possible to use a computation expression to clean things up (short version if you read on – yes you can).

Why not use Fluent Validation itself? While you can happily use NuGet packages written in C# within F# their API surface doesn’t feel very functional. Unsurprisingly if they’re written in C# they tend to focus on C# patterns and classes and so consuming them from a functional environment can feel a bit weird and look a bit odd.

As a result I started playing around with a few things and before I knew it I’d written the skeleton of a framework that used a declarative / DSL type approach to validations, it seemed promising and useful so I spent some more time fleshing out.

It’s available on GitHub, along with its documentation, and I’ll spend some time giving an overview of it here.

Simple Validation

First lets consider a very simple model to represent a customer of a store:

type Customer =
    {
        name: string
        dateOfBirth: DateTime
    }

Let’s begin by validating the name (we’ll come back to the date of birth later) to make sure its not empty and has a maximum length of an arbitrary 128 characters. We do this as shown below:

let customerValidator = createValidatorFor<Customer>() {
    validate (fun c -> c.name) [
        isNotEmpty
        hasMaxLengthOf 128
    ]
}

In this code we use the createValidatorFor function to return a validation builder for our type and within the curly braces we can declare our validations.

The validate command is one we’ll use a lot. It’s a function with two parameters – the first takes a lambda that returns the property we wish to validate and the second parameter is an array of our validators. In this case we’re saying the name property of a customer must not be empty and can have a max length of 128 characters.

If we look at customerValidator we’ll find that we’ve been returned a function with the signature Customer -> ValidateState and below is shown an example of calling this and triggering a validation failure:

let outputToConsole state =
    match state with
    | Ok -> printf "No errors\n\n"
    | Errors e -> printf "%O\n\n" e

{ name = "" ; dateOfBirth = DateTime.UtcNow.AddYears(-1) }
|> customerValidator
|> outputToConsole

If we run this we’ll see this output:

[{message = "Must not be empty";
 property = "name";
 errorCode = "isNotEmpty";}]

Complex Models

Lets extend our model to something more complex – an order that references a customer and a collection of ordered items:

type Customer =
    {
        name: string
        dateOfBirth: DateTime
    }

type Product =
    {
        name: string
        price: double
    }

type OrderItem =
    {
        product: Product
        quantity: int
    }

type Order =
    {
        customer: Customer
        items: OrderItem list
    }

We’ll now build a validator that validates:

  • That the customer is valid
  • That the order has at least 1 item
  • That each order item has a quantity of at least 1
  • That each product has a name and that name has maximum length of 128
  • And finally that each product has a price of greater than 0

This is going to introduce the concept of validating collections and we’ll also use deep property paths to validate the product:

let orderValidator = createValidatorFor<Order>() {
    validate (fun o -> o.customer) [
        withValidator customerValidator
    ]
    validate (fun o -> o.items) [
        isNotEmpty
        eachItemWith (createValidatorFor<OrderItem>() {
            validate (fun i -> i.quantity) [
                isGreaterThan 0
            ]
            validate (fun i -> i.product.price) [
                isGreaterThan 0.
            ]
            validate (fun i -> i.product.name) [
                isNotEmpty
                hasMaxLengthOf 128
            ]
        })
    ]
}

To create this validator we’ve introduced three new concepts:

  • Firstly we are referencing the existing customer validator using the withValidator command. This simply takes a validator function.
  • Next we are validating the items in the collection with the eachItemWith command. This also takes a validator function which, in this case, we are creating in line but we could take the same approach as with the customer – declare it separately and simply pass it in here.
  • Finally we are validating the product properties using multi-part property paths e.g. i.product.price. These functions literally are getter functions for our properties – the expressions are evaluated when the property runs and the return type informs what validators can be used.

First lets run this on a valid order:

{
    customer = { name = "Alice" ; dateOfBirth = DateTime.UtcNow.AddYears(-1) }
    items = [
        {
            quantity = 1
            product = { name = "Baked Beans" ; price = 3.50 }
        }
    ]
}
|> orderValidator
|> outputToConsole

This produces the output:

No errors

However lets now give the customer an empty name and set the price of a product to 0:

{
    customer = { name = "" ; dateOfBirth = DateTime.UtcNow.AddYears(-1) }
    items = [
        {
            quantity = 1
            product = { name = "Baked Beans" ; price = 0.0 }
        }
    ]
}
|> orderValidator
|> outputToConsole

When we run this we get the following (I’ve tidied up the standard output slightly for clarity):

[
    {
        message = "Must be greater than 0";
        property = "items.[0].product.price";
        errorCode = "isGreaterThan";
    };
    {
        message = "Must not be empty";
        property = "customer.name";
        errorCode = "isNotEmpty";
    }
]

We can see in the results that the expected validations have failed and that the paths to those properties identify the source of the error including the item index in the collection.

Custom Validators

Lets extend our customer validator to ensure the customer placing the order is over 18. We’ve stored their date of birth and so will need to calculate their age as part of the validation. We can do that by adding a custom validator.

Validators have a signature of string -> ‘propertyType -> ValidationState. The first parameter is the path to the property being validated, the second is the value being validated and the validator must return a ValidationState which is a discriminated union with cases of Ok and Errors.

That being the case our 18 or over validator will look like this:

let is18OrOlder propertyPath (value:DateTime) =
    let today = DateTime.Today
    let age = today.Year - value.Year - (match value.Date > today.Date with | true -> 1 | false -> 0)  
    match age >= 18 with
    | true -> Ok
    | false -> Errors([{ errorCode = "is18OrOlder" ; message="Not 18" ; property = propertyPath }])

And we can use it like any other validator:

let customerValidator = createValidatorFor<Customer>() {
    validate (fun c -> c.name) [
        isNotEmpty
        hasMaxLengthOf 128
    ]
    validate (fun c -> c.dateOfBirth) [
        is18OrOlder
    ]
}

You’ll notice that some validators take parameters – how do we do this? To accomplish that we write a function that accepts our validation parameters and returns the validator function. Lets change our validator to check if someone is over a parameterised age:

let isOverAge age =
    let validator propertyPath (value:DateTime) =
        let today = DateTime.Today
        let age = today.Year - value.Year - (match value.Date > today.Date with | true -> 1 | false -> 0)  
        match age >= age with
        | true -> Ok
        | false -> Errors([{ errorCode = "is18OrOlder" ; message="Not 18" ; property = propertyPath }])
    validator
        
let customerValidator = createValidatorFor<Customer>() {
    validate (fun c -> c.name) [
        isNotEmpty
        hasMaxLengthOf 128
    ]
    validate (fun c -> c.dateOfBirth) [
        isOverAge 18
    ]
}

Conditional Validation

Let’s now expand on our age related validation and rather than say all customers must be 18 or over let’s change the validation to be customers 18 or over cannot buy products with a name of Alcohol. We can use the validateWhen command to introduce this additional logic:

let calculateAge (dateOfBirth:DateTime) =
    let today = DateTime.Today
    let age = today.Year - dateOfBirth.Year - (match dateOfBirth.Date > today.Date with | true -> 1 | false -> 0)
    age
let orderValidator = createValidatorFor<Order>() {
    validate (fun o -> o.customer) [
        withValidator (createValidatorFor<Customer>() {
            validate (fun c -> c.name) [
                isNotEmpty
                hasMaxLengthOf 128
            ]
        })
    ]
    validate (fun o -> o.items) [
        isNotEmpty
        eachItemWith (createValidatorFor<OrderItem>() {
            validate (fun i -> i.quantity) [
                isGreaterThan 0
            ]
            validate (fun i -> i.product.price) [
                isGreaterThan 0.
            ]
            validate (fun i -> i.product.name) [
                isNotEmpty
                hasMaxLengthOf 128
            ]
        })
    ]
    validateWhen (fun o -> (o.customer.dateOfBirth |> calculateAge) < 18) (fun o -> o.items) [
        eachItemWith (createValidatorFor<OrderItem>() {                
            validate (fun i -> i.product.name) [
                isNotEqualTo "Alcohol"
            ]
        })
    ]
}

In this example our existing validations for the collection items will all run but if the customer is under 18 then we will run an additional block of validations. We can see this by running an order through the validator that includes a quantity of an item set to 0 and a product named Alcohol (for a customer younger than 18!):

{
    customer = { name = "Alice" ; dateOfBirth = DateTime.UtcNow.AddYears(-1) }
    items = [
        {
            quantity = 1
            product = { name = "Baked Beans" ; price = 3.50 }
        }
        {
            quantity = 0
            product = { name = "Alcohol" ; price = 10.50 }
        }
    ]
}
|> orderValidator
|> outputToConsole

This produces the following output:

[
    {
        message = "Must not be equal to Alcohol";
        property = "items.[1].product.name";
        errorCode = "isNotEqualTo";
    };
    {
        message = "Must be greater than 0";
        property = "items.[1].quantity";
        errorCode = "isGreaterThan";
    }
]

Correctly running the conditional and non-conditional validations for our younger customer.

Other Features and Finishing Off

The framework includes other features such as support for discriminated unions, lambda validators and additional conditional support which is all documented on the frameworks homepage.

This really was just something that “sprang out” and I’d love to hear your feedback over on Twitter.

Posted in F#

Contact

  • If you're looking for help with C#, .NET, Azure, Architecture, or would simply value an independent opinion then please get in touch here or over on Twitter.

Recent Posts

Recent Tweets

Recent Comments

Archives

Categories

Meta

GiottoPress by Enrique Chavez