Udostępnij za pośrednictwem


Praca z typami referencyjnymi obsługującymi wartości null

Typy odwołań dopuszczające wartość null w języku C# (NRT) umożliwiają dodawanie adnotacji do typów odwołań, określając, czy mogą one zawierać wartość null, czy nie. Jeśli dopiero zaczynasz korzystać z tej funkcji, zalecamy zapoznanie się z nią przez przeczytanie dokumentacji języka C#. Wyłączalne typy referencyjne są domyślnie włączone w nowych szablonach projektów, ale pozostają wyłączone w istniejących projektach, chyba że jawnie włączysz tę opcję.

Na tej stronie przedstawiono, w jaki sposób EF Core obsługuje typy odwołań dopuszczające wartość null, oraz opisano najlepsze praktyki 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 nieakceptującej wartości null, ponieważ null by zawierało. 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, wymagani członkowie zapewniają idealne rozwiązanie tego problemu:

public required string Name { get; set; }

Kompilator gwarantuje teraz, że gdy kod tworzy wystąpienie Customer, zawsze inicjuje jego właściwość Name. Ponieważ kolumna bazy danych, która jest powiązana z właściwością, jest nienullowalna, wszystkie wystąpienia ładowane przez EF zawsze zawierają także niepustą nazwę.

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 wiązanie konstruktora nie jest opcją; na przykład nie można zainicjować właściwości nawigacyjnych w ten sposób. W takich przypadkach można po prostu zainicjować właściwość do wartości null za pomocą operatora rozgrzeszającego dla 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 nawigacyjne stanowią dodatkową trudność: chociaż obiekt zależny zawsze istnieje dla danego głównego elementu, może być, ale nie musi być ładowany przez konkretne zapytanie, w zależności od potrzeb w tym momencie programu (zobacz różne wzorce ładowania danych). Jednocześnie może być niepożądane, aby te właściwości były nullable, ponieważ wymusiłoby to cały dostęp do nich w celu sprawdzenia null, nawet gdy wiadomo, że nawigacja jest załadowana 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ę na sprawdzenie, czy relacja jest załadowana, poprzez ustalenie, czy nawigacja wynosi null. W takich przypadkach uzasadnione jest rozszerzenie nawigacji o możliwość przyjmowania wartości null. Oznacza to, że wymagane nawigacje od podmiotu zależnego do głównego:

  • 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.
  • Należy umożliwić wartość null, jeśli kod aplikacji może sprawdzać nawigację w celu ustalenia, czy relacja została załadowana.

Jeśli chcesz bardziej rygorystycznego podejścia, możesz mieć właściwość niepustą z polem zapasowym umożliwiającym 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, element zależny będzie dostępny przez właściwość. 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 różnych 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, które nie mogą być null, ale inicjalizując je do null, przy użyciu operatora anulowania null (!) w celu usunięcia 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, mimo że wystąpienie rzeczywistego błędu referencji null byłoby niemożliwe. 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 null-forgiving (!), aby poinformować kompilator, że rzeczywista null wartość nie jest możliwa.

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

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

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

Jeśli okaże się, że często to robisz, a wspomniane typy jednostek są głównie (lub wyłącznie) używane w zapytaniach EF Core, rozważ utworzenie właściwości nawigacyjnych jako nieprzechowujących wartości null i skonfigurowanie ich jako opcjonalnych przez interfejs Fluent API lub adnotacje danych. Spowoduje to usunięcie wszystkich ostrzeżeń kompilatora, zachowując relację opcjonalną; Jeśli jednak encje są przetwarzane poza platformą EF Core, można zauważyć 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 API nie miała oznaczenia adnotacji wartości null (publiczny API był niezwracający uwagi na wartości null), co utrudniało korzystanie z funkcji NRT, gdy była włączona. 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 pod względem możliwości wystąpienia wartości null, począwszy od platformy EF Core 6.0.
  • Inżynieria odwrotna nie obsługiwała typów referencyjnych dopuszczających wartość null (NRT) w C# 8: EF Core zawsze generował kod C#, który zakładał, że funkcja jest wyłączona. Na przykład, kolumny tekstowe mogące przechowywać wartości null były generowane jako właściwości typu string, a nie string?, przy użyciu interfejsu Fluent API lub adnotacji danych do konfigurowania, czy właściwość jest wymagana, czy nie. Jeśli używasz starszej wersji programu EF Core, nadal możesz edytować kod szkieletowy i zastąpić je adnotacjami zapewniającymi możliwość wartości null w języku C#.