Dela via


Arbeta med referenstyper som kan vara null

Med C# nullable reference types (NRT) kan referenstyper kommenteras, vilket anger om det är giltigt för dem att innehålla null eller inte. Om du är nybörjare på den här funktionen rekommenderar vi att du bekantar dig med den genom att läsa C#-dokumenten. Nullbara referenstyper är aktiverade som standard i nya projektmallar, men förblir inaktiverade i befintliga projekt om de inte uttryckligen har valts.

Den här sidan introducerar EF Cores stöd för null-referenstyper och beskriver metodtips för att arbeta med dem.

Obligatoriska och valfria egenskaper

Huvuddokumentationen om obligatoriska och valfria egenskaper och deras interaktion med nullbara referenstyper är sidan Obligatoriska och Valfria egenskaper . Vi rekommenderar att du börjar med att läsa den sidan först.

Anmärkning

Var försiktig när du aktiverar nullbara referenstyper i ett befintligt projekt: Referenstypegenskaper som tidigare har konfigurerats som valfria konfigureras nu efter behov, såvida de inte uttryckligen kommenteras som null. När du hanterar ett relationsdatabasschema kan det leda till att migreringar genereras som ändrar databaskolumnens nullbarhet.

Egenskaper och initiering som inte kan nulliseras

När nullbara referenstyper är aktiverade genererar C#-kompilatorn varningar för alla icke-null-egenskaper som inte kan nulliseras, eftersom dessa skulle innehålla null. Därför kan inte följande vanliga sätt att skriva entitetstyper användas:

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

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

Om du använder C# 11 eller senare ger nödvändiga medlemmar den perfekta lösningen på problemet:

public required string Name { get; set; }

Kompilatorn garanterar nu att när koden instansierar en kund initieras alltid dess namnegenskap. Och eftersom databaskolumnen som mappas till egenskapen inte är nullbar innehåller alla instanser som läses in av EF alltid ett namn som inte är null.

Om du använder en äldre version av C#är konstruktorbindning en alternativ teknik för att säkerställa att dina icke-nullbara egenskaper initieras:

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

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

I vissa fall är konstruktorbindning tyvärr inte ett alternativ. navigeringsegenskaper kan till exempel inte initieras på det här sättet. I dessa fall kan du helt enkelt initiera egenskapen till null med hjälp av operatorn null-forgiving (men se nedan för mer information):

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

Nödvändiga navigeringsegenskaper

Nödvändiga navigeringsegenskaper innebär en ytterligare svårighet: även om en beroendeentitet alltid existerar för en given huvudentitet, kanske den inte laddas av en viss fråga, beroende på programmets behov vid den tidpunkten (se de olika mönstren för inläsning av data). Samtidigt kan det vara oönskat att göra dessa egenskaper nullbara, eftersom det skulle tvinga all åtkomst till dem att söka efter null, även när navigeringen är känd för att vara inläst och därför inte kan vara null.

Detta är inte nödvändigtvis ett problem! Så länge som ett nödvändigt beroende är korrekt inläst (t.ex. via Include), är det garanterat att åtkomsten till dess navigeringsegenskapen alltid returnerar något som inte är null. Å andra sidan kan programmet välja att kontrollera om relationen läses in eller inte genom att kontrollera om navigeringen är null. I sådana fall är det rimligt att göra navigeringen nullbar. Det innebär att nödvändiga navigeringar från det beroende till huvudmannen:

  • Bör vara icke-null om det anses vara ett programmerarfel att komma åt en navigering innan den är inläst.
  • Bör vara null om det är acceptabelt för programkod att kontrollera navigeringen för att avgöra om relationen läses in eller inte.

Om du vill ha en striktare metod kan du ha en icke-nullbar egenskap med ett nullbart bakgrundsfält:

private Address? _shippingAddress;

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

Så länge navigeringen är korrekt inläst kommer den beroende att vara tillgänglig via egenskapen. Men om egenskapen används utan att först läsa in den relaterade entiteten korrekt genereras en InvalidOperationException eftersom API-kontraktet har använts felaktigt.

Anmärkning

Samlingsnavigeringar, som innehåller referenser till flera relaterade entiteter, bör alltid vara icke-nullbara. En tom samling innebär att det inte finns några relaterade entiteter, men själva listan får aldrig vara null.

DbContext och DbSet

Med EF är det vanligt att ha oinitierade DbSet-egenskaper i kontexttyper.

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

Även om detta vanligtvis orsakar en kompilatorvarning undertrycker EF Core 7.0 och senare den här varningen, eftersom EF automatiskt initierar dessa egenskaper via reflektion.

På den äldre versionen av EF Core kan du kringgå det här problemet på följande sätt:

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

En annan strategi är att använda icke-nullbara automatiska egenskaper, men att initiera dem till null, med hjälp av operatorn null-forgiving (!) för att tysta kompilatorvarningen. DbContext-baskonstruktorn ser till att alla DbSet-egenskaper initieras och att null aldrig observeras på dem.

När du hanterar valfria relationer är det möjligt att stöta på kompilatorvarningar där ett faktiskt null referensfel skulle vara omöjligt. När du översätter och kör DINA LINQ-frågor garanterar EF Core att om det inte finns någon valfri relaterad entitet ignoreras all navigering till den i stället för att utlösas. Kompilatorn känner dock inte till den här EF Core-garantin och skapar varningar som om LINQ-frågan kördes i minnet, med LINQ till Objekt. Därför är det nödvändigt att använda operatorn null-forgiving (!) för att informera kompilatorn om att ett faktiskt null värde inte är möjligt:

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

Ett liknande problem uppstår när du inkluderar flera nivåer av relationer i valfria navigeringar:

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

Om du gör detta mycket, och de aktuella entitetstyperna främst (eller uteslutande) används i EF Core-frågor, kan du överväga att göra navigeringsegenskaperna icke-nullbara och konfigurera dem som valfria via Fluent API eller dataanteckningar. Detta tar bort alla kompilatorvarningar samtidigt som relationen är valfri. Men om dina entiteter passerar utanför EF Core kan du observera null värden även om egenskaperna kommenteras som icke-nullbara.

Begränsningar i äldre versioner

Före EF Core 6.0 tillämpades följande begränsningar:

  • Den offentliga API-ytan kommenterades inte för nullabilitet (det offentliga API:et var "null-oblivious"), vilket gör det ibland besvärligt att använda när NRT-funktionen är aktiverad. Detta omfattar särskilt de asynkrona LINQ-operatorer som exponeras av EF Core, till exempel FirstOrDefaultAsync. Det offentliga API:et är helt kommenterat för nullabilitet från och med EF Core 6.0.
  • Omvänd teknik har inte stöd för C# 8 nullable reference types (NRTs): EF Core genererade alltid C#-kod som antog att funktionen är inaktiverad. Till exempel har nullbara textkolumner skapats som en egenskap med typen string , inte string?, med antingen Fluent-API:et eller Dataanteckningar som används för att konfigurera om en egenskap krävs eller inte. Om du använder en äldre version av EF Core kan du fortfarande redigera den scaffolded koden och ersätta dessa med C# nullability-anteckningar.