Co nowego w programie EF Core 6.0

Program EF Core 6.0 został wysłany do pakietu NuGet. Ta strona zawiera omówienie interesujących zmian wprowadzonych w tej wersji.

Porada

Możesz uruchomić i debugować przykłady przedstawione poniżej, pobierając przykładowy kod z usługi GitHub.

tabele czasowe SQL Server

Problem z usługą GitHub: #4693.

SQL Server tabele czasowe automatycznie śledzą wszystkie dane przechowywane w tabeli, nawet po zaktualizowaniu lub usunięciu tych danych. Jest to osiągane przez utworzenie równoległej "tabeli historii", w której przechowywane są dane historyczne ze znacznikami czasu za każdym razem, gdy zostanie wprowadzona zmiana w tabeli głównej. Umożliwia to wykonywanie zapytań dotyczących danych historycznych, takich jak inspekcja lub przywracanie, takie jak odzyskiwanie po przypadkowej mutacji lub usunięciu.

Platforma EF Core obsługuje teraz następujące technologię:

  • Tworzenie tabel czasowych przy użyciu migracji
  • Przekształcanie istniejących tabel w tabele czasowe, ponownie przy użyciu funkcji Migracje
  • Wykonywanie zapytań dotyczących danych historycznych
  • Przywracanie danych z jakiegoś punktu w przeszłości

Konfigurowanie tabeli czasowej

Konstruktor modelu może służyć do konfigurowania tabeli jako czasowej. Przykład:

modelBuilder
    .Entity<Employee>()
    .ToTable("Employees", b => b.IsTemporal());

Podczas tworzenia bazy danych przy użyciu programu EF Core nowa tabela zostanie skonfigurowana jako tabela czasowa z wartościami domyślnymi SQL Server dla tabeli sygnatur czasowych i historii. Rozważmy na przykład Employee typ jednostki:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

Utworzona tabela czasowa będzie wyglądać następująco:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

Zwróć uwagę, że SQL Server tworzy dwie ukryte datetime2 kolumny o nazwie PeriodEnd i PeriodStart. Te "kolumny kropki" reprezentują zakres czasu, w którym istniały dane w wierszu. Te kolumny są mapowane na właściwości w tle w modelu EF Core, dzięki czemu mogą być używane w zapytaniach, jak pokazano później.

Ważne

Czasy w tych kolumnach są zawsze czasu UTC generowane przez SQL Server. Czasy UTC są używane dla wszystkich operacji obejmujących tabele czasowe, takie jak w zapytaniach przedstawionych poniżej.

Zwróć również uwagę, że skojarzona tabela historii o nazwie EmployeeHistory jest tworzona automatycznie. Nazwy kolumn kropki i tabeli historii można zmienić przy użyciu dodatkowej konfiguracji konstruktora modelu. Przykład:

modelBuilder
    .Entity<Employee>()
    .ToTable(
        "Employees",
        b => b.IsTemporal(
            b =>
            {
                b.HasPeriodStart("ValidFrom");
                b.HasPeriodEnd("ValidTo");
                b.UseHistoryTable("EmployeeHistoricalData");
            }));

Zostanie to odzwierciedlone w tabeli utworzonej przez SQL Server:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

Używanie tabel czasowych

W większości przypadków tabele czasowe są używane tak samo jak każda inna tabela. Oznacza to, że kolumny okresu i dane historyczne są obsługiwane w sposób niewidoczny przez SQL Server tak, aby aplikacja mogła je zignorować. Na przykład nowe jednostki można zapisać w bazie danych w normalny sposób:

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

context.SaveChanges();

Te dane można następnie wykonywać zapytania, aktualizować i usuwać w normalny sposób. Przykład:

var employee = context.Employees.Single(e => e.Name == "Rainbow Dash");
context.Remove(employee);
context.SaveChanges();

Ponadto po normalnym zapytaniu śledzenia można uzyskać dostęp do wartości z kolumn okresu bieżących danych z śledzonych jednostek. Przykład:

var employees = context.Employees.ToList();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

Ta funkcja drukuje:

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

Zwróć uwagę, że kolumna ValidTo (domyślnie nazywana PeriodEnd) zawiera maksymalną datetime2 wartość. Jest to zawsze przypadek dla bieżących wierszy w tabeli. Kolumny ValidFrom (domyślnie nazywane PeriodStart) zawierają czas UTC wstawiony wiersz.

Wykonywanie zapytań dotyczących danych historycznych

Program EF Core obsługuje zapytania obejmujące dane historyczne za pośrednictwem kilku nowych operatorów zapytań:

  • TemporalAsOf: zwraca wiersze, które były aktywne (bieżące) w danym czasie UTC. Jest to pojedynczy wiersz z bieżącej tabeli lub tabeli historii dla danego klucza podstawowego.
  • TemporalAll: zwraca wszystkie wiersze w danych historycznych. Zazwyczaj jest to wiele wierszy z tabeli historii i/lub bieżącej tabeli dla danego klucza podstawowego.
  • TemporalFromTo: zwraca wszystkie wiersze, które były aktywne między dwoma podanymi godzinami UTC. Może to być wiele wierszy z tabeli historii i/lub bieżącej tabeli dla danego klucza podstawowego.
  • TemporalBetween: Takie same jak TemporalFromTo, z tą różnicą, że wiersze są uwzględniane, które stały się aktywne na górnej granicy.
  • TemporalContainedIn: Zwraca wszystkie wiersze, które zaczęły być aktywne i kończą się aktywne między dwoma podanymi godzinami UTC. Może to być wiele wierszy z tabeli historii i/lub bieżącej tabeli dla danego klucza podstawowego.

Uwaga

Zapoznaj się z dokumentacją SQL Server tabel czasowych, aby uzyskać więcej informacji na temat dokładnie wierszy uwzględnionych dla każdego z tych operatorów.

Na przykład po wprowadzeniu niektórych aktualizacji i usunięcia danych możemy uruchomić zapytanie, używając TemporalAll polecenia , aby wyświetlić dane historyczne:

var history = context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

Zwróć uwagę na to, jak działa ef. Metoda właściwości może służyć do uzyskiwania dostępu do wartości z kolumn kropki. Jest to używane w klauzuli OrderBy do sortowania danych, a następnie w projekcji w celu uwzględnienia tych wartości w zwróconych danych.

To zapytanie przywraca następujące dane:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

Zwróć uwagę, że ostatni wiersz przestał być aktywny o godzinie 26.08.2021 16:44:59. Wynika to z tego, że wiersz rainbow dash został usunięty z tabeli głównej w tym czasie. Zobaczymy później, jak można przywrócić te dane.

Podobne zapytania można pisać przy użyciu polecenia TemporalFromTo, TemporalBetweenlub TemporalContainedIn. Przykład:

var history = context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

To zapytanie zwraca następujące wiersze:

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

Przywracanie danych historycznych

Jak wspomniano powyżej, Rainbow Dash został usunięty z Employees tabeli. To był wyraźnie błąd, więc wróćmy do punktu w czasie i przywróćmy brakujący wiersz z tego czasu.

var employee = context
    .Employees
    .TemporalAsOf(timeStamp2)
    .Single(e => e.Name == "Rainbow Dash");

context.Add(employee);
context.SaveChanges();

To zapytanie zwraca pojedynczy wiersz dla Rainbow Dash, jak to miało miejsce w danym czasie UTC. Wszystkie zapytania korzystające z operatorów czasowych domyślnie nie śledzą, więc zwracana jednostka nie jest śledzona. Ma to sens, ponieważ obecnie nie istnieje w tabeli głównej. Aby ponownie wstawić jednostkę do tabeli głównej, po prostu oznaczymy ją jako Added , a następnie wywołamy metodę SaveChanges.

Po ponownym wstawieniu wiersza Rainbow Dash zapytanie dotyczące danych historycznych pokazuje, że wiersz został przywrócony, ponieważ był w danym czasie UTC:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

Pakiety migracji

Problem z usługą GitHub: #19693.

Migracje platformy EF Core służą do generowania aktualizacji schematu bazy danych na podstawie zmian w modelu EF. Te aktualizacje schematu powinny być stosowane w czasie wdrażania aplikacji, często w ramach systemu ciągłej integracji/ciągłego wdrażania (C.I./C.D.).

Program EF Core oferuje teraz nowy sposób stosowania tych aktualizacji schematu: pakietów migracji. Pakiet migracji to mały plik wykonywalny zawierający migracje i kod potrzebny do zastosowania tych migracji do bazy danych.

Uwaga

Zobacz Wprowadzenie przyjaznych dla metodyki DevOps pakietów migracji platformy EF Core na blogu platformy .NET, aby uzyskać bardziej szczegółową dyskusję na temat migracji, pakietów i wdrożeń.

Pakiety migracji są tworzone przy użyciu dotnet ef narzędzia wiersza polecenia. Przed kontynuowaniem upewnij się, że zainstalowano najnowszą wersję narzędzia .

Pakiet wymaga dołączenia migracji. Są one tworzone przy użyciu zgodnie z dotnet ef migrations add opisem w dokumentacji migracji. Po zakończeniu migracji do wdrożenia utwórz pakiet przy użyciu polecenia dotnet ef migrations bundle. Przykład:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Dane wyjściowe są plikiem wykonywalnym odpowiednim dla docelowego systemu operacyjnego. W moim przypadku jest to system Windows x64, więc otrzymuję porzucony efbundle.exe w moim folderze lokalnym. Uruchomienie tego pliku wykonywalnego powoduje zastosowanie zawartych w nim migracji:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903083845_MyMigration'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Migracje są stosowane do bazy danych tylko wtedy, gdy nie zostały jeszcze zastosowane. Na przykład uruchomienie tego samego pakietu nie powoduje niczego, ponieważ nie ma żadnych nowych migracji do zastosowania:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
No migrations were applied. The database is already up to date.
Done.
PS C:\local\AllTogetherNow\SixOh>

Jeśli jednak zmiany są wprowadzane do modelu, a więcej migracji jest generowanych za pomocą dotnet ef migrations addpolecenia , można je połączyć w nowy plik wykonywalny gotowy do zastosowania. Przykład:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Zwróć uwagę, że --force za pomocą opcji można zastąpić istniejący pakiet nowym.

Wykonanie tego nowego pakietu dotyczy tych dwóch nowych migracji do bazy danych:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Domyślnie pakiet używa συμβολοσειρά σύνδεσης bazy danych z konfiguracji aplikacji. Można jednak zmigrować inną bazę danych, przekazując συμβολοσειρά σύνδεσης w wierszu polecenia. Przykład:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Zauważ, że tym razem wszystkie trzy migracje zostały zastosowane, ponieważ żadna z nich nie została jeszcze zastosowana do produkcyjnej bazy danych.

Inne opcje można przekazać do wiersza polecenia. Oto niektóre typowe opcje:

  • --output aby określić ścieżkę pliku wykonywalnego do utworzenia.
  • --context aby określić typ DbContext do użycia, gdy projekt zawiera wiele typów kontekstu.
  • --project aby określić projekt do użycia. Domyślnie jest to bieżący katalog roboczy.
  • --startup-project aby określić projekt startowy do użycia. Domyślnie jest to bieżący katalog roboczy.
  • --no-build aby zapobiec kompilowaniu projektu przed uruchomieniem polecenia . Powinno to być używane tylko wtedy, gdy projekt jest znany jako aktualny.
  • --verbose aby wyświetlić szczegółowe informacje o tym, co robi polecenie. Tej opcji należy używać w przypadku uwzględniania informacji w raportach o usterce.

Użyj dotnet ef migrations bundle --help polecenia , aby wyświetlić wszystkie dostępne opcje.

Należy pamiętać, że domyślnie każda migracja jest stosowana we własnej transakcji. Zobacz Problem z usługą GitHub nr 22616 , aby zapoznać się z możliwymi przyszłymi ulepszeniami w tym obszarze.

Konfiguracja modelu przed konwencją

Problem z usługą GitHub: #12229.

Poprzednie wersje programu EF Core wymagają jawnego skonfigurowania mapowania dla każdej właściwości danego typu, gdy to mapowanie różni się od domyślnego. Obejmuje to "aspekty", takie jak maksymalna długość ciągów i precyzja dziesiętna, a także konwersja wartości dla typu właściwości.

Wymaga to jednej z następujących czynności:

  • Konfiguracja konstruktora modelu dla każdej właściwości
  • Atrybut mapowania dla każdej właściwości
  • Jawna iteracja we wszystkich właściwościach wszystkich typów jednostek i korzystanie z interfejsów API metadanych niskiego poziomu podczas kompilowania modelu.

Należy pamiętać, że jawna iteracja jest podatna na błędy i jest trudna do niezawodnego wykonania, ponieważ lista typów jednostek i zamapowanych właściwości może nie być ostateczna w momencie wystąpienia tej iteracji.

Program EF Core 6.0 umożliwia jednokrotne określenie tej konfiguracji mapowania dla danego typu. Następnie zostanie zastosowana do wszystkich właściwości tego typu w modelu. Jest to nazywane "konfiguracją modelu przed konwencją", ponieważ konfiguruje aspekty modelu, które są następnie używane przez konwencje tworzenia modelu. Taka konfiguracja jest stosowana przez zastąpienie ConfigureConventions elementu :DbContext

public class SomeDbContext : DbContext
{
    protected override void ConfigureConventions(
        ModelConfigurationBuilder configurationBuilder)
    {
        // Pre-convention model configuration goes here
    }
}

Rozważmy na przykład następujące typy jednostek:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

Wszystkie właściwości ciągu można skonfigurować jako ANSI (zamiast Unicode) i mieć maksymalną długość 1024:

configurationBuilder
    .Properties<string>()
    .AreUnicode(false)
    .HaveMaxLength(1024);

Wszystkie właściwości daty/godziny można przekonwertować na 64-bitowe liczby całkowite w bazie danych przy użyciu domyślnej konwersji z DateTimes na długie:

configurationBuilder
    .Properties<DateTime>()
    .HaveConversion<long>();

Wszystkie właściwości logiczne można przekonwertować na liczby całkowite 0 lub 1 użyć jednego z wbudowanych konwerterów wartości:

configurationBuilder
    .Properties<bool>()
    .HaveConversion<BoolToZeroOneConverter<int>>();

Zakładając, że Session jest właściwością przejściową jednostki i nie należy jej utrwalać, można ją zignorować wszędzie w modelu:

configurationBuilder
    .IgnoreAny<Session>();

Konfiguracja modelu przed konwencją jest bardzo przydatna podczas pracy z obiektami wartości. Na przykład typ Money w powyższym modelu jest reprezentowany przez strukturę tylko do odczytu:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Jest to następnie serializowane do i z formatu JSON przy użyciu niestandardowego konwertera wartości:

public class MoneyConverter : ValueConverter<Money, string>
{
    public MoneyConverter()
        : base(
            v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
            v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null))
    {
    }
}

Ten konwerter wartości można skonfigurować raz dla wszystkich zastosowań Money:

configurationBuilder
    .Properties<Money>()
    .HaveConversion<MoneyConverter>()
    .HaveMaxLength(64);

Zauważ również, że można określić dodatkowe aspekty dla kolumny ciągu, w której jest przechowywany serializowany kod JSON. W takim przypadku kolumna jest ograniczona do maksymalnej długości 64.

Tabele utworzone dla SQL Server przy użyciu migracji pokazują, jak konfiguracja została zastosowana do wszystkich zamapowanych kolumn:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

Istnieje również możliwość określenia domyślnego mapowania typów dla danego typu. Przykład:

configurationBuilder
    .DefaultTypeMapping<string>()
    .IsUnicode(false);

Jest to rzadko potrzebne, ale może być przydatne, jeśli typ jest używany w zapytaniu w sposób, który nie jest skorelowany z dowolną zamapowaną właściwością modelu.

Uwaga

Zobacz Ogłoszenie programu Entity Framework Core 6.0 (wersja zapoznawcza 6: konfigurowanie konwencji na blogu platformy .NET), aby uzyskać więcej informacji i przykłady konfiguracji modelu przed konwencją.

Skompilowane modele

Problem z usługą GitHub: #1906.

Skompilowane modele mogą poprawić czas uruchamiania platformy EF Core dla aplikacji z dużymi modelami. Duży model zazwyczaj oznacza od 100 do 1000 typów jednostek i relacji.

Czas uruchamiania oznacza czas wykonywania pierwszej operacji na obiekcie DbContext, gdy ten typ DbContext jest używany po raz pierwszy w aplikacji. Należy pamiętać, że samo utworzenie wystąpienia dbContext nie powoduje zainicjowania modelu EF. Zamiast tego typowe pierwsze operacje, które powodują zainicjowanie modelu, obejmują wywołanie DbContext.Add lub wykonanie pierwszego zapytania.

Skompilowane modele są tworzone przy użyciu dotnet ef narzędzia wiersza polecenia. Przed kontynuowaniem upewnij się, że zainstalowano najnowszą wersję narzędzia .

Nowe dbcontext optimize polecenie służy do generowania skompilowanego modelu. Przykład:

dotnet ef dbcontext optimize

Opcje --output-dir i --namespace umożliwiają określenie katalogu i przestrzeni nazw, w której zostanie wygenerowany skompilowany model. Przykład:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

Dane wyjściowe z uruchomienia tego polecenia zawierają fragment kodu do kopiowania i wklejania do konfiguracji DbContext, aby spowodować, że program EF Core będzie używać skompilowanego modelu. Przykład:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder
        .UseModel(MyCompiledModels.BlogsContextModel.Instance)
        .UseSqlite(@"Data Source=test.db");

Uruchamianie skompilowanego modelu

Zwykle nie jest konieczne przyjrzenie się wygenerowanemu kodowi uruchamiania. Czasami jednak może być przydatne dostosowanie modelu lub jego ładowania. Kod uruchamiania wygląda mniej więcej tak:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

Jest to klasa częściowa z metodami częściowymi, które można zaimplementować w celu dostosowania modelu zgodnie z potrzebami.

Ponadto dla typów DbContext można wygenerować wiele skompilowanych modeli, które mogą używać różnych modeli w zależności od konfiguracji środowiska uruchomieniowego. Powinny one zostać umieszczone w różnych folderach i przestrzeniach nazw, jak pokazano powyżej. Informacje o środowisku uruchomieniowym, takie jak συμβολοσειρά σύνδεσης, można następnie zbadać i zwrócić prawidłowy model zgodnie z potrzebami. Przykład:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

Ograniczenia

Skompilowane modele mają pewne ograniczenia:

Ze względu na te ograniczenia należy używać tylko skompilowanych modeli, jeśli czas uruchamiania platformy EF Core jest zbyt wolny. Kompilowanie małych modeli zwykle nie jest tego warte.

Jeśli obsługa dowolnej z tych funkcji ma kluczowe znaczenie dla Twojego sukcesu, zagłosuj na odpowiednie problemy połączone powyżej.

Testy porównawcze

Porada

Możesz spróbować skompilować duży model i uruchomić test porównawczy, pobierając przykładowy kod z usługi GitHub.

Model w repozytorium GitHub, do których odwołuje się powyżej, zawiera 449 typów jednostek, 6390 właściwości i 720 relacji. Jest to średnio duży model. Pomiar średniej liczby pierwszych zapytań przy użyciu rozwiązania BenchmarkDotNet wynosi 1,02 sekundy na dość wydajnym laptopie. Użycie skompilowanych modeli sprowadza to do 117 milisekund na tym samym sprzęcie. 8x-10x poprawa jak ten pozostaje stosunkowo stała w miarę wzrostu rozmiaru modelu.

Poprawa wydajności skompilowanego modelu

Uwaga

Zobacz Ogłoszenie programu Entity Framework Core 6.0 w wersji zapoznawczej 5: skompilowane modele na blogu platformy .NET, aby uzyskać bardziej szczegółowe omówienie wydajności uruchamiania i skompilowanych modeli programu EF Core.

Ulepszona wydajność w witrynie TechEmpower Fortunes

Problem z usługą GitHub: #23611.

Wprowadziliśmy znaczne ulepszenia wydajności zapytań dla platformy EF Core 6.0. W szczególności:

  • Wydajność ef Core 6.0 jest teraz 70% szybsza w branżowym teście TechEmpower Fortunes, w porównaniu do 5,0.
    • Jest to ulepszenie wydajności pełnego stosu, w tym ulepszenia w kodzie testu porównawczego, środowisko uruchomieniowe platformy .NET itp.
  • Sama platforma EF Core 6.0 jest o 31% szybsza w wykonywaniu nieśledzonych zapytań.
  • Alokacje sterty zostały zmniejszone o 43% podczas wykonywania zapytań.

Po tych ulepszeniach różnica między popularnym "mikro-ORM" Dapper i EF Core w teście TechEmpower Fortunes zawęziła się z 55% do około nieco poniżej 5%.

Uwaga

Zobacz Ogłoszenie programu Entity Framework Core 6.0 w wersji zapoznawczej 4: Performance Edition na blogu platformy .NET, aby zapoznać się ze szczegółowym omówieniem ulepszeń wydajności zapytań w programie EF Core 6.0.

Ulepszenia dostawcy usługi Cosmos

Program EF Core 6.0 zawiera wiele ulepszeń dostawcy bazy danych usługi Azure Cosmos DB.

Porada

Możesz uruchamiać i debugować wszystkie przykłady specyficzne dla usługi Cosmos, pobierając przykładowy kod z usługi GitHub.

Domyślnie do niejawnej własności

Problem z usługą GitHub: #24803.

Podczas tworzenia modelu dla dostawcy usługi Cosmos program EF Core 6.0 domyślnie oznaczy typy jednostek podrzędnych jako należące do ich jednostki nadrzędnej. Eliminuje to konieczność wielu OwnsMany wywołań i OwnsOne w modelu Cosmos. Ułatwia to osadzanie typów podrzędnych w dokumencie dla typu nadrzędnego, co jest zwykle odpowiednim sposobem modelowania elementów nadrzędnych i elementów podrzędnych w bazie danych dokumentów.

Rozważmy na przykład następujące typy jednostek:

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }
    
    public string LastName { get; set; }
    public bool IsRegistered { get; set; }
    
    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

W programie EF Core 5.0 te typy zostałyby modelowane dla usługi Cosmos przy użyciu następującej konfiguracji:

modelBuilder.Entity<Family>()
    .HasPartitionKey(e => e.LastName)
    .OwnsMany(f => f.Parents);

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);        

W programie EF Core 6.0 własność jest niejawna, co zmniejsza konfigurację modelu do:

modelBuilder.Entity<Family>().HasPartitionKey(e => e.LastName);

Wynikowe dokumenty cosmos mają rodziców rodziny, dzieci, zwierząt domowych i adresów osadzonych w dokumencie rodzinnym. Przykład:

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

Uwaga

Należy pamiętać, że OwnsOne/OwnsMany konfiguracja musi być używana, jeśli trzeba jeszcze bardziej skonfigurować te typy własności.

Kolekcje typów pierwotnych

Problem z usługą GitHub: #14762.

Program EF Core 6.0 natywnie mapuje kolekcje typów pierwotnych podczas korzystania z dostawcy bazy danych Cosmos. Rozważmy na przykład ten typ jednostki:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Zarówno lista, jak i słownik można wypełnić i wstawić do bazy danych w normalny sposób:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
context.SaveChanges();

Spowoduje to wyświetlenie następującego dokumentu JSON:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Te kolekcje można następnie aktualizować w normalny sposób:

book.Quotes.Add("Pressing the emergency button lowered the rods again.");
book.Notes["48"] = "Chiesa d'Oro";

context.SaveChanges();

Ograniczenia:

  • Obsługiwane są tylko słowniki z kluczami ciągów
  • Wykonywanie zapytań dotyczących zawartości kolekcji pierwotnych nie jest obecnie obsługiwane. Zagłosuj na #16926, #25700 i #25701 , jeśli te funkcje są dla Ciebie ważne.

Tłumaczenia na wbudowane funkcje

Problem z usługą GitHub: #16143.

Dostawca usługi Cosmos tłumaczy teraz więcej metod biblioteki klas bazowych (BCL) na wbudowane funkcje usługi Cosmos. W poniższych tabelach przedstawiono tłumaczenia, które są nowe w programie EF Core 6.0.

Tłumaczenia ciągów

BCL, metoda Wbudowana funkcja Uwagi
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ Operator CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL Tylko wywołania bez uwzględniania wielkości liter

Tłumaczenia dla LOWER, , LTRIM, RTRIMTRIM, UPPER, i SUBSTRING zostały dodane przez @Marusyk. Dziękujemy!

Na przykład:

var stringResults
    = context.Triangles.Where(
            e => e.Name.Length > 4
                 && e.Name.Trim().ToLower() != "obtuse"
                 && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
        .ToList();

Co tłumaczy się na:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

Tłumaczenia matematyczne

BCL, metoda Wbudowana funkcja
Math.Abs lub MathF.Abs ABS
Math.Acos lub MathF.Acos ACOS
Math.Asin lub MathF.Asin ASIN
Math.Atan lub MathF.Atan ATAN
Math.Atan2 lub MathF.Atan2 ATN2
Math.Ceiling lub MathF.Ceiling CEILING
Math.Cos lub MathF.Cos COS
Math.Exp lub MathF.Exp EXP
Math.Floor lub MathF.Floor FLOOR
Math.Log lub MathF.Log LOG
Math.Log10 lub MathF.Log10 LOG10
Math.Pow lub MathF.Pow POWER
Math.Round lub MathF.Round ROUND
Math.Sign lub MathF.Sign SIGN
Math.Sin lub MathF.Sin SIN
Math.Sqrt lub MathF.Sqrt SQRT
Math.Tan lub MathF.Tan TAN
Math.Truncate lub MathF.Truncate TRUNC
DbFunctions.Random RAND

Te tłumaczenia zostały dodane przez @Marusyk. Dziękujemy!

Na przykład:

var hypotenuse = 42.42;
var mathResults
    = context.Triangles.Where(
            e => (Math.Round(e.Angle1) == 90.0
                  || Math.Round(e.Angle2) == 90.0)
                 && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                     || hypotenuse * Math.Cos(e.Angle2) > 30.0))
        .ToList();

Co tłumaczy się na:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

Tłumaczenia daty/godziny

BCL, metoda Wbudowana funkcja
DateTime.UtcNow GetCurrentDateTime

Te tłumaczenia zostały dodane przez @Marusyk. Dziękujemy!

Na przykład:

var timeResults
    = context.Triangles.Where(
            e => e.InsertedOn <= DateTime.UtcNow)
        .ToList();

Co tłumaczy się na:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (c["InsertedOn"] <= GetCurrentDateTime()))

Nieprzetworzone zapytania SQL z bazą danych FromSql

Problem z usługą GitHub: #17311.

Czasami konieczne jest wykonanie nieprzetworzonego zapytania SQL zamiast używania LINQ. Jest to teraz obsługiwane przez dostawcę usługi Cosmos przy użyciu FromSql metody . Działa to tak samo jak w przypadku dostawców relacyjnych. Przykład:

var maxAngle = 60;
var rawResults
    = context.Triangles.FromSqlRaw(
            @"SELECT * FROM root c WHERE c[""Angle1""] <= {0} OR c[""Angle2""] <= {0}",
            maxAngle)
        .ToList();

Który jest wykonywany jako:

SELECT c
FROM (
         SELECT * FROM root c WHERE c["Angle1"] <= @p0 OR c["Angle2"] <= @p0
     ) c
WHERE (c["Discriminator"] = "Triangle")

Odrębne zapytania

Problem z usługą GitHub: #16144.

Proste zapytania korzystające z usługi Distinct są teraz tłumaczone. Przykład:

var distinctResults
    = context.Triangles
        .Select(e => e.Angle1).OrderBy(e => e).Distinct()
        .ToList();

Co tłumaczy się na:

SELECT DISTINCT c["Angle1"]
FROM root c
WHERE (c["Discriminator"] = "Triangle")
ORDER BY c["Angle1"]

Diagnostyka

Problem z usługą GitHub: #17298.

Dostawca usługi Cosmos rejestruje teraz więcej informacji diagnostycznych, w tym zdarzenia dotyczące wstawiania, wykonywania zapytań, aktualizowania i usuwania danych z bazy danych. Jednostki żądań (RU) są uwzględniane w tych zdarzeniach zawsze, gdy jest to odpowiednie.

Uwaga

Dzienniki są wyświetlane w tym miejscu, EnableSensitiveDataLogging() aby wyświetlić wartości identyfikatorów.

Wstawianie elementu do bazy danych Cosmos powoduje wygenerowanie CosmosEventId.ExecutedCreateItem zdarzenia. Na przykład ten kod:

var triangle = new Triangle
{
    Name = "Impossible",
    PartitionKey = "TrianglesPartition",
    Angle1 = 90,
    Angle2 = 90,
    InsertedOn = DateTime.UtcNow
};
context.Add(triangle);
context.SaveChanges();

Rejestruje następujące zdarzenie diagnostyczne:

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Pobieranie elementów z bazy danych Cosmos przy użyciu zapytania powoduje wygenerowanie CosmosEventId.ExecutingSqlQuery zdarzenia, a następnie odczytanie co najmniej jednego CosmosEventId.ExecutedReadNext zdarzenia dla elementów. Na przykład ten kod:

var equilateral = context.Triangles.Single(e => e.Name == "Equilateral");

Rejestruje następujące zdarzenia diagnostyczne:

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

Pobieranie pojedynczego elementu z bazy danych Cosmos przy użyciu Find klucza partycji powoduje wygenerowanie zdarzeń CosmosEventId.ExecutingReadItem i CosmosEventId.ExecutedReadItem . Na przykład ten kod:

var isosceles = context.Triangles.Find("Isosceles", "TrianglesPartition");

Rejestruje następujące zdarzenia diagnostyczne:

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

Zapisanie zaktualizowanego elementu w bazie danych Cosmos powoduje wygenerowanie CosmosEventId.ExecutedReplaceItem zdarzenia. Na przykład ten kod:

triangle.Angle2 = 89;
context.SaveChanges();

Rejestruje następujące zdarzenie diagnostyczne:

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Usunięcie elementu z bazy danych Cosmos powoduje wygenerowanie CosmosEventId.ExecutedDeleteItem zdarzenia. Na przykład ten kod:

context.Remove(triangle);
context.SaveChanges();

Rejestruje następujące zdarzenie diagnostyczne:

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Konfigurowanie przepływności

Problem z usługą GitHub: #17301.

Model usługi Cosmos można teraz skonfigurować przy użyciu przepływności ręcznej lub automatycznej. Te wartości umożliwiają aprowizację przepływności w bazie danych. Przykład:

modelBuilder.HasManualThroughput(2000);
modelBuilder.HasAutoscaleThroughput(4000);

Ponadto poszczególne typy jednostek można skonfigurować do aprowizowania przepływności dla odpowiedniego kontenera. Przykład:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasManualThroughput(5000);
        entityTypeBuilder.HasAutoscaleThroughput(3000);
    });

Konfigurowanie czasu wygaśnięcia

Problem z usługą GitHub: #17307.

Typy jednostek w modelu Cosmos można teraz skonfigurować przy użyciu domyślnego czasu wygaśnięcia i czasu wygaśnięcia dla magazynu analitycznego. Przykład:

modelBuilder.Entity<Family>(
    entityTypeBuilder =>
    {
        entityTypeBuilder.HasDefaultTimeToLive(100);
        entityTypeBuilder.HasAnalyticalStoreTimeToLive(200);
    });

Rozwiązywanie problemów z fabryką klienta HTTP

Problem z usługą GitHub: #21274. Ta funkcja została udostępniona przez @dnperfors. Dziękujemy!

Używane HttpClientFactory przez dostawcę usługi Cosmos można teraz jawnie ustawić. Może to być szczególnie przydatne podczas testowania, na przykład w celu obejścia weryfikacji certyfikatu podczas korzystania z emulatora usługi Cosmos w systemie Linux:

optionsBuilder
    .EnableSensitiveDataLogging()
    .UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==",
        "PrimitiveCollections",
        cosmosOptionsBuilder =>
        {
            cosmosOptionsBuilder.HttpClientFactory(
                () => new HttpClient(
                    new HttpClientHandler
                    {
                        ServerCertificateCustomValidationCallback =
                            HttpClientHandler.DangerousAcceptAnyServerCertificateValidator
                    }));
        });

Uwaga

Zobacz Artykuł Taking the EF Core Azure Cosmos DB Provider for a Test Drive on the .NET Blog (Biorąc dostawcę usługi Azure Cosmos DB dla platformy EF Core dla wersji testowej na blogu platformy .NET), aby uzyskać szczegółowy przykład zastosowania ulepszeń dostawcy usługi Cosmos do istniejącej aplikacji.

Ulepszenia tworzenia szkieletów z istniejącej bazy danych

Program EF Core 6.0 zawiera kilka ulepszeń podczas odtwarzania modelu EF z istniejącej bazy danych.

Tworzenie szkieletu relacji wiele-do-wielu

Problem z usługą GitHub: #22475.

Program EF Core 6.0 wykrywa proste tabele sprzężenia i automatycznie generuje mapowanie wiele-do-wielu. Rozważmy na przykład tabele dla Posts i Tags, a tabela PostTag sprzężenia łącząca je:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

Te tabele można szkieletować z poziomu wiersza polecenia. Przykład:

dotnet ef dbcontext scaffold "Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=BloggingWithNRTs" Microsoft.EntityFrameworkCore.SqlServer

Spowoduje to utworzenie klasy Post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

I klasa tagu:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Ale nie ma klasy dla PostTag tabeli. Zamiast tego konfiguracja relacji wiele-do-wielu jest szkieletem:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Tworzenie szkieletu typów odwołań dopuszczanych do wartości null w języku C#

Problem z usługą GitHub: #15520.

Program EF Core 6.0 tworzy teraz szkielet modelu EF i typów jednostek, które używają typów odwołań dopuszczających wartość null w języku C# (NRT). Użycie nrT jest automatycznie podzielone na szkielety, gdy obsługa NRT jest włączona w projekcie języka C#, w którym kod jest szkieletowany.

Na przykład poniższa Tags tabela zawiera kolumny ciągów o wartości null, które nie dopuszczają wartości null:

CREATE TABLE [Tags] (
  [Id] int NOT NULL IDENTITY,
  [Name] nvarchar(max) NOT NULL,
  [Description] nvarchar(max) NULL,
  CONSTRAINT [PK_Tags] PRIMARY KEY ([Id]));

Powoduje to utworzenie odpowiednich właściwości ciągu dopuszczalnego do wartości null i ciągu bez wartości null w wygenerowanej klasie:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Podobnie poniższe Posts tabele zawierają wymaganą relację z tabelą Blogs :

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

Powoduje to tworzenie szkieletu relacji niepustej (wymaganej) między blogami:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

Wpisy i wpisy:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Na koniec właściwości DbSet w wygenerowanym obiekcie DbContext są tworzone w sposób przyjazny dla nrT. Przykład:

public virtual DbSet<Blog> Blogs { get; set; } = null!;
public virtual DbSet<Post> Posts { get; set; } = null!;
public virtual DbSet<Tag> Tags { get; set; } = null!;

Komentarze bazy danych są szkieletami komentarzy do kodu

Problem z usługą GitHub: #19113. Ta funkcja została udostępniona przez @ErikEJ. Dziękujemy!

Komentarze do tabel i kolumn SQL są teraz szkieletem typów jednostek utworzonych podczas odtwarzania modelu EF Core z istniejącej bazy danych SQL Server.

/// <summary>
/// The Blog table.
/// </summary>
public partial class Blog
{
    /// <summary>
    /// The primary key.
    /// </summary>
    [Key]
    public int Id { get; set; }
}

Ulepszenia zapytań LINQ

Program EF Core 6.0 zawiera kilka ulepszeń tłumaczenia i wykonywania zapytań LINQ.

Ulepszona obsługa grupowania wg

Problemy z usługą GitHub: #12088, #13805 i #22609.

Program EF Core 6.0 zawiera lepszą obsługę GroupBy zapytań. W szczególności ef Core teraz:

  • Przetłumacz grupę według, po której FirstOrDefault następuje (lub podobne) w grupie
  • Obsługuje wybieranie pierwszych N wyników z grupy
  • Rozwija nawigacje po zastosowaniu GroupBy operatora

Poniżej przedstawiono przykładowe zapytania z raportów klientów i ich tłumaczenia na SQL Server.

Przykład 1:

var people = context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToList();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

Przykład 2:

var group = context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .First();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

Przykład 3:

var people = context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

Przykład 4:

var results = (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToList();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

Przykład 5:

var results = context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

Przykład 6:

var results = context.People.Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

Przykład 7:

var size = 11;
var results
    = context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToList();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

Przykład 8:

var result = context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .Count();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

Przykład 9:

var results = context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToList();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

Przykład 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

Przykład 11:

var grouping = context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToList();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

Przykład 12:

var grouping = context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToList();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

Przykład 13:

var grouping = context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToList();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

Model

Typy jednostek używane w tych przykładach to:

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

Tłumaczenie ciągu.Concat z wieloma argumentami

Problem z usługą GitHub: #23859. Ta funkcja została udostępniona przez @wmeints. Dziękujemy!

Począwszy od programu EF Core 6.0, wywołania funkcji String.Concat z wieloma argumentami są teraz tłumaczone na język SQL. Na przykład następujące zapytanie:

var shards = context.Shards
    .Where(e => string.Concat(e.Token1, e.Token2, e.Token3) != e.TokensProcessed).ToList();

W przypadku używania SQL Server zostanie przetłumaczony na następujący kod SQL:

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

Smoother integration with System.Linq.Async

Problem z usługą GitHub: #24041.

Pakiet System.Linq.Async dodaje przetwarzanie LINQ po stronie klienta. Korzystanie z tego pakietu z poprzednimi wersjami programu EF Core było kłopotliwe z powodu starcia przestrzeni nazw dla asynchronicznych metod LINQ. W programie EF Core 6.0 skorzystaliśmy z dopasowywania wzorców języka C# w IAsyncEnumerable<T> taki sposób, że uwidoczniona platforma EF Core DbSet<TEntity> nie musi bezpośrednio implementować interfejsu.

Należy pamiętać, że większość aplikacji nie musi używać narzędzia System.Linq.Async, ponieważ zapytania EF Core są zwykle w pełni tłumaczone na serwerze.

Problem z usługą GitHub: #23921.

W programie EF Core 6.0 złagodziliśmy wymagania dotyczące parametrów dla FreeText(DbFunctions, String, String) systemów i Contains. Dzięki temu te funkcje mogą być używane z kolumnami binarnymi lub z kolumnami mapowanym przy użyciu konwertera wartości. Rozważmy na przykład typ jednostki z właściwością Name zdefiniowaną jako obiekt wartości:

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

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

Jest to mapowane na kod JSON w bazie danych:

modelBuilder.Entity<Customer>()
    .Property(e => e.Name)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Name>(v, (JsonSerializerOptions)null));

Zapytanie można teraz wykonać przy użyciu Contains polecenia lub FreeText nawet jeśli typ właściwości nie stringjest Name . Przykład:

var result = context.Customers.Where(e => EF.Functions.Contains(e.Name, "Martin")).ToList();

Spowoduje to wygenerowanie następującego kodu SQL podczas korzystania z SQL Server:

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE CONTAINS([c].[Name], N'Martin')

Tłumaczenie na ciąg toString na sqLite

Problem z usługą GitHub: #17223. Ta funkcja została utworzona przez @ralmsdeveloper. Dziękujemy!

Wywołania do ToString() usługi są teraz tłumaczone na język SQL podczas korzystania z dostawcy bazy danych SQLite. Może to być przydatne w przypadku wyszukiwania tekstu obejmującego kolumny inne niż ciągi. Rozważmy na przykład User typ jednostki, który przechowuje numery telefonów jako wartości liczbowe:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public long PhoneNumber { get; set; }
}

ToString można użyć do przekonwertowania liczby na ciąg w bazie danych. Następnie możemy użyć tego ciągu z funkcją, taką jak LIKE w celu znalezienia liczb pasujących do wzorca. Aby na przykład znaleźć wszystkie liczby zawierające 555:

var users = context.Users.Where(u => EF.Functions.Like(u.PhoneNumber.ToString(), "%555%")).ToList();

Przekłada się to na następującą bazę danych SQL podczas korzystania z bazy danych SQLite:

SELECT "u"."Id", "u"."PhoneNumber", "u"."Username"
FROM "Users" AS "u"
WHERE CAST("u"."PhoneNumber" AS TEXT) LIKE '%555%'

Należy pamiętać, że tłumaczenie ToString() dla SQL Server jest już obsługiwane w programie EF Core 5.0 i może być również obsługiwane przez innych dostawców baz danych.

Ef. Functions.Random

Problem z usługą GitHub: #16141. Ta funkcja została utworzona przez @RaymondHuy. Dziękujemy!

EF.Functions.Random mapuje na funkcję bazy danych zwracającą pseudo-losową liczbę z zakresu od 0 do 1 wykluczania. Tłumaczenia zostały zaimplementowane w repozytorium EF Core dla SQL Server, SQLite i Cosmos. Rozważmy na przykład User typ jednostki z właściwością Popularity :

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    public int Popularity { get; set; }
}

Popularity może mieć wartości z zakresu od 1 do 5 włącznie. Za pomocą EF.Functions.Random polecenia możemy napisać zapytanie, aby zwrócić wszystkich użytkowników z losowo wybraną popularnością:

var users = context.Users.Where(u => u.Popularity == (int)(EF.Functions.Random() * 4.0) + 1).ToList();

Przekłada się to na następującą bazę danych SQL podczas korzystania z bazy danych SQL Server:

SELECT [u].[Id], [u].[Popularity], [u].[Username]
FROM [Users] AS [u]
WHERE [u].[Popularity] = (CAST((RAND() * 4.0E0) AS int) + 1)

Ulepszone tłumaczenie SQL Server dla isNullOrWhitespace

Problem z usługą GitHub: #22916. Ta funkcja została utworzona przez @Marusyk. Dziękujemy!

Rozpatrzmy następujące zapytanie:

var users = context.Users.Where(
    e => string.IsNullOrWhiteSpace(e.FirstName)
         || string.IsNullOrWhiteSpace(e.LastName)).ToList();

Przed programem EF Core 6.0 zostało to przetłumaczone na następujące SQL Server:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

To tłumaczenie zostało ulepszone dla platformy EF Core 6.0 do:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

Definiowanie zapytania dla dostawcy w pamięci

Problem z usługą GitHub: #24600.

Nowa metoda ToInMemoryQuery może służyć do pisania zapytania definiującego względem bazy danych w pamięci dla danego typu jednostki. Jest to najbardziej przydatne w przypadku tworzenia odpowiedników widoków w bazie danych w pamięci, zwłaszcza gdy te widoki zwracają typy jednostek bez klucza. Rozważmy na przykład bazę danych klienta dla klientów z siedzibą w Wielkiej Brytanii. Każdy klient ma adres:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Załóżmy, że chcemy wyświetlić te dane, które pokazują, ilu klientów znajduje się w każdym obszarze kodu pocztowego. Możemy utworzyć bezklukowy typ jednostki, aby reprezentować następujące elementy:

public class CustomerDensity
{
    public string Postcode { get; set; }
    public int CustomerCount { get; set; }
}

Zdefiniuj dla niej właściwość DbSet w obiekcie DbContext wraz z zestawami dla innych typów jednostek najwyższego poziomu:

public DbSet<Customer> Customers { get; set; }
public DbSet<CustomerDensity> CustomerDensities { get; set; }

Następnie w pliku OnModelCreatingmożemy napisać zapytanie LINQ definiujące dane, które mają zostać zwrócone dla CustomerDensitieselementu :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

Następnie można wykonać zapytanie w taki sam sposób, jak każda inna właściwość DbSet:

var results = context.CustomerDensities.ToList();

Tłumaczenie podciągów za pomocą pojedynczego parametru

Problem z usługą GitHub: #20173. Ta funkcja została utworzona przez @stevendarby. Dziękujemy!

Program EF Core 6.0 tłumaczy teraz zastosowania string.Substring z jednym argumentem. Przykład:

var result = context.Customers
    .Select(a => new { Name = a.Name.Substring(3) })
    .FirstOrDefault(a => a.Name == "hur");

Przekłada się to na następujący kod SQL podczas korzystania z SQL Server:

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

Split-queries for non-navigation collections (Dzielenie zapytań dla kolekcji innych niż nawigacja)

Problem z usługą GitHub: #21234.

Platforma EF Core obsługuje dzielenie pojedynczego zapytania LINQ na wiele zapytań SQL. W programie EF Core 6.0 ta obsługa została rozszerzona, aby uwzględnić przypadki, w których kolekcje inne niż nawigacji znajdują się w projekcji zapytania.

Poniżej przedstawiono przykładowe zapytania pokazujące tłumaczenie na SQL Server do pojedynczego zapytania lub wielu zapytań.

Przykład 1:

Zapytanie LINQ:

context.Customers
    .Select(
        c => new
        {
            c,
            Orders = c.Orders
                .Where(o => o.Id > 1)
        })
    .ToList();

Pojedyncze zapytanie SQL:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Wiele zapytań SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Przykład 2:

Zapytanie LINQ:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
        })
    .ToList();

Pojedyncze zapytanie SQL:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Wiele zapytań SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Przykład 3:

Zapytanie LINQ:

context.Customers
    .Select(
        c => new
        {
            c,
            OrderDates = c.Orders
                .Where(o => o.Id > 1)
                .Select(o => o.OrderDate)
                .Distinct()
        })
    .ToList();

Pojedyncze zapytanie SQL:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Wiele zapytań SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Usuwanie ostatniej klauzuli ORDER BY podczas dołączania do kolekcji

Problem z usługą GitHub: #19828.

Podczas ładowania powiązanych jednostek jeden do wielu platforma EF Core dodaje klauzule ORDER BY, aby upewnić się, że wszystkie powiązane jednostki dla danej jednostki są zgrupowane razem. Jednak ostatnia klauzula ORDER BY nie jest niezbędna do wygenerowania wymaganych grup przez ef i może mieć wpływ na wydajność. W związku z tym program EF Core 6.0 jest usuwany z tej klauzuli.

Rozważmy na przykład następujące zapytanie:

context.Customers
    .Select(
        e => new
        {
            e.Id,
            FirstOrder = e.Orders.Where(i => i.Id == 1).ToList()
        })
    .ToList();

W przypadku platformy EF Core 5.0 w SQL Server to zapytanie jest tłumaczone na:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

W przypadku platformy EF Core 6.0 jest ona zamiast tego tłumaczona na:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Tagowanie zapytań o nazwę pliku i numer wiersza

Problem z usługą GitHub: #14176. Ta funkcja została utworzona przez @michalczerwinski. Dziękujemy!

Tagi zapytań umożliwiają dodawanie tagu tekstowego do zapytania LINQ w taki sposób, że jest ono następnie uwzględniane w wygenerowanym języku SQL. W programie EF Core 6.0 można użyć go do tagowania zapytań przy użyciu nazwy pliku i numeru wiersza kodu LINQ. Przykład:

var results1 = context
    .Customers
    .TagWithCallSite()
    .Where(c => c.Name.StartsWith("A"))
    .ToList();

Spowoduje to wygenerowanie następującego kodu SQL podczas korzystania z SQL Server:

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

Zmiany w opcjonalnej obsłudze zależnej należącej do użytkownika

Problem z usługą GitHub: #24558.

Staje się trudne, aby dowiedzieć się, czy opcjonalna jednostka zależna istnieje, czy nie, gdy udostępnia tabelę z jednostką główną. Dzieje się tak, ponieważ istnieje wiersz w tabeli zależnej, ponieważ podmiot zabezpieczeń potrzebuje go, niezależnie od tego, czy istnieje zależność. Sposób obsługi tego jednoznacznie polega na upewnieniu się, że obiekt zależny ma co najmniej jedną wymaganą właściwość. Ponieważ wymagana właściwość nie może mieć wartości null, oznacza to, że wartość w kolumnie dla tej właściwości ma wartość null, wówczas jednostka zależna nie istnieje.

Rozważmy na przykład klasę Customer , w której każdy klient ma własność Address:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

Adres jest opcjonalny, co oznacza, że ważne jest zapisanie klienta bez adresu:

context.Customers1.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

Jeśli jednak klient ma adres, ten adres musi mieć co najmniej kod pocztowy o wartości innej niż null:

context.Customers1.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
        {
            Postcode = "AN1 1PL",
        }
    });

Jest to zapewnione przez oznaczenie Postcode właściwości jako Required.

Teraz, gdy klienci są zapytani, jeśli kolumna Kod pocztowy ma wartość null, oznacza to, że klient nie ma adresu, a Customer.Address właściwość nawigacji pozostaje zerowa. Na przykład iterowanie za pośrednictwem klientów i sprawdzanie, czy adres ma wartość null:

foreach (var customer in context.Customers1)
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

Generuje następujące wyniki:

Foul Ole Ron has no address.
Havelock Vetinari has postcode AN1 1PL.

Zamiast tego należy wziąć pod uwagę przypadek, w którym nie jest wymagana żadna właściwość poza adresem:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Teraz można zapisać zarówno klienta bez adresu, jak i klienta z adresem, w którym wszystkie właściwości adresu mają wartość null:

context.Customers2.Add(
    new()
    {
        Name = "Foul Ole Ron"
    });

context.Customers2.Add(
    new()
    {
        Name = "Havelock Vetinari",
        Address = new()
    });

Jednak w bazie danych te dwa przypadki są nie do odróżnienia, ponieważ widzimy, że bezpośrednio wykonuje zapytanie o kolumny bazy danych:

Id  Name               House   Street  City    Postcode
1   Foul Ole Ron       NULL    NULL    NULL    NULL
2   Havelock Vetinari  NULL    NULL    NULL    NULL

Z tego powodu program EF Core 6.0 wyświetli teraz ostrzeżenie podczas zapisywania opcjonalnego zależnego, w którym wszystkie jej właściwości mają wartość null. Przykład:

ostrzeżenie: 27.09.2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) Jednostka typu "Address" z wartościami klucza podstawowego {CustomerId: -2147482646} jest opcjonalnym zależnym od udostępniania tabel. Jednostka nie ma żadnej właściwości z wartością inną niż domyślna, aby określić, czy jednostka istnieje. Oznacza to, że po wysłaniu zapytania nie zostanie utworzone żadne wystąpienie obiektu zamiast wystąpienia ze wszystkimi właściwościami ustawionymi na wartości domyślne. Wszystkie zagnieżdżone zależności również zostaną utracone. Nie należy zapisywać żadnego wystąpienia tylko z wartościami domyślnymi lub oznaczać nawigację przychodzącą zgodnie z wymaganiami w modelu.

Staje się to jeszcze bardziej trudne, gdy samo zależne opcjonalne pełni rolę podmiotu zabezpieczeń dla dodatkowego opcjonalnego zależnego, również zamapowanego na tę samą tabelę. Zamiast tylko ostrzeżenia, program EF Core 6.0 nie zezwala tylko na przypadki zagnieżdżonych opcjonalnych zależności. Rozważmy na przykład następujący model, w którym ContactInfo element jest własnością Customer elementu i Address jest włączony przez ContactInfousługę :

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Teraz, jeśli ContactInfo.Phone ma wartość null, program EF Core nie utworzy wystąpienia, Address jeśli relacja jest opcjonalna, mimo że sam adres może zawierać dane. W przypadku tego rodzaju modelu program EF Core 6.0 zgłosi następujący wyjątek:

System.InvalidOperationException: Typ jednostki "ContactInfo" jest opcjonalnym zależnym od udostępniania tabel i zawierającym inne elementy zależne bez konieczności współużytkowania właściwości w celu określenia, czy jednostka istnieje. Jeśli wszystkie właściwości dopuszczane do wartości null zawierają wartość null w bazie danych, wystąpienie obiektu nie zostanie utworzone w zapytaniu, co spowoduje utratę wartości zagnieżdżonych zależnych. Dodaj wymaganą właściwość, aby utworzyć wystąpienia z wartościami null dla innych właściwości lub oznaczyć przychodzącą nawigację zgodnie z wymaganiami, aby zawsze utworzyć wystąpienie.

W tym miejscu należy unikać przypadku, w którym opcjonalne zależne może zawierać wszystkie wartości właściwości dopuszczalnych wartości null i współdzieli tabelę z jej podmiotem zabezpieczeń. Istnieją trzy proste sposoby, aby tego uniknąć:

  1. Ustaw wartość zależną od wymaganej. Oznacza to, że jednostka zależna zawsze będzie mieć wartość po wykonaniu zapytania, nawet jeśli wszystkie jej właściwości mają wartość null.
  2. Upewnij się, że zależne zawiera co najmniej jedną wymaganą właściwość, jak opisano powyżej.
  3. Zapisz opcjonalne zależności we własnej tabeli, zamiast udostępniać tabelę podmiotowi zabezpieczeń.

Zależne może być wymagane przy użyciu atrybutu Required na jego nawigacji:

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

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Lub określając, że jest to wymagane w pliku OnModelCreating:

modelBuilder.Entity<WithRequiredNavigation.Customer>(
    b =>
        {
            b.OwnsOne(e => e.Address);
            b.Navigation(e => e.Address).IsRequired();
        });

Zależności można zapisywać w innej tabeli, określając tabele do użycia w programie OnModelCreating:

modelBuilder
    .Entity<WithDifferentTable.Customer>(
        b =>
            {
                b.ToTable("Customers");
                b.OwnsOne(
                    e => e.Address,
                    b => b.ToTable("CustomerAddresses"));
            });

Zobacz OptionalDependentsSample w usłudze GitHub, aby uzyskać więcej przykładów opcjonalnych zależności, w tym przypadków z zagnieżdżonym opcjonalnymi zależnościami.

Nowe atrybuty mapowania

Program EF Core 6.0 zawiera kilka nowych atrybutów, które można zastosować do kodu w celu zmiany sposobu mapowania na bazę danych.

UnicodeAttribute

Problem z usługą GitHub: #19794. Ta funkcja została udostępniona przez @RaymondHuy. Dziękujemy!

Począwszy od programu EF Core 6.0, właściwość ciągu można teraz mapować na kolumnę inną niż Unicode przy użyciu atrybutu mapowania bez bezpośredniego określania typu bazy danych. Rozważmy na przykład Book typ jednostki z właściwością International Standard Book Number (ISBN) w postaci "ISBN 978-3-16-148410-0":

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

Ponieważ nazwy ISBN nie mogą zawierać żadnych znaków innych niż Unicode, Unicode atrybut spowoduje, że będzie używany typ ciągu innego niż Unicode. Ponadto MaxLength służy do ograniczania rozmiaru kolumny bazy danych. Na przykład w przypadku korzystania z SQL Server powoduje to wyświetlenie kolumny bazy danych w kolumnie varchar(22):

CREATE TABLE [Book] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NULL,
    [Isbn] varchar(22) NULL,
    CONSTRAINT [PK_Book] PRIMARY KEY ([Id]));

Uwaga

Program EF Core domyślnie mapuje właściwości ciągu na kolumny Unicode. UnicodeAttribute jest ignorowany, gdy system bazy danych obsługuje tylko typy Unicode.

PrecisionAttribute

Problem z usługą GitHub: #17914. Ta funkcja została udostępniona przez @RaymondHuy. Dziękujemy!

Dokładność i skala kolumny bazy danych można teraz skonfigurować przy użyciu atrybutów mapowania bez bezpośredniego określania typu bazy danych. Rozważmy na przykład Product typ jednostki z właściwością dziesiętną Price :

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

    [Precision(precision: 10, scale: 2)]
    public decimal Price { get; set; }
}

Program EF Core zamapuje tę właściwość na kolumnę bazy danych z dokładnością 10 i skalą 2. Na przykład w SQL Server:

CREATE TABLE [Product] (
    [Id] int NOT NULL IDENTITY,
    [Price] decimal(10,2) NOT NULL,
    CONSTRAINT [PK_Product] PRIMARY KEY ([Id]));

EntityTypeConfigurationAttribute

Problem z usługą GitHub: #23163. Ta funkcja została udostępniona przez @KaloyanIT. Dziękujemy!

IEntityTypeConfiguration<TEntity> wystąpienia umożliwiają ModelBuilder konfigurację dla każdego typu jednostki, które mają być zawarte w własnej klasie konfiguracji. Przykład:

public class BookConfiguration : IEntityTypeConfiguration<Book>
{
    public void Configure(EntityTypeBuilder<Book> builder)
    {
        builder
            .Property(e => e.Isbn)
            .IsUnicode(false)
            .HasMaxLength(22);
    }
}

Zwykle klasę konfiguracji należy utworzyć wystąpienie i wywołać do klasy z klasy DbContext.OnModelCreating. Przykład:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    new BookConfiguration().Configure(modelBuilder.Entity<Book>());
}

Począwszy od programu EF Core 6.0, można umieścić element na typie jednostki, EntityTypeConfigurationAttribute tak aby program EF Core mógł znaleźć odpowiednią konfigurację i użyć jej. Przykład:

[EntityTypeConfiguration(typeof(BookConfiguration))]
public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Isbn { get; set; }
}

Ten atrybut oznacza, że platforma EF Core będzie używać określonej IEntityTypeConfiguration implementacji za każdym razem, gdy Book typ jednostki jest uwzględniony w modelu. Typ jednostki jest uwzględniany w modelu przy użyciu jednego z normalnych mechanizmów. Na przykład przez utworzenie DbSet<TEntity> właściwości dla typu jednostki:

public class BooksContext : DbContext
{
    public DbSet<Book> Books { get; set; }

    //...

Lub rejestrując go w programie OnModelCreating:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Book>();
}

Uwaga

EntityTypeConfigurationAttribute typy nie zostaną automatycznie odnalezione w zestawie. Typy jednostek należy dodać do modelu, zanim atrybut zostanie odnaleziony w tym typie jednostki.

Ulepszenia tworzenia modelu

Oprócz nowych atrybutów mapowania program EF Core 6.0 zawiera kilka innych ulepszeń procesu tworzenia modelu.

Obsługa SQL Server kolumn rozrzedzywanych

Problem z usługą GitHub: #8023.

SQL Server rozrzedzonych kolumn to zwykłe kolumny zoptymalizowane pod kątem przechowywania wartości null. Może to być przydatne w przypadku używania mapowania dziedziczenia TPH , w przypadku którego właściwości rzadko używanego podtypu będą powodować wartości kolumn null dla większości wierszy w tabeli. Rozważmy na przykład klasę ForumModerator , która rozciąga się od ForumUserklasy :

public class ForumUser
{
    public int Id { get; set; }
    public string Username { get; set; }
}

public class ForumModerator : ForumUser
{
    public string ForumName { get; set; }
}

Mogą istnieć miliony użytkowników, z których tylko garstka jest moderatorami. Oznacza to, że mapowanie ForumName tak rozrzedło może mieć sens w tym miejscu. Teraz można to skonfigurować przy użyciu polecenia IsSparse w programie OnModelCreating. Przykład:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<ForumModerator>()
        .Property(e => e.ForumName)
        .IsSparse();
}

Migracje platformy EF Core będą następnie oznaczać kolumnę jako rozrzedzoną. Przykład:

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

Uwaga

Rozrzedne kolumny mają ograniczenia. Pamiętaj, aby przeczytać dokumentację SQL Server rozrzednych kolumn, aby upewnić się, że rozrzedane kolumny są właściwym wyborem dla danego scenariusza.

Ulepszenia interfejsu API hasConversion

Problem z usługą GitHub: #25468.

Przed programem EF Core 6.0 ogólne przeciążenia metod używały HasConversion parametru ogólnego w celu określenia typu do konwersji na. Rozważmy na przykład wyliczenie Currency :

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

Program EF Core można skonfigurować do zapisywania wartości tego wyliczenia jako ciągów "UsDollars", "PoundsStirling" i "Euro" przy użyciu polecenia HasConversion<string>. Przykład:

modelBuilder.Entity<TestEntity1>()
    .Property(e => e.Currency)
    .HasConversion<string>();

Począwszy od programu EF Core 6.0, typ ogólny może zamiast tego określić typ konwertera wartości. Może to być jeden z wbudowanych konwerterów wartości. Na przykład aby przechowywać wartości wyliczenia jako liczby 16-bitowe w bazie danych:

modelBuilder.Entity<TestEntity2>()
    .Property(e => e.Currency)
    .HasConversion<EnumToNumberConverter<Currency, short>>();

Może to być też typ konwertera wartości niestandardowych. Rozważmy na przykład konwerter, który przechowuje wartości wyliczenia jako symbole waluty:

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

Teraz można to skonfigurować przy użyciu metody ogólnej HasConversion :

modelBuilder.Entity<TestEntity3>()
    .Property(e => e.Currency)
    .HasConversion<CurrencyToSymbolConverter>();

Mniej konfiguracji dla relacji wiele-do-wielu

Problem z usługą GitHub: #21535.

Jednoznaczne relacje wiele-do-wielu między dwoma typami jednostek są wykrywane zgodnie z konwencją. W razie potrzeby lub w razie potrzeby można jawnie określić nawigacje. Przykład:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats);

W obu tych przypadkach program EF Core tworzy jednostkę współdzieloną w oparciu o Dictionary<string, object> funkcję jednostki sprzężenia między tymi dwoma typami. Począwszy od programu EF Core 6.0, można dodać do konfiguracji, UsingEntity aby zmienić tylko ten typ bez konieczności dodatkowej konfiguracji. Przykład:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>();

Ponadto typ jednostki sprzężenia można skonfigurować bez konieczności jawnego określania relacji po lewej i prawej stronie. Przykład:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

Na koniec można podać pełną konfigurację. Przykład:

modelBuilder.Entity<Cat>()
    .HasMany(e => e.Humans)
    .WithMany(e => e.Cats)
    .UsingEntity<CatHuman>(
        e => e.HasOne<Human>().WithMany().HasForeignKey(e => e.CatsId),
        e => e.HasOne<Cat>().WithMany().HasForeignKey(e => e.HumansId),
        e => e.HasKey(e => new { e.CatsId, e.HumansId }));

Zezwalaj konwerterom wartości na konwertowanie wartości null

Problem z usługą GitHub: #13850.

Ważne

Ze względu na problemy opisane poniżej konstruktory ValueConverter , które zezwalają na konwersję wartości null, zostały oznaczone [EntityFrameworkInternal] dla wersji EF Core 6.0. Użycie tych konstruktorów spowoduje teraz wygenerowanie ostrzeżenia dotyczącego kompilacji.

Konwertery wartości zazwyczaj nie zezwalają na konwersję wartości null na inną wartość. Jest to spowodowane tym, że ten sam konwerter wartości może być używany zarówno dla typów dopuszczających wartość null, jak i niepustych, co jest bardzo przydatne w przypadku kombinacji PK/FK, gdzie klucz FK jest często dopuszczany do wartości null, a klucz PK nie jest.

Począwszy od programu EF Core 6.0, można utworzyć konwerter wartości, który konwertuje wartości null. Jednak weryfikacja tej funkcji wykazała, że jest to bardzo problematyczne w praktyce z wieloma pułapkami. Przykład:

Nie są to proste problemy i w przypadku problemów z zapytaniami nie są łatwe do wykrycia. W związku z tym oznaczyliśmy tę funkcję jako wewnętrzną dla platformy EF Core 6.0. Nadal można go używać, ale zostanie wyświetlone ostrzeżenie kompilatora. Ostrzeżenie można wyłączyć przy użyciu polecenia #pragma warning disable EF1001.

Jednym z przykładów, w których konwertowanie wartości null może być przydatne, jest to, że baza danych zawiera wartości null, ale typ jednostki chce użyć innej wartości domyślnej dla właściwości. Rozważmy na przykład wyliczenie, w którym jego wartość domyślna to "Nieznany":

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Jednak baza danych może mieć wartości null, gdy rasa jest nieznana. W programie EF Core 6.0 można użyć konwertera wartości, aby uwzględnić następujące elementy:

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

Koty z rasą "Nieznany" będą miały ich Breed kolumnę ustawioną na wartość null w bazie danych. Przykład:

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

context.SaveChanges();

Spowoduje to wygenerowanie następujących instrukcji insert w SQL Server:

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Ulepszenia fabryki DbContext

AddDbContextFactory rejestruje również obiekt DbContext bezpośrednio

Problem z usługą GitHub: #25164.

Czasami warto mieć zarówno typ DbContext , jak i fabrykę dla kontekstów tego typu zarejestrowanych w kontenerze wstrzykiwania zależności aplikacji (D.I.). Dzięki temu można na przykład rozwiązać zakresowe wystąpienie obiektu DbContext z zakresu żądania, podczas gdy fabryka może służyć do tworzenia wielu niezależnych wystąpień w razie potrzeby.

Aby to obsługiwać, AddDbContextFactory teraz rejestruje również typ DbContext jako usługę o określonym zakresie. Rozważmy na przykład tę rejestrację w kontenerze D.I. aplikacji:

var container = services
    .AddDbContextFactory<SomeDbContext>(
        builder => builder.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample"))
    .BuildServiceProvider();

Dzięki tej rejestracji fabryki można rozpoznać z głównego kontenera D.I., podobnie jak w poprzednich wersjach:

var factory = container.GetService<IDbContextFactory<SomeDbContext>>();
using (var context = factory.CreateDbContext())
{
    // Contexts obtained from the factory must be explicitly disposed
}

Należy pamiętać, że wystąpienia kontekstowe utworzone przez fabrykę muszą zostać jawnie usunięte.

Ponadto wystąpienie dbContext można rozpoznać bezpośrednio z zakresu kontenera:

using (var scope = container.CreateScope())
{
    var context = scope.ServiceProvider.GetService<SomeDbContext>();
    // Context is disposed when the scope is disposed
}

W takim przypadku wystąpienie kontekstu jest usuwane po usunięciu zakresu kontenera; kontekst nie powinien być jawnie usuwany.

Na wyższym poziomie oznacza to, że do innych typów D.I. można wstrzyknąć element DbContext fabryki. Przykład:

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = context1.Blogs.ToList();
        var results2 = context2.Blogs.ToList();
        
        // Contexts obtained from the factory must be explicitly disposed
    }
}

Lub:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public void DoSomething()
    {
        var results = _context.Blogs.ToList();

        // Injected context is disposed when the request scope is disposed
    }
}

DbContextFactory ignoruje konstruktor bez parametrów DbContext

Problem z usługą GitHub: #24124.

Program EF Core 6.0 umożliwia teraz zarówno konstruktor DbContext bez parametrów, jak i konstruktor, który ma DbContextOptions być używany w tym samym typie kontekstu, gdy fabryka jest zarejestrowana za pomocą polecenia AddDbContextFactory. Na przykład kontekst używany w powyższych przykładach zawiera oba konstruktory:

public class SomeDbContext : DbContext
{
    public SomeDbContext()
    {
    }

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }
    
    public DbSet<Blog> Blogs { get; set; }
}

Buforowanie dbContext może być używane bez wstrzykiwania zależności

Problem z usługą GitHub: #24137.

Typ PooledDbContextFactory został upubliczniony, aby można było go użyć jako autonomicznej puli dla wystąpień dbContext bez konieczności posiadania kontenera wstrzykiwania zależności przez aplikację. Pula jest tworzona przy użyciu wystąpienia DbContextOptions , które będzie używane do tworzenia wystąpień kontekstu:

var options = new DbContextOptionsBuilder<SomeDbContext>()
    .EnableSensitiveDataLogging()
    .UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=EFCoreSample")
    .Options;

var factory = new PooledDbContextFactory<SomeDbContext>(options);

Fabrykę można następnie użyć do tworzenia wystąpień i puli. Przykład:

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

Wystąpienia są zwracane do puli po ich usunięciu.

Różne ulepszenia

Na koniec program EF Core zawiera kilka ulepszeń w obszarach, które nie zostały omówione powyżej.

Użyj polecenia [ColumnAttribute.Order] podczas tworzenia tabel

Problem z usługą GitHub: #10059.

Właściwość Order można ColumnAttribute teraz użyć do porządkowenia kolumn podczas tworzenia tabeli z migracjami. Rozważmy na przykład następujący model:

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

    [Required]
    public string Postcode { get; set; }
}

Domyślnie platforma EF Core najpierw porządkuje kolumny klucza podstawowego, postępując według właściwości typu jednostki i typów należących do użytkownika, a na koniec właściwości z typów bazowych. Na przykład w SQL Server zostanie utworzona następująca tabela:

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

W programie EF Core 6.0 ColumnAttribute można użyć do określenia innej kolejności kolumn. Przykład:

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

W SQL Server wygenerowana tabela jest teraz:

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

Spowoduje to przeniesienie FistName kolumn i LastName do góry, mimo że są one zdefiniowane w typie podstawowym. Zwróć uwagę, że wartości kolejności kolumn mogą zawierać luki, dzięki czemu zakresy mogą być używane do umieszczania kolumn na końcu, nawet w przypadku użycia przez wiele typów pochodnych.

W tym przykładzie pokazano również, jak można użyć tej samej ColumnAttribute nazwy kolumny i kolejności.

Kolejność kolumn można również skonfigurować przy użyciu interfejsu ModelBuilder API w programie OnModelCreating. Przykład:

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

Kolejność w konstruktorze modeli z HasColumnOrder pierwszeństwem przed dowolną kolejnością określoną za pomocą ColumnAttributepolecenia . Oznacza HasColumnOrder to, że można użyć do zastąpienia kolejności wykonanej z atrybutami, w tym rozwiązywania konfliktów, gdy atrybuty na różnych właściwościach określają ten sam numer zamówienia.

Ważne

Należy pamiętać, że w ogólnym przypadku większość baz danych obsługuje tylko porządkowanie kolumn po utworzeniu tabeli. Oznacza to, że atrybut order kolumny nie może być używany do ponownego zamawiania kolumn w istniejącej tabeli. Jednym z godnych uwagi wyjątkiem jest SQLite, gdzie migracje ponownie skompilują całą tabelę z nowymi zamówieniami kolumn.

Minimalny interfejs API platformy EF Core

Problem z usługą GitHub: #25192.

Platforma .NET Core 6.0 zawiera zaktualizowane szablony, które upraszczają funkcję "minimalnych interfejsów API", które usuwają wiele kodu kociołowego tradycyjnie potrzebnego w aplikacjach platformy .NET.

Ef Core 6.0 zawiera nową metodę rozszerzenia, która rejestruje typ DbContext i dostarcza konfigurację dostawcy bazy danych w jednym wierszu. Przykład:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCosmos<MyDbContext>(
    "https://localhost:8081",
    "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw==");

Są one dokładnie równoważne:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseCosmos(
        "https://localhost:8081",
        "C2y6yDjf5/R+ob0N8A7Cgv30VRDJIWEHLM+4QDU5DE2nQ9nDuVTqobD4b8mGGyPMbIZnqyMsEcaGQy67XIw/Jw=="));

Uwaga

Minimalne interfejsy API platformy EF Core obsługują tylko bardzo podstawową rejestrację i konfigurację obiektu DbContext i dostawcy. Użyj AddDbContextpolecenia , AddDbContextPool, AddDbContextFactoryitp., aby uzyskać dostęp do wszystkich typów rejestracji i konfiguracji dostępnych w programie EF Core.

Zapoznaj się z tymi zasobami, aby dowiedzieć się więcej o minimalnych interfejsach API:

Zachowaj kontekst synchronizacji w pliku SaveChangesAsync

Problem z usługą GitHub: #23971.

Zmieniliśmy kod platformy EF Core w wersji 5.0 tak, aby został ustawiony Task.ConfigureAwaitfalse na wartość we wszystkich miejscach, w których await kod asynchroniczny. Jest to zazwyczaj lepszy wybór w przypadku użycia platformy EF Core. Jest to jednak szczególny przypadek, SaveChangesAsync ponieważ program EF Core ustawi wygenerowane wartości na śledzone jednostki po zakończeniu operacji asynchronicznego bazy danych. Te zmiany mogą następnie wyzwalać powiadomienia, które na przykład mogą być uruchamiane w wątku U.I. W związku z tym przywracamy tę zmianę tylko dla metody EF Core 6.0 SaveChangesAsync .

Baza danych w pamięci: sprawdź, czy wymagane właściwości nie mają wartości null

Problem z usługą GitHub: #10613. Ta funkcja została udostępniona przez @fagnercarvalho. Dziękujemy!

Baza danych ef Core w pamięci zgłosi teraz wyjątek, jeśli zostanie podjęta próba zapisania wartości null dla właściwości oznaczonej jako wymagana. Rozważmy na przykład User typ z wymaganą Username właściwością:

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

    [Required]
    public string Username { get; set; }
}

Próba zapisania jednostki z wartością null Username spowoduje następujący wyjątek:

Microsoft.EntityFrameworkCore.DbUpdateException: brak wymaganych właściwości "{'Username'}" dla wystąpienia typu jednostki "User" z wartością klucza {Id: 1}.

W razie potrzeby tę walidację można wyłączyć. Przykład:

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
    optionsBuilder
        .LogTo(Console.WriteLine, new[] { InMemoryEventId.ChangesSaved })
        .UseInMemoryDatabase("UserContextWithNullCheckingDisabled", b => b.EnableNullChecks(false));
}

Informacje o źródle polecenia dla diagnostyki i przechwytywania

Problem z usługą GitHub: #23719. Ta funkcja została udostępniona przez @Giorgi. Dziękujemy!

Dostarczone CommandEventData do źródeł diagnostycznych i przechwytujących zawiera teraz wartość wyliczeniową wskazującą, która część ef była odpowiedzialna za utworzenie polecenia. Może być używany jako filtr w diagnostyce lub przechwytywaniu. Na przykład możemy chcieć przechwytywać, który ma zastosowanie tylko do poleceń pochodzących z polecenia SaveChanges:

public class CommandSourceInterceptor : DbCommandInterceptor
{
    public override InterceptionResult<DbDataReader> ReaderExecuting(
        DbCommand command, CommandEventData eventData, InterceptionResult<DbDataReader> result)
    {
        if (eventData.CommandSource == CommandSource.SaveChanges)
        {
            Console.WriteLine($"Saving changes for {eventData.Context!.GetType().Name}:");
            Console.WriteLine();
            Console.WriteLine(command.CommandText);
        }

        return result;
    }
}

Powoduje to filtrowanie przechwytywania tylko do zdarzeń SaveChanges , gdy są używane w aplikacji, która generuje również migracje i zapytania. Przykład:

Saving changes for CustomersContext:

SET NOCOUNT ON;
INSERT INTO [Customers] ([Name])
VALUES (@p0);
SELECT [Id]
FROM [Customers]
WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Lepsza obsługa wartości tymczasowych

Problem z usługą GitHub: #24245.

Program EF Core nie ujawnia wartości tymczasowych w wystąpieniach typu jednostki. Rozważmy na przykład Blog typ jednostki z kluczem wygenerowanym przez magazyn:

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

    public ICollection<Post> Posts { get; } = new List<Post>();
}

Właściwość Id klucza otrzyma tymczasową wartość zaraz po Blog śledzeniu przez kontekst. Na przykład podczas wywoływania polecenia DbContext.Add:

var blog = new Blog();
context.Add(blog);

Wartość tymczasową można uzyskać z monitora zmian kontekstu, ale nie jest ustawiona na wystąpienie jednostki. Na przykład ten kod:

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

Generuje następujące dane wyjściowe:

Blog.Id value on entity instance = 0
Blog.Id value tracked by EF = -2147482647

Jest to dobre, ponieważ zapobiega to tymczasowemu wyciekowi wartości do kodu aplikacji, w którym może zostać przypadkowo potraktowana jako nie tymczasowa. Jednak czasami warto radzić sobie bezpośrednio z wartościami tymczasowymi. Na przykład aplikacja może chcieć wygenerować własne wartości tymczasowe dla grafu jednostek, zanim zostaną one śledzone, aby można było używać ich do tworzenia relacji przy użyciu kluczy obcych. Można to zrobić, jawnie oznaczając wartości jako tymczasowe. Przykład:

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

W programie EF Core 6.0 wartość pozostanie w wystąpieniu jednostki, mimo że jest ona teraz oznaczona jako tymczasowa. Na przykład powyższy kod generuje następujące dane wyjściowe:

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

Podobnie wartości tymczasowe generowane przez platformę EF Core można ustawić jawnie na wystąpienia jednostki i oznaczone jako wartości tymczasowe. Może to służyć do jawnego ustawiania relacji między nowymi jednostkami przy użyciu ich tymczasowych wartości klucza. Przykład:

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

Wynikowe:

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

Program EF Core z adnotacjami dla typów odwołań dopuszczanych do wartości null w języku C#

Problem z usługą GitHub: #19007.

Baza kodu platformy EF Core używa teraz typów odwołań dopuszczalnych do wartości null języka C# (NRT) w całym środowisku. Oznacza to, że podczas korzystania z programu EF Core 6.0 z własnego kodu uzyskasz poprawne wskazania kompilatora dotyczące użycia wartości null.

Microsoft.Data.Sqlite 6.0

Porada

Możesz uruchomić i debugować wszystkie przykłady pokazane poniżej, pobierając przykładowy kod z usługi GitHub.

Pula połączeń

Problem z usługą GitHub: #13837.

Powszechną praktyką jest utrzymywanie otwartych połączeń z bazą danych przez możliwie najkrótszy czas. Pomaga to zapobiec rywalizacji o zasób połączenia. Dlatego biblioteki takie jak EF Core otwierają połączenie bezpośrednio przed wykonaniem operacji bazy danych i zamykają je ponownie natychmiast po. Rozważmy na przykład ten kod platformy EF Core:

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = context.Users.ToList();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        context.SaveChanges();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

Dane wyjściowe z tego kodu z włączonym rejestrowaniem połączeń to:

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

Zwróć uwagę, że połączenie jest otwierane i zamykane szybko dla każdej operacji.

Jednak w przypadku większości systemów baz danych otwarcie fizycznego połączenia z bazą danych jest kosztowną operacją. W związku z tym większość dostawców ADO.NET tworzy pulę połączeń fizycznych i wynajmuje je do DbConnection wystąpień zgodnie z potrzebami.

SqLite jest nieco inny, ponieważ dostęp do bazy danych zwykle uzyskuje dostęp do pliku. Oznacza to, że otwarcie połączenia z bazą danych SQLite jest zwykle bardzo szybkie. Jednak nie zawsze tak jest. Na przykład otwarcie połączenia z zaszyfrowaną bazą danych może być bardzo powolne. W związku z tym połączenia SQLite są teraz w puli w przypadku korzystania z biblioteki Microsoft.Data.Sqlite 6.0.

Data i czas pomocy technicznej

Problem z usługą GitHub: #24506.

Program Microsoft.Data.Sqlite 6.0 obsługuje nowe DateOnly typy i TimeOnly z platformy .NET 6. Mogą one być również używane w programie EF Core 6.0 z dostawcą SQLite. Jak zawsze w przypadku sqlite, jego natywny system typów oznacza, że wartości z tych typów muszą być przechowywane jako jeden z czterech obsługiwanych typów. Microsoft.Data.Sqlite przechowuje je jako TEXT. Na przykład jednostka korzystająca z następujących typów:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    
    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

Mapuje na następującą tabelę w bazie danych SQLite:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

Następnie można zapisywać, wykonywać zapytania i aktualizować wartości w normalny sposób. Na przykład to zapytanie LINQ platformy EF Core:

var users = context.Users.Where(u => u.Birthday < new DateOnly(1900, 1, 1)).ToList();

Jest tłumaczony na następujące elementy w sqlite:

SELECT "u"."Id", "u"."Birthday", "u"."TokensRenewed", "u"."Username"
FROM "Users" AS "u"
WHERE "u"."Birthday" < '1900-01-01'

I zwraca tylko zastosowania z urodzinami przed 1900 CE:

Found 'ajcvickers'
Found 'wendy'

Interfejs API zapisywania punktów

Problem z usługą GitHub: #20228.

Standardyzacja wspólnego interfejsu API dla punktów zapisywania w ADO.NET dostawców. Microsoft.Data.Sqlite obsługuje teraz ten interfejs API, w tym:

Użycie punktu zapisu umożliwia wycofanie części transakcji bez wycofywania całej transakcji. Na przykład poniższy kod:

  • Tworzy transakcję
  • Wysyła aktualizację do bazy danych
  • Tworzy punkt zapisywania
  • Wysyła kolejną aktualizację do bazy danych
  • Cofa się do utworzonego wcześniej punktu zapisywania
  • Zatwierdza transakcję
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
connection.Open();

using var transaction = connection.BeginTransaction();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    command.ExecuteNonQuery();
}

transaction.Save("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    command.ExecuteNonQuery();
}

transaction.Rollback("MySavepoint");

transaction.Commit();

Spowoduje to zatwierdzenie pierwszej aktualizacji bazy danych, podczas gdy druga aktualizacja nie zostanie zatwierdzona, ponieważ punkt zapisywania został wycofany przed zatwierdzeniem transakcji.

Limit czasu polecenia w συμβολοσειρά σύνδεσης

Problem z usługą GitHub: #22505. Ta funkcja została udostępniona przez @nmichels. Dziękujemy!

ADO.NET dostawcy obsługują dwa różne limity czasu:

  • Limit czasu połączenia, który określa maksymalny czas oczekiwania podczas nawiązywania połączenia z bazą danych.
  • Limit czasu polecenia, który określa maksymalny czas oczekiwania na wykonanie polecenia.

Limit czasu polecenia można ustawić na podstawie kodu przy użyciu polecenia DbCommand.CommandTimeout. Wielu dostawców udostępnia teraz również ten limit czasu polecenia w συμβολοσειρά σύνδεσης. Microsoft.Data.Sqlite obserwuje ten trend za pomocą słowa kluczowego Command Timeout συμβολοσειρά σύνδεσης. Na przykład "Command Timeout=60;DataSource=test.db" funkcja użyje 60 sekund jako domyślnego limitu czasu dla poleceń utworzonych przez połączenie.

Porada

Sqlite traktuje Default Timeout jako synonim Command Timeout i dlatego można go użyć zamiast tego, jeśli jest to preferowane.