Verwenden von Nullable-Verweistypen

Mit C# 8 wurde eine neue Funktion namens Nullable-Verweistypen (NRT) eingeführt, die es ermöglicht, Verweistypen mit Anmerkungen zu versehen, die angeben, ob sie null enthalten dürfen oder nicht. Wenn Sie mit diesem Feature noch nicht vertraut sind, empfiehlt es sich, sich damit vertraut zu machen, indem Sie die C#-Dokumente lesen. Nullable-Verweistypen sind standardmäßig in neuen Projektvorlagen aktiviert, bleiben aber in vorhandenen Projekten deaktiviert, es sei denn, sie werden explizit aktiviert.

Auf dieser Seite wird die Unterstützung von EF Core für Nullable-Verweistypen vorgestellt und es werden bewährte Methoden für die Arbeit mit ihnen beschrieben.

Erforderliche und optionale Eigenschaften

Die Hauptdokumentation zu erforderlichen und optionalen Eigenschaften und deren Interaktion mit Nullable-Verweistypen ist die Seite Erforderliche und optionale Eigenschaften. Es wird empfohlen, zuerst diese Seite zu lesen.

Hinweis

Seien Sie vorsichtig, wenn Sie Nullable-Verweistypen für ein vorhandenes Projekt aktivieren: Verweistypeigenschaften, die zuvor als optional konfiguriert wurden, werden jetzt als erforderlich konfiguriert, es sei denn, sie werden explizit als Nullwerte zulassende Verweistypen gekennzeichnet. Beim Verwalten eines relationalen Datenbankschemas kann dies dazu führen, dass Migrationen generiert werden, die die NULL-Zulässigkeit der Datenbankspalte ändern.

Non-Nullable-Eigenschaften und Initialisierung

Wenn Nullable-Verweistypen aktiviert sind, gibt der C#-Compiler Warnungen für alle nicht initialisierten Non-Nullable-Eigenschaften aus, da diese null enthalten würden. Daher können die folgenden allgemeinen Methoden zum Schreiben von Entitätstypen nicht verwendet werden:

public class Customer
{
    public int Id { get; set; }

    // Generates CS8618, uninitialized non-nullable property:
    public string Name { get; set; }
}

Wenn Sie C# 11 oder höher verwenden, bieten erforderliche Mitglieder die perfekte Lösung für dieses Problem:

public required string Name { get; set; }

Der Compiler garantiert jetzt, dass der Code beim Instanziieren eines Kunden immer seine Name-Eigenschaft initialisiert. Und da die Datenbankspalte, die der Eigenschaft zugeordnet ist, non-nullable ist, enthalten alle Instanzen, die von EF geladen wurden, immer auch einen Nicht-Null-Namen.

Wenn Sie eine ältere Version von C# verwenden, ist Konstruktorbindung eine alternative Technik, um sicherzustellen, dass Ihre Non-Nullable-Eigenschaften initialisiert werden:

public class CustomerWithConstructorBinding
{
    public int Id { get; set; }
    public string Name { get; set; }

    public CustomerWithConstructorBinding(string name)
    {
        Name = name;
    }
}

Leider ist die Konstruktorbindung in einigen Szenarien keine Option; Navigationseigenschaften können z. B. nicht auf diese Weise initialisiert werden. In diesen Fällen können Sie die Eigenschaft einfach auf null initialisieren, indem Sie den NULL-toleranten Operator verwenden (aber beachten Sie die weiteren Details dazu weiter unten):

public Product Product { get; set; } = null!;

Erforderliche Navigationseigenschaften

Erforderliche Navigationseigenschaften stellen eine zusätzliche Schwierigkeit dar: Obwohl eine abhängige Eigenschaft für einen bestimmten Prinzipal immer vorhanden ist, kann es sein, dass sie von einer bestimmten Abfrage geladen wird oder auch nicht, je nach den Bedürfnissen an diesem Punkt im Programm (siehe die verschiedenen Muster für das Laden von Daten). Gleichzeitig kann es unerwünscht sein, diese Eigenschaften Nullwerte zulassend zu machen, da dies den gesamten Zugriff auf sie erzwingen würde, um nach null zu suchen, auch wenn die Navigation als geladen bekannt ist und daher nicht null sein kann.

Das ist nicht unbedingt ein Problem! Solange ein erforderlicher abhängiger Wert ordnungsgemäß geladen wird (z. B. über Include), wird der Zugriff auf seine Navigationseigenschaft garantiert immer nicht NULL zurückgegeben. Andererseits kann die Anwendung prüfen, ob die Beziehung geladen wird, indem überprüft wird, ob die Navigation null ist. In solchen Fällen ist es sinnvoll, dass die Navigation Nullwerte zulässt. Dies bedeutet, dass die erforderlichen Navigationen vom abhängigen Element zum Prinzipal:

  • non-nullable sein sollten, wenn es als Programmierfehler angesehen wird, auf eine Navigation zuzugreifen, wenn sie nicht geladen ist.
  • Nullwerte zulassen sollten, wenn es für den Anwendungscode akzeptabel ist, die Navigation zu überprüfen, um festzustellen, ob die Beziehung geladen ist oder nicht.

Wenn Sie einen strengeren Ansatz wünschen, können Sie eine Non-Nullable-Eigenschaft mit einem Sicherungsfeld haben, das Nullwerte zulässt:

private Address? _shippingAddress;

public Address ShippingAddress
{
    set => _shippingAddress = value;
    get => _shippingAddress
           ?? throw new InvalidOperationException("Uninitialized property: " + nameof(ShippingAddress));
}

Solange die Navigation ordnungsgemäß geladen ist, kann über die Eigenschaft auf das abhängige Element zugegriffen werden. Wenn jedoch auf die Eigenschaft zugegriffen wird, ohne zuerst die zugehörige Entität ordnungsgemäß zu laden, wird ein InvalidOperationException ausgelöst, da der API-Vertrag falsch verwendet wurde.

Hinweis

Sammlungsnavigationen, die Verweise auf mehrere verwandte Entitäten enthalten, sollten immer non-nullable sein. Eine leere Auflistung bedeutet, dass keine verwandten Entitäten vorhanden sind, aber die Liste selbst sollte niemals null werden.

DbContext und DbSet

Bei EF ist es üblich, nicht initialisierte DbSet-Eigenschaften für Kontexttypen zu verwenden:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers { get; set;}
}

Obwohl dies im Allgemeinen zu einer Compilerwarnung führt, unterdrücken EF Core 7.0 und höher diese Warnung, da EF diese Eigenschaften automatisch über Spiegelung initialisiert.

Bei älteren Versionen von EF Core können Sie dieses Problem wie folgt umgehen:

public class MyContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
}

Eine weitere Strategie besteht darin, Non-Nullable-Auto-Eigenschaften zu verwenden, aber sie in null zu initialisieren, indem sie den NULL-toleranten Operator (!) verwenden, um die Compilerwarnung auszuschalten. Der DbContext-Basiskonstruktor stellt sicher, dass alle DbSet-Eigenschaften initialisiert werden, und NULL wird für sie nie beobachtet.

Beim Umgang mit optionalen Beziehungen ist es möglich, Compilerwarnungen zu finden, bei denen eine tatsächliche null-Verweisausnahme unmöglich wäre. Beim Übersetzen und Ausführen ihrer LINQ-Abfragen garantiert EF Core, dass, wenn keine optionale verknüpfte Entität vorhanden ist, eine navigationsfähige Entität einfach ignoriert und nicht ausgelöst wird. Der Compiler kennt diese EF Core-Garantie jedoch nicht und erzeugt Warnungen, als ob die LINQ-Abfrage im Arbeitsspeicher mit LINQ to Objects ausgeführt wurde. Daher ist es notwendig, den NULL-toleranten Operator (!) zu verwenden, um den Compiler darüber zu informieren, dass ein tatsächlicher null Wert nicht möglich ist:

var order = context.Orders
    .Where(o => o.OptionalInfo!.SomeProperty == "foo")
    .ToList();

Ein ähnliches Problem tritt auf, wenn mehrere Ebenen von Beziehungen über optionale Navigationselemente hinweg eingeschlossen werden:

var order = context.Orders
    .Include(o => o.OptionalInfo!)
    .ThenInclude(op => op.ExtraAdditionalInfo)
    .Single();

Wenn Sie dies häufig tun und die betreffenden Entitätstypen überwiegend (oder exklusiv) in EF Core-Abfragen verwendet werden, erwägen Sie, die Navigationseigenschaften nicht nullfähig zu machen und sie über die Fluent-API oder Datenanmerkungen als optional zu konfigurieren. Dadurch werden alle Compilerwarnungen entfernt, während die Beziehung optional bleibt. Wenn Ihre Entitäten jedoch außerhalb von EF Core durchlaufen werden, können Sie null-Werte beobachten, obwohl die Eigenschaften als nicht nullfähig gekennzeichnet sind.

Einschränkungen in älteren Versionen

Vor EF Core 6.0 gelten die folgenden Einschränkungen:

  • Die öffentliche API-Oberfläche wurde nicht für NULL-Zulässigkeit kommentiert (die öffentliche API war „null-oblivious“), wodurch sie manchmal ungünstig ist, wenn das NRT-Feature aktiviert ist. Dies schließt insbesondere die asynchronen LINQ-Operatoren ein, die von EF Core verfügbar gemacht werden, z. B. FirstOrDefaultAsync. Die öffentliche API ist ab EF Core 6.0 vollständig für NULL-Zulässigkeit kommentiert.
  • Reverse Engineering unterstützte C# 8 Nullable-Verweistypen (NRTs) nicht: EF Core generierte immer C#-Code, der davon ausging, dass das Feature deaktiviert ist. Beispielsweise wurden Nullable-Textspalten per Gerüstbau als Eigenschaft mit Typ string und nicht string? erstellt, wobei die Fluent-API oder Datenanmerkungen verwendet wurden, um zu konfigurieren, ob eine Eigenschaft erforderlich ist oder nicht. Wenn Sie eine ältere Version von EF Core verwenden, können Sie den Gerüstcode weiterhin bearbeiten und diesen durch C#-Anmerkungen mit NULL-Zulässigkeit ersetzen.