Co nowego w programie EF Core 6.0

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

Napiwek

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

Tabele czasowe programu SQL Server

Problem z usługą GitHub: #4693.

Tabele czasowe programu SQL Server 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, takich jak odzyskiwanie po przypadkowej mutacji lub usunięciu.

Program EF Core obsługuje teraz następujące funkcje:

  • 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 tabeli czasowej. Przykład:

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

W przypadku tworzenia bazy danych przy użyciu programu EF Core nowa tabela zostanie skonfigurowana jako tabela czasowa z wartościami domyślnymi programu SQL Server dla sygnatur czasowych i tabeli 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 program SQL Server tworzy dwie ukryte datetime2 kolumny o nazwie PeriodEnd i PeriodStart. Te "kolumny okresu" 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 czasem UTC generowanym przez program SQL Server. Czasy UTC są używane dla wszystkich operacji obejmujących tabele czasowe, takie jak w zapytaniach przedstawionych poniżej.

Zauważ również, że skojarzona tabela historii o nazwie EmployeeHistory jest tworzona automatycznie. Nazwy kolumn okresów 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");
            }));

Jest to odzwierciedlone w tabeli utworzonej przez program 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]))');

Korzystanie z 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 dla programu SQL Server, tak aby aplikacja mogła je zignorować. Na przykład nowe jednostki można zapisywać 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 mogą być następnie odpytywane, aktualizowane i usuwane 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}");
}

Te wydruki:

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 wartość maksymalną datetime2 . 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) o danej godzinie 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: To samo co 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ą tabel czasowych programu SQL Server, 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 przy użyciu polecenia TemporalAll , 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 sposób ef . Metoda właściwości może służyć do uzyskiwania dostępu do wartości z kolumn okresu. Jest on używany w klauzuli OrderBy do sortowania danych, a następnie w projekcji w celu uwzględnienia tych wartości w zwracanych 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 4:44:59. Wynika to z faktu, że wiersz tęczowej kreski został usunięty z tabeli głównej w tym czasie. Później zobaczymy, jak można przywrócić te dane.

Podobne zapytania można zapisywać przy użyciu metody 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. Był to 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 tęczowej kreski, tak jak w danym czasie UTC. Wszystkie zapytania korzystające z operatorów czasowych domyślnie nie śledzą, więc zwrócona jednostka w tym miejscu 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 względem danych historycznych pokazuje, że wiersz został przywrócony, tak jak 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żywane 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 wymagany 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ółowe omówienie 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, aby uwzględnić migracje. 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 narzędzia 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 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 w modelu zostaną wprowadzone i zostaną wygenerowane kolejne migracje 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 opcja może służyć do zastępowania istniejącego pakietu przy użyciu nowego.

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 parametry połączenia bazy danych z konfiguracji aplikacji. Można jednak zmigrować inną bazę danych, przekazując parametry połączenia 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. Użyj tej opcji w przypadku dołączania 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 #22616, aby zapoznać się z omówieniem możliwych przyszłych ulepszeń w tym obszarze.

Konfiguracja modelu przed konwencją

Problem z usługą GitHub: #12229.

Poprzednie wersje platformy 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.

To wymagało:

  • 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 wykonania w sposób niezawodny, 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 określenie konfiguracji mapowania raz 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 konwersji domyślnej z daty/godziny na długie:

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

Wszystkie właściwości logiczne można przekonwertować na liczby całkowite 0 lub 1 jedną 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ą ignorować 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ń Usługi Money:

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

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

Tabele utworzone dla programu SQL Server korzystające z 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 jest niekorzystywany z dowolną mapowaną właściwością modelu.

Uwaga

Zobacz Ogłoszenie programu Entity Framework Core 6.0 w wersji zapoznawczej 6: Konfigurowanie konwencji na blogu platformy .NET, aby uzyskać więcej dyskusji i przykładów 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 wykonania pierwszej operacji w obiekcie DbContext, gdy typ DbContext jest używany po raz pierwszy w aplikacji. Pamiętaj, że 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 mogą służyć do określania 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ć użycie skompilowanego modelu programu EF Core. Przykład:

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

Uruchamianie skompilowanego modelu

Zazwyczaj nie jest konieczne przyjrzenie się wygenerowanemu kodowi uruchamiania. Czasami jednak może być przydatne dostosowanie modelu lub jego ładowania. Kod bootstrapping wygląda następująco:

[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 można wygenerować wiele skompilowanych modeli dla typów DbContext, 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 parametry połączenia, 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 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

Napiwek

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 umiarkowanie duży model. Użycie aplikacji BenchmarkDotNet do pomiaru średni czas pierwszego zapytania wynosi 1,02 sekundy na dość wydajnym laptopie. Użycie skompilowanych modeli sprowadza się do 117 milisekund na tym samym sprzęcie. 8-10-krotne ulepszenie, takie jak to, pozostaje stosunkowo stałe w miarę wzrostu rozmiaru modelu.

Compiled model performance improvement

Uwaga

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

Zwiększona wydajność w witrynie TechEmpower Fortunes

Problem z usługą GitHub: #23611.

Wprowadziliśmy znaczące ulepszenia wydajności zapytań dla platformy EF Core 6.0. Szczególnie:

  • Wydajność platformy EF Core 6.0 jest teraz o 70% szybsza w branżowym benchmarku TechEmpower Fortunes w porównaniu do 5,0.
    • Jest to poprawa wydajności pełnego stosu, w tym ulepszenia kodu testu porównawczego, środowisko uruchomieniowe platformy .NET itp.
  • Sam program EF Core 6.0 jest o 31% szybszy 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 benchmarku 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 uzyskać szczegółowe omówienie ulepszeń wydajności zapytań w programie EF Core 6.0.

Ulepszenia dostawcy usługi Azure Cosmos DB

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

Napiwek

Możesz uruchomić 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 Azure Cosmos DB program EF Core 6.0 będzie domyślnie oznaczać typy jednostek podrzędnych jako należące do ich jednostki nadrzędnej. Eliminuje to konieczność wielu OwnsMany wywołań i OwnsOne w modelu usługi Azure Cosmos DB. 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 Azure Cosmos DB z następującą konfiguracją:

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 usługi Azure Cosmos DB mają rodziców rodziny, dzieci, zwierzęta domowe i adresy osadzone w dokumencie rodzinnym. Na 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 usługi Azure Cosmos DB. 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 listę, jak i słownik można wypełnić i wstawić w bazie 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 utworzenie 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ć, również 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
  • Uruchamianie zapytań dotyczących zawartości kolekcji pierwotnych nie jest obecnie obsługiwane. Zagłosuj na problemy #16926, #25700 i #25701, jeśli te funkcje są dla Ciebie ważne.

Tłumaczenia do wbudowanych funkcji

Problem z usługą GitHub: #16143.

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

Tłumaczenia ciągów

Metoda BCL 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 = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

Co przekłada 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

Metoda BCL 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 = await 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))
    .ToListAsync();

Co przekłada 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

Metoda BCL Wbudowana funkcja
DateTime.UtcNow GetCurrentDateTime

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

Na przykład:

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

Co przekłada 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 Azure Cosmos DB za pomocą FromSql metody . Działa to tak samo jak zawsze w przypadku dostawców relacyjnych. Przykład:

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

Który jest wykonywany jako:

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

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 = await context.Triangles
    .Select(e => e.Angle1).OrderBy(e => e).Distinct()
    .ToListAsync();

Co przekłada 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 Azure Cosmos DB 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 konieczne.

Uwaga

W dziennikach pokazano tutaj użycie EnableSensitiveDataLogging() wartości identyfikatora.

Wstawianie elementu do bazy danych usługi Azure Cosmos DB 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 usługi Azure Cosmos DB przy użyciu zapytania generuje CosmosEventId.ExecutingSqlQuery zdarzenie, a następnie co najmniej jedno CosmosEventId.ExecutedReadNext zdarzenie dla odczytanych 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 usługi Azure Cosmos DB 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 usługi Azure Cosmos DB 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 usługi Azure Cosmos DB 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 Azure Cosmos DB 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ć tak, aby aprowizować przepływność 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 usługi Azure Cosmos DB 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żywany HttpClientFactory przez dostawcę usługi Azure Cosmos DB 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 Azure Cosmos DB 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

Aby uzyskać szczegółowy przykład stosowania ulepszeń dostawcy usługi Azure Cosmos DB dla istniejącej aplikacji, zobacz Artykuł Taking the EF Core Azure Cosmos DB Provider for a Test Drive on the .NET Blog (Pobieranie dostawcy usługi Azure Cosmos DB dla wersji testowej na blogu platformy .NET), aby uzyskać szczegółowy przykład stosowania ulepszeń dostawcy usługi Azure Cosmos DB 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 Tagsi i tabelę sprzężenia PostTag łączącą 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 wystąpienie 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");
            });

Typy odwołań do tworzenia szkieletu języka C# dopuszczane do wartości null

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 szkieletowe, 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 mogą być dopuszczane do 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, że odpowiednie właściwości ciągu dopuszczane do wartości null i niepuste 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 szkieletów relacji między blogami bez wartości null (wymagane):

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 przyjazny dla języka NRT sposób. 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ą szkieletowe do komentarzy 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 szkieletowe do typów jednostek utworzonych podczas odwrotnej inżynierii modelu EF Core z istniejącej bazy danych programu 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 funkcji GroupBy

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:

  • Tłumaczenie elementu GroupBy, po którym FirstOrDefault następuje (lub podobne) w grupie
  • Obsługuje wybieranie pierwszych N wyników z grupy
  • Rozwija nawigację po zastosowaniu GroupBy operatora

Poniżej przedstawiono przykładowe zapytania z raportów klientów i ich tłumaczenia w programie 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 z String.Concat 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 korzystania z programu 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

Bezproblemowa integracja z aplikacją System.Linq.Async

Problem z usługą GitHub: #24041.

Pakiet System.Linq.Async dodaje asynchroniczne przetwarzanie LINQ po stronie klienta. Użycie 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 IAsyncEnumerable<T> wzorców języka C#, tak aby uwidoczniony program EF Core DbSet<TEntity> nie musiał 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 mapowanych przy użyciu konwertera wartości. Rozważmy na przykład typ jednostki z właściwością zdefiniowaną Name 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 metody 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 programu SQL Server:

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

Tłumaczenie elementu ToString na SQLite

Problem z usługą GitHub: #17223. Ta funkcja została udostępniona 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 wyszukiwań tekstowych obejmujących 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ący kod 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 programu 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 udostępniona przez @RaymondHuy. Dziękujemy!

EF.Functions.Random mapuje na funkcję bazy danych zwracającą pseudolosową liczbę z zakresu od 0 do 1 na wyłączność. Tłumaczenia zostały zaimplementowane w repozytorium EF Core dla programów SQL Server, SQLite i Azure Cosmos DB. 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ący kod SQL podczas korzystania z bazy danych programu 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 programu SQL Server dla elementu IsNullOrWhitespace

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

Rozważ 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 elementy w programie 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 w przypadku programu 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.

Nową metodę ToInMemoryQuery można użyć do zapisania 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; }
}

Teraz 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ć typ jednostki bez klucza, 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, które definiuje dane, które mają być zwracane dla elementu CustomerDensities:

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 udostępniona przez @stevendarby. Dziękujemy!

Program EF Core 6.0 tłumaczy teraz zastosowania z string.Substring 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 programu 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.

Program EF Core obsługuje dzielenie pojedynczego zapytania LINQ na wiele zapytań SQL. W programie EF Core 6.0 ta obsługa została rozszerzona w celu uwzględnienia przypadków, w których kolekcje nienawigacyjne znajdują się w projekcji zapytania.

Poniżej przedstawiono przykładowe zapytania przedstawiające tłumaczenie w programie SQL Server w jednym zapytaniu lub wielu zapytaniach.

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]

Usuń ostatnią klauzulę ORDER BY podczas dołączania do kolekcji

Problem z usługą GitHub: #19828.

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

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 programu EF Core 5.0 w programie 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 zamiast tego jest 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ń przy użyciu nazwy pliku i numeru wiersza

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

Tagi zapytań umożliwiają dodawanie tagu tekstowego do zapytania LINQ, tak aby zostało ono następnie uwzględnione 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 programu 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 posiadaniu opcjonalnej obsługi zależnej

Problem z usługą GitHub: #24558.

Staje się trudne, aby dowiedzieć się, czy opcjonalna jednostka zależna istnieje, czy nie, gdy udostępnia tabelę swojej jednostce głównej. Jest to spowodowane tym, że w tabeli znajduje się wiersz zależny, ponieważ podmiot zabezpieczeń wymaga go, niezależnie od tego, czy istnieje zależność. Sposób obsługi tego jednoznacznie polega na upewnieniu się, że zależne 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 jest prawidłowy, aby zapisać klienta bez adresu:

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

Jeśli jednak klient ma 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ą pytani, jeśli kolumna Kod pocztowy ma wartość null, oznacza to, że klient nie ma adresu, a Customer.Address właściwość nawigacji ma wartość null. Na przykład iteracja 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.

Rozważ zamiast tego 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, jak widać, bezpośrednio wykonując zapytania dotyczące kolumn 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 tabeli. 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 z 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 opcjonalny zależny działa jako podmiot zabezpieczeń dla dodatkowego opcjonalnego zależnego, również zamapowany na tę samą tabelę. Zamiast tylko ostrzegać, 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 jest własnością i Address jest własnością Customer elementu ContactInfo:

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; }
}

ContactInfo.Phone Jeśli wartość ma wartość null, program EF Core nie utworzy wystąpieniaAddress, 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 żadnej wymaganej właściwości innej niż współdzielona 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 zależnych zagnieżdżonych. Dodaj wymaganą właściwość, aby utworzyć wystąpienia z wartościami null dla innych właściwości lub oznaczyć nawigację przychodzącą zgodnie z wymaganiami, aby zawsze utworzyć wystąpienie.

Najważniejsze jest, aby uniknąć przypadku, w którym opcjonalne zależne może zawierać wszystkie wartości właściwości dopuszczalnych wartości null i udostępnić tabelę z jej podmiotem zabezpieczeń. Istnieją trzy proste sposoby, aby tego uniknąć:

  1. Ustaw wymaganą zależność. 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 obiekt zależny zawiera co najmniej jedną wymaganą właściwość, zgodnie z powyższym opisem.
  3. Zapisz opcjonalne zależności do własnej tabeli, zamiast udostępniać tabelę podmiotowi zabezpieczeń.

Zależność może być wymagana przy użyciu atrybutu Required w 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 Element 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 programu SQL Server spowoduje to wyświetlenie kolumny bazy danych :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!

Precyzja 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 programie 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óra ma być zawarta we 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 ta klasa konfiguracji musi zostać utworzone i wywołana 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 EntityTypeConfigurationAttribute na typie jednostki, tak aby program EF Core mógł znaleźć i użyć odpowiedniej konfiguracji. 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 program 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 rozrzedzywanych kolumn programu SQL Server

Problem z usługą GitHub: #8023.

Kolumny rozrzedzonych programu SQL Server 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ę z klasy :ForumUser

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 kilka jest moderatorami. Oznacza to, że mapowanie ForumName jako rozrzedliwego może mieć sens tutaj. Można to teraz 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 oznaczą kolumnę jako rozrzedzoną. Na 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

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

Ulepszenia interfejsu API usługi 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 do 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ć niestandardowy typ konwertera wartości. 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)
    {
    }
}

Można to teraz skonfigurować przy użyciu metody ogólnej HasConversion :

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

Mniej konfiguracji relacji wiele-do-wielu

Problem z usługą GitHub: #21535.

Jednoznaczne relacje wiele-do-wielu między dwoma typami jednostek są odnajdywane 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 współdzieloną jednostkę typową na Dictionary<string, object> podstawie tego, aby działać jako jednostka sprzężenia między dwoma typami. Począwszy od programu EF Core 6.0, UsingEntity można dodać do konfiguracji, 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 dodatkowo 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 zezwalające na konwersję wartości null zostały oznaczone jako dla [EntityFrameworkInternal] wersji EF Core 6.0. Użycie tych konstruktorów spowoduje teraz wygenerowanie ostrzeżenia dotyczącego kompilacji.

Konwertery wartości zwykle 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 innych niż null, co jest bardzo przydatne w przypadku kombinacji PK/FK, w których 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 walidacja 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 "Unknown":

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 kolumnę Breed 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 programie 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ż element 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 rozpoznać wystąpienie w zakresie dbContext z zakresu żądania, podczas gdy fabryka może służyć do tworzenia wielu niezależnych wystąpień w razie potrzeby.

Aby to umożliwić, AddDbContextFactory teraz również rejestruje 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 można rozpoznać fabrykę z głównego kontenera D.I.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 kontekstu utworzone przez fabrykę muszą być 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, gdy zakres kontenera jest usuwany; kontekst nie powinien być jawnie usuwany.

Na wyższym poziomie oznacza to, że albo dbContext fabryki może być wstrzykiwany do innych typów D.I. 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 bez parametrów konstruktor DbContext, jak i konstruktor, który ma DbContextOptions być używany w tym samym typie kontekstu, gdy fabryka jest zarejestrowana za pomocą AddDbContextFactorymetody . Na przykład kontekst użyty 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 wystąpień dbContext bez konieczności posiadania kontenera wstrzykiwania zależności przez aplikację. Pula zostanie utworzona 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 elementu [ColumnAttribute.Order] podczas tworzenia tabel

Problem z usługą GitHub: #10059.

Właściwość OrderColumnAttribute elementu można 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 zamawia najpierw kolumny klucza podstawowego, postępując zgodnie z właściwościami typu jednostki i należącymi do nich typami, a na koniec właściwościami z typów podstawowych. Na przykład poniższa tabela jest tworzona w programie SQL Server:

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 programie 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ć tego samego ColumnAttribute do określenia zarówno nazwy kolumny, jak 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 modelu ma HasColumnOrder pierwszeństwo przed dowolną kolejnością określoną za pomocą ColumnAttributepolecenia . Oznacza HasColumnOrder to, że można użyć do zastąpienia kolejności wykonanej za pomocą atrybutów, 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 podczas tworzenia tabeli. Oznacza to, że atrybut order kolumny nie może być używany do ponownego porządkownia kolumn w istniejącej tabeli. Jednym z istotnych wyjątków jest SQLite, w którym 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ą "minimalne interfejsy API", które usuwają wiele kodów standardowych tradycyjnie potrzebnych w aplikacjach platformy .NET.

Program 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ę elementu 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 na temat minimalnych interfejsów API:

Zachowywanie kontekstu synchronizacji w programie SaveChangesAsync

Problem z usługą GitHub: #23971.

Zmieniliśmy kod platformy EF Core w wersji 5.0 tak, aby był ustawiony Task.ConfigureAwait na false wartość we wszystkich miejscach, w których await kod asynchroniczny. Zazwyczaj jest to 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 USA. 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 "Użytkownik" z wartością klucza {Id: 1}.

W razie potrzeby można wyłączyć tę walidację. 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 poleceń 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 to być używane jako filtr w diagnostyce lub przechwytywaniu. Na przykład możemy chcieć przechwytywać, który ma zastosowanie tylko do poleceń pochodzących z SaveChangespolecenia :

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, co 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 uzyska 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ż uniemożliwia tymczasowe wycieki wartości do kodu aplikacji, w którym może być przypadkowo traktowane jako nie tymczasowe. 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 przed ich śledzeniem, aby można było ich używać 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 program EF Core można jawnie ustawić na wystąpienia jednostek 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ń dopuszczanych do wartości null w języku 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 dla użycia wartości null.

Microsoft.Data.Sqlite 6.0

Napiwek

Możesz uruchomić i debugować we wszystkich przykładach przedstawionych poniżej, pobierając przykładowy kod z usługi GitHub.

Pula połączeń

Problem z usługą GitHub: #13837.

Częstą 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łączonymi 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 podczas korzystania z microsoft.Data.Sqlite 6.0.

Obsługa wartości DateOnly i TimeOnly

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 używająca 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; }
}

Mapy do poniższej tabeli 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.

Standardizowaliśmy wspólny interfejs API dla punktów zapisywania u dostawców ADO.NET. Microsoft.Data.Sqlite obsługuje teraz ten interfejs API, w tym:

Użycie punktu zapisywania 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 parametry połączenia

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 parametry połączenia. Microsoft.Data.Sqlite obserwuje ten trend za pomocą słowa kluczowego Command Timeout parametry połączenia. Na przykład "Command Timeout=60;DataSource=test.db" użyje 60 sekund jako domyślnego limitu czasu dla poleceń utworzonych przez połączenie.

Napiwek

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