EF Core 9'daki Yenilikler

EF Core 9 (EF9), EF Core 8'den sonraki sürümdür ve Kasım 2024'te kullanıma sunulacaktır. Ayrıntılar için bkz . Entity Framework Core 9 planı.

EF9, en son EF9 özelliklerini ve API ince ayarlarını içeren günlük derlemeler olarak kullanılabilir. Buradaki örneklerde bu günlük derlemeler kullanılır.

İpucu

GitHub'dan örnek kodu indirerek örnekleri çalıştırabilir ve hata ayıklayabilirsiniz. Aşağıdaki her bölüm, o bölüme özgü kaynak koduna bağlanır.

EF9, .NET 8'i hedefler ve bu nedenle .NET 8 (LTS) veya .NET 9 önizlemesi ile kullanılabilir.

İpucu

Yenilikler belgeleri her önizleme için güncelleştirilir. Tüm örnekler ef9 günlük derlemelerini kullanacak şekilde ayarlanmıştır ve bu derlemeler genellikle en son önizlemeye kıyasla birkaç hafta daha tamamlanan çalışmaya sahiptir. Testinizi eski bitlere karşı yapmamanız için yeni özellikleri test ederken günlük derlemelerin kullanılmasını kesinlikle öneririz.

LINQ ve SQL çevirisi

Ekip, JSON eşleme ve belge veritabanlarında sürekli iyileştirmelerimizin bir parçası olarak EF Core 9'daki sorgu işlem hattında bazı önemli mimari değişiklikleri üzerinde çalışmaktadır. Bu, sizin gibi kişilerin kodunuzu bu yeni iç bileşenlerde çalıştırmasını istediğimiz anlamına gelir. (Yayının bu noktasında "Yenilikler" belgesini okuyorsanız topluluğun gerçekten meşgul bir parçasısınızdır; teşekkür ederiz!) 120.000'den fazla testimiz var ama yeterli değil! Sorunları bulmak ve sağlam bir sürüm göndermek için bitlerimizde gerçek kod çalıştıran kişilere ihtiyacımız var!

OPENJSON'un WITH yan tümcesine geçirilen sütunları ayıklama

İpucu

Burada gösterilen kod JsonColumnsSample.cs gelir.

EF9 çağrılırken OPENJSON WITHgereksiz sütunları kaldırır. Örneğin, bir koşul kullanarak bir JSON koleksiyonundan sayı elde eden bir sorgu düşünün:

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

EF8'de bu sorgu, Azure SQL veritabanı sağlayıcısını kullanırken aşağıdaki SQL'i oluşturur:

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

ve değerinin UpdatedByCommits bu sorguda gerekli olmadığını fark edin. EF9'dan başlayarak, bu sütunlar artık budanır:

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

Bazı senaryolarda bu durum yan tümcesinin tamamen kaldırılmasına WITH neden olur. Örneğin:

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

EF8'de bu sorgu aşağıdaki SQL'e çevrilir:

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

EF9'da bu, şu şekilde geliştirilmiştir:

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

GREATEST/LEAST içeren çeviriler

İpucu

Burada gösterilen kod LeastGreatestSample.cs gelir.

ve LEAST SQL işlevlerini kullanan GREATEST birkaç yeni çeviri kullanıma sunulmuştur.

Önemli

GREATEST 2022 sürümünde SQL Server/Azure SQL veritabanlarına sunulan ve LEAST işlevleri. Visual Studio 2022 varsayılan olarak SQL Server 2019'u yükler. BU yeni çevirileri EF9'da denemek için SQL Server Developer Edition 2022'yi yüklemenizi öneririz.

Örneğin, veya Math.Min kullanan Math.Max sorgular artık sırasıyla ve LEAST kullanılarak GREATEST Azure SQL için çevrilir. Örneğin:

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

BU sorgu, SQL Server 2022'de YÜRÜTÜLEN EF9 kullanılırken aşağıdaki SQL'e çevrilir:

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 ve Math.Max ilkel bir koleksiyonun değerleri üzerinde de kullanılabilir. Örneğin:

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

BU sorgu, SQL Server 2022'de YÜRÜTÜLEN EF9 kullanılırken aşağıdaki SQL'e çevrilir:

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 Son olarak ve RelationalDbFunctionsExtensions.Greatest doğrudan SQL'de veya Greatest işlevini çağırmak Least için kullanılabilir. Örneğin:

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

BU sorgu, SQL Server 2022'de YÜRÜTÜLEN EF9 kullanılırken aşağıdaki SQL'e çevrilir:

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]

Sorgu parametreleştirmesini zorlama veya engelleme

İpucu

Burada gösterilen kod QuerySample.cs gelir.

Bazı özel durumlar dışında EF Core, LINQ sorgusunda kullanılan değişkenleri parametreleştirir, ancak oluşturulan SQL'deki sabitleri içerir. Örneğin, aşağıdaki sorgu yöntemini göz önünde bulundurun:

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

Bu, Azure SQL kullanılırken aşağıdaki SQL'e ve parametrelere çevrilir:

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

Bu değer sorgudan sorguya değişmeyecek olduğundan EF'nin SQL'de ".NET Blogu" için bir sabit oluşturduğuna dikkat edin. Sabit kullanmak, sorgu planı oluşturulurken bu değerin veritabanı altyapısı tarafından incelenmesine olanak tanır ve bu da daha verimli bir sorguya neden olabilir.

Öte yandan, aynı sorgu için idbirçok farklı değerle id yürütülebileceğinden değeri parametresizleştirilir. Bu durumda bir sabit oluşturmak, yalnızca parametre değerlerinde farklılık gösteren çok sayıda sorguyla sorgu önbelleğinin kirlenmesine neden olur. Bu, veritabanının genel performansı için çok kötüdür.

Genel olarak bakıldığında, bu varsayılanlar değiştirilmemelidir. Ancak EF Core 8.0.2, varsayılan olarak bir EF.Constant parametre kullanılsa bile EF'yi sabit kullanmaya zorlayan bir yöntem ekler. Örneğin:

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

Çeviri artık değer için id bir sabit içerir:

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, tersini EF.Parameter yapmak için yöntemini tanıtır. Başka bir ifadeyle, değer kodda sabit olsa bile EF'yi parametre kullanmaya zorlar. Örneğin:

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

Çeviri artık ".NET Blogu" dizesi için bir parametre içeriyor:

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

Sıralı olmayan alt sorgular

İpucu

Burada gösterilen kod QuerySample.cs gelir.

EF8'de, başka bir sorguda başvurulan bir IQueryable ayrı bir veritabanı gidiş dönüş olarak yürütülebilir. Örneğin, aşağıdaki LINQ sorgusunu göz önünde bulundurun:

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

EF8'de sorgusu dotnetPosts tek gidiş dönüş olarak yürütülür ve son sonuçlar ikinci sorgu olarak yürütülür. Örneğin, SQL Server'da:

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

EF9'dadotnetPosts, IQueryable içindekiler tek bir gidiş dönüşle sonuçlanır:

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

Yeni ToHashSetAsync<T> yöntemler

İpucu

Burada gösterilen kod QuerySample.cs gelir.

Enumerable.ToHashSet.NET Core 2.0'dan bu yana yöntemler mevcut. EF9'da eşdeğer zaman uyumsuz yöntemler eklenmiştir. Örneğin:

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

Bu geliştirme, @wertzui tarafından katkıda bulundu. Çok teşekkürler!

ExecuteUpdate ve ExecuteDelete

Karmaşık tür örneklerinin ExecuteUpdate'e geçirilmesine izin ver

İpucu

Burada gösterilen kod ExecuteUpdateSample.cs gelir.

ExecuteUpdate API, EF7'de, veya SaveChangesizlemeden veritabanında anında, doğrudan güncelleştirmeler gerçekleştirmek için kullanıma sunulmuştur. Örneğin:

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

Bu kodu çalıştırmak aşağıdaki sorguyu yürüterek "Deutschland" olarak güncelleştirir Region :

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

EF8'de ExecuteUpdate karmaşık tür özelliklerinin değerlerini güncelleştirmek için de kullanılabilir. Ancak, karmaşık türün her üyesi açıkça belirtilmelidir. Örneğin:

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

Bu kodun çalıştırılması aşağıdaki sorgu yürütmesine neden olur:

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'

EF9'da, karmaşık tür örneğinin kendisini geçirerek aynı güncelleştirme gerçekleştirilebilir. Yani, her üyenin açıkça belirtilmesi gerekmez. Örneğin:

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

Bu kodun çalıştırılması, önceki örnekle aynı sorgu yürütmesine neden olur:

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'

Hem karmaşık tür özelliklerinde hem de basit özelliklerde birden çok güncelleştirme tek bir çağrısında ExecuteUpdatebirleştirilebilir. Örneğin:

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

Bu kodun çalıştırılması, önceki örnekle aynı sorgu yürütmesine neden olur:

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

Geçişler

Geliştirilmiş zamana bağlı tablo geçişleri

Mevcut bir tablo zamansal tabloya değiştirilirken oluşturulan geçiş EF9 için boyut olarak küçültüldü. Örneğin, EF8'de tek bir mevcut tabloyu zamana bağlı bir tablo haline getirmek aşağıdaki geçişle sonuçlandığında:

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

EF9'da aynı işlem artık çok daha küçük bir geçişe neden olur:

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

Model oluşturma

Diziler için önbelleğe almayı belirtme

İpucu

Burada gösterilen kod ModelBuildingSample.cs gelir.

EF9, bunu destekleyen herhangi bir ilişkisel veritabanı sağlayıcısı için veritabanı dizileri için önbelleğe alma seçeneklerinin ayarlanmasına olanak tanır. Örneğin, UseCache önbelleğe almayı açıkça açmak ve önbellek boyutunu ayarlamak için kullanılabilir:

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

Bu, SQL Server kullanılırken aşağıdaki sıra tanımıyla sonuçlandığında:

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

Benzer şekilde, UseNoCache önbelleğe almayı açıkça kapatır:

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;

UseCacheUseNoCache Veya çağrılmazsa, önbelleğe alma belirtilmez ve veritabanı varsayılan değeri ne olursa olsun kullanır. Bu, farklı veritabanları için farklı bir varsayılan değer olabilir.

Bu geliştirme, @bikbov tarafından katkıda bulundu. Çok teşekkürler!

Anahtarlar ve dizinler için dolgu faktörü belirtme

İpucu

Burada gösterilen kod ModelBuildingSample.cs gelir.

EF9, anahtarlar ve dizinler oluşturmak için EF Core Migrations kullanılırken SQL Server dolgu faktörünün belirtimini destekler. SQL Server belgelerinden ,"Dizin oluşturulduğunda veya yeniden oluşturulduğunda, doldurma faktörü değeri her yaprak düzeyindeki sayfada verilerle doldurulacak alan yüzdesini belirler ve her sayfada kalan alanı gelecekteki büyüme için boş alan olarak ayırır."

Doldurma faktörü tek veya bileşik birincil anahtarlar ile alternatif anahtarlar ve dizinler üzerinde ayarlanabilir. Örneğin:

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

Mevcut tablolara uygulandığında, bu, tabloları dolgu faktörüne dönüştürerek kısıtlamayı değiştirir:

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

Not

Şu anda önizleme 2'de, tablo ilk kez oluşturulduğunda doldurma faktörlerinin dahil edilmediği bir hata vardır. Bu, Sorun #33269 tarafından izlenir

Bu geliştirme, @deano-hunter tarafından katkıda bulundu. Çok teşekkürler!

Mevcut model oluşturma kurallarını daha genişletilebilir hale getirme

İpucu

Burada gösterilen kod CustomConventionsSample.cs gelir.

Uygulamalar için genel model oluşturma kuralları EF7'de kullanıma sunulmuştur. EF9'da, mevcut kuralların bazılarını genişletmeyi kolaylaştırdık. Örneğin, EF7'de özniteliklere göre özellikleri eşleme kodu şu şekildedir:

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

EF9'da bu, aşağıdakilere kadar basitleştirilebilir:

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

Genel olmayan oluşturucuları çağırmak için ApplyConfigurationsFromAssembly güncelleştirme

EF Core'un ApplyConfigurationsFromAssembly önceki sürümlerinde yöntemi yalnızca genel, parametresiz oluşturucularla yapılandırma türlerinin örneğini oluşturmuş. EF9'da , hem bu başarısız olduğunda oluşturulan hata iletilerini geliştirdik hem de genel olmayan oluşturucu tarafından örneklemeyi etkinleştirdik. Bu, yapılandırmayı hiçbir zaman uygulama koduyla örneklenmemesi gereken özel iç içe geçmiş bir sınıfta birlikte konumlandırırken kullanışlıdır. Örneğin:

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

Bir yana, bazı kişiler bu düzenin bir iğrençlik olduğunu düşünür çünkü varlık türünü yapılandırmayla eşleştirir. Diğer kişiler bunun çok yararlı olduğunu düşünür çünkü varlık türüyle yapılandırmayı birlikte bulur. Bunu burada tartışmayalım. :-)

SQL Server HierarchyId

İpucu

Burada gösterilen kod HierarchyIdSample.cs gelir.

HierarchyId yol oluşturma için Şeker

EF8'de SQL Server HierarchyId türü için birinci sınıf destek eklendi. EF9'da, ağaç yapısında yeni alt düğümler oluşturmayı kolaylaştırmak için bir şeker yöntemi eklenmiştir. Örneğin, aşağıdaki kod özelliğine sahip HierarchyId mevcut bir varlık için sorgular:

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

Bu HierarchyId özellik daha sonra herhangi bir açık dize işlemesi olmadan alt düğümler oluşturmak için kullanılabilir. Örneğin:

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

daisy bir HierarchyId varsa/4/1/3/1/, child1HierarchyId "/1/4/3/1/1/" child2 ve "/4/1/3/1/2/" alır HierarchyId .

Bu iki alt öğe arasında düğüm oluşturmak için ek bir alt düzey kullanılabilir. Örneğin:

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

Bu, ile bteween child1 ve child2koyan bir HierarchyId/4/1/3/1/1.5/düğüm oluşturur.

Bu geliştirme, @Rezakazemi890 tarafından katkıda bulundu. Çok teşekkürler!

Araçlar

Daha az yeniden derleme

Komut dotnet ef satırı aracı , aracı yürütmeden önce projenizi varsayılan olarak oluşturur. Bunun nedeni, aracı çalıştırmadan önce yeniden derlenmemesi, işler çalışmadığında yaygın bir karışıklık kaynağıdır. Deneyimli geliştiriciler bu derlemeyi --no-build önlemek için bu seçeneği kullanabilir ve bu da yavaş olabilir. Ancak, bu seçenek bile --no-build projenin EF araçlarının dışında bir sonraki derlenmesinde yeniden derlenmesine neden olabilir.

@Suchiman topluluk katkılarının bunu düzeltmiş olduğunu düşünüyoruz. Ancak MSBuild davranışlarıyla ilgili ince ayarların istenmeyen sonuçlar doğurma eğilimine sahip olduğunu da biliyoruz. Bu nedenle sizin gibi insanlardan bunu denemelerini ve sahip olduğunuz olumsuz deneyimleri tekrar raporlamalarını istiyoruz.