Domain Concepts
Back in 2015, I wrote about concepts. The idea behind these are that you encapsulate types that has meaning to your domain as well known types. Rather than relying on technical types or even primitive types, you then formalize these types as something you use throughout your codebase. This provides you with a few benefits, such as readability and potentially also give you compile time type checking and errors. It does also provide you with a way to adhere to the element of least surprise principle. Its also a great opportunity to use the encapsulation to deal with cross cutting concerns, for instance values that adhere to compliance such as GDPR or security concerns where you want to encrypt while in motion etc.
Throughout the years at the different places I’ve been at were we’ve used these, we’ve evolved this from a very simple implementation to a more evolved one. Both these implementations aims at making it easy to deal with equability and the latter one also with comparisons. That becomes very complex when having to support different types and scenarios.
Luckily now, with C# 9 we got records which lets us truly simplify this:
1
2
3
4
5
6
7
8
9
10
public record ConceptAs<T>
{
public ConceptAs(T value)
{
ArgumentNullException.ThrowIfNull(value, nameof(value));
Value = value;
}
public T Value { get; init; }
}
With record we don’t have to deal with equability nor comparable, it is dealt with automatically - at least for primitive types.
Using this is then pretty straight forward:
1
public record SocialSecurityNumber(string value) : ConceptAs<string>(value);
A full implementation can be found here - an implementation using it here.
Implicit conversions
One of the things that can also be done in the base class is to provide an implicit operator for converting from ConeptAs type to the underlying type (e.g. Guid). Within an implementation you could also provide the other way, going from the underlying type to the specific. This has some benefits, but also some downsides. If you want the compiler to catch errors - obviously, if all yours **ConceptAs
Serialization
When going across the wire with JSON for instance, you probably don’t want the full construct with a **{ value:
Summary
I highly recommend using strong types for your domain concepts. It will make your APIs more obvious, as you would then avoid methods like:
1
Task Commit(Guid eventSourceId, Guid eventType, string content);
And then get a more clearer method like:
1
Task Commit(EventSourceId eventSourceId, EventType eventType, string content);