Arbeiten mit Nullable-Referenztypen

C# 8 führte ein neues Feature namens Nullable Reference Types (NRT) ein, wodurch Referenztypen annotiert werden können, die angibt, ob es gültig ist, null oder nicht zu enthalten. Wenn Sie neu für dieses Feature sind, empfiehlt es sich, sich selbst vertraut zu machen, indem Sie die C#-Dokumente lesen. Nullable Referenztypen sind standardmäßig in neuen Projektvorlagen aktiviert, bleiben jedoch in vorhandenen Projekten deaktiviert, es sei denn, es wurde explizit entschieden.

Auf dieser Seite wird die Unterstützung von EF Core für nullable Referenztypen eingeführt und 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 nullablen Referenztypen ist die Seite "Erforderliche und optionale Eigenschaften ". Es wird empfohlen, zuerst diese Seite zu lesen.

Hinweis

Achten Sie bei der Aktivierung von Nullable-Referenztypen auf einem vorhandenen Projekt: Referenztypeigenschaften, die zuvor als optional konfiguriert wurden, werden jetzt als erforderlich konfiguriert, es sei denn, sie werden explizit als Nullwert bezeichnet. Beim Verwalten eines relationalen Datenbankschemas kann dies dazu führen, dass Migrationen generiert werden, die die Nullbarkeit der Datenbankspalte ändern.

Nicht nullable Eigenschaften und Initialisierung

Wenn nullable Referenztypen aktiviert sind, gibt der C#-Compiler Warnungen für alle nicht nullablen Eigenschaften aus, da diese null enthalten würden. Daher können die folgenden gängigen Arten von Entitätstypen nicht verwendet werden:

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

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

Die Konstruktorbindung ist eine nützliche Methode, um sicherzustellen, dass Ihre nicht nullablen 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 beispielsweise nicht auf diese Weise initialisiert werden.

Erforderliche Navigationseigenschaften stellen eine zusätzliche Schwierigkeit dar: Obwohl für einen bestimmten Prinzipal immer ein abhängiger Wert vorhanden ist, kann es von einer bestimmten Abfrage abhängig von den Anforderungen an diesem Punkt im Programm geladen werden (siehe die verschiedenen Muster zum Laden von Daten). Gleichzeitig ist es unerwünscht, diese Eigenschaften nullfähig zu machen, da das alle Zugriff auf sie erzwingen würde, um auf null zu überprüfen, auch wenn sie erforderlich sind.

Eine Möglichkeit zum Umgang mit diesen Szenarien besteht darin, eine nicht nullable Eigenschaft mit einem nullablen Backing-Feld zu haben:

private Address? _shippingAddress;

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

Da die Navigationseigenschaft nicht nullfähig ist, ist eine erforderliche Navigation konfiguriert; und solange die Navigation ordnungsgemäß geladen wird, wird die abhängige Eigenschaft über die Eigenschaft zugänglich sein. Wenn jedoch auf die Eigenschaft zugegriffen wird, ohne zuerst die zugehörige Entität ordnungsgemäß zu laden, wird eine InvalidOperationException ausgelöst, da der API-Vertrag falsch verwendet wurde. Beachten Sie, dass EF so konfiguriert werden muss, dass immer auf das Backingfeld zugegriffen werden muss und nicht die Eigenschaft, da er darauf angewiesen ist, den Wert auch dann zu lesen, wenn er nicht festgelegt ist; wenden Sie sich an die Dokumentation zum Sichern von Feldern , um dies zu tun, und sollten Sie angeben PropertyAccessMode.Field , dass die Konfiguration richtig ist.

Als terser Alternative ist es möglich, die Eigenschaft einfach mit Hilfe des Null-Verzeiger-Operators (!) zu initialisieren:

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

Ein tatsächlicher Nullwert wird niemals beobachtet, außer aufgrund eines Programmierfehlers, z. B. beim Zugriff auf die Navigationseigenschaft, ohne die zugehörige Entität zuvor ordnungsgemäß zu laden.

Hinweis

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

DbContext und DbSet

Die allgemeine Praxis, dass nicht initialisierte DbSet-Eigenschaften in Kontexttypen vorhanden sind, ist auch problematisch, da der Compiler jetzt Warnungen für sie sendet. Dies kann wie folgt behoben werden:

public class NullableReferenceTypesContext : DbContext
{
    public DbSet<Customer> Customers => Set<Customer>();
    public DbSet<Order> Orders => Set<Order>();

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder
            .UseSqlServer(
                @"Server=(localdb)\mssqllocaldb;Database=EFNullableReferenceTypes;Trusted_Connection=True");
}

Eine andere Strategie besteht darin, nicht nullable automatische Eigenschaften zu verwenden, aber sie mithilfe des Null-Verzeiger-Operators (!) zu initialisieren, um die Compilerwarnung zu stillen. Der DbContext-Basiskonstruktor stellt sicher, dass alle DbSet-Eigenschaften initialisiert werden, und null wird niemals auf ihnen beobachtet.

Beim Umgang mit optionalen Beziehungen ist es möglich, Compilerwarnungen zu finden, bei denen eine tatsächliche NULL-Referenz-Ausnahme unmöglich wäre. Beim Übersetzen und Ausführen ihrer LINQ-Abfragen garantiert EF Core, dass eine optionale verwandte Entität nicht vorhanden ist, wird die Navigation einfach ignoriert und nicht ausgelöst. Der Compiler ist jedoch nicht mit dieser EF Core-Garantie vertraut und erzeugt Warnungen, wie wenn die LINQ-Abfrage im Arbeitsspeicher ausgeführt wurde, mit LINQ to Objects. Daher ist es notwendig, den Null-Verzeiger-Operator (!) zu verwenden, um den Compiler zu informieren, dass ein tatsächlicher Nullwert nicht möglich ist:

Console.WriteLine(order.OptionalInfo!.ExtraAdditionalInfo!.SomeExtraAdditionalInfo);

Ein ähnliches Problem tritt auf, wenn mehrere Beziehungen auf optionalen Navigationsebenen eingeschlossen werden:

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

Wenn Sie dies sehr viel tun, und die betreffenden Entitätstypen werden überwiegend (oder ausschließlich) in EF Core-Abfragen verwendet, sollten Sie die Navigationseigenschaften nicht nullfähig machen und sie über die Fluent-API oder Datenanmerkungen als optional 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 nicht nullierbar 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 nullability annotiert (die öffentliche API war "null-oblivious"), wodurch sie manchmal unauffällig ist, zu verwenden, wenn das NRT-Feature aktiviert ist. Dies umfasst insbesondere die asynchronen LINQ-Operatoren, die von EF Core verfügbar gemacht werden, z. B. FirstOrDefaultAsync. Die öffentliche API ist vollständig für die Nullbarkeit mit EF Core 6.0 versehen.
  • Reverse Engineering unterstützt nicht C# 8 nullable Referenztypen (NRTs): EF Core generierten C#-Code, der angenommen hat, dass das Feature deaktiviert ist. Beispielsweise wurden nullfähige Textspalten als Eigenschaft mit Typ string gerüstet, nicht string?mit der Fluent-API oder Datenanmerkungen, die verwendet werden, 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 diese durch C# Nullability-Anmerkungen ersetzen.