Sdílet prostřednictvím


Novinky v EF Core 6.0

EF Core 6.0 se odeslala do NuGetu. Tato stránka obsahuje přehled zajímavých změn zavedených v této verzi.

Tip

Ukázky uvedené níže můžete spustit a ladit stažením ukázkového kódu z GitHubu.

Dočasné tabulky SQL Serveru

Problém GitHubu: #4693.

Dočasné tabulky SQL Serveru automaticky sledují všechna data uložená v tabulce i po aktualizaci nebo odstranění těchto dat. Toho dosáhnete tak, že vytvoříte paralelní tabulku historie, do které se ukládají historická data s časovým razítkem při každé změně hlavní tabulky. To umožňuje dotazování historických dat, jako je auditování nebo obnovení, například obnovení po náhodném namětování nebo odstranění.

EF Core teď podporuje:

  • Vytvoření dočasných tabulek pomocí migrací
  • Transformace existujících tabulek do dočasných tabulek znovu pomocí migrací
  • Dotazování historických dat
  • Obnovení dat z nějakého bodu v minulosti

Konfigurace dočasné tabulky

Tvůrce modelů lze použít ke konfiguraci tabulky jako dočasné. Příklad:

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

Při použití EF Core k vytvoření databáze se nová tabulka nakonfiguruje jako dočasná tabulka s výchozími nastaveními SQL Serveru pro časová razítka a tabulku historie. Představte si Employee například typ entity:

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

Vytvořená dočasná tabulka bude vypadat takto:

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]))');

Všimněte si, že SQL Server vytvoří dva skryté datetime2 sloupce volaný PeriodEnd a PeriodStart. Tyto "sloupce období" představují časový rozsah, během kterého existovala data v řádku. Tyto sloupce se mapují na stínové vlastnosti v modelu EF Core, což umožňuje jejich použití v dotazech, jak je znázorněno později.

Důležité

Časy v těchto sloupcích jsou vždy čas UTC vygenerovaný SQL Serverem. Časy UTC se používají pro všechny operace zahrnující dočasné tabulky, například v níže uvedených dotazech.

Všimněte si také, že se automaticky vytvoří přidružená EmployeeHistory tabulka historie. Názvy sloupců období a tabulky historie je možné změnit s další konfigurací tvůrce modelů. Příklad:

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

To se projeví v tabulce vytvořené SQL Serverem:

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]))');

Použití dočasných tabulek

Ve většině případů se dočasné tabulky používají stejně jako všechny ostatní tabulky. To znamená, že sloupce období a historická data jsou transparentně zpracovávány SQL Serverem tak, aby je aplikace mohl ignorovat. Nové entity je například možné uložit do databáze běžným způsobem:

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();

Tato data se pak dají dotazovat, aktualizovat a odstraňovat běžným způsobem. Příklad:

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

Po normálním sledovacím dotazu je také možné získat přístup ke hodnotám ze sloupců období aktuálních dat ze sledovaných entit. Příklad:

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

Vytiskne se:

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

Všimněte si, že ValidTo sloupec (ve výchozím nastavení volaný PeriodEnd) obsahuje datetime2 maximální hodnotu. To platí vždy pro aktuální řádky v tabulce. Sloupce ValidFrom (ve výchozím nastavení volané PeriodStart) obsahují čas UTC, kdy byl řádek vložen.

Dotazování historických dat

EF Core podporuje dotazy, které zahrnují historická data prostřednictvím několika nových operátorů dotazů:

  • TemporalAsOf: Vrátí řádky, které byly aktivní (aktuální) v daném čase UTC. Toto je jeden řádek z aktuální tabulky nebo tabulky historie pro daný primární klíč.
  • TemporalAll: Vrátí všechny řádky v historických datech. Obvykle se jedná o mnoho řádků z tabulky historie nebo aktuální tabulky pro daný primární klíč.
  • TemporalFromTo: Vrátí všechny řádky, které byly aktivní mezi dvěma danými časy UTC. Může to být mnoho řádků z tabulky historie nebo aktuální tabulky pro daný primární klíč.
  • TemporalBetween: Stejné jako TemporalFromTov případě, že řádky jsou zahrnuty, které se staly aktivními na horní hranici.
  • TemporalContainedIn: Vrátí všechny řádky, které začaly být aktivní a skončily aktivní mezi dvěma danými časy UTC. Může to být mnoho řádků z tabulky historie nebo aktuální tabulky pro daný primární klíč.

Poznámka

Další informace o tom, které řádky jsou zahrnuty pro každý z těchto operátorů, najdete v dokumentaci k dočasným tabulkám SQL Serveru.

Například po provedení některých aktualizací a odstranění dat můžeme spustit dotaz pomocí TemporalAll zobrazení historických dat:

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

Všimněte si, jak ef . Metodu vlastnosti lze použít pro přístup k hodnotám ze sloupců období. Používá se v OrderBy klauzuli k seřazení dat a pak v projekci zahrnout tyto hodnoty do vrácených dat.

Tento dotaz vrátí následující data:

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

Všimněte si, že poslední vrácený řádek přestal být aktivní v 26. 8. 2021 4:44:59. Důvodem je, že řádek pro Rainbow Dash byl v té době odstraněn z hlavní tabulky. Později se podíváme, jak se tato data dají obnovit.

Podobné dotazy lze psát pomocí TemporalFromTo, TemporalBetweennebo TemporalContainedIn. Příklad:

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();

Tento dotaz vrátí následující řádky:

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

Obnovení historických dat

Jak už bylo zmíněno výše, Duha Dash byla z Employees tabulky odstraněna. To byla jasně chyba, takže se vrátíme k určitému bodu v čase a obnovíme chybějící řádek z této doby.

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

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

Tento dotaz vrátí jeden řádek pro Rainbow Dash, protože byl v daném čase UTC. Všechny dotazy používající dočasné operátory ve výchozím nastavení nesledují, takže vrácená entita zde není sledována. To dává smysl, protože v hlavní tabulce aktuálně neexistuje. Pokud chcete entitu znovu vložit do hlavní tabulky, jednoduše ji označíme jako Added a pak zavoláme SaveChanges.

Po opětovném vložení řádku Rainbow Dash se dotazem na historická data zobrazí, že se řádek obnovil tak, jak byl v daném čase 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

Sady prostředků migrace

Problém GitHubu: #19693.

Migrace EF Core se používají k vygenerování aktualizací schématu databáze na základě změn modelu EF. Tyto aktualizace schématu by se měly používat v době nasazení aplikace, často jako součást systému kontinuální integrace nebo průběžného nasazování (C.I./C.D.).

EF Core teď obsahuje nový způsob, jak tyto aktualizace schématu použít: sady prostředků migrace. Sada migrace je malý spustitelný soubor obsahující migrace a kód potřebný k použití těchto migrací na databázi.

Poznámka

Podrobnější informace o migracích, sadách a nasazeních migrace, které jsou vhodné pro DevOps, najdete na blogu .NET.

Sady migrace se vytvářejí pomocí nástroje příkazového dotnet ef řádku. Než budete pokračovat, ujistěte se, že jste nainstalovali nejnovější verzi nástroje .

Sada potřebuje migrace, které se mají zahrnout. Ty se vytvářejí podle dotnet ef migrations add popisu v dokumentaci k migracím. Jakmile budete mít migrace připravené k nasazení, vytvořte sadu pomocí nástroje dotnet ef migrations bundle. Příklad:

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>

Výstup je spustitelný soubor vhodný pro váš cílový operační systém. V mém případě to je Windows x64, takže se mi v efbundle.exe místní složce zahodí. Spuštění tohoto spustitelného souboru použije migrace obsažené v něm:

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

Migrace se použijí na databázi jenom v případě, že ještě nebyly použity. Například opětovné spuštění stejné sady nedělá nic, protože neexistují žádné nové migrace, které by se použily:

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

Pokud ale dojde ke změnám modelu a vygenerují dotnet ef migrations addse další migrace, dají se sbalit do nového spustitelného souboru připraveného k použití. Příklad:

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>

Všimněte si, že --force možnost lze použít k přepsání existující sady novou.

Spuštěním této nové sady se na databázi použijí tyto dvě nové migrace:

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

Ve výchozím nastavení sada používá databázi připojovací řetězec z konfigurace vaší aplikace. Jinou databázi ale můžete migrovat předáním připojovací řetězec na příkazovém řádku. Příklad:

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>

Všimněte si, že tentokrát byly použity všechny tři migrace, protože žádná z nich ještě nebyla použita pro produkční databázi.

Další možnosti lze předat do příkazového řádku. Mezi běžné možnosti patří:

  • --output zadejte cestu spustitelného souboru, který se má vytvořit.
  • --context zadejte typ DbContext, který se má použít, pokud projekt obsahuje více typů kontextu.
  • --project a zadejte projekt, který se má použít. Výchozí hodnota je aktuální pracovní adresář.
  • --startup-project pro zadání spouštěcího projektu, který se má použít. Výchozí hodnota je aktuální pracovní adresář.
  • --no-build chcete-li zabránit sestavení projektu před spuštěním příkazu. Tento postup by se měl použít jenom v případě, že je o projektu známo, že je aktuální.
  • --verbose podrobné informace o tom, co příkaz dělá. Tuto možnost použijte, pokud do sestav chyb zahrnete informace.

Umožňuje dotnet ef migrations bundle --help zobrazit všechny dostupné možnosti.

Všimněte si, že ve výchozím nastavení se každá migrace použije ve své vlastní transakci. Informace o možných budoucíchvylepšeních

Konfigurace modelu před konvencí

Problém Na GitHubu: č. 12229.

Předchozí verze EF Core vyžadují, aby mapování pro každou vlastnost daného typu bylo nakonfigurováno explicitně, když se mapování liší od výchozího nastavení. To zahrnuje "omezující vlastnosti", jako je maximální délka řetězců a desetinná přesnost, stejně jako převod hodnoty pro typ vlastnosti.

To vyžadovalo jednu z těchto akcí:

  • Konfigurace tvůrce modelů pro každou vlastnost
  • Atribut mapování pro každou vlastnost
  • Explicitní iterace pro všechny vlastnosti všech typů entit a použití rozhraní API metadat nízké úrovně při sestavování modelu.

Všimněte si, že explicitní iterace je náchylná k chybám a obtížně se provádí robustně, protože seznam typů entit a mapovaných vlastností nemusí být v době, kdy k této iteraci dojde, konečný.

EF Core 6.0 umožňuje zadat tuto konfiguraci mapování jednou pro daný typ. Pak se použije na všechny vlastnosti tohoto typu v modelu. Tomu se říká "konfigurace modelu před konvencí", protože konfiguruje aspekty modelu, které se pak používají konvencí vytváření modelu. Tato konfigurace se použije přepsáním ConfigureConventions na :DbContext

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

Představte si například následující typy entit:

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

Všechny vlastnosti řetězce lze nakonfigurovat tak, aby byly ANSI (místo Unicode) a mají maximální délku 1024:

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

Všechny vlastnosti DateTime lze v databázi převést na 64bitové celá čísla pomocí výchozího převodu z DateTimes na dlouhé hodnoty:

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

Všechny logické vlastnosti lze převést na celá čísla 0 nebo 1 pomocí některého z předdefinovaných převaděčů hodnot:

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

Za předpokladu Session , že je přechodná vlastnost entity a neměla by být zachována, lze ji ignorovat všude v modelu:

configurationBuilder
    .IgnoreAny<Session>();

Konfigurace modelu před konvencí je velmi užitečná při práci s objekty hodnot. Například typ Money výše uvedeného modelu je reprezentován strukturou jen pro čtení:

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
}

To se pak serializuje do a z JSON pomocí vlastního převaděče hodnot:

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

Tento převaděč hodnot lze nakonfigurovat jednou pro všechna použití Money:

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

Všimněte si také, že pro sloupec řetězce, do kterého je uložen serializovaný JSON, je možné zadat další omezující vlastnosti. V tomto případě je sloupec omezen na maximální délku 64.

Tabulky vytvořené pro SQL Server pomocí migrací ukazují, jak byla konfigurace použita pro všechny mapované sloupce:

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])
);

Pro daný typ je také možné zadat výchozí mapování typů. Příklad:

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

To je zřídka potřeba, ale může být užitečné, pokud se typ používá v dotazu způsobem, který nesouvisí s libovolnou mapovanou vlastností modelu.

Kompilované modely

Problém Na GitHubu: #1906.

Kompilované modely můžou zlepšit dobu spouštění EF Core pro aplikace s velkými modely. Velký model obvykle znamená 100 až 1 000 typů entit a relací.

Čas spuštění znamená čas provedení první operace v DbContext, pokud se typ DbContext používá poprvé v aplikaci. Všimněte si, že pouhé vytvoření instance DbContext nezpůsobí inicializaci modelu EF. Místo toho typické první operace, které způsobují inicializaci modelu, zahrnují volání DbContext.Add nebo spuštění prvního dotazu.

Kompilované modely se vytvářejí pomocí nástroje příkazového dotnet ef řádku. Než budete pokračovat, ujistěte se, že jste nainstalovali nejnovější verzi nástroje .

K vygenerování kompilovaného modelu se používá nový dbcontext optimize příkaz. Příklad:

dotnet ef dbcontext optimize

Pomocí --output-dir možností --namespace lze určit adresář a obor názvů, do kterého se bude kompilovaný model generovat. Příklad:

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>

Výstup spuštění tohoto příkazu zahrnuje část kódu, která zkopíruje a vloží do konfigurace DbContext, aby ef Core používala zkompilovaný model. Příklad:

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

Bootstrapping kompilovaného modelu

Obvykle není nutné se podívat na vygenerovaný kód bootstrappingu. Někdy ale může být užitečné přizpůsobit model nebo jeho načítání. Kód bootstrappingu vypadá přibližně takto:

[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();
}

Jedná se o částečnou třídu s částečnými metodami, které je možné implementovat pro přizpůsobení modelu podle potřeby.

Kromě toho lze vygenerovat více kompilovaných modelů pro typy DbContext, které můžou používat různé modely v závislosti na určité konfiguraci modulu runtime. Ty by se měly umístit do různých složek a oborů názvů, jak je znázorněno výše. Informace o modulu runtime, jako je například připojovací řetězec, je pak možné prozkoumat a podle potřeby vrátit správný model. Příklad:

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.");
            });
}

Omezení

Kompilované modely mají určitá omezení:

Z důvodu těchto omezení byste měli použít pouze kompilované modely, pokud je čas spuštění EF Core příliš pomalý. Kompilace malých modelů obvykle nestojí za to.

Pokud je podpora některé z těchto funkcí pro váš úspěch důležitá, hlasujte prosím o odpovídajících problémech, které jsou propojené výše.

Srovnávací testy

Tip

Můžete zkusit kompilovat velký model a spustit na něm srovnávací test stažením ukázkového kódu z GitHubu.

Model v úložišti GitHubu, na který odkazuje výše, obsahuje 449 typů entit, 6390 vlastností a 720 relací. Jedná se o středně velký model. Pomocí BenchmarkDotNetu k měření je průměrná doba prvního dotazu 1,02 sekund na přiměřeně výkonném přenosném počítači. Použití zkompilovaných modelů to přináší na stejný hardware až 117 milisekund. 8x až 10x vylepšení, jako je toto, zůstává relativně konstantní, jak se velikost modelu zvyšuje.

Compiled model performance improvement

Poznámka

Viz Oznámení Entity Framework Core 6.0 Preview 5: Kompilované modely na blogu .NET, kde najdete podrobnější diskuzi o výkonu a kompilovaných modelech EF Core.

Vylepšený výkon na TechEmpower Fortunes

Problém Na GitHubu: #23611.

Výrazně jsme vylepšili výkon dotazů pro EF Core 6.0. Konkrétně:

  • Výkon EF Core 6.0 je nyní 70 % rychlejší na standardním srovnávacím testu TechEmpower Fortunes oproti 5,0.
    • Jedná se o vylepšení výkonu v plném zásobníku, včetně vylepšení v kódu srovnávacího testu, modulu runtime .NET atd.
  • Samotné EF Core 6.0 je 31 % rychlejší spouštění nesledovaných dotazů.
  • Přidělení haldy se při provádění dotazů snížila o 43 %.

Po těchto vylepšeních se rozdíl mezi oblíbeným dapperem "micro-ORM" a EF Core v srovnávacím testu TechEmpower Fortunes zúžil z 55 % na kolem o něco pod 5 %.

Poznámka

Podívejte se na oznámení Entity Framework Core 6.0 Preview 4: Performance Edition na blogu .NET, kde najdete podrobnou diskuzi o vylepšení výkonu dotazů v EF Core 6.0.

Vylepšení zprostředkovatele služby Azure Cosmos DB

EF Core 6.0 obsahuje řadu vylepšení poskytovatele databáze Azure Cosmos DB.

Tip

Všechny ukázky specifické pro Cosmos můžete spustit a ladit stažením ukázkového kódu z GitHubu.

Výchozí nastavení implicitního vlastnictví

Problém GitHubu: #24803.

Při vytváření modelu pro zprostředkovatele služby Azure Cosmos DB označí EF Core 6.0 podřízené typy entit jako vlastněné jejich nadřazenou entitou ve výchozím nastavení. Tím se eliminuje potřeba velké části OwnsMany volání modelu OwnsOne Azure Cosmos DB. To usnadňuje vkládání podřízených typů do dokumentu pro nadřazený typ, což je obvykle vhodný způsob, jak modelovat rodiče a podřízené položky v databázi dokumentů.

Představte si například tyto typy entit:

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

V EF Core 5.0 by byly tyto typy modelovány pro službu Azure Cosmos DB s následující konfigurací:

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

V EF Core 6.0 je vlastnictví implicitní a snižuje konfiguraci modelu na:

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

Výsledné dokumenty Azure Cosmos DB mají rodiče rodiny, děti, domácí zvířata a adresu vloženou do rodinného dokumentu. Příklad:

{
  "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
}

Poznámka

Je důležité si uvědomit, že OwnsOne/OwnsMany konfigurace se musí použít, pokud potřebujete tyto vlastní typy dále nakonfigurovat.

Kolekce primitivních typů

Problém Na GitHubu: #14762

EF Core 6.0 nativně mapuje kolekce primitivních typů při použití zprostředkovatele databáze Azure Cosmos DB. Představte si například tento typ entity:

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

Jak seznam, tak slovník lze naplnit a vložit do databáze běžným způsobem:

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);
await context.SaveChangesAsync();

Výsledkem je následující dokument 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
}

Tyto kolekce je pak možné aktualizovat, a to opět běžným způsobem:

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

await context.SaveChangesAsync();

Omezení:

  • Podporují se jen slovníky s řetězcovými klíči.
  • Dotazování na obsah primitivních kolekcí se v současné době nepodporuje. Pokud jsou pro vás tyto funkce důležité, hlasujte pro #16926, #25700 a #25701.

Překlady na předdefinované funkce

Problém Na GitHubu: #16143.

Zprostředkovatel Služby Azure Cosmos DB teď překládá více metod knihovny základních tříd (BCL) na integrované funkce Azure Cosmos DB. Následující tabulky ukazují překlady, které jsou v EF Core 6.0 nové.

Překlady řetězců

Metoda BCL Integrovaná funkce Poznámky
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ Operátor CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUALS Pouze volání nerozlišující malá a velká písmena

Překlady pro LOWER, , LTRIM, TRIMRTRIM, UPPER, a SUBSTRING byly přispěly @Marusyk. Mnohokrát děkujeme!

Příklad:

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();

To se překládá 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)))

Matematické překlady

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

Těmito překlady přispěl @Marusyk. Mnohokrát děkujeme!

Příklad:

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();

To se překládá 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))))

Překlady data a času

Metoda BCL Integrovaná funkce
DateTime.UtcNow GetCurrentDateTime

Těmito překlady přispěl @Marusyk. Mnohokrát děkujeme!

Příklad:

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

To se překládá na:

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

Nezpracované dotazy SQL s využitím FromSql

Problém na GitHubu: #17311.

Někdy je nutné místo použití LINQ spustit nezpracovaný dotaz SQL. Tato funkce je teď podporovaná u poskytovatele služby Azure Cosmos DB prostřednictvím této FromSql metody. To funguje stejně jako u relačních poskytovatelů. Příklad:

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

To se provede takto:

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

Odlišné dotazy

Problém Na GitHubu: #16144.

Jednoduché dotazy, které se používají Distinct , se teď překládají. Příklad:

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

To se překládá na:

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

Diagnostika

Problém Na GitHubu: č. 17298.

Zprostředkovatel služby Azure Cosmos DB teď protokoluje další diagnostické informace, včetně událostí pro vkládání, dotazování, aktualizaci a odstraňování dat z databáze. Jednotky žádostí (RU) se do těchto událostí zahrnou, kdykoli je to vhodné.

Poznámka

Tady se zobrazují EnableSensitiveDataLogging() protokoly, aby se zobrazily hodnoty ID.

Vložení položky do databáze Azure Cosmos DB vygeneruje CosmosEventId.ExecutedCreateItem událost. Například tento kód:

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

Zaznamená následující diagnostickou událost:

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'

Načtení položek z databáze Azure Cosmos DB pomocí dotazu vygeneruje CosmosEventId.ExecutingSqlQuery událost a pak jednu nebo více CosmosEventId.ExecutedReadNext událostí pro přečtené položky. Například tento kód:

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

Zaznamená následující diagnostické události:

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

Načtení jedné položky z databáze Azure Cosmos DB pomocí Find klíče oddílu CosmosEventId.ExecutingReadItem generuje události a CosmosEventId.ExecutedReadItem události. Například tento kód:

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

Zaznamená následující diagnostické události:

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'

Uložení aktualizované položky do databáze Azure Cosmos DB vygeneruje CosmosEventId.ExecutedReplaceItem událost. Například tento kód:

triangle.Angle2 = 89;
await context.SaveChangesAsync();

Zaznamená následující diagnostickou událost:

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'

Odstranění položky z databáze Azure Cosmos DB vygeneruje CosmosEventId.ExecutedDeleteItem událost. Například tento kód:

context.Remove(triangle);
await context.SaveChangesAsync();

Zaznamená následující diagnostickou událost:

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'

Konfigurace propustnosti

Problém GitHubu: #17301.

Model Azure Cosmos DB je teď možné nakonfigurovat s ruční nebo automatickou propustností. Tyto hodnoty zřizují propustnost databáze. Příklad:

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

Kromě toho je možné nakonfigurovat jednotlivé typy entit tak, aby zřídily propustnost pro odpovídající kontejner. Příklad:

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

Konfigurace hodnoty TTL (time-to-live)

Problém GitHubu: #17307.

Typy entit v modelu Azure Cosmos DB se teď dají nakonfigurovat s výchozím časem a časem a časem v analytickém úložišti. Příklad:

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

Řešení potíží s klientskou továrnou HTTP

Problém na GitHubu: #21274. Touto funkcí přispěl @dnperfors. Mnohokrát děkujeme!

Zprostředkovatel HttpClientFactory služby Azure Cosmos DB je teď možné nastavit explicitně. To může být zvlášť užitečné při testování, například k obejití ověření certifikátu při použití emulátoru služby Azure Cosmos DB v Linuxu:

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

Vylepšení generování uživatelského rozhraní z existující databáze

EF Core 6.0 obsahuje několik vylepšení při zpětné analýze modelu EF z existující databáze.

Generování relací M:N

Problém Na GitHubu: #22475.

EF Core 6.0 detekuje jednoduché tabulky spojení a automaticky pro ně vygeneruje mapování M:N. Zvažte například tabulky pro Posts tabulky a Tagsspojovací tabulku PostTag , která je propojí:

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

Tyto tabulky se dají vygenerovat z příkazového řádku. Příklad:

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

Výsledkem je třída 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; }
}

A třída pro Tag:

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 žádná třída tabulky PostTag . Místo toho se konfigurace relace M:N vygeneruje:

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

Generování referenčních typů c# s možnou hodnotou null

Problém Na GitHubu: #15520.

EF Core 6.0 teď vygeneruje model EF a typy entit, které používají odkazové typy s možnou hodnotou null jazyka C#(NRT). Použití NRT se automaticky vygeneruje, když je v projektu C# povolená podpora NRT, do kterého se kód vygeneruje.

Například následující Tags tabulka obsahuje oba sloupce řetězců s možnou hodnotou null:

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

Výsledkem jsou odpovídající vlastnosti řetězce s možnou hodnotou null a nenulovou hodnotou ve vygenerované třídě:

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

Podobně následující Posts tabulky obsahují požadovanou relaci s Blogs tabulkou:

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]));

Výsledkem je generování relace bez hodnoty null (povinné) mezi blogy:

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

A příspěvky:

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

Nakonec se vlastnosti DbSet vygenerované DbContext vytvoří způsobem, který je přívětivý pro NRT. Příklad:

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

Komentáře k databázi se vygenerují do komentářů kódu.

Problém Na GitHubu: č. 19113. Touto funkcí přispěl @ErikEJ. Mnohokrát děkujeme!

Komentáře k tabulkám a sloupcům SQL se teď vygenerují do typů entit vytvořených při zpětné analýze modelu EF Core z existující databáze SQL Serveru.

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

Vylepšení dotazů LINQ

EF Core 6.0 obsahuje několik vylepšení překladu a provádění dotazů LINQ.

Vylepšená podpora GroupBy

Problémy s GitHubem: #12088, #13805 a #22609.

EF Core 6.0 obsahuje lepší podporu pro GroupBy dotazy. Konkrétně EF Core teď:

  • Přeložit GroupBy následovaný FirstOrDefault (nebo podobným) přes skupinu
  • Podporuje výběr prvních N výsledků ze skupiny.
  • Rozbalí navigaci po použití operátoru GroupBy .

Následuje příklad dotazů ze sestav zákazníků a jejich překlad na SQL Serveru.

Příklad 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]

Příklad 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]

Příklad 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)

Příklad 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]

Příklad 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))

Příklad 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]))

Příklad 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]

Příklad 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]

Příklad 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]

Příklad 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]

Příklad 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]

Příklad 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]

Příklad 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 entit používané pro tyto příklady:

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

Překlad String.Concat s více argumenty

Problém Na GitHubu: #23859. Touto funkcí přispěl @wmeints. Mnohokrát děkujeme!

Od EF Core 6.0 se teď volání String.Concat s více argumenty přeloží do SQL. Například následující dotaz:

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

Při použití SQL Serveru se přeloží do následujícího SQL Serveru:

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

Plynulejší integrace se System.Linq.Async

Problém GitHubu: #24041.

Balíček System.Linq.Async přidává asynchronní zpracování LINQ na straně klienta. Použití tohoto balíčku s předchozími verzemi EF Core bylo těžkopádné kvůli kolizí oboru názvů pro asynchronní metody LINQ. V EF Core 6.0 jsme využili párování vzorů jazyka C#, IAsyncEnumerable<T> aby vystavený EF Core DbSet<TEntity> nemusel rozhraní implementovat přímo.

Všimněte si, že většina aplikací nemusí používat System.Linq.Async, protože dotazy EF Core jsou obvykle plně přeloženy na server.

Problém Na GitHubu: #23921.

V EF Core 6.0 jsme uvolnili požadavky na parametry a FreeText(DbFunctions, String, String) Contains. To umožňuje použití těchto funkcí s binárními sloupci nebo se sloupci namapovanými pomocí převaděče hodnot. Představte si například typ entity s vlastností definovanou Name jako objekt hodnoty:

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

Toto je namapováno na JSON v databázi:

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

Dotaz lze nyní spustit pomocí Contains nebo i když typ vlastnosti není stringName .FreeText Příklad:

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

Tím se při použití SQL Serveru vygeneruje následující SQL:

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

Translate ToString on SQLite

Problém Na GitHubu: č. 17223. Touto funkcí přispěl @ralmsdeveloper. Mnohokrát děkujeme!

ToString() Při použití zprostředkovatele databáze SQLite se teď volání přeloží do SQL. To může být užitečné pro vyhledávání textu zahrnující sloupce, které nejsou řetězcové. Představte si User například typ entity, který ukládá telefonní čísla jako číselné hodnoty:

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

ToString lze použít k převodu čísla na řetězec v databázi. Tento řetězec pak můžeme použít s funkcí, například LIKE k vyhledání čísel, která odpovídají vzoru. Pokud například chcete najít všechna čísla obsahující 555:

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

To se při použití databáze SQLite přeloží na následující SQL:

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

Všimněte si, že překlad ToString() pro SQL Server je již podporován v EF Core 5.0 a může být podporován také jinými poskytovateli databáze.

EF. Functions.Random

Problém na GitHubu: #16141. Touto funkcí přispěl @RaymondHuy. Mnohokrát děkujeme!

EF.Functions.Random mapuje na funkci databáze vracející pseudonáhodné číslo mezi 0 a 1 výhradním číslem. Překlady byly implementovány v úložišti EF Core pro SQL Server, SQLite a Azure Cosmos DB. Představte si User například typ entity s Popularity vlastností:

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

Popularity může mít hodnoty od 1 do 5 včetně. Pomocí EF.Functions.Random dotazu můžeme napsat dotaz, který vrátí všechny uživatele s náhodně zvolenou popularitou:

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

To se při použití databáze SQL Serveru přeloží na následující SQL:

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

Vylepšený překlad SQL Serveru pro IsNullOrWhitespace

Problém Na GitHubu: #22916. Touto funkcí přispěl @Marusyk. Mnohokrát děkujeme!

Představte si následující dotaz:

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

Před EF Core 6.0 se na SQL Serveru přeložil na následující položky:

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''))

Tento překlad byl vylepšen pro EF Core 6.0 tak, aby:

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''))

Definování dotazu pro zprostředkovatele v paměti

Problém GitHubu: #24600.

Novou metodu ToInMemoryQuery lze použít k zápisu definičního dotazu na databázi v paměti pro daný typ entity. To je nejužitečnější pro vytvoření ekvivalentu zobrazení v databázi v paměti, zejména pokud tato zobrazení vracejí typy entit bez klíčů. Představte si například databázi zákazníků, která je založená na Spojeném království. Každý zákazník má adresu:

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

Představte si, že chceme zobrazit tato data, která ukazují, kolik zákazníků je v každé oblasti PSČ. Můžeme vytvořit typ entity bez klíčů, který bude reprezentovat tento:

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

A definujte vlastnost DbSet pro ni v DbContext spolu se sadami pro další typy entit nejvyšší úrovně:

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

Pak můžeme OnModelCreatingnapsat dotaz LINQ, který definuje data, která se mají vrátit pro 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()
                        }));
}

To se pak dá dotazovat stejně jako jakákoli jiná vlastnost DbSet:

var results = context.CustomerDensities.ToList();

Překlad podřetědce s jedním parametrem

Problém GitHubu: #20173. Touto funkcí přispěl @stevendarby. Mnohokrát děkujeme!

EF Core 6.0 teď překládá použití string.Substring s jedním argumentem. Příklad:

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

To se při použití SQL Serveru přeloží na následující SQL:

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'

Rozdělené dotazy pro jiné než navigační kolekce

Problém Na GitHubu: č. 21234.

EF Core podporuje rozdělení jednoho dotazu LINQ na několik dotazů SQL. V EF Core 6.0 byla tato podpora rozšířena tak, aby zahrnovala případy, kdy v projekci dotazu nejsou kolekce navigace.

Následuje příklad dotazů, které ukazují překlad na SQL Serveru do jednoho dotazu nebo více dotazů.

Příklad 1:

Dotaz LINQ:

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

Jeden dotaz 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]

Více dotazů 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]

Příklad 2:

Dotaz LINQ:

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

Jeden dotaz 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]

Více dotazů 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]

Příklad 3:

Dotaz LINQ:

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

Jeden dotaz 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]

Více dotazů 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]

Odebrání poslední klauzule ORDER BY při připojování ke kolekci

Problém Na GitHubu: #19828.

Při načítání souvisejících entit 1:N ef Core přidá klauzule ORDER BY, aby se zajistilo, že jsou všechny související entity pro danou entitu seskupené dohromady. Poslední klauzule ORDER BY však není nutná pro generování potřebných seskupení EF a může mít vliv na výkon. Proto ef Core 6.0 tato klauzule je odebrána.

Představte si například tento dotaz:

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

S EF Core 5.0 na SQL Serveru se tento dotaz přeloží 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]

U EF Core 6.0 se místo toho přeloží 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]

Označování dotazů s názvem souboru a číslem řádku

Problém GitHubu: #14176. Touto funkcí přispěl @michalczerwinski. Mnohokrát děkujeme!

Značky dotazů umožňují přidání texturové značky do dotazu LINQ tak, aby se pak zahrnula do vygenerovaného SQL. V EF Core 6.0 se dá použít k označení dotazů pomocí názvu souboru a čísla řádku kódu LINQ. Příklad:

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

Výsledkem je následující vygenerovaný SQL při použití SQL Serveru:

-- 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%')

Změny ve vlastnictví volitelného závislého zpracování

Problém Na GitHubu: #24558.

Je obtížné zjistit, jestli volitelná závislá entita existuje, nebo ne, když sdílí tabulku se svou hlavní entitou. Důvodem je to, že v tabulce je řádek závislý, protože objekt zabezpečení ho potřebuje, bez ohledu na to, jestli závislý existuje nebo ne. Způsob, jak to jednoznačně zpracovat, je zajistit, aby závislý má alespoň jednu požadovanou vlastnost. Vzhledem k tomu, že požadovaná vlastnost nemůže mít hodnotu null, znamená to, že pokud je hodnota ve sloupci pro tuto vlastnost null, závislá entita neexistuje.

Představte si Customer například třídu, ve které má každý zákazník vlastní: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; }
}

Adresa je nepovinná, což znamená, že je platná pro uložení zákazníka bez adresy:

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

Pokud však zákazník má adresu, musí mít tato adresa alespoň nenulové poštovní heslo:

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

To je zajištěno označením Postcode vlastnosti jako Required.

Když se teď zákazníci dotazují, pokud má sloupec PsČ hodnotu null, znamená to, že zákazník nemá adresu a Customer.Address navigační vlastnost má hodnotu null. Iterace prostřednictvím zákazníků a kontrola, jestli má adresa hodnotu 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}.");
    }
}

Vygeneruje následující výsledky:

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

Zvažte místo toho případ, kdy není vyžadována žádná vlastnost mimo adresu:

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

Teď je možné uložit zákazníka bez adresy i zákazníka s adresou, kde všechny vlastnosti adresy mají hodnotu null:

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

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

V databázi jsou ale tyto dva případy nerozlišitelné, jak vidíme přímo dotazováním sloupců databáze:

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

Z tohoto důvodu ef Core 6.0 vás nyní upozorní při uložení volitelné závislé, kde všechny její vlastnosti mají hodnotu null. Příklad:

upozornění: 9/27/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) Entita typu Address s hodnotami primárního klíče {CustomerId: -2147482646} je volitelná závislost při sdílení tabulky. Entita nemá žádnou vlastnost s jinou než výchozí hodnotou k identifikaci, zda entita existuje. To znamená, že při dotazování nebude vytvořena žádná instance objektu místo instance se všemi vlastnostmi nastavenými na výchozí hodnoty. Dojde také ke ztrátě vnořených závislých závislosti. Buď neuložte žádnou instanci s pouze výchozími hodnotami, nebo označte příchozí navigaci jako požadovanou v modelu.

To se stává ještě složitější, když nepovinný závislý sám působí objekt zabezpečení pro další volitelné závislé, také namapovaný na stejnou tabulku. Ef Core 6.0 místo pouhého upozornění zakáže pouze případy vnořených volitelných závislých osob. Představte si například následující model, ve ContactInfo kterém Customer je vlastníkem a Address který je vlastněný 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; }
}

Pokud ContactInfo.Phone je hodnota null, ef Core nevytvoří instanci Address , pokud je relace nepovinná, i když samotná adresa může obsahovat data. Pro tento typ modelu ef Core 6.0 vyvolá následující výjimku:

System.InvalidOperationException: Typ entity ContactInfo je volitelný závislý pomocí sdílení tabulky a obsahuje další závislé objekty bez jakékoli požadované nesdílené vlastnosti k identifikaci, zda entita existuje. Pokud všechny vlastnosti s možnou hodnotou null obsahují hodnotu null v databázi, instance objektu nebude vytvořena v dotazu, což způsobí ztrátu vnořených závislých hodnot. Přidejte požadovanou vlastnost pro vytváření instancí s hodnotami null pro jiné vlastnosti nebo označte příchozí navigaci jako požadovanou pro vždy vytvoření instance.

V tomto dolním řádku se vyhnete případu, kdy volitelný závislý objekt může obsahovat všechny hodnoty vlastností s možnou hodnotou null a sdílí tabulku s jejím objektem zabezpečení. Existují tři jednoduché způsoby, jak se tomu vyhnout:

  1. Proveďte požadovanou závislost. To znamená, že závislá entita bude mít po dotazech vždy hodnotu, i když všechny její vlastnosti mají hodnotu null.
  2. Ujistěte se, že závislý objekt obsahuje aspoň jednu požadovanou vlastnost, jak je popsáno výše.
  3. Místo sdílení tabulky s objektem zabezpečení uložte volitelné závislé osoby do vlastní tabulky.

Závislost může být vyžadována pomocí atributu Required v navigaci:

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

Nebo zadáním požadovaného parametru:OnModelCreating

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

Závislé osoby lze uložit do jiné tabulky zadáním tabulek, které se mají použít v OnModelCreating:

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

Další příklady volitelnýchzávislých

Nové atributy mapování

EF Core 6.0 obsahuje několik nových atributů, které je možné použít pro kód, aby se změnil způsob mapování na databázi.

UnicodeAttribute

Problém GitHubu: #19794. Touto funkcí přispěl @RaymondHuy. Mnohokrát děkujeme!

Od EF Core 6.0 je teď možné vlastnost řetězce namapovat na sloupec, který není unicode, pomocí atributu mapování bez přímého zadání typu databáze. Představte si Book například typ entity s vlastností pro číslo mezinárodního standardního knihy (ISBN) ve tvaru "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; }
}

Vzhledem k tomu, že sítě ISBN nemohou obsahovat žádné znaky bez kódování Unicode, Unicode atribut způsobí použití typu řetězce jiného typu než Unicode. Kromě toho MaxLength se používá k omezení velikosti sloupce databáze. Pokud například používáte SQL Server, výsledkem je sloupec varchar(22)databáze:

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

Poznámka

EF Core ve výchozím nastavení mapuje vlastnosti řetězců na sloupce Unicode. UnicodeAttribute je ignorována, pokud databázový systém podporuje pouze typy Unicode.

PrecisionAttribute

Problém Na GitHubu: #17914. Touto funkcí přispěl @RaymondHuy. Mnohokrát děkujeme!

Přesnost a škálování databázového sloupce je teď možné nakonfigurovat pomocí atributů mapování bez přímého zadání typu databáze. Představte si Product například typ entity s desetinnou Price vlastností:

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

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

EF Core namapuje tuto vlastnost na sloupec databáze s přesností 10 a škálováním 2. Například na SQL Serveru:

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

EntityTypeConfigurationAttribute

Problém Na GitHubu: #23163. Touto funkcí přispěl @KaloyanIT. Mnohokrát děkujeme!

IEntityTypeConfiguration<TEntity> Instance umožňují ModelBuilder konfiguraci pro každý typ entity obsažené ve vlastní třídě konfigurace. Příklad:

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

Za normálních okolností musí být tato třída konfigurace vytvořena a volána z DbContext.OnModelCreating. Příklad:

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

Od EF Core 6.0 je možné umístit typ entity tak, EntityTypeConfigurationAttribute aby EF Core mohl najít a použít odpovídající konfiguraci. Příklad:

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

Tento atribut znamená, že EF Core použije zadanou IEntityTypeConfiguration implementaci při každém Book zahrnutí typu entity do modelu. Typ entity je součástí modelu pomocí jednoho z normálních mechanismů. Například vytvořením DbSet<TEntity> vlastnosti pro typ entity:

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

    //...

Nebo ho zaregistrujete v OnModelCreating:

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

Poznámka

EntityTypeConfigurationAttribute typy nebudou automaticky zjištěny v sestavení. Typy entit musí být přidány do modelu před zjištěním atributu u tohoto typu entity.

Vylepšení vytváření modelů

Kromě nových atributů mapování obsahuje EF Core 6.0 několik dalších vylepšení procesu sestavování modelu.

Podpora řídkých sloupců SQL Serveru

Problém Na GitHubu: #8023.

Řídké sloupce SQL Serveru jsou běžné sloupce, které jsou optimalizované pro ukládání hodnot null. To může být užitečné při použití mapování dědičnosti TPH, kde vlastnosti zřídka používaného podtypu způsobí, že hodnoty sloupců null pro většinu řádků v tabulce. Představte si ForumModerator například třídu, která rozšiřuje:ForumUser

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

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

Můžou existovat miliony uživatelů, jen s několika z těchto moderátorů. To znamená, že mapování ForumName jako řídké by tady mohlo dávat smysl. Tato možnost je nyní možné nakonfigurovat pomocí funkce IsSparse OnModelCreating. Příklad:

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

Migrace EF Core pak označí sloupec jako zhuštěný. Příklad:

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]));

Poznámka

Řídké sloupce mají omezení. Nezapomeňte si přečíst dokumentaci ke řídkým sloupcům SQL Serveru, abyste měli jistotu, že jsou pro váš scénář nejvhodnější řídké sloupce.

Vylepšení rozhraní HasConversion API

Problém na GitHubu: #25468.

Před EF Core 6.0 obecné přetížení HasConversion metod pomocí obecného parametru určit typ, na který se má převést. Představte si Currency například výčet:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core lze nakonfigurovat tak, aby ukládaly hodnoty tohoto výčtu jako řetězce "UsDollars", "PoundsStirling" a "Euro" pomocí HasConversion<string>. Příklad:

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

Od EF Core 6.0 může obecný typ místo toho zadat typ převaděče hodnot. Může to být jeden z integrovaných převaděčů hodnot. Pokud chcete například uložit hodnoty výčtu jako 16bitová čísla v databázi:

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

Nebo to může být vlastní typ převaděče hodnot. Představte si například převaděč, který ukládá hodnoty výčtu jako symboly měny:

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)
    {
    }
}

Tuto možnost je teď možné nakonfigurovat pomocí obecné HasConversion metody:

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

Menší konfigurace relací M:N

Problém na GitHubu: #21535.

Konvence zjišťuje jednoznačné relace M:N mezi dvěma typy entit. V případě potřeby nebo v případě potřeby je možné navigace explicitně zadat. Příklad:

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

V obou těchto případech EF Core vytvoří sdílenou entitu založenou na Dictionary<string, object> tom, aby fungovala jako entita spojení mezi těmito dvěma typy. Od EF Core 6.0 UsingEntity je možné do konfigurace přidat pouze tento typ, aniž by bylo nutné provést další konfiguraci. Příklad:

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

Kromě toho může být typ entity spojení dodatečně nakonfigurovaný, aniž by bylo nutné explicitně zadávat relace vlevo a vpravo. Příklad:

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

A nakonec můžete zadat úplnou konfiguraci. Příklad:

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

Povolit převaděčům hodnot převést hodnoty null

Problém na GitHubu: #13850.

Důležité

Z důvodu níže uvedených problémů byly konstruktory umožňující ValueConverter převod hodnot null označeny [EntityFrameworkInternal] pro verzi EF Core 6.0. Použití těchto konstruktorů teď vygeneruje upozornění sestavení.

Převaděče hodnot obecně neumožňují převod hodnoty null na jinou hodnotu. Je to proto, že stejný převaděč hodnot lze použít pro typy s možnou hodnotou null i bez null, což je velmi užitečné pro kombinace PK/FK, kde FK je často nullable a PK není.

Počínaje EF Core 6.0 lze vytvořit převaděč hodnot, který převádí hodnoty null. Ověření této funkce však ukázalo, že je velmi problematické v praxi s mnoha nástrahami. Příklad:

Nejedná se o triviální problémy a u problémů s dotazy, které se nedají snadno rozpoznat. Proto jsme tuto funkci označili jako interní pro EF Core 6.0. Můžete ho dál používat, ale zobrazí se upozornění kompilátoru. Upozornění lze zakázat pomocí #pragma warning disable EF1001.

Jedním z příkladů, kdy převod hodnot null může být užitečný, je, když databáze obsahuje hodnoty null, ale typ entity chce pro vlastnost použít jinou výchozí hodnotu. Představte si například výčet, ve kterém je jeho výchozí hodnota "Unknown":

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Databáze však může mít hodnoty null, pokud je plemeno neznámé. V EF Core 6.0 lze k tomuto účtu použít převaděč hodnot:

    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
    }

Kočky s plemenem "Neznámý" budou mít ve Breed své databázi nastavenou hodnotu null. Příklad:

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();

Tím se vygenerují následující příkazy insert na SQL Serveru:

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();

Vylepšení objektu pro vytváření dbContext

AddDbContextFactory také zaregistruje DbContext přímo.

Problém na GitHubu: #25164.

Někdy je užitečné mít typ DbContext i továrnu pro kontexty tohoto typu zaregistrované v kontejneru injektáž závislostí aplikací (D.I.). To například umožňuje překlad rozsahu instance DbContext z oboru požadavku, zatímco továrna může být použita k vytvoření více nezávislých instancí v případě potřeby.

Aby to bylo podporováno, AddDbContextFactory zaregistruje teď také typ DbContext jako vymezenou službu. Zvažte například tuto registraci v kontejneru D.I. aplikace:

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

Při této registraci lze továrnu vyřešit z kořenového kontejneru D.I. stejně jako v předchozích verzích:

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

Mějte na paměti, že kontextové instance vytvořené továrnou musí být explicitně odstraněny.

Kromě toho lze instanci DbContext přeložit přímo z oboru kontejneru:

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

V tomto případě je instance kontextu uvolněna při odstranění oboru kontejneru; kontext by neměl být explicitně uvolněn.

Na vyšší úrovni to znamená, že buď DbContext továrny lze vložit do jiných typů D.I. Příklad:

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

Nebo:

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ů DbContext.

Problém na GitHubu: #24124.

EF Core 6.0 teď umožňuje jak konstruktor DbContext bez parametrů, tak konstruktor, který se použije DbContextOptions na stejném typu kontextu, když je továrna zaregistrována prostřednictvím AddDbContextFactory. Například kontext použitý v příkladech výše obsahuje oba konstruktory:

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

    public SomeDbContext(DbContextOptions<SomeDbContext> options)
        : base(options)
    {
    }

    public DbSet<Blog> Blogs { get; set; }
}

Sdružování DbContext je možné použít bez injektáže závislostí.

Problém GitHubu: #24137.

Typ PooledDbContextFactory byl zpřístupněn jako veřejný, aby ho bylo možné použít jako samostatný fond pro instance DbContext, aniž by vaše aplikace potřebovala kontejner injektáž závislostí. Fond se vytvoří s instancí DbContextOptions , která se použije k vytvoření kontextových instancí:

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

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

Objekt pro vytváření a sdružování pak lze použít k vytvoření a sdružování instancí. Příklad:

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

Instance se vrátí do fondu, když jsou uvolněny.

Různá vylepšení

A nakonec EF Core obsahuje několik vylepšení v oblastech, které nejsou popsané výše.

Při vytváření tabulek použijte [ColumnAttribute.Order]

Problém GitHubu: #10059.

Order Vlastnost ColumnAttribute teď můžete použít k seřazení sloupců při vytváření tabulky s migracemi. Představte si například následující 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; }
}

EF Core ve výchozím nastavení nejprve objednává sloupce primárního klíče podle vlastností typu entity a vlastněných typů a nakonec vlastností ze základních typů. Například na SQL Serveru se vytvoří následující tabulka:

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]));

V EF Core 6.0 ColumnAttribute lze použít k určení jiného pořadí sloupců. Příklad:

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

Na SQL Serveru je teď vygenerovaná tabulka:

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]));

Tím se přesunou FistName sloupce LastName do horní části, i když jsou definované v základním typu. Všimněte si, že hodnoty pořadí sloupců můžou mít mezery, což umožňuje, aby se rozsahy vždy umístily na konec, i když je používá více odvozených typů.

Tento příklad také ukazuje, jak je možné použít stejné ColumnAttribute k zadání názvu sloupce i pořadí.

Řazení sloupců lze nakonfigurovat také pomocí ModelBuilder rozhraní API v OnModelCreating. Příklad:

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

Řazení v tvůrci modelů s HasColumnOrder předností před libovolným pořadím zadaným pomocí ColumnAttribute. To znamená, že HasColumnOrder lze použít k přepsání řazení provedené s atributy, včetně řešení jakýchkoli konfliktů, když atributy na různých vlastnostech určují stejné číslo objednávky.

Důležité

Všimněte si, že v obecném případě většina databází podporuje řazení sloupců pouze při vytváření tabulky. To znamená, že atribut pořadí sloupců nelze použít k opětovnému uspořádání sloupců v existující tabulce. Jednou z hledných výjimek je SQLite, kdy migrace znovu sestaví celou tabulku s novými objednávkami sloupců.

Minimální rozhraní API EF Core

Problém Na GitHubu: #25192.

.NET Core 6.0 obsahuje aktualizované šablony, které zjednodušují "minimální rozhraní API", která v aplikacích .NET tradičně potřebují velké množství často používaného kódu.

EF Core 6.0 obsahuje novou metodu rozšíření, která zaregistruje typ DbContext a poskytuje konfiguraci pro zprostředkovatele databáze na jednom řádku. Příklad:

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==");

Přesně odpovídají těmto:

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;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);

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

Poznámka

Minimální rozhraní API EF Core podporují pouze velmi základní registraci a konfiguraci DbContext a poskytovatele. Pro přístup ke všem typům registrace a konfigurace, které jsou dostupné v EF Core, použijte AddDbContext, AddDbContextPoolAddDbContextFactoryatd.

Další informace o minimálníchrozhraních

Zachování kontextu synchronizace v SaveChangesAsync

Problém Na GitHubu: #23971.

Změnili jsme kód EF Core ve verzi 5.0 tak, aby se nastavil Task.ConfigureAwait na false všech místech, kde jsme await asynchronní kód. Obecně se jedná o lepší volbu pro použití EF Core. Jedná se ale o zvláštní případ, SaveChangesAsync protože EF Core po dokončení operace asynchronní databáze nastaví vygenerované hodnoty do sledovaných entit. Tyto změny pak můžou aktivovat oznámení, která například musí běžet ve vlákně USA. Proto tuto změnu vracíme pouze v EF Core 6.0 pro metodu SaveChangesAsync .

Databáze v paměti: Ověření požadovaných vlastností není null

Problém Na GitHubu: #10613. Touto funkcí přispěl @fagnercarvalho. Mnohokrát děkujeme!

Databáze EF Core v paměti teď vyvolá výjimku, pokud se pokusí uložit hodnotu null pro vlastnost označenou jako povinnou. Představte si User například typ s požadovanou Username vlastností:

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

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

Při pokusu o uložení entity s hodnotou null Username dojde k následující výjimce:

Microsoft.EntityFrameworkCore.DbUpdateException: Chybí požadované vlastnosti {'Username'} pro instanci typu entity User s hodnotou klíče {ID: 1}.

Toto ověření je možné v případě potřeby zakázat. Příklad:

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

Informace o zdroji příkazů pro diagnostiku a průsečíky

Problém Na GitHubu: #23719. Touto funkcí přispěl @Giorgi. Mnohokrát děkujeme!

Dodaný CommandEventData do diagnostických zdrojů a průsečíků teď obsahuje hodnotu výčtu označující, která část EF byla zodpovědná za vytvoření příkazu. To lze použít jako filtr v diagnostice nebo průsečíku. Můžeme například chtít průsečík, který se vztahuje pouze na příkazy, které pocházejí z SaveChanges:

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

        return result;
    }
}

Tím se průsečík vyfiltruje jenom SaveChanges na události, které se používají v aplikaci, které také generují migrace a dotazy. Příklad:

Saving changes for CustomersContext:

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

Lepší zpracování dočasných hodnot

Problém Na GitHubu: #24245.

EF Core nezpřístupňuje dočasné hodnoty u instancí typu entity. Představte si Blog například typ entity s klíčem vygenerovaným úložištěm:

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

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

Vlastnost Id klíče získá dočasnou hodnotu, jakmile Blog je sledován kontextem. Například při volání DbContext.Add:

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

Dočasnou hodnotu lze získat z sledování změn kontextu, ale není nastavena do instance entity. Například tento kód:

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 následující výstup:

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

To je dobré, protože brání dočasnému úniku hodnoty do kódu aplikace, kde může být omylem považována za ne dočasnou. Někdy je ale užitečné pracovat s dočasnými hodnotami přímo. Aplikace může například chtít před sledováním grafu entit vygenerovat vlastní dočasné hodnoty, aby je bylo možné použít k vytvoření relací pomocí cizích klíčů. To lze provést explicitním označením hodnot jako dočasných. Příklad:

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

V EF Core 6.0 zůstane hodnota v instanci entity, i když je nyní označena jako dočasná. Výše uvedený kód například vygeneruje následující výstup:

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

Stejně tak dočasné hodnoty vygenerované EF Core je možné explicitně nastavit na instance entit a označit je jako dočasné hodnoty. Dá se použít k explicitnímu nastavení relací mezi novými entitami pomocí jejich dočasných hodnot klíče. Příklad:

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

Výsledkem je:

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

EF Core opatřené poznámkami pro odkazové typy s možnou hodnotou null jazyka C#

Problém GitHubu: #19007.

Základ kódu EF Core teď používá odkazové typy s možnou hodnotou null (NRT) jazyka C#. To znamená, že při použití EF Core 6.0 z vlastního kódu získáte správné indikace kompilátoru pro použití s hodnotou null.

Microsoft.Data.Sqlite 6.0

Tip

Všechny níže uvedené ukázky můžete spustit a ladit stažením ukázkového kódu z GitHubu.

Sdružování připojení

Problém GitHubu: #13837.

Běžně se doporučuje udržovat připojení k databázi otevřená co nejmenší dobu. To pomáhá zabránit kolizím u prostředku připojení. To je důvod, proč knihovny, jako je EF Core, otevřou připojení bezprostředně před provedením operace databáze a okamžitě po ní zavřete. Představte si například tento kód 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.");
    }
}

Výstup z tohoto kódu se zapnutým protokolováním pro připojení je:

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.

Všimněte si, že připojení je otevřeno a uzavřeno rychle pro každou operaci.

U většiny databázových systémů je ale otevření fyzického připojení k databázi nákladnou operací. Většina poskytovatelů ADO.NET proto podle potřeby vytvoří fond fyzických připojení a pronajímá je instancím DbConnection .

SQLite se trochu liší, protože přístup k databázi obvykle jen přistupuje k souboru. To znamená, že otevření připojení k databázi SQLite je obvykle velmi rychlé. To ale není vždy případ. Otevření připojení k šifrované databázi může být například velmi pomalé. Proto jsou připojení SQLite nyní ve fondu při použití Microsoft.Data.Sqlite 6.0.

Podpora DateOnly a TimeOnly

Problém GitHubu: #24506.

Microsoft.Data.Sqlite 6.0 podporuje nové DateOnly a TimeOnly typy z .NET 6. Můžete je také použít v EF Core 6.0 s poskytovatelem SQLite. Stejně jako vždy u SQLite znamená jeho nativní systém typů, že hodnoty z těchto typů musí být uloženy jako jeden ze čtyř podporovaných typů. Microsoft.Data.Sqlite je ukládá jako TEXT. Například entita používající tyto typy:

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 následující tabulky v databázi 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);

Hodnoty se pak dají uložit, dotazovat a aktualizovat běžným způsobem. Například tento dotaz LINQ EF Core:

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

Na SQLite se přeloží na následující:

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

A vrátí se pouze s narozeninami před 1900 CE:

Found 'ajcvickers'
Found 'wendy'

Rozhraní API pro ukládání bodů

Problém Na GitHubu: #20228.

Standardizovali jsme běžné rozhraní API pro šetřovací body ve ADO.NET poskytovatelích. Microsoft.Data.Sqlite teď podporuje toto rozhraní API, včetně:

Použití bodu uložení umožňuje, aby se část transakce vrátila zpět bez vrácení celé transakce zpět. Například následující kód:

  • Vytvoří transakci.
  • Odešle aktualizaci do databáze.
  • Vytvoří bod uložení.
  • Odešle do databáze další aktualizaci.
  • Vrátí se zpět k dříve vytvořenému bodu uložení.
  • Potvrdí transakci.
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();

Výsledkem bude potvrzení první aktualizace do databáze, zatímco druhá aktualizace není potvrzena, protože bod uložení byl vrácen zpět před potvrzením transakce.

Časový limit příkazu v připojovací řetězec

Problém GitHubu: #22505. Touto funkcí přispěl @nmichels. Mnohokrát děkujeme!

poskytovatelé ADO.NET podporují dva různé časové limity:

  • Časový limit připojení, který určuje maximální dobu čekání při připojení k databázi.
  • Časový limit příkazu, který určuje maximální dobu čekání na dokončení provádění příkazu.

Časový limit příkazu lze nastavit z kódu pomocí DbCommand.CommandTimeout. V připojovací řetězec teď také vystavuje časový limit tohoto příkazu. Microsoft.Data.Sqlite sleduje tento trend s klíčovým slovem Command Timeout připojovací řetězec. Například "Command Timeout=60;DataSource=test.db" jako výchozí časový limit pro příkazy vytvořené připojením použijete 60 sekund.

Tip

Sqlite považuje Default Timeout za synonymum, Command Timeout a proto je možné místo toho použít, pokud dáváte přednost.