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
recordPerson{
publicstring First {get; }
publicstring 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;
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.
recordBaseId : IComparable<BaseId>{
protected Guid _id {get; init;};
publicvirtual BaseId Default(){
returnnew BaseId{ _id = default()};
}
publicvirtual BaseId New(){
//...exposing new makes it hard to use sequential id types like int, but you may not care }
publicint CompareTo(BaseId other){
//... }
}
recordChildId : BaseId, IComparable<ChildId>{
// Notice how the override returns the child type. That wasn't possible in C# 8.publicoverride ChildId Default(){
returnnew ChildId{ _id = default()};
}
publicoverride ChildId New(){
returnnew ChildId{_id = base.New()._id};
}
publicint CompareTo(ChildId other){
//... }
}
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!