Lavorare con tipi di riferimento nullable

C# 8 ha introdotto una nuova funzionalità denominata tipi di riferimento nullable (NRT), consentendo l'annotazione dei tipi riferimento, che indica se è valida per contenere null o meno. Se non si ha familiarità con questa funzionalità, è consigliabile acquisire familiarità con questa funzionalità leggendo la documentazione di C#. I tipi riferimento nullable sono abilitati per impostazione predefinita nei nuovi modelli di progetto, ma rimangono disabilitati nei progetti esistenti, a meno che non venga esplicitamente scelto.

Questa pagina presenta il supporto di EF Core per i tipi riferimento nullable e descrive le procedure consigliate per usarle.

Proprietà obbligatorie e facoltative

La documentazione principale sulle proprietà obbligatorie e facoltative e la relativa interazione con i tipi riferimento nullable è la pagina Proprietà obbligatorie e facoltative. È consigliabile iniziare leggendo prima la pagina.

Nota

Prestare attenzione quando si abilitano i tipi riferimento nullable in un progetto esistente: le proprietà del tipo di riferimento configurate in precedenza come facoltative verranno ora configurate come obbligatorie, a meno che non siano annotate in modo esplicito come nullable. Quando si gestisce uno schema di database relazionale, è possibile che vengano generate migrazioni che modificano il supporto dei valori Null della colonna di database.

Proprietà e inizializzazione non nullable

Quando i tipi riferimento nullable sono abilitati, il compilatore C# genera avvisi per qualsiasi proprietà non inizializzata non nullable, in quanto contengono null. Di conseguenza, non è possibile usare il metodo comune di scrittura dei tipi di entità:

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

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

Se si usa C# 11 o versione successiva, i membri necessari forniscono la soluzione perfetta a questo problema:

public required string Name { get; set; }

Il compilatore garantisce ora che quando il codice crea un'istanza di customer, inizializza sempre la relativa proprietà Name. Poiché la colonna di database mappata alla proprietà non è nullable, tutte le istanze caricate da EF contengono sempre un nome non Null.

Se si usa una versione precedente di C#, l'associazione di costruttori è una tecnica alternativa per assicurarsi che le proprietà non nullable siano inizializzate:

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

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

Sfortunatamente, in alcuni scenari l'associazione di costruttori non è un'opzione; le proprietà di navigazione, ad esempio, non possono essere inizializzate in questo modo. In questi casi, è sufficiente inizializzare la proprietà su null con l'aiuto dell'operatore null-forgiving (ma vedere di seguito per altri dettagli):

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

Proprietà di navigazione necessarie

Le proprietà di navigazione necessarie presentano difficoltà aggiuntive: anche se un dipendente esisterà sempre per una determinata entità, potrebbe o non essere caricato da una determinata query, a seconda delle esigenze in quel punto del programma (vedere i diversi modelli per il caricamento dei dati). Allo stesso tempo, potrebbe essere indesiderato rendere Null queste proprietà, poiché ciò forza l'accesso a tali proprietà per verificare la presenza di null, anche quando lo spostamento è noto per essere caricato e pertanto non può essere null.

Questo non è necessariamente un problema! Se un dipendente richiesto viene caricato correttamente (ad esempio tramite Include), l'accesso alla proprietà di navigazione garantisce che restituisca sempre un valore diverso da Null. D'altra parte, l'applicazione può scegliere di controllare se la relazione viene caricata controllando se lo spostamento è null. In questi casi, è ragionevole rendere nullable la navigazione. Ciò significa che gli spostamenti necessari dall'oggetto dipendente all'entità:

  • Deve essere non nullable se viene considerato un errore del programmatore per accedere a una navigazione quando non viene caricato.
  • Deve essere nullable se è accettabile che il codice dell'applicazione controlli la navigazione per determinare se la relazione viene caricata o meno.

Se si vuole un approccio più rigoroso, è possibile avere una proprietà non nullable con un campo sottostante nullable:

private Address? _shippingAddress;

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

Se lo spostamento viene caricato correttamente, il dipendente sarà accessibile tramite la proprietà . Se, tuttavia, si accede alla proprietà senza prima caricare correttamente l'entità correlata, viene generata un'eccezione InvalidOperationException perché il contratto API è stato usato in modo non corretto.

Nota

Gli spostamenti della raccolta, che contengono riferimenti a più entità correlate, devono essere sempre non nullable. Una raccolta vuota significa che non esistono entità correlate, ma l'elenco stesso non deve mai essere null.

DbContext e DbSet

Con EF, è pratica comune avere proprietà DbSet non inizializzate sui tipi di contesto:

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

Anche se questo genera in genere un avviso del compilatore, EF Core 7.0 e versioni successive eliminano questo avviso, poiché EF inizializza automaticamente queste proprietà tramite reflection.

Nella versione precedente di EF Core è possibile risolvere questo problema nel modo seguente:

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

Un'altra strategia consiste nell'usare proprietà automatiche non nullable, ma per inizializzarle in null, usando l'operatore null-forgiving (!) per disattivare l'avviso del compilatore. Il costruttore di base DbContext garantisce che tutte le proprietà DbSet vengano inizializzate e null non verranno mai osservate su di esse.

Quando si gestiscono relazioni facoltative, è possibile riscontrare avvisi del compilatore in cui un'eccezione di riferimento effettiva null sarebbe impossibile. Durante la conversione e l'esecuzione delle query LINQ, EF Core garantisce che, se non esiste un'entità correlata facoltativa, qualsiasi navigazione verrà semplicemente ignorata, anziché generare un'eccezione. Tuttavia, il compilatore non è a conoscenza di questa garanzia di EF Core e genera avvisi come se la query LINQ fosse stata eseguita in memoria, con LINQ to Objects. Di conseguenza, è necessario usare l'operatore null-forgiving (!) per informare il compilatore che un valore effettivo null non è possibile:

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

Si verifica un problema simile quando si includono più livelli di relazioni tra gli spostamenti facoltativi:

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

Se questa operazione viene eseguita molto e i tipi di entità in questione sono prevalentemente (o esclusivamente) usati nelle query di EF Core, è consigliabile rendere le proprietà di navigazione non nullable e configurarle come facoltative tramite l'API Fluent o le annotazioni dei dati. In questo modo verranno rimossi tutti gli avvisi del compilatore mantenendo la relazione facoltativa; Tuttavia, se le entità vengono attraversate all'esterno di EF Core, è possibile osservare i valori null anche se le proprietà vengono annotate come non nullable.

Limitazioni nelle versioni precedenti

Prima di EF Core 6.0, sono state applicate le limitazioni seguenti:

  • La superficie dell'API pubblica non è stata annotata per il supporto dei valori Null (l'API pubblica era "null-oblivious"), rendendo talvolta difficile l'uso quando la funzionalità NRT è attivata. Ciò include in particolare gli operatori LINQ asincroni esposti da EF Core, ad esempio FirstOrDefaultAsync. L'API pubblica è completamente annotata per i valori Null a partire da EF Core 6.0.
  • Reverse engineering non supportava i tipi di riferimento nullable C# 8( NRT): codice C# sempre generato da EF Core che presuppone che la funzionalità sia disattivata. Ad esempio, le colonne di testo che ammettono i valori Null sono state sottoposte a scaffolding come proprietà con tipo string, non string?, con l'API Fluent o le annotazioni dati usate per configurare se una proprietà è necessaria o meno. Se si usa una versione precedente di EF Core, è comunque possibile modificare il codice con scaffolding e sostituire tali elementi con le annotazioni che ammettono i valori Null di C#.