More adventures in strongly-typed database ID fields. A follow-up (of sorts) to a post from 2013.
In that previous post I described a way of adding strong typing to database ID references in C# code, without really any runtime overhead, and interoperability with existing code which passes database IDs as integers. This post presents a refinement which is more flexible, and produces less cluttered code.
Background
In a lot of database-heavy apps, at least the ones I’ve been involved in, you spend a lot of time passing database IDs around in the code. Usually these are integers (32- or 64-bit), but they could also be UUIDs or strings.
The trouble is that an integer representing a customer ID has the same static type as an integer representing a user ID, invoice line ID, product ID, or for that matter an integer representing a quantity. The compiler will not complain at you when you pass an integer representing a user ID to a function expecting an integer representing a customer ID—because they are all just undifferentiated integers.
So it’s an appealing idea to somehow introduce static type checking for database entity IDs. Of course, we should avoid bloating the code or introducing any runtime overhead and it should easily interoperate with whatever the native key type is for the database entities.
Ideally the scheme should even cope with composite primary keys, though in my experience composite primary keys are pretty rare (at least when using an ORM which doesn’t directly expose joining tables).
Previous approach
In ID: Type-safety in database code, I described a C# generic struct type, ID<>
for representing strongly-typed database IDs. It worked, but had the following shortcomings:
- It was verbose: ID types look like
ID<Customer>
orID<Invoice>
, which is awkward to type and visually messy. - It was limited: It assumed that database IDs are always 32-bit integers. Different types of keys—for example, some tables with string keys and others with integers— cannot be mixed in a single project without creating multiple, different-named
ID
classes.
On the positive side:
- IDs were ‘struct’ objects, and hence caused zero space overhead and minimal speed overhead.
New approach
Ideally key types would be named EntityName.Id
, but how can we do that while keeping them as structs, and without requiring each entity to redefine its own Id
struct?
The answer is to make it an inner type of a parameterised Entity class (parameterised by database key type and Entity subtype). Subclasses instantiate the parameter types, and get an Id
struct type strongly typed with respect to their key and Entity type.
- ID types now look like
Customer.ID
orInvoice.ID
—which is visually less noisy, and puts the entity name first. - ID (entity key) types can be anything—
Int32
,Int64
,String
, anything—which implementsIEquatable
. - Entities have an ‘
Id
’ property which is of typeEntityName.ID
. - Entities have a ‘
Key
’ property which is of the underlying primary key type.
The downside is that all entity classes must inherit from the same Entity<>
base class in order to be able to have-strongly typed ID types. However, since the entity ‘knows’ about its ID
type, it can expose an Id
field of that type.
It’s possible for many entities which share the same underlying key type (and key field name) to inherit from a common subclass of Entity
, specialised to their key type.
The Code
// base class of all entities:
public abstract class Entity<K, E>: Entity<K, E>.IDOrEntity
where E: Entity<K, E>
where K: IEquatable<K>
{
public ID Id => new ID(Key);
// Subclasses must implement this:
public abstract K Key { get; set; }
public bool IsNot(Entity<K, E> other) => !Is(other);
public bool Is(Entity<K, E> other) => this.Id == other.Id;
// Union type of Entity and ID
public interface IDOrEntity
{
ID Id { get; }
K Key { get; }
}
// The ID type, unique to the Entity type:
public struct ID: IEquatable<ID>, IDOrEntity
{
private readonly K _key;
public ID(K key)
{
this._key = key;
}
public K Key => _key;
public ID IDOrEntity.Id => this;
public override bool Equals(object obj) => this == (obj as ID?);
public static bool operator !=(ID first, ID second) => !(first == second);
public static bool operator ==(ID first, ID second) => first.Equals(second);
public bool Equals(ID other) => this.Key.Equals(other.Key);
public override int GetHashCode() => Key.GetHashCode();
public override string ToString() => Key.ToString();
public static implicit operator ID(K value) => new ID(value);
}
}
You’ll notice that there is one abstract property on Entity
: Key
; this represents the entity’s (primary) key as its underlying type. Making this abstract allows subclasses to decide how they want to store all their fields—the Entity class itself does not store any state.
(It might be possible to make this Key
field protected
.)
Examples
var cust = new Customer();
var cust1 = new Customer();
var custId = (Customer.ID)89;
var order = new Order();
public bool CheckCustomer(Customer.ID id);
var ok = CheckCustomer(cust.id);
// var doesNotCompile1 = CheckCustomer(order.id);
var idsMatch = custId == cust1.id;
// var doesNotCompile2 = custId == order.Id;
var sameEntity = cust.Is(cust1); // Compares Id values for equality.
Customer.ID custId = 33; // Allowed (if key type is integer).
// Customer.ID custId2 = order.Id // Not allowed (no matter if they share key types).
Entities which use the same key type/key name
I have a few line-of-business applications most of which use 32-bit integer entity IDs. Table key names are almost always ‘Id’, and they use Microsoft’s EntityFramework for database access. We can abstract the common bits of the 32-bit-ID database entities like this:
// All (or most) entities in the application inherit (directly) from this:
public abstract class BaseEntity<E> : Entity<int, E>
where E: BaseEntity<E>
{
[Key("Id")]
public override int Key { get; set; }
}
This says that all inheriting entities have an Int32
key field (and hence an ID
type based on ints), represented in the database as a field called ‘Id
‘.
Accepting IDs or entities
As with my previous approach, it includes a mechanism for methods to receive as parameters objects which can be either an ID or a whole entity.
This is useful because frequently business logic already has an entity object, and it’s a useful optimisation for called methods not to have to retrieve the same entity again from the database.
We specify an interface to represent the union of an Entity type and its corresponding ID type, called EntityName.IDOrEntity
. Entities and their IDs implement this interface, and an extension method on the interface, GetEntity(Func<ID, Entity>)
, provides a mechanism to either return the entity, or to look up the entity from its ID.
In other words, if you provide an entity, the method can use it directly; if you provide just an ID, it can look up the entity itself.
public static class IDOrEntityExtensions
{
public static EntityType GetInstance<K, EntityType>(
this Entity<K, EntityType>.IDOrEntity idOrEntity,
Func<Entity<K, EntityType>.ID, EntityType> getter)
where EntityType : Entity<K, EntityType>
where K : IComparable
{
return (idOrEntity as EntityType)
?? getter(idOrEntity.Id);
}
}
Summary
It’s only a single class (plus one extension class), but it provides a nice (and simple) mechanism for enforcing a bit more compile-time safety on a database application.
The new version is more intuitive too and makes the code clearer and cleaner.
Pingback: Removing boxing in IdOrEntity object | Andrew’s Mental Dribbling!