Novinky v EF Core 9

EF Core 9 (EF9) je příští verze po EF Core 8 a plánuje se vydat v listopadu 2024. Podrobnosti najdete v tématu Plán pro Entity Framework Core 9 .

EF9 je k dispozici jako denní buildy , které obsahují všechny nejnovější funkce EF9 a vylepšení rozhraní API. Zde uvedené ukázky využívají tyto denní buildy.

Tip

Ukázky můžete spustit a ladit stažením ukázkového kódu z GitHubu. Každá část níže odkazuje na zdrojový kód specifický pro tento oddíl.

EF9 cílí na .NET 8, a proto se dá použít s .NET 8 (LTS) nebo .NET 9 Preview.

Tip

Dokumentace Co je nového se aktualizuje pro jednotlivé verze Preview. Všechny ukázky jsou nastavené tak, aby používaly denní buildy EF9, které mají v porovnání s nejnovější verzí Preview obvykle několik dalších týdnů dokončené práce. Důrazně doporučujeme používat denní buildy při testování nových funkcí, abyste neprovádí testování proti zastaralým bitům.

Překlad LINQ a SQL

Tým pracuje na několika důležitých změnách architektury kanálu dotazů v EF Core 9 v rámci našich trvalých vylepšení mapování JSON a databází dokumentů. To znamená, že potřebujeme, aby lidé, jako jste vy spustili svůj kód, na těchto nových interních prostředcích. (Pokud v tomto okamžiku v této verzi čtete dokument "Co je nového", pak jste opravdu zapojeni do komunity. Děkujeme vám!) Máme více než 120 000 testů, ale nestačí! Potřebujeme vás, lidi, kteří na našich bitech spouští skutečný kód, aby našli problémy a mohli odeslat solidní verzi.

Vyřazení sloupců předaných klauzuli OPENJSON WITH

Tip

Zde uvedený kód pochází z JsonColumnsSample.cs.

EF9 odebere nepotřebné sloupce při volání OPENJSON WITH. Představte si například dotaz, který získá počet z kolekce JSON pomocí predikátu:

var postsUpdatedOn = await context.Posts
    .Where(p => p.Metadata!.Updates.Count(e => e.UpdatedOn >= date) == 1)
    .ToListAsync();

V EF8 tento dotaz vygeneruje následující SQL při použití poskytovatele databáze Azure SQL:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedBy] nvarchar(max) '$.UpdatedBy',
        [UpdatedOn] date '$.UpdatedOn',
        [Commits] nvarchar(max) '$.Commits' AS JSON
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

Všimněte si, že v UpdatedBytomto dotazu nejsou potřebné a Commits nejsou potřeba. Od EF9 jsou teď tyto sloupce vyřazené:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Metadata], '$.Updates') WITH (
        [PostedFrom] nvarchar(45) '$.PostedFrom',
        [UpdatedOn] date '$.UpdatedOn'
    ) AS [u]
    WHERE [u].[UpdatedOn] >= @__date_0) = 1

V některých scénářích to vede k úplnému WITH odebrání klauzule. Příklad:

var tagsWithCount = await context.Tags.Where(p => p.Text.Length == 1).ToListAsync();

V EF8 se tento dotaz přeloží na následující SQL:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) WITH ([value] nvarchar(max) '$') AS [t0]) = 1

V EF9 jsme vylepšili následující možnosti:

SELECT [t].[Id], [t].[Text]
FROM [Tags] AS [t]
WHERE (
    SELECT COUNT(*)
    FROM OPENJSON([t].[Text]) AS [t0]) = 1

Překlady zahrnující GREATEST/LEAST

Tip

Zde uvedený kód pochází z LeastGreatestSample.cs.

Zavedlo se několik nových překladů, které používají GREATEST funkce SQL.LEAST

Důležité

Funkce GREATEST a LEAST funkce , které byly zavedeny do databází SQL Serveru nebo Azure SQL ve verzi 2022. Visual Studio 2022 ve výchozím nastavení nainstaluje SQL Server 2019. Doporučujeme nainstalovat SQL Server Developer Edition 2022 vyzkoušet tyto nové překlady v EF9.

Například dotazy používající Math.Max nebo Math.Min se teď překládají pro Azure SQL pomocí GREATEST a LEAST v uvedeném pořadí. Příklad:

var walksUsingMin = await context.Walks
    .Where(e => Math.Min(e.DaysVisited.Count, e.ClosestPub.Beers.Length) > 4)
    .ToListAsync();

Tento dotaz se při spouštění EF9 na SQL Server 2022 přeloží do následujícího SQL Serveru:

SELECT [w].[Id], [w].[ClosestPubId], [w].[DaysVisited], [w].[Name], [w].[Terrain]
FROM [Walks] AS [w]
INNER JOIN [Pubs] AS [p] ON [w].[ClosestPubId] = [p].[Id]
WHERE LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([w].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b])) >

Math.Min a Math.Max lze je také použít pro hodnoty primitivní kolekce. Příklad:

var pubsInlineMax = await context.Pubs
    .SelectMany(e => e.Counts)
    .Where(e => Math.Max(e, threshold) > top)
    .ToListAsync();

Tento dotaz se při spouštění EF9 na SQL Server 2022 přeloží do následujícího SQL Serveru:

SELECT [c].[value]
FROM [Pubs] AS [p]
CROSS APPLY OPENJSON([p].[Counts]) WITH ([value] int '$') AS [c]
WHERE GREATEST([c].[value], @__threshold_0) > @__top_1

RelationalDbFunctionsExtensions.Least Nakonec a RelationalDbFunctionsExtensions.Greatest lze ji použít k přímému Least vyvolání nebo Greatest funkce v SQL. Příklad:

var leastCount = await context.Pubs
    .Select(e => EF.Functions.Least(e.Counts.Length, e.DaysVisited.Count, e.Beers.Length))
    .ToListAsync();

Tento dotaz se při spouštění EF9 na SQL Server 2022 přeloží do následujícího SQL Serveru:

SELECT LEAST((
    SELECT COUNT(*)
    FROM OPENJSON([p].[Counts]) AS [c]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[DaysVisited]) AS [d]), (
    SELECT COUNT(*)
    FROM OPENJSON([p].[Beers]) AS [b]))
FROM [Pubs] AS [p]

Vynucení nebo zabránění parametrizaci dotazů

Tip

Zde uvedený kód pochází z QuerySample.cs.

Kromě některých speciálních případů EF Core parametrizuje proměnné použité v dotazu LINQ, ale zahrnuje konstanty ve vygenerovaném SQL. Představte si například následující metodu dotazu:

async Task<List<Post>> GetPosts(int id)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && e.Id == id)
        .ToListAsync();

To se při použití Azure SQL přeloží na následující sql a parametry:

info: 2/5/2024 15:43:13.789 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (1ms) [Parameters=[@__id_0='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
      FROM [Posts] AS [p]
      WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = @__id_0

Všimněte si, že EF vytvořil konstantu v SQL pro ".NET Blog", protože tato hodnota se nezmění z dotazu na dotaz. Použití konstanty umožňuje, aby tato hodnota byla zkoumána databázovým strojem při vytváření plánu dotazů, což může mít za následek efektivnější dotaz.

Na druhou stranu je hodnota id parametrizována, protože stejný dotaz může být proveden s mnoha různými hodnotami pro id. Výsledkem vytvoření konstanty v tomto případě je znečištění mezipaměti dotazů s velkým množstvím dotazů, které se liší pouze v hodnotách parametrů. To je velmi špatné pro celkový výkon databáze.

Obecně řečeno, tyto výchozí hodnoty by neměly být změněny. EF Core 8.0.2 však zavádí metodu EF.Constant , která vynutí ef použití konstanty i v případě, že by byl parametr použit ve výchozím nastavení. Příklad:

async Task<List<Post>> GetPostsForceConstant(int id)
    => await context.Posts
        .Where(
            e => e.Title == ".NET Blog" && e.Id == EF.Constant(id))
        .ToListAsync();

Překlad teď obsahuje konstantu id pro hodnotu:

info: 2/5/2024 15:43:13.812 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed DbCommand (1ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
      FROM [Posts] AS [p]
      WHERE [p].[Title] = N'.NET Blog' AND [p].[Id] = 1

EF9 zavádí metodu EF.Parameter , která provede opačnou akci. To znamená, že ef vynutit použití parametru i v případě, že hodnota je konstanta v kódu. Příklad:

async Task<List<Post>> GetPostsForceParameter(int id)
    => await context.Posts
        .Where(
            e => e.Title == EF.Parameter(".NET Blog") && e.Id == id)
        .ToListAsync();

Překlad teď obsahuje parametr pro řetězec ".NET Blog":

info: 2/5/2024 15:43:13.803 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@__p_0='.NET Blog' (Size = 4000), @__id_1='1'], CommandType='Text', CommandTimeout='30']
      SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
      FROM [Posts] AS [p]
      WHERE [p].[Title] = @__p_0 AND [p].[Id] = @__id_1

Vložené nesouvisející poddotazy

Tip

Zde uvedený kód pochází z QuerySample.cs.

V EF8 je možné spustit příkaz IQueryable odkazovaný v jiném dotazu jako samostatnou odezvu databáze. Představte si například následující dotaz LINQ:

var dotnetPosts = context
    .Posts
    .Where(p => p.Title.Contains(".NET"));

var results = dotnetPosts
    .Where(p => p.Id > 2)
    .Select(p => new { Post = p, TotalCount = dotnetPosts.Count() })
    .Skip(2).Take(10)
    .ToArray();

V EF8 se dotaz provede dotnetPosts jako jedna odezva a konečné výsledky se spustí jako druhý dotaz. Například na SQL Serveru:

SELECT COUNT(*)
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%'

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata]
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_1 ROWS FETCH NEXT @__p_2 ROWS ONLY

V EF9 IQueryable se inlinuje, dotnetPosts což vede k jedné zpáteční cestě:

SELECT [p].[Id], [p].[Archived], [p].[AuthorId], [p].[BlogId], [p].[Content], [p].[Discriminator], [p].[PublishedOn], [p].[Title], [p].[PromoText], [p].[Metadata], (
    SELECT COUNT(*)
    FROM [Posts] AS [p0]
    WHERE [p0].[Title] LIKE N'%.NET%')
FROM [Posts] AS [p]
WHERE [p].[Title] LIKE N'%.NET%' AND [p].[Id] > 2
ORDER BY (SELECT 1)
OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY

Nové ToHashSetAsync<T> metody

Tip

Zde uvedený kód pochází z QuerySample.cs.

Metody Enumerable.ToHashSet existovaly od verze .NET Core 2.0. V EF9 byly přidány ekvivalentní asynchronní metody. Příklad:

var set1 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync();

var set2 = await context.Posts
    .Where(p => p.Tags.Count > 3)
    .ToHashSetAsync(ReferenceEqualityComparer.Instance);

Tímto vylepšením přispěl @wertzui. Mnohokrát děkujeme!

ExecuteUpdate a ExecuteDelete

Povolit předávání instancí komplexního typu do ExecuteUpdate

Tip

Zde uvedený kód pochází z ExecuteUpdateSample.cs.

Rozhraní ExecuteUpdate API bylo zavedeno v EF7 pro okamžité přímé aktualizace databáze bez sledování nebo SaveChanges. Příklad:

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.Region, "Deutschland"));

Spuštěním tohoto kódu se spustí následující dotaz, který aktualizuje Region hodnotu "Deutschland":

UPDATE [s]
SET [s].[Region] = N'Deutschland'
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

V EF8 ExecuteUpdate lze také použít k aktualizaci hodnot komplexních vlastností typu. Každý člen komplexního typu však musí být zadán explicitně. Příklad:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Deutschland")
    .ExecuteUpdateAsync(
        s => s.SetProperty(b => b.StoreAddress.Line1, newAddress.Line1)
            .SetProperty(b => b.StoreAddress.Line2, newAddress.Line2)
            .SetProperty(b => b.StoreAddress.City, newAddress.City)
            .SetProperty(b => b.StoreAddress.Country, newAddress.Country)
            .SetProperty(b => b.StoreAddress.PostCode, newAddress.PostCode));

Spuštěním tohoto kódu se spustí následující dotaz:

UPDATE [s]
SET [s].[StoreAddress_PostCode] = @__newAddress_PostCode_4,
    [s].[StoreAddress_Country] = @__newAddress_Country_3,
    [s].[StoreAddress_City] = @__newAddress_City_2,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_Line1] = @__newAddress_Line1_0
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Deutschland'

V EF9 lze stejnou aktualizaci provést předáním samotné instance komplexního typu. To znamená, že každý člen nemusí být explicitně určen. Příklad:

var newAddress = new Address("Gressenhall Farm Shop", null, "Beetley", "Norfolk", "NR20 4DR");

await context.Stores
    .Where(e => e.Region == "Germany")
    .ExecuteUpdateAsync(s => s.SetProperty(b => b.StoreAddress, newAddress));

Spuštěním tohoto kódu se spustí stejný dotaz jako v předchozím příkladu:

UPDATE [s]
SET [s].[StoreAddress_City] = @__complex_type_newAddress_0_City,
    [s].[StoreAddress_Country] = @__complex_type_newAddress_0_Country,
    [s].[StoreAddress_Line1] = @__complex_type_newAddress_0_Line1,
    [s].[StoreAddress_Line2] = NULL,
    [s].[StoreAddress_PostCode] = @__complex_type_newAddress_0_PostCode
FROM [Stores] AS [s]
WHERE [s].[Region] = N'Germany'

Více aktualizací komplexních vlastností typu i jednoduchých vlastností lze kombinovat v jednom volání ExecuteUpdate. Příklad:

await context.Customers
    .Where(e => e.Name == name)
    .ExecuteUpdateAsync(
        s => s.SetProperty(
                b => b.CustomerInfo.WorkAddress, new Address("Gressenhall Workhouse", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.HomeAddress, new Address("Gressenhall Farm", null, "Beetley", "Norfolk", "NR20 4DR"))
            .SetProperty(b => b.CustomerInfo.Tag, "Tog"));

Spuštěním tohoto kódu se spustí stejný dotaz jako v předchozím příkladu:

UPDATE [c]
SET [c].[CustomerInfo_Tag] = N'Tog',
    [c].[CustomerInfo_HomeAddress_City] = N'Beetley',
    [c].[CustomerInfo_HomeAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_HomeAddress_Line1] = N'Gressenhall Farm',
    [c].[CustomerInfo_HomeAddress_Line2] = NULL,
    [c].[CustomerInfo_HomeAddress_PostCode] = N'NR20 4DR',
    [c].[CustomerInfo_WorkAddress_City] = N'Beetley',
    [c].[CustomerInfo_WorkAddress_Country] = N'Norfolk',
    [c].[CustomerInfo_WorkAddress_Line1] = N'Gressenhall Workhouse',
    [c].[CustomerInfo_WorkAddress_Line2] = NULL,
    [c].[CustomerInfo_WorkAddress_PostCode] = N'NR20 4DR'
FROM [Customers] AS [c]
WHERE [c].[Name] = @__name_0

Migrace

Vylepšené dočasné migrace tabulek

Migrace vytvořená při změně existující tabulky na dočasnou tabulku byla pro EF9 zmenšena. Například v EF8 vytvoření jedné existující tabulky dočasnou tabulkou vznikne následující migrace:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "SiteUri",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<string>(
        name: "Name",
        table: "Blogs",
        type: "nvarchar(max)",
        nullable: false,
        oldClrType: typeof(string),
        oldType: "nvarchar(max)")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AlterColumn<int>(
        name: "Id",
        table: "Blogs",
        type: "int",
        nullable: false,
        oldClrType: typeof(int),
        oldType: "int")
        .Annotation("SqlServer:Identity", "1, 1")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart")
        .OldAnnotation("SqlServer:Identity", "1, 1");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");
}

V EF9 teď stejná operace vede k mnohem menší migraci:

protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.AlterTable(
        name: "Blogs")
        .Annotation("SqlServer:IsTemporal", true)
        .Annotation("SqlServer:TemporalHistoryTableName", "BlogsHistory")
        .Annotation("SqlServer:TemporalHistoryTableSchema", null)
        .Annotation("SqlServer:TemporalPeriodEndColumnName", "PeriodEnd")
        .Annotation("SqlServer:TemporalPeriodStartColumnName", "PeriodStart");

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodEnd",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodEndColumn", true);

    migrationBuilder.AddColumn<DateTime>(
        name: "PeriodStart",
        table: "Blogs",
        type: "datetime2",
        nullable: false,
        defaultValue: new DateTime(1, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified))
        .Annotation("SqlServer:TemporalIsPeriodStartColumn", true);
}

Vytváření modelů

Určení ukládání do mezipaměti pro sekvence

Tip

Zde uvedený kód pochází z ModelBuildingSample.cs.

EF9 umožňuje nastavit možnosti ukládání do mezipaměti pro sekvence databáze pro libovolného zprostředkovatele relační databáze, který to podporuje. Můžete například UseCache použít k explicitní zapnutí ukládání do mezipaměti a nastavení velikosti mezipaměti:

modelBuilder.HasSequence<int>("MyCachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseCache(3);

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

CREATE SEQUENCE [MyCachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE CACHE 3;

Podobně explicitně UseNoCache vypne ukládání do mezipaměti:

modelBuilder.HasSequence<int>("MyUncachedSequence")
    .HasMin(10).HasMax(255000)
    .IsCyclic()
    .StartsAt(11).IncrementsBy(2)
    .UseNoCache();
CREATE SEQUENCE [MyUncachedSequence] AS int START WITH 11 INCREMENT BY 2 MINVALUE 10 MAXVALUE 255000 CYCLE NO CACHE;

Pokud ani UseCache není volána, UseNoCache není zadáno ukládání do mezipaměti a databáze bude používat cokoli, co je výchozí. To může být jiné výchozí nastavení pro různé databáze.

Tímto vylepšením přispěl @bikbov. Mnohokrát děkujeme!

Určení faktoru fill-factor pro klíče a indexy

Tip

Zde uvedený kód pochází z ModelBuildingSample.cs.

EF9 podporuje specifikaci výplňového faktoru SQL Serveru při použití migrací EF Core k vytváření klíčů a indexů. Z dokumentace k SQL Serveru: Při vytvoření nebo opětovném vytvoření indexu určuje hodnota faktoru výplně procento mezery na každé stránce na úrovni listu, která se mají vyplnit daty, a zbytek na každé stránce si zarezervuje jako volné místo pro budoucí růst."

Výplňový faktor lze nastavit pro jeden nebo složený primární a alternativní klíč a indexy. Příklad:

modelBuilder.Entity<User>()
    .HasKey(e => e.Id)
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasAlternateKey(e => new { e.Region, e.Ssn })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Name })
    .HasFillFactor(80);

modelBuilder.Entity<User>()
    .HasIndex(e => new { e.Region, e.Tag })
    .HasFillFactor(80);

Při použití u existujících tabulek se tím změní tabulky na výplň na omezení:

ALTER TABLE [User] DROP CONSTRAINT [AK_User_Region_Ssn];
ALTER TABLE [User] DROP CONSTRAINT [PK_User];
DROP INDEX [IX_User_Name] ON [User];
DROP INDEX [IX_User_Region_Tag] ON [User];

ALTER TABLE [User] ADD CONSTRAINT [AK_User_Region_Ssn] UNIQUE ([Region], [Ssn]) WITH (FILLFACTOR = 80);
ALTER TABLE [User] ADD CONSTRAINT [PK_User] PRIMARY KEY ([Id]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Name] ON [User] ([Name]) WITH (FILLFACTOR = 80);
CREATE INDEX [IX_User_Region_Tag] ON [User] ([Region], [Tag]) WITH (FILLFACTOR = 80);

Poznámka:

Ve verzi Preview 2 je v současné době chyba, kdy při prvním vytvoření tabulky nejsou zahrnuté faktory výplně. Tento problém sleduje problém č. 33269

Tímto vylepšením přispěl @deano-hunter. Mnohokrát děkujeme!

Větší rozšiřitelnost stávajících konvencí vytváření modelů

Tip

Zde uvedený kód pochází z CustomConventionsSample.cs.

Zásady vytváření veřejných modelů pro aplikace byly zavedeny v EF7. V EF9 jsme usnadnili rozšíření některých stávajících konvencí. Kód pro mapování vlastností podle atributu v EF7 je například tento:

public class AttributeBasedPropertyDiscoveryConvention : PropertyDiscoveryConvention
{
    public AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
        : base(dependencies)
    {
    }

    public override void ProcessEntityTypeAdded(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionContext<IConventionEntityTypeBuilder> context)
        => Process(entityTypeBuilder);

    public override void ProcessEntityTypeBaseTypeChanged(
        IConventionEntityTypeBuilder entityTypeBuilder,
        IConventionEntityType? newBaseType,
        IConventionEntityType? oldBaseType,
        IConventionContext<IConventionEntityType> context)
    {
        if ((newBaseType == null
             || oldBaseType != null)
            && entityTypeBuilder.Metadata.BaseType == newBaseType)
        {
            Process(entityTypeBuilder);
        }
    }

    private void Process(IConventionEntityTypeBuilder entityTypeBuilder)
    {
        foreach (var memberInfo in GetRuntimeMembers())
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                entityTypeBuilder.Property(memberInfo);
            }
            else if (memberInfo is PropertyInfo propertyInfo
                     && Dependencies.TypeMappingSource.FindMapping(propertyInfo) != null)
            {
                entityTypeBuilder.Ignore(propertyInfo.Name);
            }
        }

        IEnumerable<MemberInfo> GetRuntimeMembers()
        {
            var clrType = entityTypeBuilder.Metadata.ClrType;

            foreach (var property in clrType.GetRuntimeProperties()
                         .Where(p => p.GetMethod != null && !p.GetMethod.IsStatic))
            {
                yield return property;
            }

            foreach (var property in clrType.GetRuntimeFields())
            {
                yield return property;
            }
        }
    }
}

V EF9 je to možné zjednodušit takto:

public class AttributeBasedPropertyDiscoveryConvention(ProviderConventionSetBuilderDependencies dependencies)
    : PropertyDiscoveryConvention(dependencies)
{
    protected override bool IsCandidatePrimitiveProperty(
        MemberInfo memberInfo, IConventionTypeBase structuralType, out CoreTypeMapping? mapping)
    {
        if (base.IsCandidatePrimitiveProperty(memberInfo, structuralType, out mapping))
        {
            if (Attribute.IsDefined(memberInfo, typeof(PersistAttribute), inherit: true))
            {
                return true;
            }

            structuralType.Builder.Ignore(memberInfo.Name);
        }

        mapping = null;
        return false;
    }
}

Aktualizace ApplyConfigurationsFromAssembly pro volání neveřejných konstruktorů

Vpředchozíchch ApplyConfigurationsFromAssembly V EF9 jsme vylepšili chybové zprávy vygenerované v případě selhání a také povolili vytváření instancí nepřístupnou konstruktorem. To je užitečné při spolulokaci konfigurace v privátní vnořené třídě, která by nikdy neměla být vytvořena instancí kódu aplikace. Příklad:

public class Country
{
    public int Code { get; set; }
    public required string Name { get; set; }

    private class FooConfiguration : IEntityTypeConfiguration<Country>
    {
        private FooConfiguration()
        {
        }

        public void Configure(EntityTypeBuilder<Country> builder)
        {
            builder.HasKey(e => e.Code);
        }
    }
}

Kromě toho si někteří lidé myslí, že tento vzor je obominace, protože spojuje typ entity s konfigurací. Jiní lidé si myslí, že je velmi užitečné, protože společně vyhledá konfiguraci s typem entity. Nediskutujme o tom tady. :-)

SQL Server HierarchyId

Tip

Zde uvedený kód pochází z HierarchyIdSample.cs.

Generování cesty Sugar for HierarchyId

V EF8 byla přidána podpora první třídy pro typ SQL ServeruHierarchyId. V EF9 byla přidána metoda cukru, která usnadňuje vytváření nových podřízených uzlů ve stromové struktuře. Například následující kód se dotazuje na existující entitu HierarchyId s vlastností:

var daisy = await context.Halflings.SingleAsync(e => e.Name == "Daisy");

Tuto HierarchyId vlastnost lze pak použít k vytvoření podřízených uzlů bez explicitní manipulace s řetězci. Příklad:

var child1 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1), "Toast");
var child2 = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 2), "Wills");

Pokud daisy má hodnotu HierarchyId/4/1/3/1/, child1 získáte HierarchyId hodnotu /4/1/3/1/1/" a child2 získá HierarchyId hodnotu /4/1/3/1/2/.

K vytvoření uzlu mezi těmito dvěma podřízenými objekty je možné použít další dílčí úroveň. Příklad:

var child1b = new Halfling(HierarchyId.Parse(daisy.PathFromPatriarch, 1, 5), "Toast");

Tím se vytvoří uzel s hodnotou HierarchyId/4/1/3/1/1.5/, umístěním bteween child1 a child2.

Tímto vylepšením přispěl @Rezakazemi890. Mnohokrát děkujeme!

Nástroje

Méně opětovného sestavení

Nástroj dotnet ef příkazového řádku ve výchozím nastavení sestaví projekt před spuštěním nástroje. Důvodem je to, že před spuštěním nástroje není opětovné sestavení, což je běžný zdroj nejasností, když věci nefungují. Zkušení vývojáři můžou použít --no-build možnost vyhnout se tomuto sestavení, což může být pomalé. I tato --no-build možnost ale může způsobit opětovné sestavení projektu při příštím sestavení mimo nástroje EF.

Věříme, že příspěvek komunity od @Suchiman to vyřešil. Uvědomujeme si ale také, že vylepšení chování nástroje MSBuild mají tendenci mít nezamýšlené důsledky, takže žádáme lidi, jako byste to vyzkoušeli, a hlásíme zpět všechny negativní zkušenosti, které máte.