Strongly Typed IDs mit EF Core
Strongly Typed IDs gehören zu den Konzepten, bei denen man sich fragt, warum sie nicht längst Standard sind.
Strongly Typed IDs mit EF Core
Irgendwann passiert es uns allen: Eine Methode erwartet mehrere Entity-IDs – und schon sind sie vertauscht. Je mehr Entities es gibt, desto größer ist die Gefahr.
Mit Strongly Typed IDs können wir dem entgegenwirken.
Was ist eine Strongly Typed ID?
Statt direkt mit primitiven Typen wie Guid
, int
oder string
zu arbeiten, definieren wir uns einen eigenen Typen:
1
public readonly record struct VanId(Guid Value);
INFO
In Domain-Driven Design sind Strongly Typed IDs ein typisches Beispiel für ein Value Object: Ein unveränderliches Objekt, das einen oder mehrere Werte kapselt.
Zu viel Arbeit?
Das bezweifle ich. Vermutlich wirkt die Einstiegshürde hoch, wenn man noch nie damit gearbeitet hat. Ich hoffe, dieser Artikel hilft dabei, diese Hürde zu senken. ☺️
Ich verwende Strongly Typed IDs standardmäßig in Greenfield-Projekten, wenn es sich nicht um ein kurzlebiges Projekt (z. B. Prototypen) handelt und die Domäne mehr als zwei Aggregate Roots enthält.
Beispiel 1: Greenfield
In einer heilen Welt und zu Beginn eines Projekts können wir jede Entity mit Strongly Typed IDs ausstatten:
1
2
3
4
5
6
7
8
public readonly record struct VanId(Guid Value);
public class Van
{
public VanId Id { get; init; }
public string Name { get; private set; }
// ...
}
Weitere Entities, wie z. B. der MaintenanceRecord
der eine Fremdschlüsselbeziehung zum Van
hat, referenzieren ihn über die VanId
:
1
2
3
4
5
6
7
8
9
10
public readonly record struct MaintenanceRecordId(Guid Value);
public class MaintenanceRecord
{
public MaintenanceRecordId Id { get; init; }
public VanId VanId { get; private set; } // FK zum Van
public DateTime Date { get; private set; }
public string Description { get; private set; }
// ...
}
Damit EF Core damit umgehen kann verwenden wir in der EntityTypeConfiguration
des Vans eine Conversion:
1
2
3
4
5
6
7
8
9
10
11
12
public class VanEntityTypeConfiguration : IEntityTypeConfiguration<Van>
{
public void Configure(EntityTypeBuilder<Van> builder)
{
builder.HasKey(x => x.Id); // PK
// Die Conversion kann auch global konfiguriert werden.
builder
.Property(v => v.Id)
.HasConversion(id => id.Value, value => new VanId(value));
}
}
In der EntityTypeConfiguration
des MaintenanceRecord
konfigurieren wir zusätzlich die Fremdschlüsselbeziehung zum Van
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MaintenanceRecordEntityTypeConfiguration : IEntityTypeConfiguration<MaintenanceRecord>
{
public void Configure(EntityTypeBuilder<MaintenanceRecord> builder)
{
builder.HasKey(x => x.Id); // PK
builder
.Property(v => v.Id)
.HasConversion(id => id.Value, value => new MaintenanceRecordId(value));
builder.HasOne<Van> // FK zum Van
.WithMany()
.HasForeignKey(m => m.VanId);
}
}
Gar nicht so kompliziert oder? 😉 Auch in Brownfield Projekten gibt es eine relativ elegante Möglichkeit Strongly Typed IDs zu verwenden.
Beispiel 2: Brownfield
Angenommen die Entity Van
gab es bereits und hat leider keine Strongly Typed ID:
1
2
3
4
5
6
public class Van
{
public Guid Id { get; init; }
public string Name { get; private set; }
// ...
}
Eine Änderung der Guid
in eine VanId
ist uns zum aktuellen Zeitpunkt zu aufwändig, daher implementieren wir nur im MaintenanceRecord
die Strongly Typed IDs, richtig? Fast.
INFO
EF Core ordnet Beziehungen nur zu, wenn die FK-Eigenschaft exakt denselben Typ hat wie die PK-Eigenschaft des Ziels.
Damit wir durch die Tür kommen, müssen wir EF Core demnach einen Wert mit einer Guid
bereitstellen. Am besten geht das mit einem privaten Feld. Über die computed Property erfolgt der Zugriff auf die VanId
:
1
2
3
4
5
6
7
public class MaintenanceRecord
{
// ...
private Guid _vanId; // FK zum Van
public VanId VanId => new VanId(_vanId);
// ...
}
Für die Fremdschlüsselbeziehung geben wir explizit den Namen des Felds an:
1
2
3
4
5
6
7
8
9
10
public class MaintenanceRecordEntityTypeConfiguration : IEntityTypeConfiguration<MaintenanceRecord>
{
public void Configure(EntityTypeBuilder<MaintenanceRecord> builder)
{
// ...
builder.HasOne<Van> // FK zum Van
.WithMany()
.HasForeignKey("_vanId");
}
}
HINWEIS
Ich gehe davon aus, dass es mindestens einen Test gibt, der mit dieser Fremdschlüsselbeziehung arbeitet, sodass ich hier das Risiko einer Umbenennung vernachlässige.
Fazit
Strongly Typed IDs sind ein Geschenk für die Codequalität. In Greenfield-Projekten gibt es wenig Gründe, sie nicht von Anfang an zu nutzen.
In Brownfield-Projekten: Nur, wenn ich entweder Module neu schreibe oder bereit bin, FKs komplett umzubauen. Alles andere ist halbherzig und bringt eher Schmerzen.
Meine Meinung? Lieber sauber anfangen als hinterher aufzuräumen. 🧹