I previously wrote on the difficulties of abstracting ID types in C#. Good news, C# 9 record types greatly simplify this design decision.
Record types bring the functional definition of data to C#. That is, they are immutable with value-based equality semantics.
In other words
1
2
3
4
5
6
| record Person{
public string First {get; }
public string Last {get; }
}
bool equal = new Person { First = "Bob", Last = "Person"} == new Person {First = "Bob", Last = "Person"}; // true
|
And
1
2
3
4
| Person original = new Person { First = "Bob", Last = "Person"};
Person updated = original with { First = "Robert"};
bool equal = original == updated; // false;
|
This makes id types pretty trivial
1
2
3
4
5
6
7
8
9
| record PersonId : IComparable<PersonId>{
private Guid _id {get; init;}
public static readonly PersonId Default = new PersonId { _id = Guid.Empty };
public int CompareTo(PersonId other){
return _id.CompareTo(other._id);
}
}
|
All we have to do is implement CompareTo
and expose a default value, everything else is automatic.
But wait, aren’t structs suppose to have value-semantics? Ids wouldn’t be too big for a value-type either (records are reference types).
Well… structs have value semantics, but only implement .Equal()
. Any operators like ==
and !=
have to custom implemented, which means you also have to implement GetHashCode
. It ends up being a lot of boiler-plate code.
Inheritance and Covariance#
C# 9 also introduces covariant overrides, which make inter-operable ID types relatively simple too.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
| record BaseId : IComparable<BaseId>{
protected Guid _id {get; init;};
public virtual BaseId Default(){
return new BaseId{ _id = default()};
}
public virtual BaseId New(){
//...exposing new makes it hard to use sequential id types like int, but you may not care
}
public int CompareTo(BaseId other){
//...
}
}
record ChildId : BaseId, IComparable<ChildId>{
// Notice how the override returns the child type. That wasn't possible in C# 8.
public override ChildId Default(){
return new ChildId{ _id = default()};
}
public override ChildId New(){
return new ChildId{_id = base.New()._id};
}
public int CompareTo(ChildId other){
//...
}
}
|
Closing thoughts#
I normally don’t spend much time ogling over language features, but I gotta say that C# 9 introduces some powerful changes.
Just these two improvements allow me to much better encode my design intent. At last I will have practical ID abstractions!