Praca z typami referencyjnymi dopuszczanymi wartościami null

W języku C# 8 wprowadzono nową funkcję o nazwie typy referencyjne dopuszczające wartości null (NRT), umożliwiając dodawanie adnotacji do typów odwołań, wskazującą, czy są one prawidłowe dla nich, czy null nie. Jeśli dopiero zaczynasz korzystać z tej funkcji, zalecamy zapoznanie się z nią przez przeczytanie dokumentacji języka C#. Typy odwołań dopuszczane do wartości null są domyślnie włączone w nowych szablonach projektów, ale pozostaną wyłączone w istniejących projektach, chyba że jawnie zdecydujesz się na.

Na tej stronie przedstawiono obsługę platformy EF Core dla typów odwołań dopuszczanych do wartości null i opisano najlepsze rozwiązania dotyczące pracy z nimi.

Wymagane i opcjonalne właściwości

Główną dokumentacją dotyczącą wymaganych i opcjonalnych właściwości oraz interakcji z typami referencyjnymi dopuszczanymi do wartości null jest strona Wymagane i Opcjonalne właściwości . Zaleca się rozpoczęcie od przeczytania pierwszej strony.

Uwaga

Zachowaj ostrożność podczas włączania typów odwołań dopuszczających wartość null w istniejącym projekcie: właściwości typu odwołania, które zostały wcześniej skonfigurowane jako opcjonalne, będą teraz konfigurowane zgodnie z wymaganiami, chyba że są jawnie oznaczone jako dopuszczające wartość null. Podczas zarządzania schematem relacyjnej bazy danych może to spowodować wygenerowanie migracji, które zmieniają wartość null kolumny bazy danych.

Właściwości i inicjowanie bez wartości null

Gdy typy odwołań dopuszczające wartość null są włączone, kompilator języka C# emituje ostrzeżenia dla każdej niezainicjowanej właściwości niezwiązanej z wartością null, ponieważ zawierają nullone wartość . W związku z tym nie można użyć następującego wspólnego sposobu pisania typów jednostek:

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

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

Jeśli używasz języka C# 11 lub nowszego, wymagane elementy członkowskie zapewniają idealne rozwiązanie tego problemu:

public required string Name { get; set; }

Kompilator gwarantuje teraz, że gdy kod tworzy wystąpienie klienta, zawsze inicjuje jego właściwość Name. Ponieważ kolumna bazy danych zamapowana na właściwość jest niepusta, wszystkie wystąpienia ładowane przez program EF zawsze zawierają również nazwę inną niż null.

Jeśli używasz starszej wersji języka C#, powiązanie konstruktora jest alternatywną techniką, aby upewnić się, że właściwości niezwiązane z wartością null są inicjowane:

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

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

Niestety w niektórych scenariuszach powiązanie konstruktora nie jest opcją; na przykład nie można zainicjować właściwości nawigacji w ten sposób. W takich przypadkach można po prostu zainicjować null właściwość za pomocą operatora forgiving o wartości null (ale zobacz poniżej, aby uzyskać więcej szczegółów):

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

Wymagane właściwości nawigacji

Wymagane właściwości nawigacji stanowią dodatkową trudność: chociaż zależność zawsze istnieje dla danego podmiotu zabezpieczeń, może lub nie może być ładowana przez określone zapytanie, w zależności od potrzeb w tym momencie programu (zobacz różne wzorce ładowania danych). Jednocześnie może to być niepożądane, aby te właściwości były dopuszczane do wartości null, ponieważ wymusiłoby to cały dostęp do nich w celu sprawdzenia nullelementu , nawet jeśli nawigacja jest znana do załadowania i dlatego nie może być null.

To niekoniecznie jest problem! O ile wymagane zależne jest poprawnie załadowane (np. za pośrednictwem Include), uzyskanie dostępu do jej właściwości nawigacji gwarantuje, że zawsze zwraca wartość inną niż null. Z drugiej strony aplikacja może zdecydować się sprawdzić, czy relacja jest ładowana, sprawdzając, czy nawigacja to null. W takich przypadkach uzasadnione jest, aby nawigacja była dopuszczana do wartości null. Oznacza to, że wymagane nawigacje od podmiotu zależnego od podmiotu zabezpieczeń:

  • Jeśli jest uważany za błąd programisty, powinien mieć wartość inną niż null, aby uzyskać dostęp do nawigacji, gdy nie zostanie załadowany.
  • Jeśli kod aplikacji powinien mieć wartość null, powinien mieć wartość null, aby sprawdzić nawigację w celu określenia, czy relacja jest ładowana.

Jeśli chcesz bardziej rygorystyczne podejście, możesz mieć właściwość niepustą z polem kopii zapasowej dopuszczającej wartość null:

private Address? _shippingAddress;

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

O ile nawigacja zostanie prawidłowo załadowana, zależne będzie dostępne za pośrednictwem właściwości . Jeśli jednak dostęp do właściwości jest uzyskiwany bez wcześniejszego prawidłowego ładowania powiązanej jednostki, InvalidOperationException jest zgłaszany, ponieważ kontrakt interfejsu API został użyty niepoprawnie.

Uwaga

Nawigacje kolekcji, które zawierają odwołania do wielu powiązanych jednostek, zawsze powinny być niepuste. Pusta kolekcja oznacza, że nie istnieją żadne powiązane jednostki, ale sama lista nigdy nie powinna mieć wartości null.

DbContext i DbSet

W przypadku platformy EF typowe jest posiadanie niezainicjowanych właściwości dbSet dla typów kontekstu:

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

Mimo że zazwyczaj powoduje to ostrzeżenie kompilatora, program EF Core 7.0 lub nowszy pomija to ostrzeżenie, ponieważ program EF automatycznie inicjuje te właściwości za pośrednictwem odbicia.

W starszej wersji programu EF Core można obejść ten problem w następujący sposób:

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

Inną strategią jest użycie właściwości automatycznych bez wartości null, ale inicjowanie ich do nullmetody przy użyciu operatora forgiving null (!) w celu wyciszenia ostrzeżenia kompilatora. Konstruktor podstawowy DbContext gwarantuje, że wszystkie właściwości dbSet zostaną zainicjowane, a wartość null nigdy nie będzie na nich obserwowana.

W przypadku relacji opcjonalnych można napotkać ostrzeżenia kompilatora, gdy rzeczywisty null wyjątek odwołania byłby niemożliwy. Podczas tłumaczenia i wykonywania zapytań LINQ program EF Core gwarantuje, że jeśli opcjonalna powiązana jednostka nie istnieje, każda nawigacja do niej będzie po prostu ignorowana, a nie zgłaszana. Jednak kompilator nie zna tej gwarancji platformy EF Core i generuje ostrzeżenia tak, jakby zapytanie LINQ zostało wykonane w pamięci z linQ to Objects. W związku z tym należy użyć operatora forgiving o wartości null (!), aby poinformować kompilator, że rzeczywista null wartość nie jest możliwa:

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

Podobny problem występuje podczas dołączania wielu poziomów relacji między opcjonalnymi nawigacjami:

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

Jeśli okaże się, że robisz to dużo, a kwestionowane typy jednostek są głównie (lub wyłącznie) używane w zapytaniach EF Core, rozważ utworzenie właściwości nawigacji bez wartości null i skonfigurowanie ich jako opcjonalne za pośrednictwem interfejsu API Fluent lub adnotacji danych. Spowoduje to usunięcie wszystkich ostrzeżeń kompilatora przy zachowaniu relacji opcjonalnej; Jeśli jednak jednostki są przechodzenie poza platformę EF Core, można obserwować null wartości, chociaż właściwości są oznaczone jako niepuste.

Ograniczenia w starszych wersjach

Przed programem EF Core 6.0 zastosowano następujące ograniczenia:

  • Publiczna powierzchnia interfejsu API nie została oznaczona adnotacją dla wartości null (publiczny interfejs API był "nieświadomy wartości null"), co sprawia, że czasami niezręczne użycie funkcji NRT jest włączone. Obejmuje to w szczególności asynchroniczne operatory LINQ uwidocznione przez program EF Core, takie jak FirstOrDefaultAsync. Publiczny interfejs API jest w pełni oznaczony jako wartość null, począwszy od platformy EF Core 6.0.
  • Inżynieria odwrotna nie obsługiwała typów odwołań dopuszczających wartość null w języku C# 8: program EF Core zawsze wygenerował kod języka C#, który zakładał, że funkcja jest wyłączona. Na przykład kolumny tekstowe umożliwiające wartości null były szkieletem jako właściwość typu string, a nie string?, z interfejsem API Fluent lub adnotacjami danych używanymi do konfigurowania tego, czy właściwość jest wymagana, czy też nie. Jeśli używasz starszej wersji programu EF Core, nadal możesz edytować kod szkieletowy i zastąpić to adnotacjami języka C# z opcją dopuszczania wartości null.