A raw port of the previous F# demo code.

This article is part of a short article series on applicative validation with a twist. The twist is that validation, when it fails, should return not only a list of error messages; it should also retain that part of the input that was valid.

In the previous article I showed F# demo code, and since the original forum question that prompted the article series was about F# code, for a long time, I left it there.

Recently, however, I've found myself writing about validation in a broader context:

Perhaps I should consider adding a validation tag to the blog...

In that light I thought that it might be illustrative to continue this article series with a port to C#.

Here, I use techniques already described on this site to perform the translation. Follow the links for details.

The translation given here is direct so produces some fairly non-idiomatic C# code.

Building blocks #

The original problem is succinctly stated, and I follow it as closely as possible. This includes potential errors that may be present in the original post.

The task is to translate some input to a Domain Model with good encapsulation. The input type looks like this, translated to a C# record:

public sealed record Input(stringName, DateTime? DoBstringAddress)

Notice that every input may be null. This indicates poor encapsulation, but is symptomatic of most input. At the boundaries, static types are illusory. Perhaps it would have been more idiomatic to model such input as a Data Transfer Object, but it makes little difference to what comes next.

I consider validation a solved problem, because it's possible to model the process as an applicative functor. Really, validation is a parsing problem.

Since my main intent with this article is to demonstrate a technique, I will allow myself a few shortcuts. Like I did when I first encountered the Args kata, I start by copying the Validated code from An applicative reservation validation example in C#; you can go there if you're interested in it. I'm not going to repeat it here.

The target type looks similar to the above Input record, but doesn't allow null values:

public sealed record ValidInput(string Name, DateTime DoBstring Address);

This could also have been a 'proper' class. The following code doesn't depend on that.

Validating names #

Since I'm now working in an ostensibly object-oriented language, I can make the various validation functions methods on the Input record. Since I'm treating validation as a parsing problem, I'm going to name those methods with the TryParse prefix:

private Validated<(Func<Input, Input>, IReadOnlyCollection<string>), string>
    TryParseName()
{
    if (Name is null)
        return Validated.Fail<(Func<Input, Input>, IReadOnlyCollection<string>), string>(
            (x => x, new[] { "name is required" }));
    if (Name.Length <= 3)
        return Validated.Fail<(Func<Input, Input>, IReadOnlyCollection<string>), string>(
            (i => i with { Name = null }, new[] { "no bob and toms allowed" }));
 
    return Validated.Succeed<(Func<Input, Input>, IReadOnlyCollection<string>), string>(Name);
}

As the two previous articles have explained, the result of trying to parse input is a type isomorphic to Either, but here called Validated<FS>. (The reason for this distinction is that we don't want the monadic behaviour of Either, because monads short-circuit.)

When parsing succeeds, the TryParseName method returns the Name wrapped in a Success case.

Parsing the name may fail in two different ways. If the name is missing, the method returns the input and the error message "name is required". If the name is present, but too short, TryParseName returns another error message, and also resets Name to null.

Compare the C# code with the corresponding F# or Haskell code and notice how much more verbose the C# has to be.

While it's possible to translate many functional programming concepts to a language like C#, syntax does matter, because it affects readability.

Validating date of birth #

From here, the port is direct, if awkward. Here's how to validate the date-of-birth field:

private Validated<(Func<Input, Input>, IReadOnlyCollection<string>), DateTime>
    TryParseDoB(DateTime now)
{
    if (!DoB.HasValue)
        return Validated.Fail<(Func<Input, Input>, IReadOnlyCollection<string>), DateTime>(
            (x => x, new[] { "dob is required" }));
    if (DoB.Value <= now.AddYears(-12))
        return Validated.Fail<(Func<Input, Input>, IReadOnlyCollection<string>), DateTime>(
            (i => i with { DoB = null }, new[] { "get off my lawn" }));
 
    return Validated.Succeed<(Func<Input, Input>, IReadOnlyCollection<string>), DateTime>(
        DoB.Value);
}

I suspect that the age check should really have been a greater-than relation, but I'm only reproducing the original code.

Validating addresses #

The final building block is to parse the input address:

private Validated<(Func<Input, Input>, IReadOnlyCollection<string>), string>
    TryParseAddress()
{
    if (Address is null)
        return Validated.Fail<(Func<Input, Input>, IReadOnlyCollection<string>), string>(
            (x => x, new[] { "add1 is required" }));
 
    return Validated.Succeed<(Func<Input, Input>, IReadOnlyCollection<string>), string>(
        Address);
}

The TryParseAddress only checks whether or not the Address field is present.

Composition #

The above methods are private because the entire problem is simple enough that I can test the composition as a whole. Had I wanted to, however, I could easily have made them public and tested them individually.

You can now use applicative composition to produce a single validation method:

public Validated<(Input, IReadOnlyCollection<string>), ValidInput>
    TryParse(DateTime now)
{
    var name = TryParseName();
    var dob = TryParseDoB(now);
    var address = TryParseAddress();
 
    Func<string, DateTime, string, ValidInput> createValid =
        (nda) => new ValidInput(n, d, a);
    static (Func<Input, Input>, IReadOnlyCollection<string>) combineErrors(
        (Func<Input, Input> f, IReadOnlyCollection<string> es) x,
        (Func<Input, Input> g, IReadOnlyCollection<string> es) y)
    {
        return (z => y.g(x.f(z)), y.es.Concat(x.es).ToArray());
    }
 
    return createValid
        .Apply(name, combineErrors)
        .Apply(dob, combineErrors)
        .Apply(address, combineErrors)
        .SelectFailure(x => (x.Item1(this), x.Item2));
}

This is where the Validated API is still awkward. You need to explicitly define a function to compose error cases. In this case, combineErrors composes the endomorphisms and concatenates the collections.

The final step 'runs' the endomorphism. x.Item1 is the endomorphism, and this is the Input value being validated. Again, this isn't readable in C#, but it's where the endomorphism removes the invalid values from the input.

Tests #

Since applicative validation is a functional technique, it's intrinsically testable.

Testing a successful validation is as easy as this:

[Fact]
public void ValidationSucceeds()
{
    var now = DateTime.Now;
    var eightYearsAgo = now.AddYears(-8);
    var input = new Input("Alice", eightYearsAgo, "x");
 
    var actual = input.TryParse(now);
 
    var expected = Validated.Succeed<(Input, IReadOnlyCollection<string>), ValidInput>(
        new ValidInput("Alice", eightYearsAgo, "x"));
    Assert.Equal(expected, actual);
}

As is often the case, the error conditions are more numerous, or more interesting, if you will, than the success case, so this requires a parametrised test:

[Theory, ClassData(typeof(ValidationFailureTestCases))]
public void ValidationFails(
    Input input,
    Input expected,
    IReadOnlyCollection<stringexpectedMessages)
{
    var now = DateTime.Now;
 
    var actual = input.TryParse(now);
 
    var (inpmsgs) = Assert.Single(actual.Match(
        onFailure: x => new[] { x },
        onSuccess: _ => Array.Empty<(Input, IReadOnlyCollection<string>)>()));
    Assert.Equal(expected, inp);
    Assert.Equal(expectedMessages, msgs);
}

I also had to take actual apart in order to inspects its individual elements. When working with a pure and immutable data structure, I consider that a test smell. Rather, one should be able to use structural equality for better tests. Unfortunately, .NET collections don't have structural equality, so the test has to pull the message collection out of actual in order to verify it.

Again, in F# or Haskell you don't have that problem, and the tests are much more succinct and robust.

The test cases are implemented by this nested ValidationFailureTestCases class:

private class ValidationFailureTestCases :
    TheoryData<Input, Input, IReadOnlyCollection<string>>
{
    public ValidationFailureTestCases()
        {
            Add(new Input(nullnullnull),
                new Input(nullnullnull),
                new[] { "add1 is required""dob is required""name is required" });
            Add(new Input("Bob"nullnull),
                new Input(nullnullnull),
                new[] { "add1 is required""dob is required""no bob and toms allowed" });
            Add(new Input("Alice"nullnull),
                new Input("Alice"nullnull),
                new[] { "add1 is required""dob is required" });
            var eightYearsAgo = DateTime.Now.AddYears(-8);
            Add(new Input("Alice", eightYearsAgo, null),
                new Input("Alice", eightYearsAgo, null),
                new[] { "add1 is required" });
            var fortyYearsAgo = DateTime.Now.AddYears(-40);
            Add(new Input("Alice", fortyYearsAgo, "x"),
                new Input("Alice"null"x"),
                new[] { "get off my lawn" });
            Add(new Input("Tom", fortyYearsAgo, "x"),
                new Input(nullnull"x"),
                new[] { "get off my lawn""no bob and toms allowed" });
            Add(new Input("Tom", eightYearsAgo, "x"),
                new Input(null, eightYearsAgo, "x"),
                new[] { "no bob and toms allowed" });
        }
}

All eight tests pass.

Conclusion #

Once you know how to model sum types (discriminated unions) in C#, translating something like applicative validation isn't difficult per se. It's a fairly automatic process.

The code is hardly idiomatic C#, and the type annotations are particularly annoying. Things work as expected though, and it isn't difficult to imagine how one could refactor some of this code to a more idiomatic form.



Wish to comment?

You can add a comment to this post by sending me a pull request. Alternatively, you can discuss this post on Twitter or somewhere else with a permalink. Ping me with the link, and I may respond.

Published

Monday, 30 October 2023 11:52:00 UTC

Tags



"Our team wholeheartedly endorses Mark. His expert service provides tremendous value."
Hire me!
Published: Monday, 30 October 2023 11:52:00 UTC