Strongly typed Id in f#
Motivation
Way too often I see code where someone defines an id as a simple int
or Guid
or something along those lines. This is a classic case of the Primitive Obsession smell and has a variety of potential problems, as well as may pollute your code with Guard clauses.
I am in the process of trying to create a Kanban board to explore Event Sourcing and Domain-Driven Design and learn F#. Here I need an Id on my Board
type, to put on Events and pass around.
In-depth motivation
You might be able to skip this part. These are just my personal key points from the articles linked above. Reasons to have a type-strong id:
You don't want to be able to pass a
ColumnId
in a context where aBoardId
is required.Whether a
BoardId
is aGuid
,int
,string
, or something else, is an implementation detail. Any consumer of the domain doesn't need/want to know.It improves readability. It's quite obvious what kind of Id a method takes if it is strongly typed.
If it's an int, you probably don't want to allow a negative integer. Moving this validation into a
BoardId
type is nice.
The Solution
Initial (WRONG) solution
I initially expected to be able to do this.
type BoardId = Guid
It seemed like it would work initially, however you can then cast them back and forth implicitly. This is simply a type alias, which doesn't solve any problems regarding actual encapsulation, misuse or validation.
The (Minimal) Solution
After searching for some time I found this StackOverflow post
[<Struct>]type ProductId = ProductId of Guid
The (Better) Solution
Improving a bit on their result (which included helper methods), I've added additional helpers and made this a bit stronger, covering the use cases that I had.
namespace Fanban.Domainopen System[<Struct>]type BoardId = private | BoardId of Guid static member New() = BoardId(Guid.NewGuid()) static member Parse (value: string) = BoardId(Guid.Parse(value)) static member TryParse (value: string) = let couldParse, result = Guid.TryParse(value) if couldParse then Some (BoardId result) else None member this.Value = let (BoardId i) = this in i override this.ToString() = this.Value.ToString()
This makes it possible to write code like:
let CreateBoardEvent name (columns: ColumnName list) = { Id = BoardId.New() Name = name ColumnNames = columns }
and types can define the id type very explicitly:
and SetBoardNameEvent = { BoardId: BoardId Name: string }