ArtShowTools: Part 2

Prototyping the Artwork entity in F#

In my previous post (ArtShowTools: Part 1), I created the first draft of the domain model. In this post, I'll describe some of the prototyping I've been doing in F#. While I've worked with C# for nearly 20 years, I've only been working with F# for about a year. I did last year's Advent of Code 2022 using F# after reading the excellent book Domain Modeling Made Functional. Based on that experience, I decided to use F# for the ArtShowTools application but there's a definite learning curve.

The Artwork Entity

I decided to do a simple implementation of the Artwork entity to answer some of the questions I asked in my previous post.

Here's the pseudocode for the Artwork entity implementation. At this stage, I'm only going include a couple of the simpler fields.

// Entity
Artwork {
    Id (guid)
    Title (string)
    Year (integer)
}

I picked a few of the use cases from the domain model and added some requirements based on my discussion with the domain expert.

  • Create Artwork.

  • Change Artwork title.

    • The title cannot be empty.

    • The title cannot exceed more than 128 characters.

  • Change Artwork year.

    • The year is in the Gregorian calendar.

    • The year cannot be less than 1.

To satisfy the use cases, I'll need to implement some commands to handle the state changes and some events to model the state changes.

// Commands 
Create Artwork {
    Id (guid)
    Title (string)
    Year (integer)
}

Change Artwork Title {
    Id (guid)
    Title (string)
}

Change Artwork Year {
    Id (guid)
    Year (integer)
}
// Events
Artwork Created {
    Id (guid)
    Title (string)
    Year (integer)
}

Artwork Title Changed {
    Id (guid)
    Title (string)
}

Artwork Year Changed {
    Id (guid)
    Year (integer)
}

Finally, I'll need to model the errors associated with the execution of the command and the application of the events. Most examples of event sourcing I've found online use exceptions, but I plan on using railway-oriented programming (ROP) for error handling as F# provides excellent support with its Result type.

// Errors
Artwork Already Exists
Artwork Does Not Exist
Wrong Artwork
Invalid Artwork Title
Invalid Artwork Year

Modeling the State of the Artwork

One of the questions I posed in the previous post was how to handle the state of an Artwork. As I pondered the implementation, my first thought was to use an enumeration field on the Artwork entity. But I discarded this approach in favor of implementing the Artwork entity as an "OR" type in F# (i.e. a discriminated union). The benefit of this approach was two-fold. First, the use of a discriminated union allows for different representations of the Artwork entity with possibly more or fewer data depending on its state. Second, the use of a discriminated union makes adding new states (or removing existing states) much easier.

The use of discriminated unions does not end with the Artwork entity implementation. Commands, events, errors, and even value objects will be implemented as discriminated unions.

The Types

First, I'll convert the pseudocode above into F# records. I'll need some value objects to represent the fields for the Artwork entity (I'll add validation in the future). I'm avoiding primitive obsession to improve type safety and consolidate validation rules into a single location. For more information, check out Vladimir Khorikov's excellent article on the subject: Functional C#: Primitive obsession.

open System

type ArtworkId = ArtworkId of Guid
type ArtworkTitle = ArtworkTitle of string
type ArtworkYear = ArtworkYear of int

Next, I'll create the types for the commands, events, and errors using discriminated unions.

type ArtworkCommand =
    | Create of ArtworkCreate
    | ChangeTitle of ArtworkChangeTitle
    | ChangeYear of ArtworkChangeYear
and ArtworkCreate = {
    Id: ArtworkId
    Title: ArtworkTitle
    Year: ArtworkYear
}
and ArtworkChangeTitle = {
    Id: ArtworkId
    Title: ArtworkTitle
}
and ArtworkChangeYear = {
    Id: ArtworkId
    Year: ArtworkYear
}

type ArtworkEvent =
    | Created of ArtworkCreated
    | TitleChanged of ArtworkTitleChanged
    | YearChanged of ArtworkYearChanged
and ArtworkCreate = {
    Id: ArtworkId
    Title: ArtworkTitle
    Year: ArtworkYear
}
and ArtworkTitleChanged = {
    Id: ArtworkId
    Title: ArtworkTitle
}
and ArtworkYearChanged = {
    Id: ArtworkId
    Year: ArtworkYear
}

Next, I'll implement the error types as a discriminated union. The first two errors occur when the command is applied to the wrong state of the Artwork entity. The last command occurs when the entity ID in the command does not match the ID of the entity the command is executing on. This prevents a command from being applied to the wrong entity.

type ArtworkError = 
    | ArtworkAlreadyExists
    | ArtworkDoesNotExist
    | WrongArtwork

Finally, I'll define the Artwork entity using a discriminated union.

type Artwork =
    | Initial
    | Existing of ArtworkInfo
and ArtworkInfo = {
    Id: ArtworkId
    Title: ArtworkTitle
    Year: Artwork
}

The Initial state represents a potential artwork while the Existing state represents an actual artwork. The CreateArtwork command will transition the state of Artwork entity from Initial to Existing when it is executed by the command handler.

Note that the two states of the Artwork entity contain different amounts of data which I find makes the implementation much simpler to understand. If I simply implemented the Artwork entity as a single type with a State field to represent the different types, I would have to define valid values for the other fields for all of the possible states. This problem is partially alleviated by the use of value objects for the fields as I could define instances of each type to represent valid values. For example, for the Initial state of the Artwork, I could define ArtworkId.Initial, ArtworkTitle.Initial, and ArtworkYear.Initial and default the Artwork fields to those values. But this approach quickly become unmanageable as the number of states increases.

Implementing the Command Handler

With the types defined, the next step is to implement the command handler. The command handler takes a command and an Artwork entity as inputs and outputs a set of events or an error.

First I implement a skeleton method to define the method signature with a default return value.

module Artwork =    
    let handle artwork command : Result<ArtworkEvent list, ArtworkError> =
        Ok List.empty

Next, I implement a pair of tests to validate the logic for the ArtworkCommand.Create command. I love F#'s ability to name test methods explicitly and succinctly using the double backtick syntax (Using F# for testing).

open System
open NUnit.Framework

let id = Guid("350EA2A6-6316-44DE-9316-2D545E5CA2C5") |> ArtworkId
let title = "Title" |> ArtworkTitle
let year = 2023 |> ArtworkYear
let artwork = Existing { Id = id; Title = title; Year = year }

[<Test>]
let ``Artwork.handle Create fails if artwork exists`` () =
    let command = Create { Id = id; Title = title; Year = year }
    let result = command |> Artwork.handle artwork
    match result with
    | Ok _ -> Assert.Fail("command should fail")
    | Error error ->
        match error with
        | ArtworkAlreadyExists -> ()
        | _ -> Assert.Fail("wrong error type")

[<Test>]
let ``Artwork.handle Create succeeds`` () =
    let command = Create { Id = id; Title = title; Year = year }
    let result = command |> Artwork.handle Initial
    match result with
    | Ok events ->
        Assert.That(events.Length, Is.EqualTo(1))
        match events |> List.head with
        | Created created ->
            Assert.That(created.Id, Is.EqualTo(id))
            Assert.That(created.Title, Is.EqualTo(title))
            Assert.That(created.Year, Is.EqualTo(year))
        | _ -> Assert.Fail("unexpected event type")
    | Error _ -> Assert.Fail("command should succeed")

Next, I implement the logic for handling ArtworkCommand.Create command.

module Artwork =
    let handle artwork command : Result<ArtworkEvent list, ArtworkError> =
        match command with
        | Create create ->
            match artwork with
            | Initial _ ->
                Ok(
                    Created
                        { Id = create.Id
                          Title = create.Title
                          Year = create.Year }
                    |> List.singleton
                )
            | Existing _ -> Error ArtworkAlreadyExists
        | _ -> Ok List.empty

Then, I run the tests in the JetBrains Rider IDE to verify the behavior is correct.

Finally, I implemented the ArtworkCommand.ChangeTitle and ArtworkCommand.ChangeYear commands and verify their behavior with unit tests.

module Artwork =
    let handle artwork command : Result<ArtworkEvent list, ArtworkError> =
        match command with
        | Create create ->
            match artwork with
            | Initial _ ->
                Ok(
                    Created
                        { Id = create.Id
                          Title = create.Title
                          Year = create.Year }
                    |> List.singleton
                )
            | Existing _ -> Error ArtworkAlreadyExists
        | ChangeTitle changeTitle ->
            match artwork with
            | Initial _ -> Error ArtworkDoesNotExist
            | Existing existing ->
                if (existing.Id = changeTitle.Id) then
                    Ok(
                        TitleChanged
                            { Id = changeTitle.Id
                              Title = changeTitle.Title }
                        |> List.singleton
                    )
                else
                    Error WrongArtwork
        | ChangeYear changeYear ->
            match artwork with
            | Initial -> Error ArtworkDoesNotExist
            | Existing existing ->
                if (existing.Id = changeYear.Id) then
                    Ok(
                        YearChanged
                            { Id = changeYear.Id
                              Year = changeYear.Year }
                        |> List.singleton
                    )
                else
                    Error WrongArtwork
open System
open NUnit.Framework

let id = Guid("350EA2A6-6316-44DE-9316-2D545E5CA2C5") |> ArtworkId
let title = "Title" |> ArtworkTitle
let year = 2023 |> ArtworkYear
let artwork = Existing { Id = id; Title = title; Year = year }
let wrongId = Guid("D96CD8A5-E757-4798-8753-769E710AFA5A") |> ArtworkId
let wrongArtwork = Existing { Id = wrongId; Title = title; Year = year }
let newTitle = "New Title" |> ArtworkTitle
let newYear = 2025 |> ArtworkYear

[<Test>]
let ``Artwork.handle Create fails if artwork exists`` () =
    let command = Create { Id = id; Title = title; Year = year }
    let result = command |> Artwork.handle artwork
    match result with
    | Ok _ -> Assert.Fail("command should fail")
    | Error error ->
        match error with
        | ArtworkAlreadyExists -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.handle Create succeeds`` () =
    let command = Create { Id = id; Title = title; Year = year }
    let result = command |> Artwork.handle Initial
    match result with
    | Ok events ->
        Assert.That(events.Length, Is.EqualTo(1))
        match events |> List.head with
        | Created created ->
            Assert.That(created.Id, Is.EqualTo(id))
            Assert.That(created.Title, Is.EqualTo(title))
            Assert.That(created.Year, Is.EqualTo(year))
        | _ -> Assert.Fail("unexpected event type")
    | Error _ -> Assert.Fail("command should succeed")

[<Test>]
let ``Artwork.handle ChangeTitle fails if artwork does not exist`` () =
    let command = ChangeTitle { Id = id; Title = newTitle }
    let result = command |> Artwork.handle Initial
    match result with
    | Ok _ -> Assert.Fail("command should not succeed")
    | Error error ->
        match error with
        | ArtworkDoesNotExist -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.handle ChangeTitle fails on wrong ID`` () =
    let command = ChangeTitle { Id = id; Title = newTitle }
    let result = command |> Artwork.handle wrongArtwork
    match result with
    | Ok _ -> Assert.Fail("command should not succeed")
    | Error error ->
        match error with
        | WrongArtwork -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.handle ChangeTitle succeeds`` () =
    let command = ChangeTitle { Id = id; Title = newTitle }
    let result = command |> Artwork.handle artwork
    match result with
    | Ok events ->
        Assert.That(events.Length, Is.EqualTo(1))
        match events |> List.head with
        | TitleChanged titleChanged ->
            Assert.That(titleChanged.Id, Is.EqualTo(id))
            Assert.That(titleChanged.Title, Is.EqualTo(newTitle))
        | _ -> Assert.Fail("unexpected event type")
    | Error _ -> Assert.Fail("command should succeed")

[<Test>]
let ``Artwork.handle ChangeYear fails if artwork does not exist`` () =
    let command = ChangeYear { Id = id; Year = newYear }
    let result = command |> Artwork.handle Initial
    match result with
    | Ok _ -> Assert.Fail("command should not succeed")
    | Error error ->
        match error with
        | ArtworkDoesNotExist -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.handle ChangeYear fails on wrong ID`` () =
    let command = ChangeYear { Id = id; Year = newYear }
    let result = command |> Artwork.handle wrongArtwork
    match result with
    | Ok _ -> Assert.Fail("command should not succeed")
    | Error error ->
        match error with
        | WrongArtwork -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.handle ChangeYear succeeds`` () =
    let command = ChangeYear { Id = id; Year = newYear }
    let result = command |> Artwork.handle artwork
    match result with
    | Ok events ->
        Assert.That(events.Length, Is.EqualTo(1))
        match events |> List.head with
        | YearChanged yearChanged ->
            Assert.That(yearChanged.Id, Is.EqualTo(id))
            Assert.That(yearChanged.Year, Is.EqualTo(newYear))
        | _ -> Assert.Fail("unexpected event type")
    | Error _ -> Assert.Fail("command should not succeed")

Implementing the Event Handler

With event sourcing, commands do not directly change the state of the entity they are executed upon. Instead, the entity is reconstructed by applying the set of events generated by the commands.

Like the command handler implementation, I created a skeleton method first.

module Artwork =
    let private applyEvent result event: Result<Artwork, ArtworkError> =
        result

    let apply artwork events : Result<Artwork, ArtworkError> =
        events |> List.fold applyEvent (Ok artwork)

Next, I implemented unit tests to verify the behavior of the event handler.

open System
open NUnit.Framework

let id = Guid("350EA2A6-6316-44DE-9316-2D545E5CA2C5") |> ArtworkId
let title = "Title" |> ArtworkTitle
let year = 2023 |> ArtworkYear
let artwork = Existing { Id = id; Title = title; Year = year }
let wrongId = Guid("D96CD8A5-E757-4798-8753-769E710AFA5A") |> ArtworkId
let wrongArtwork = Existing { Id = wrongId; Title = title; Year = year }
let newTitle = "New Title" |> ArtworkTitle
let newYear = 2025 |> ArtworkYear

[<Test>]
let ``Artwork.apply Created succeeds`` () =
    let events = Created { Id = id; Title = title; Year = year } |> List.singleton
    let result = events |> Artwork.apply Initial
    match result with
    | Ok updated ->
        match updated with
        | Initial _ -> Assert.Fail("unexpected state")
        | Existing existing ->
            Assert.That(existing.Id, Is.EqualTo(id))
            Assert.That(existing.Title, Is.EqualTo(title))
            Assert.That(existing.Year, Is.EqualTo(year))
    | Error _ -> Assert.Fail("apply should succeed")

[<Test>]
let ``Artwork.apply Created fails if artwork exists`` () =
    let events = Created { Id = id; Title = title; Year = year } |> List.singleton
    let result = events |> Artwork.apply artwork
    match result with
    | Ok _ -> Assert.Fail("apply should not succeed")
    | Error error ->
        match error with
        | ArtworkAlreadyExists -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.apply TitleChanged succeeds`` () =
    let events = TitleChanged { Id = id; Title = newTitle } |> List.singleton
    let result = events |> Artwork.apply artwork
    match result with
    | Ok updated ->
        match updated with
        | Initial _ -> Assert.Fail("unexpected state")
        | Existing existing ->
            Assert.That(existing.Id, Is.EqualTo(id))
            Assert.That(existing.Title, Is.EqualTo(newTitle))
    | Error _ -> Assert.Fail("apply should have succeeded")

[<Test>]
let ``Artwork.apply TitleChanged fails if artwork does not exist`` () =
    let events = TitleChanged { Id = id; Title = newTitle } |> List.singleton
    let result = events |> Artwork.apply Initial
    match result with
    | Ok _ -> Assert.Fail("apply should not have succeeded")
    | Error error ->
        match error with
        | ArtworkDoesNotExist -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.apply TitleChanged fails on wrong ID`` () =
    let events = TitleChanged { Id = id; Title = newTitle } |> List.singleton
    let result = events |> Artwork.apply wrongArtwork
    match result with
    | Ok _ -> Assert.Fail("apply should not have succeeded")
    | Error error ->
        match error with
        | WrongArtwork -> ()
        | _ -> Assert.Fail("unexpected error type")


[<Test>]
let ``Artwork.apply YearChanged succeeds`` () =
    let events = YearChanged { Id = id; Year = newYear } |> List.singleton
    let result = events |> Artwork.apply artwork
    match result with
    | Ok updated ->
        match updated with
        | Initial -> Assert.Fail("unexpected state")
        | Existing existing ->
            Assert.That(existing.Id, Is.EqualTo(id))
            Assert.That(existing.Year, Is.EqualTo(newYear))
    | Error _ -> Assert.Fail("apply should have succeeded")

[<Test>]
let ``Artwork.apply YearChanged fails if artwork does not exist`` () =
    let events = YearChanged { Id = id; Year = newYear } |> List.singleton
    let result = events |> Artwork.apply Initial
    match result with
    | Ok _ -> Assert.Fail("apply should not have succeeded")
    | Error error ->
        match error with
        | ArtworkDoesNotExist -> ()
        | _ -> Assert.Fail("unexpected error type")

[<Test>]
let ``Artwork.apply YearChanged fails on wrong ID`` () =
    let events = YearChanged { Id = id; Year = newYear } |> List.singleton
    let result = events |> Artwork.apply wrongArtwork
    match result with
    | Ok _ -> Assert.Fail("apply should not have succeeded")
    | Error error ->
        match error with
        | WrongArtwork -> ()
        | _ -> Assert.Fail("unexpected error type")

Next, I implemented the event handler.

module Artwork =
    let private applyEvent result event : Result<Artwork, ArtworkError> =
        match result with
        | Error _ -> result
        | Ok artwork ->
            match event with
            | Created created ->
                match artwork with
                | Initial ->
                    Ok(
                        Existing
                            { Id = created.Id
                              Title = created.Title
                              Year = created.Year }
                    )
                | _ -> Error ArtworkAlreadyExists
            | TitleChanged titleChanged ->
                match artwork with
                | Initial -> Error ArtworkDoesNotExist
                | Existing existing ->
                    if (existing.Id = titleChanged.Id) then
                        Ok(Existing { existing with Title = titleChanged.Title })
                    else
                        Error WrongArtwork
            | YearChanged yearChanged ->
                match artwork with
                | Initial -> Error ArtworkDoesNotExist
                | Existing existing ->
                    if (existing.Id = yearChanged.Id) then
                        Ok(Existing { existing with Year = yearChanged.Year })
                    else
                        Error WrongArtwork

    let apply artwork events : Result<Artwork, ArtworkError> =
        events |> List.fold applyEvent (Ok artwork)

Finally, I ran the unit tests and verified the event handler's behavior.

Combining Apply and Handle

With the command and event handlers implemented, the two functions can be consolidated into a single method.

    let applyAndHandle events command : Result<ArtworkEvent list, ArtworkError> =
        events
        |> apply Initial
        |> Result.bind (fun artwork -> command |> handle artwork)

This method will be useful when I implement event persistence with a database later on. The diagram below illustrates the eventual workflow which takes a command, reads the appropriate events from the event store, recreates the Artwork entity from the events, executes the command using the entity, and then writes the events generated from the command handler back to the event store.

Next Steps

Now that the basic implementation for the Artwork entity is in place, several areas of implementation need to be addressed. Validation needs to be added to the current value objects to ensure the state of the Artwork entity is always valid. Value objects like Size, Weight, and Price need to be implemented so they can be added to the Artwork entity. Finally, I need to decide how I'm going to implement the event store to persist the events.