all Technical posts

Advanced and Realistic Domain Model Validation Building Blocks in F# FPrimitive with C# Interop

Model validation is a big and important topic. Lots of application security issues are related to input validation. See how FPrimitive transcends simple validations and can be used in more advanced scenarios.

FPrimitive is a F# library of mine with full C# support. It handles all kinds of model validations by defining specifications for your model. In this blog post, I’m going to assume that you have a basic understanding of the library and of domain model validation. If not, I recommend a couple of my other posts to get you started.

Optional Types

Optional types are types that are part of the model but are not necessary to create a valid model. In the context of Corona, let’s create a model with such an example. Consider a guest at a concert that has to show his Corona Pass before entering. The guest either has or hasn’t a pass with them, but still remains a ‘valid’ possible guest.

type CoronaPass =
| Vaccinated
| Tested
| Recovered
type Guest =
{ Pass : CoronaPass option }

You’ll see that the pass is optional here to create a guest model. With the FPrimitive library, we can easily model this with the Spec.optional function. In this case, we use the Option.ofObj to determine if the incoming `string` is null. In that case, None will be used. The Union.create function is also available in the FPrimitive library and lets you create Discriminated Unions based on string values. It creates an Result based on the result, which makes it perfectly combinable with the specification functions.

open FPrimitive
type Guest =
{ Pass : CoronaPass option } with
static member create pass = result {
let! pass = Spec.optional Option.ofObj Union.create<CoronaPass> pass
return { Pass = pass } }

With the Spec.optional you can therefore choose to only validate a certain input when a function ('a -> 'b option) holds. It makes it a good specification combinator for optional types while doing domain modeling.

Invariants

Domain invariants are relationships within the model. Rules about two or more sets of models. This relationship should also be expressed fluently and descriptive in the domain. FPrimitive made this happen. The Spec.invariant takes in two specifications and creates a new specification of the combination. There are is also a function to take in 3 specifications. If more is needed, the system is flexible enough to create any more combinations you want.

A good and simple example of such an invariant, is designing a range model with a minimum and maximum value. The domain invariant in this case will make sure that the minimum is always smaller than the maximum value.

open FPrimitive
type Int100 =
private Int100 of int with
static member spec = spec {
inclusiveBetween 1 100 "integer should be between 1-100 inclusive" }
type Range =
{ Min : Int100
Max : Int100 } with
static member create min max =
Spec.invariant Int100.spec Int100.spec
|> Spec.verify (fun (min, max) -> min < max) "minimum should be less than maximum"
|> Spec.createModel (fun (min, max) -> { Min = Int100 min; Max = Int100 max }) (min, max)

Dependencies

Dependency validations in the context of domain modeling will make sure that a dependency is validated before the main validation. This concept is important when handling rather complex validations and/or you want to re-use specifications. In that case, FPrimitive has several combinators that let you define specification dependencies that will run before the specification you define.

Imagine you want to model that represents a series of text. There will often be similar things that you need to check, such as blank input, special characters. In those cases, we can make use of the dependency validation.
This makes for great re-use of specifications.

open FPrimitive
module Spec =
let reasonablString tag = specTag tag {
notNullOrWhiteSpace $"{tag} should not be blank"
alphabethical $"{tag} should only contain alphabethical characters" }
type Title =
private Title of string with
static member create x = specModel Title x {
dependsOn (Spec.reasonableString "title")
stringLengthMax 100 "title should not be longer than 100 characters" }

Conclusion

In this post I’ve barely scratched the surface of domain modeling with FPrimitive. There are a lot of advanced combinators to describe complex models. Spec.structure, Spec.filter, Spec.subset, Spec.listetc. are all great combinators too, but that will be for another time.

Please consider F# in your next project, even if it’s just for domain modeling and internal business rules. I strongly believe that the combination of F#/C# is the sweet spot we’re all trying to find.

Thanks for reading!
Stijn

P.S. Here’s what this looks like in C#:

using FPrimitive;
public enum CoronaPass { Vaccinated, Tested, Recovered }
public class Guest
{
private Guest(Maybe<CoronaPass> pass) { Pass = pass; }
public Maybe<CoronaPass> Pass { get; }
public static ValidationResult<Guest> Create(string pass)
=> Spec.Optional(Maybe.JustOrNothing, x =>
{
return Enum.TryParse(x, out CoronaPass result)
? ValidationResult<CoronaPass>.Success(result)
: ValidationResult<CoronaPass>.Failure("could not parse value to Corona pass");
}, pass)
.Select(maybePass => new Guest(maybePass));
}
public class Range
{
private readonly int _min, _max;
private Range(int min, int max) { _min = min; _max = max; }
public static ValidationResult<Range> Create(int min, int max)
{
var int100 = Spec.Of<int>().InclusiveBetween(1, 100, "integer should be between 1-100 inclusive");
return Spec.Invariant(int100, int100)
.Add(t => t.Item1 < t.Item2, "minimum should be less than maximum")
.CreateModel((min, max), t => new Range(t.Item1, t.Item2));
}
}
public static class Specs
{
public static Spec<string> CreateReasonableStringSpec(string tag)
=> Spec.Of<string>(tag)
.NotNullOrWhiteSpace("should not be blank")
.Alphabetical("should only contain characters of the alphabet");
}
public class Title
{
private readonly string _title;
private Title(string title) { _title = title; }
public static ValidationResult<Title> Create(string title)
=> Spec.Of<string>()
.DependsOn(Specs.CreateReasonableStringSpec("title"))
.LengthMax(100, "should not be greater than 100 characters")
.CreateModel(title, value => new Title(value));
}

Subscribe to our RSS feed

Hi there,
how can we help?

Got a project in mind?

Connect with us

Let's talk

Let's talk

Thanks, we'll be in touch soon!

Call us

Thanks, we've sent the link to your inbox

Invalid email address

Submit

Your download should start shortly!

Stay in Touch - Subscribe to Our Newsletter

Keep up to date with industry trends, events and the latest customer stories

Invalid email address

Submit

Great you’re on the list!