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.