Arbeiten mit Nullable-Verweistypen
In C# 8 wurde ein neues Feature namens Nullable Reference Types (NRT) eingeführt, mit dem Verweistypen kommentiert werden können, was angibt, ob sie für sie null
gültig sind oder nicht. Wenn Sie noch nicht mit diesem Feature vertraut sind, empfiehlt es sich, sich damit vertraut zu machen, indem Sie die C#-Dokumentation lesen. Verweistypen, die nullwerte zulassen, sind in neuen Projektvorlagen standardmäßig aktiviert, bleiben aber in vorhandenen Projekten deaktiviert, es sei denn, sie wurden explizit angemeldet.
Auf dieser Seite wird die Unterstützung von EF Core für Nullable-Verweistypen vorgestellt und bewährte Methoden für die Arbeit mit ihnen beschrieben.
Erforderliche und optionale Eigenschaften
Die Standard Dokumentation zu erforderlichen und optionalen Eigenschaften und deren Interaktion mit Nullable-Verweistypen ist die Seite Erforderliche und optionale Eigenschaften. Es wird empfohlen, zunächst diese Seite zu lesen.
Hinweis
Seien Sie beim Aktivieren von Nullable-Verweistypen für ein vorhandenes Projekt vorsichtig: Verweistypeigenschaften, die zuvor als optional konfiguriert wurden, werden nun als erforderlich konfiguriert, es sei denn, sie werden explizit als NULL-wertewerte gekennzeichnet. Bei der Verwaltung eines relationalen Datenbankschemas kann dies dazu führen, dass Migrationen generiert werden, die die NULL-Zulässigkeit der Datenbankspalte ändern.
Eigenschaften und Initialisierung, die keine Nullwerte zulassen
Wenn Nullable-Verweistypen aktiviert sind, gibt der C#-Compiler Warnungen für jede nicht initialisierte Eigenschaft aus, die nicht nullable ist, da diese enthalten null
würden. Daher kann die folgende gängige Methode 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 Member die perfekte Lösung für dieses Problem:
public required string Name { get; set; }
Der Compiler garantiert nun, dass, wenn Ihr Code einen Kunden instanziiert, immer dessen Name-Eigenschaft initialisiert. Und da die datenbankspalte, die der -Eigenschaft zugeordnet ist, nicht NULL-werte zulassen kann, enthalten alle von EF geladenen Instanzen immer auch einen Namen ungleich NULL.
Wenn Sie eine ältere Version von C# verwenden, ist die Konstruktorbindung eine alternative Methode, um sicherzustellen, dass Ihre Eigenschaften, die keine Nullwerte zulassen, initialisiert werden:
public class CustomerWithConstructorBinding
{
public int Id { get; set; }
public string Name { get; set; }
public CustomerWithConstructorBinding(string name)
{
Name = name;
}
}
Leider ist in einigen Szenarien die Konstruktorbindung keine Option. Navigationseigenschaften können beispielsweise nicht auf diese Weise initialisiert werden. In diesen Fällen können Sie die Eigenschaft null
einfach mit Hilfe des NULL-vergebenden Operators initialisieren (weitere Details finden Sie jedoch unten):
public Product Product { get; set; } = null!;
Erforderliche Navigationseigenschaften
Erforderliche Navigationseigenschaften stellen eine zusätzliche Schwierigkeit dar: Obwohl ein abhängiger Für einen bestimmten Prinzipal immer vorhanden ist, kann er von einer bestimmten Abfrage geladen werden, je nach den Anforderungen an diesem Punkt im Programm (siehe die verschiedenen Muster zum Laden von Daten). Gleichzeitig kann es nicht wünschenswert sein, diese Eigenschaften nullable zu machen, da dies erzwingen würde, dass alle Zugriffe auf sie nach suchen null
, auch wenn die Navigation als geladen bekannt ist und daher nicht sein null
kann.
Das ist nicht unbedingt ein Problem! Solange ein erforderlicher Abhängiger ordnungsgemäß geladen wird (z. B. über Include
), wird der Zugriff auf seine Navigationseigenschaft garantiert immer ungleich NULL zurückgegeben. Auf der anderen Seite kann die Anwendung überprüfen, ob die Beziehung geladen wird, indem überprüft wird, ob die Navigation ist null
. In solchen Fällen ist es sinnvoll, die Navigation nullwertfähig zu machen. Dies bedeutet, dass erforderliche Navigationen vom abhängigen zum Prinzipal erforderlich sind:
- Sollte nicht nullable sein, wenn es als Programmiererfehler beim Zugreifen auf eine Navigation gilt, wenn sie nicht geladen wird.
- Sollte NULL-Werte zulassen, wenn es für Anwendungscode akzeptabel ist, die Navigation zu überprüfen, um festzustellen, ob die Beziehung geladen wird.
Wenn Sie einen strikteren Ansatz wünschen, können Sie über eine Eigenschaft verfügen, die nicht nullable ist, und ein Nullable-Backing-Feld:
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 auf die abhängige über die -Eigenschaft 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 null-wertefähig sein. Eine leere Auflistung bedeutet, dass keine verknüpften Entitäten vorhanden sind, aber die Liste selbst sollte nie sein null
.
DbContext und DbSet
Die gängige Praxis, nicht initialisierte DbSet-Eigenschaften für Kontexttypen zu verwenden, ist ebenfalls problematisch, da der Compiler jetzt Warnungen für diese ausgibt. 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 weitere Strategie besteht darin, automatische Eigenschaften zu verwenden, die keine Nullwerte zulassen, aber sie mit initialisieren, null
indem Sie den NULL-vergebenden Operator (!) verwenden, um die Compilerwarnung zum Schweigen zu bringen. Der DbContext-Basiskonstruktor stellt sicher, dass alle DbSet-Eigenschaften initialisiert werden, und null wird nie für sie beobachtet.
Navigieren und Einschließen von Beziehungen, die NULL-Werte zulassen
Bei optionalen Beziehungen ist es möglich, Compilerwarnungen zu erhalten, bei denen eine tatsächliche null
Verweis-Ausnahme unmöglich wäre. Beim Übersetzen und Ausführen Ihrer LINQ-Abfragen garantiert EF Core, dass, wenn eine optionale verwandte Entität nicht vorhanden ist, jede Navigation zu ihr einfach ignoriert wird, anstatt auszulösen. Der Compiler erkennt diese EF Core-Garantie jedoch nicht und erzeugt Warnungen, als ob die LINQ-Abfrage im Arbeitsspeicher mit LINQ to Objects ausgeführt würde. Daher ist es notwendig, den NULL-vergebenden 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 Beziehungsebenen über optionale Navigationen 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 ausschließlich) in EF Core-Abfragen verwendet werden, sollten Sie erwägen, die Navigationseigenschaften als nicht NULL-zulässig 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 Werte beobachten null
, obwohl die Eigenschaften als nicht nullable kommentiert werden.
Einschränkungen in älteren Versionen
Vor EF Core 6.0 gelten die folgenden Einschränkungen:
- Die öffentliche API-Oberfläche wurde aus Gründen der NULL-Zulässigkeit nicht kommentiert (die öffentliche API war "NULL-oblivious"), sodass die Verwendung manchmal umständlich ist, wenn das NRT-Feature aktiviert ist. Dazu gehören insbesondere die asynchronen LINQ-Operatoren, die von EF Core verfügbar gemacht werden, z. B. FirstOrDefaultAsync. Die öffentliche API wird ab EF Core 6.0 vollständig zur NULL-Zulässigkeit kommentiert.
- Reverse Engineering hat keine C# 8-Nullable-Verweistypen (Nullable Reference Types, NRTs) unterstützt: 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 nichtstring?
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.