Eintrag

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

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. 🧹

Dieser Eintrag ist vom Autor unter CC BY 4.0 lizensiert.