Bagikan melalui


Yang Baru di EF Core 9

EF Core 9 (EF9) adalah rilis berikutnya setelah EF Core 8 dan dijadwalkan rilis pada November 2024.

EF9 tersedia sebagai build harian yang berisi semua fitur EF9 terbaru dan tweak API. Sampel di sini menggunakan build harian ini.

Tip

Anda dapat menjalankan dan men-debug ke dalam sampel dengan mengunduh kode sampel dari GitHub. Setiap bagian di bawah ini menautkan ke kode sumber khusus untuk bagian tersebut.

EF9 menargetkan .NET 8, dan karenanya dapat digunakan dengan pratinjau .NET 8 (LTS) atau .NET 9.

Tip

Dokumen Apa yang Baru diperbarui untuk setiap pratinjau. Semua sampel disiapkan untuk menggunakan build harian EF9, yang biasanya memiliki beberapa minggu tambahan pekerjaan yang selesai dibandingkan dengan pratinjau terbaru. Kami sangat mendorong penggunaan build harian saat menguji fitur baru sehingga Anda tidak melakukan pengujian terhadap bit basi.

Azure Cosmos DB for NoSQL

Kami sedang mengerjakan pembaruan signifikan di EF9 ke penyedia database EF Core untuk Azure Cosmos DB untuk NoSQL.

Kunci partisi hierarkis

Tip

Kode yang ditampilkan di sini berasal dari HierarchicalPartitionKeysSample.cs.

Setiap dokumen yang disimpan dalam database Cosmos memiliki ID sumber daya yang unik. Selain itu, setiap dokumen dapat berisi "kunci partisi" yang menentukan pemartisian logis data sehingga database dapat diskalakan secara efektif. Informasi selengkapnya tentang memilih kunci partisi dapat ditemukan di Pemartisian dan penskalaan horizontal di Azure Cosmos DB.

Rilis terbaru Azure Cosmos DB for NoSQL (Cosmos SDK versi 3.33.0 atau yang lebih baru) telah memperluas kemampuan partisi untuk mendukung subpartisi melalui spesifikasi hingga tiga tingkat hierarki dalam kunci partisi. EF Core 9 mendukung spesifikasi kunci partisi hierarkis dalam model, ekstraksi otomatis nilai-nilai ini dari kueri, dan spesifikasi manual kunci partisi hierarkis untuk kueri tertentu.

Mengonfigurasi kunci partisi hierarkis

Kunci partisi ditentukan menggunakan API pembuatan model, biasanya di DbContext.OnModelCreating. Harus ada properti yang dipetakan dalam jenis entitas untuk setiap tingkat kunci partisi. Misalnya, pertimbangkan UserSession jenis entitas:

public class UserSession
{
    // Item ID
    public Guid Id { get; set; }

    // Partition Key
    public string TenantId { get; set; } = null!;
    public Guid UserId { get; set; }
    public int SessionId { get; set; }

    // Other members
    public string Username { get; set; } = null!;
}

Kode berikut menentukan kunci partisi tiga tingkat menggunakan TenantIdproperti , , UserIddan SessionId :

modelBuilder
    .Entity<UserSession>()
    .HasPartitionKey(e => new { e.TenantId, e.UserId, e.SessionId });

Tip

Definisi kunci partisi ini mengikuti contoh yang diberikan dalam Memilih kunci partisi hierarkis Anda dari dokumentasi Azure Cosmos DB.

Perhatikan bagaimana, dimulai dengan EF Core 9, properti dari jenis yang dipetakan dapat digunakan dalam kunci partisi. Untuk bool jenis numerik dan, seperti int SessionId properti , nilai digunakan langsung di kunci partisi. Jenis lain, seperti Guid UserId properti , secara otomatis dikonversi ke string.

Menyimpan dokumen dengan kunci partisi hierarkis

Menyimpan dokumen baru dengan kunci partisi hierarkis sama dengan menyimpan dokumen baru dengan EF Core. Properti kunci primer dan kunci partisi harus memiliki nilai non-default, atau pembuatan nilai EF Core dapat digunakan untuk membuat nilai. Misalnya, kode berikut menyisipkan UserSession dokumen di mana Id properti dihasilkan oleh EF Core, dan semua properti kunci partisi telah diatur secara eksplisit:

var tenantId = "Microsoft";
var sessionId = 7;

context.AddRange(
    new UserSession
    {
        TenantId = tenantId,
        UserId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C"),
        SessionId = sessionId,
        Username = "mac"
    },
    new UserSession
    {
        TenantId = tenantId,
        UserId = new Guid("ADAE5DDE-8A67-432D-9DEC-FD7EC86FD9F6"),
        SessionId = sessionId,
        Username = "toast"
    },
    new UserSession
    {
        TenantId = tenantId,
        UserId = new Guid("61967254-AFF8-493A-B7F8-E62DA36D8367"),
        SessionId = sessionId,
        Username = "willow"
    },
    new UserSession
    {
        TenantId = tenantId,
        UserId = new Guid("BC0150CF-5147-44B8-8823-865F4F2323E1"),
        SessionId = sessionId,
        Username = "alice"
    });

await context.SaveChangesAsync();

Log dari panggilan SaveChangesAsync ditampilkan dalam panggilan berikut CreateItem :

info: 6/10/2024 18:41:04.456 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed CreateItem (167 ms, 7.81 RU) ActivityId='23891b55-7375-40e5-aa4b-2c57ca6a376e', Container='UserSessionContext', Id='UserSession|d5e2614b-71f2-4e6b-d41a-08dc89748055', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]'
info: 6/10/2024 18:41:04.478 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed CreateItem (14 ms, 7.81 RU) ActivityId='7fdcfb3e-455c-45dd-b444-02b66575a28f', Container='UserSessionContext', Id='UserSession|01cc0102-5212-4785-d41b-08dc89748055', Partition='["Microsoft","adae5dde-8a67-432d-9dec-fd7ec86fd9f6",7.0]'
info: 6/10/2024 18:41:04.491 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed CreateItem (13 ms, 7.81 RU) ActivityId='3f7e6026-8edf-4f2c-8918-09434dc039bf', Container='UserSessionContext', Id='UserSession|e5a467c0-bb1e-4ffe-d41c-08dc89748055', Partition='["Microsoft","61967254-aff8-493a-b7f8-e62da36d8367",7.0]'
info: 6/10/2024 18:41:04.507 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed CreateItem (15 ms, 7.81 RU) ActivityId='04c6f4b2-0ad0-4708-874e-dc8967726d18', Container='UserSessionContext', Id='UserSession|fd47726a-fb68-4c63-d41d-08dc89748055', Partition='["Microsoft","bc0150cf-5147-44b8-8823-865f4f2323e1",7.0]'

Perhatikan bahwa nilai kunci partisi telah diekstrak dari instans entitas dan disertakan dalam panggilan untuk CreateItem memastikan efisiensi maksimum pada server.

Baca titik menggunakan kunci partisi hierarkis

Menurut konvensi, EF Core menyertakan properti kunci partisi dalam definisi kunci utama untuk jenis entitas. Misalnya, memeriksa tampilan debug model menunjukkan pemetaan berikut untuk UserSession jenis entitas:

EntityType: UserSession
  Properties: 
    Id (Guid) Required PK AfterSave:Throw ValueGenerated.OnAdd
    TenantId (string) Required PK AfterSave:Throw
    UserId (Guid) Required PK AfterSave:Throw
    SessionId (int) Required PK AfterSave:Throw
    Discriminator (no field, string) Shadow Required AfterSave:Throw
    Username (string)
    __id (no field, string) Shadow Required AlternateKey AfterSave:Throw
    __jObject (no field, JObject) Shadow BeforeSave:Ignore AfterSave:Ignore ValueGenerated.OnAddOrUpdate
  Keys: 
    Id, TenantId, UserId, SessionId PK
    __id, TenantId, UserId, SessionId

Perhatikan bahwa definisi kunci utama adalah Id, TenantId, UserId, SessionId. Ini berarti bahwa DbSet<TEntity>.FindAsync dapat digunakan untuk mencari dokumen. Contohnya:

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var session = await context.Sessions.FindAsync(
    userSessionId, tenantId, userId, sessionId);

Pengelogan dari EF Core menunjukkan bahwa point-read (menggunakan ReadItem) dijalankan untuk efisiensi maksimum:

info: 6/10/2024 18:41:04.651 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command) 
      Reading resource 'UserSession|e5a467c0-bb1e-4ffe-d41c-08dc89748055' item from container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]'.
info: 6/10/2024 18:41:04.668 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executed ReadItem (8 ms, 1 RU) ActivityId='a016f26c-6bd0-4c66-953b-a8f1297df41a', Container='UserSessionContext', Id='UserSession|e5a467c0-bb1e-4ffe-d41c-08dc89748055', Partition='["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]'

Kueri menggunakan kunci partisi hierarkis

EF Core akan mengekstrak nilai kunci partisi dari kueri dan menerapkannya ke API kueri Cosmos untuk memastikan kueri dibatasi dengan tepat hingga jumlah partisi sekecil mungkin. Misalnya, pertimbangkan kueri LINQ yang menyediakan nilai untuk semua tingkat kunci partisi:

var tenantId = "Microsoft";
var sessionId = 7;
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.SessionId == sessionId
             && e.Username.Contains("a"))
    .ToListAsync();

Saat menjalankan kueri ini, EF Core akan mengekstrak nilai tenantIdparameter , , userIddan sessionId , dan meneruskannya ke API kueri Cosmos sebagai nilai kunci partisi. Misalnya, lihat log dari menjalankan kueri di atas:

info: 6/10/2024 19:06:00.017 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Perhatikan bahwa perbandingan kunci partisi telah dihapus dari WHERE klausul, dan sebaliknya diteruskan langsung ke API Cosmos sebagai kunci ["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c",7.0]partisi .

Penting

Karena kueri menyertakan nilai untuk semua bagian kunci partisi, kueri dirutekan ke partisi tunggal yang berisi data untuk nilai yang ditentukan dari TenantId, UserId, dan SessionId. Ini lebih efisien daripada kueri di bawah ini yang hanya menggunakan tidak ada, atau hanya beberapa, dari nilai kunci partisi.

Dengan partisi hierarkis, kueri yang lebih efisien masih dapat dihasilkan ketika hanya hierarki kunci partisi teratas yang diketahui. Misalnya, kueri LINQ berikut menggunakan dua bagian teratas dari kunci partisi --yaitu, TenantId dan UserId:

var tenantId = "Microsoft";
var userId = new Guid("99A410D7-E467-4CC5-92DE-148F3FC53F4C");

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.UserId == userId
             && e.Username.Contains("a"))
    .ToListAsync();

EF Core masih mengekstrak nilai kunci partisi saat menjalankan kueri ini:

info: 6/10/2024 19:24:46.581 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft","99a410d7-e467-4cc5-92de-148f3fc53f4c"]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Kueri ini tidak menyertakan SessionId, sehingga tidak dapat menargetkan satu partisi. Namun, ini masih akan menjadi kueri lintas partisi yang ditargetkan yang mengembalikan data untuk semua sesi penyewa tunggal dan ID pengguna.

Demikian juga, jika hanya nilai teratas dalam hierarki yang ditentukan, maka nilai tersebut akan digunakan sendiri. Contohnya:

var tenantId = "Microsoft";

var sessions = await context.Sessions
    .Where(
        e => e.TenantId == tenantId
             && e.Username.Contains("a"))
    .ToListAsync();

Yang menghasilkan log berikut:

info: 6/11/2024 09:30:42.532 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'UserSessionContext' in partition '["Microsoft"]' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "UserSession") AND CONTAINS(c["Username"], "a"))

Karena kueri ini hanya berisi TenantId bagian dari kunci partisi, kueri tidak dapat menargetkan satu partisi. Namun, seperti contoh sebelumnya masih akan menjadi kueri lintas partisi yang ditargetkan yang mengembalikan data untuk semua sesi dan pengguna dalam satu penyewa.

Penting untuk dipahami bahwa menggunakan nilai kedua dan/atau ketiga dari kunci partisi hierarkis, tanpa menyertakan nilai pertama, akan menghasilkan kueri yang mencakup semua partisi. Misalnya, pertimbangkan kueri termasuk SessionId dan UserId, tetapi tidak termasuk TenantId:

var sessions3 = await context.Sessions
    .Where(
        e => e.SessionId == sessionId
             && e.UserId == userId
             && e.Username.Contains("a"))
    .ToListAsync();

Log menunjukkan bahwa ini diterjemahkan tanpa kunci partisi, karena TenantId hilang:

info: 6/11/2024 09:30:42.553 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command) 
      Executing SQL query for container 'UserSessionContext' in partition 'None' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE (c["Discriminator"] = "UserSession")

Catatan

Masalah #33960 melacak bug dalam terjemahan ini.

Akses berbasis peran

Azure Cosmos DB for NoSQL menyertakan sistem kontrol akses berbasis peran (RBAC) bawaan. Ini sekarang didukung oleh EF9 untuk manajemen dan penggunaan kontainer. Tidak ada perubahan yang diperlukan untuk kode aplikasi. Lihat Masalah #32197 untuk informasi selengkapnya.

Akses sinkron diblokir secara default

Tip

Kode yang ditampilkan di sini berasal dari CosmosSyncApisSample.cs.

Azure Cosmos DB for NoSQL tidak mendukung akses sinkron (memblokir) dari kode aplikasi. Sebelumnya, EF menutupi ini secara default dengan memblokir untuk Anda pada panggilan asinkron. Namun, ini mendorong penggunaan sinkronisasi, yang merupakan praktik buruk, dan dapat menyebabkan kebuntuan. Oleh karena itu, dimulai dengan EF9, pengecualian dilemparkan ketika akses sinkron dicoba. Contohnya:

System.InvalidOperationException: An error was generated for warning 'Microsoft.EntityFrameworkCore.Database.SyncNotSupported':
 Azure Cosmos DB does not support synchronous I/O. Make sure to use and correctly await only async methods when using
 Entity Framework Core to access Azure Cosmos DB. See https://aka.ms/ef-cosmos-nosync for more information.
 This exception can be suppressed or logged by passing event ID 'CosmosEventId.SyncNotSupported' to the 'ConfigureWarnings'
 method in 'DbContext.OnConfiguring' or 'AddDbContext'.
   at Microsoft.EntityFrameworkCore.Diagnostics.EventDefinition.Log[TLoggerCategory](IDiagnosticsLogger`1 logger, Exception exception)
   at Microsoft.EntityFrameworkCore.Cosmos.Diagnostics.Internal.CosmosLoggerExtensions.SyncNotSupported(IDiagnosticsLogger`1 diagnostics)
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosClientWrapper.DeleteDatabase()
   at Microsoft.EntityFrameworkCore.Cosmos.Storage.Internal.CosmosDatabaseCreator.EnsureDeleted()
   at Microsoft.EntityFrameworkCore.Infrastructure.DatabaseFacade.EnsureDeleted()

Seperti yang dikatakan pengecualian, akses sinkronisasi masih dapat digunakan untuk saat ini dengan mengonfigurasi tingkat peringatan dengan tepat. Misalnya, pada OnConfiguring jenis Anda DbContext :

protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    => optionsBuilder.ConfigureWarnings(b => b.Ignore(CosmosEventId.SyncNotSupported));

Namun, perhatikan bahwa kami berencana untuk sepenuhnya menghapus dukungan sinkronisasi di EF11, jadi mulailah memperbarui untuk menggunakan metode asinkron seperti ToListAsync dan SaveChangesAsync sesegera mungkin!

Koleksi primitif yang ditingkatkan

Tip

Kode yang ditampilkan di sini berasal dari CosmosPrimitiveTypesSample.cs.

Penyedia Cosmos DB telah mendukung koleksi primitif dalam bentuk terbatas sejak EF Core 6. Ini adalah dukungan yang sedang ditingkatkan di EF9, dimulai dengan konsolidasi metadata dan permukaan API untuk koleksi primitif dalam database dokumen agar selaras dengan koleksi primitif dalam database relasional. Ini berarti bahwa koleksi primitif sekarang dapat dipetakan secara eksplisit menggunakan API pembuatan model, memungkinkan faset jenis elemen dikonfigurasi. Misalnya, untuk memetakan daftar string yang diperlukan (yaitu non-null):

modelBuilder.Entity<Book>()
    .PrimitiveCollection(e => e.Quotes)
    .ElementType(b => b.IsRequired());

Lihat Apa yang baru dalam EF8: koleksi primitif untuk informasi selengkapnya tentang API pembuatan model.

AOT dan kueri yang telah dikompilasi sebelumnya

Seperti disebutkan dalam pengenalan, ada banyak pekerjaan yang terjadi di belakang layar untuk memungkinkan EF Core berjalan tanpa kompilasi just-in-time (JIT). Sebagai gantinya, EF mengkompilasi semua yang diperlukan untuk menjalankan kueri dalam aplikasi. Kompilasi AOT dan pemrosesan terkait ini akan terjadi sebagai bagian dari membangun dan menerbitkan aplikasi. Pada titik ini dalam rilis EF9, tidak banyak yang tersedia yang dapat digunakan oleh Anda, pengembang aplikasi. Namun, bagi mereka yang tertarik, masalah yang telah selesai di EF9 yang mendukung AOT dan kueri yang telah dikompilasi sebelumnya adalah:

Periksa kembali di sini untuk contoh cara menggunakan kueri yang telah dikompilasi sebelumnya saat pengalaman berkumpul.

Terjemahan LINQ dan SQL

Tim sedang mengerjakan beberapa perubahan arsitektur yang signifikan pada alur kueri di EF Core 9 sebagai bagian dari peningkatan berkelanjutan kami pada pemetaan JSON dan database dokumen. Ini berarti kita perlu membuat orang seperti Anda menjalankan kode Anda di internal baru ini. (Jika Anda membaca dokumen "Apa yang Baru" pada saat ini dalam rilis, maka Anda benar-benar terlibat bagian dari komunitas; terima kasih!) Kami memiliki lebih dari 120.000 tes, tetapi itu tidak cukup! Kami membutuhkan Anda, orang yang menjalankan kode nyata pada bit kami, untuk menemukan masalah dan mengirimkan rilis yang solid!

Jenis kompleks GroupBy

Tip

Kode yang ditampilkan di sini berasal dari ComplexTypesSample.cs.

EF9 mendukung pengelompokan berdasarkan instans jenis kompleks. Contohnya:

var groupedAddresses = await context.Stores
    .GroupBy(b => b.StoreAddress)
    .Select(g => new { g.Key, Count = g.Count() })
    .ToListAsync();

EF menerjemahkan ini sebagai pengelompokan oleh setiap anggota jenis kompleks, yang selaras dengan semantik jenis kompleks sebagai objek nilai. Misalnya, di Azure SQL:

SELECT [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode], COUNT(*) AS [Count]
FROM [Stores] AS [s]
GROUP BY [s].[StoreAddress_City], [s].[StoreAddress_Country], [s].[StoreAddress_Line1], [s].[StoreAddress_Line2], [s].[StoreAddress_PostCode]

Pangkas kolom yang diteruskan ke klausa WITH OPENJSON

Tip

Kode yang ditampilkan di sini berasal dari JsonColumnsSample.cs.

EF9 menghapus kolom yang tidak perlu saat memanggil OPENJSON WITH. Misalnya, pertimbangkan kueri yang mendapatkan hitungan dari koleksi JSON menggunakan predikat:

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

Di EF8, kueri ini menghasilkan SQL berikut saat menggunakan penyedia database 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

Perhatikan bahwa UpdatedBy, dan Commits tidak diperlukan dalam kueri ini. Dimulai dengan EF9, kolom-kolom ini sekarang diprakirakan:

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

Dalam beberapa skenario, ini menghasilkan penghapusan klausa yang WITH lengkap. Contohnya:

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

Di EF8, kueri ini diterjemahkan ke SQL berikut:

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

Di EF9, ini telah ditingkatkan menjadi:

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

Terjemahan yang melibatkan GREATEST/LEAST

Tip

Kode yang ditampilkan di sini berasal dari LeastGreatestSample.cs.

Beberapa terjemahan baru telah diperkenalkan yang menggunakan GREATEST fungsi dan LEAST SQL.

Penting

Fungsi GREATEST dan LEAST diperkenalkan ke database SQL Server/Azure SQL dalam versi 2022. Visual Studio 2022 menginstal SQL Server 2019 secara default. Sebaiknya instal SQL Server Developer Edition 2022 untuk mencoba terjemahan baru ini di EF9.

Misalnya, kueri yang menggunakan Math.Max atau Math.Min sekarang diterjemahkan untuk Azure SQL menggunakan GREATEST dan LEAST masing-masing. Contohnya:

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

Kueri ini diterjemahkan ke SQL berikut saat menggunakan EF9 yang dijalankan terhadap SQL Server 2022:

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 dan Math.Max juga dapat digunakan pada nilai koleksi primitif. Contohnya:

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

Kueri ini diterjemahkan ke SQL berikut saat menggunakan EF9 yang dijalankan terhadap SQL Server 2022:

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

Terakhir, RelationalDbFunctionsExtensions.Least dan RelationalDbFunctionsExtensions.Greatest dapat digunakan untuk langsung memanggil Least fungsi atau Greatest di SQL. Contohnya:

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

Kueri ini diterjemahkan ke SQL berikut saat menggunakan EF9 yang dijalankan terhadap SQL Server 2022:

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]

Paksa atau cegah parameterisasi kueri

Tip

Kode yang ditampilkan di sini berasal dari QuerySample.cs.

Kecuali dalam beberapa kasus khusus, EF Core membuat parameter variabel yang digunakan dalam kueri LINQ, tetapi menyertakan konstanta dalam SQL yang dihasilkan. Misalnya, pertimbangkan metode kueri berikut:

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

Ini diterjemahkan ke SQL dan parameter berikut saat menggunakan Azure SQL:

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

Perhatikan bahwa EF membuat konstanta di SQL untuk ".NET Blog" karena nilai ini tidak akan berubah dari kueri ke kueri. Menggunakan konstanta memungkinkan nilai ini diperiksa oleh mesin database saat membuat rencana kueri, berpotensi menghasilkan kueri yang lebih efisien.

Di sisi lain, nilai id diparameterkan, karena kueri yang sama dapat dijalankan dengan banyak nilai yang berbeda untuk id. Membuat konstanta dalam kasus ini menghasilkan polusi cache kueri dengan banyak kueri yang hanya berbeda dalam nilai parameter. Ini sangat buruk untuk performa keseluruhan database.

Secara umum, default ini tidak boleh diubah. Namun, EF Core 8.0.2 memperkenalkan EF.Constant metode yang memaksa EF untuk menggunakan konstanta bahkan jika parameter akan digunakan secara default. Contohnya:

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

Terjemahan sekarang berisi konstanta untuk id nilai :

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 memperkenalkan EF.Parameter metode untuk melakukan kebalikannya. Artinya, paksa EF untuk menggunakan parameter meskipun nilainya adalah konstanta dalam kode. Contohnya:

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

Terjemahan sekarang berisi parameter untuk string ".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

Subkueri yang tidak terkait dengan inlined

Tip

Kode yang ditampilkan di sini berasal dari QuerySample.cs.

Di EF8, IQueryable yang direferensikan dalam kueri lain dapat dijalankan sebagai roundtrip database terpisah. Misalnya, pertimbangkan kueri LINQ berikut:

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

Di EF8, kueri untuk dotnetPosts dijalankan sebagai satu perjalanan pulang pergi, lalu hasil akhir dijalankan sebagai kueri kedua. Misalnya, di SQL Server:

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

Dalam EF9, IQueryable dalam dotnetPosts inlined, menghasilkan satu perjalanan pulang pergi:

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

Metode baru ToHashSetAsync<T>

Tip

Kode yang ditampilkan di sini berasal dari QuerySample.cs.

Metode Enumerable.ToHashSet telah ada sejak .NET Core 2.0. Dalam EF9, metode asinkron yang setara telah ditambahkan. Contohnya:

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

Peningkatan ini dikontribusikan oleh @wertzui. Terima kasih banyak!

Kueri menggunakan Count != 0 dioptimalkan

Tip

Kode yang ditampilkan di sini berasal dari QuerySample.cs.

Di EF8, kueri LINQ berikut diterjemahkan untuk menggunakan fungsi SQL COUNT :

var blogsWithPost = await context.Blogs
    .Where(b => b.Posts.Count > 0)
    .ToListAsync();

EF9 sekarang menghasilkan terjemahan yang lebih efisien menggunakan EXISTS:

SELECT "b"."Id", "b"."Name", "b"."SiteUri"
FROM "Blogs" AS "b"
WHERE EXISTS (
    SELECT 1
    FROM "Posts" AS "p"
    WHERE "b"."Id" = "p"."BlogId")

Metode lainnya TimeOnly diterjemahkan untuk Azure SQL/SQL Server

Tip

Kode yang ditampilkan di sini berasal dari QuerySample.cs.

Kueri yang menggunakan TimeOnly.FromDateTime dan TimeOnly.FromTimeSpan sekarang diterjemahkan saat menggunakan SQL Server atau Azure SQL. Misalnya, kueri LINQ berikut menggunakan FromDateTime untuk mengekstrak nilai time-of-day dari kolom dan membandingkannya dengan nilai yang TimeOnly diteruskan:

var visitedTime = new TimeOnly(12, 0);
var visited = await context.Schools
    .Where(p => TimeOnly.FromDateTime(p.LastVisited) >= visitedTime)
    .ToListAsync();

Ini diterjemahkan ke yang berikut ini saat menggunakan SQL Azure atau SQL Server:

SELECT [s].[Id], [s].[Founded], [s].[LastVisited], [s].[LegacyTime], [s].[Name], [s].[OpeningHours]
FROM [Schools] AS [s]
WHERE CAST([s].[LastVisited] AS time) >= @__visitedTime_0

FromTimeSpan diterjemahkan dengan cara yang sama.

ExecuteUpdate dan ExecuteDelete

Izinkan meneruskan instans jenis kompleks ke ExecuteUpdate

Tip

Kode yang ditampilkan di sini berasal dari ExecuteUpdateSample.cs.

ExecuteUpdate API diperkenalkan di EF7 untuk melakukan pembaruan langsung dan langsung ke database tanpa pelacakan atau SaveChanges. Contohnya:

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

Menjalankan kode ini menjalankan kueri berikut untuk memperbarui Region ke "Deutschland":

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

Di EF8 ExecuteUpdate juga dapat digunakan untuk memperbarui nilai properti jenis kompleks. Namun, setiap anggota jenis kompleks harus ditentukan secara eksplisit. Contohnya:

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

Menjalankan kode ini menghasilkan eksekusi kueri berikut:

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'

Di EF9, pembaruan yang sama dapat dilakukan dengan melewati instans jenis kompleks itu sendiri. Artinya, setiap anggota tidak perlu ditentukan secara eksplisit. Contohnya:

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

Menjalankan kode ini menghasilkan eksekusi kueri yang sama dengan contoh sebelumnya:

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'

Beberapa pembaruan untuk properti jenis kompleks dan properti sederhana dapat dikombinasikan dalam satu panggilan ke ExecuteUpdate. Contohnya:

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

Menjalankan kode ini menghasilkan eksekusi kueri yang sama dengan contoh sebelumnya:

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

Migrasi

Peningkatan migrasi tabel temporal

Migrasi yang dibuat saat mengubah tabel yang ada menjadi tabel temporal telah dikurangi ukurannya untuk EF9. Misalnya, di EF8 membuat satu tabel yang ada tabel temporal menghasilkan migrasi berikut:

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

Di EF9, operasi yang sama sekarang menghasilkan migrasi yang jauh lebih kecil:

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

Bangunan model

Model yang dikompilasi otomatis

Tip

Kode yang ditampilkan di sini berasal dari sampel NewInEFCore9.CompiledModels .

Model yang dikompilasi dapat meningkatkan waktu startup untuk aplikasi dengan model besar--yaitu jumlah jenis entitas dalam 100s atau 1000s. Dalam versi EF Core sebelumnya, model yang dikompilasi harus dibuat secara manual, menggunakan baris perintah. Contohnya:

dotnet ef dbcontext optimize

Setelah menjalankan perintah, baris seperti, .UseModel(MyCompiledModels.BlogsContextModel.Instance) harus ditambahkan ke OnConfiguring untuk memberi tahu EF Core untuk menggunakan model yang dikompilasi.

Dimulai dengan EF9, baris ini .UseModel tidak lagi diperlukan ketika jenis aplikasi DbContext berada dalam proyek/rakitan yang sama dengan model yang dikompilasi. Sebagai gantinya, model yang dikompilasi akan terdeteksi dan digunakan secara otomatis. Ini dapat dilihat dengan memiliki log EF setiap kali membangun model. Menjalankan aplikasi sederhana kemudian menunjukkan EF membangun model saat aplikasi dimulai:

Starting application...
>> EF is building the model...
Model loaded with 2 entity types.

Output dari berjalan dotnet ef dbcontext optimize pada proyek model adalah:

PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> dotnet ef dbcontext optimize

Build succeeded in 0.3s

Build succeeded in 0.3s
Build started...
Build succeeded.
>> EF is building the model...
>> EF is building the model...
Successfully generated a compiled model, it will be discovered automatically, but you can also call 'options.UseModel(BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model> 

Perhatikan bahwa output log menunjukkan bahwa model dibangun saat menjalankan perintah. Jika kita sekarang menjalankan aplikasi lagi, setelah membangun kembali tetapi tanpa membuat perubahan kode apa pun, maka outputnya adalah:

Starting application...
Model loaded with 2 entity types.

Perhatikan bahwa model tidak dibangun saat memulai aplikasi karena model yang dikompilasi terdeteksi dan digunakan secara otomatis.

Integrasi MSBuild

Dengan pendekatan di atas, model yang dikompilasi masih perlu diregenerasi secara manual ketika jenis atau DbContext konfigurasi entitas diubah. Namun, EF9 dikirim dengan paket MSBuild dan target yang dapat secara otomatis memperbarui model yang dikompilasi ketika proyek model dibangun! Untuk memulai, instal paket NuGet Microsoft.EntityFrameworkCore.Tasks . Contohnya:

dotnet add package Microsoft.EntityFrameworkCore.Tasks --version 9.0.0-preview.4.24205.3

Tip

Gunakan versi paket dalam perintah di atas yang cocok dengan versi EF Core yang Anda gunakan.

Kemudian aktifkan integrasi dengan mengatur properti ke EFOptimizeContext file Anda .csproj . Contohnya:

<PropertyGroup>
    <EFOptimizeContext>true</EFOptimizeContext>
</PropertyGroup>

Ada properti MSBuild tambahan, opsional untuk mengontrol bagaimana model dibangun, setara dengan opsi yang diteruskan pada baris perintah ke dotnet ef dbcontext optimize. Ini termasuk:

Properti MSBuild Deskripsi
EFOptimizeContext Atur ke true untuk mengaktifkan model yang dikompilasi otomatis.
DbContextName Kelas DbContext yang akan digunakan. Nama kelas saja atau sepenuhnya memenuhi syarat dengan namespace layanan. Jika opsi ini dihilangkan, EF Core akan menemukan kelas konteks. Jika ada beberapa kelas konteks, opsi ini diperlukan.
EFStartupProject Jalur relatif ke proyek startup. Nilai default adalah folder saat ini.
EFTargetNamespace Namespace layanan yang digunakan untuk semua kelas yang dihasilkan. Default yang dihasilkan dari namespace layanan akar dan direktori output ditambah CompiledModels.

Dalam contoh kami, kita perlu menentukan proyek startup:

<PropertyGroup>
  <EFOptimizeContext>true</EFOptimizeContext>
  <EFStartupProject>..\App\App.csproj</EFStartupProject>
</PropertyGroup>

Sekarang, jika kita membangun proyek, kita dapat melihat pengelogan pada waktu build yang menunjukkan bahwa model yang dikompilasi sedang dibangun:

Optimizing DbContext...
dotnet exec --depsfile D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.deps.json
  --additionalprobingpath G:\packages 
  --additionalprobingpath "C:\Program Files (x86)\Microsoft Visual Studio\Shared\NuGetPackages" 
  --runtimeconfig D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.runtimeconfig.json G:\packages\microsoft.entityframeworkcore.tasks\9.0.0-preview.4.24205.3\tasks\net8.0\..\..\tools\netcoreapp2.0\ef.dll dbcontext optimize --output-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\obj\Release\net8.0\ 
  --namespace NewInEfCore9 
  --suffix .g 
  --assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model\bin\Release\net8.0\Model.dll --startup-assembly D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App\bin\Release\net8.0\App.dll 
  --project-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\Model 
  --root-namespace NewInEfCore9 
  --language C# 
  --nullable 
  --working-dir D:\code\EntityFramework.Docs\samples\core\Miscellaneous\NewInEFCore9.CompiledModels\App 
  --verbose 
  --no-color 
  --prefix-output 

Dan menjalankan aplikasi menunjukkan bahwa model yang dikompilasi telah terdeteksi dan karenanya model tidak dibangun lagi:

Starting application...
Model loaded with 2 entity types.

Sekarang, setiap kali model berubah, model yang dikompilasi akan secara otomatis dibangun kembali segera setelah proyek dibangun.

[CATATAN!] Kami sedang mengerjakan beberapa masalah performa dengan perubahan yang dilakukan pada model yang dikompilasi di EF8 dan EF9. Lihat Masalah 33483# untuk informasi selengkapnya.

Koleksi primitif baca-saja

Tip

Kode yang ditampilkan di sini berasal dari PrimitiveCollectionsSample.cs.

EF8 memperkenalkan dukungan untuk pemetaan array dan daftar jenis primitif yang dapat diubah. Ini telah diperluas di EF9 untuk menyertakan koleksi/daftar baca-saja. Secara khusus, EF9 mendukung koleksi yang dititik sebagai IReadOnlyList, , IReadOnlyCollectionatau ReadOnlyCollection. Misalnya, dalam kode berikut, DaysVisited akan dipetakan oleh konvensi sebagai kumpulan tanggal primitif:

public class DogWalk
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ReadOnlyCollection<DateOnly> DaysVisited { get; set; }
}

Koleksi baca-saja dapat didukung oleh koleksi normal yang dapat diubah jika diinginkan. Misalnya, dalam kode berikut, DaysVisited dapat dipetakan sebagai kumpulan tanggal primitif, sambil tetap mengizinkan kode di kelas untuk memanipulasi daftar yang mendasar.

    public class Pub
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public IReadOnlyCollection<string> Beers { get; set; }

        private List<DateOnly> _daysVisited = new();
        public IReadOnlyList<DateOnly> DaysVisited => _daysVisited;
    }

Koleksi ini kemudian dapat digunakan dalam kueri dengan cara normal. Misalnya, kueri LINQ ini:

var walksWithADrink = await context.Walks.Select(
    w => new
    {
        WalkName = w.Name,
        PubName = w.ClosestPub.Name,
        Count = w.DaysVisited.Count(v => w.ClosestPub.DaysVisited.Contains(v)),
        TotalCount = w.DaysVisited.Count
    }).ToListAsync();

Yang diterjemahkan ke SQL berikut di SQLite:

SELECT "w"."Name" AS "WalkName", "p"."Name" AS "PubName", (
    SELECT COUNT(*)
    FROM json_each("w"."DaysVisited") AS "d"
    WHERE "d"."value" IN (
        SELECT "d0"."value"
        FROM json_each("p"."DaysVisited") AS "d0"
    )) AS "Count", json_array_length("w"."DaysVisited") AS "TotalCount"
FROM "Walks" AS "w"
INNER JOIN "Pubs" AS "p" ON "w"."ClosestPubId" = "p"."Id"

Tentukan penembolokan untuk urutan

Tip

Kode yang ditampilkan di sini berasal dari ModelBuildingSample.cs.

EF9 memungkinkan pengaturan opsi penembolokan untuk urutan database untuk penyedia database relasional apa pun yang mendukung ini. Misalnya, UseCache dapat digunakan untuk mengaktifkan penembolokan secara eksplisit dan mengatur ukuran cache:

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

Ini menghasilkan definisi urutan berikut saat menggunakan SQL Server:

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

Demikian pula, UseNoCache secara eksplisit menonaktifkan penembolokan:

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;

Jika tidak atau dipanggil UseCacheUseNoCache , maka penembolokan tidak ditentukan dan database akan menggunakan apa pun defaultnya. Ini mungkin default yang berbeda untuk database yang berbeda.

Peningkatan ini dikontribusikan oleh @bikbov. Terima kasih banyak!

Tentukan faktor pengisian untuk kunci dan indeks

Tip

Kode yang ditampilkan di sini berasal dari ModelBuildingSample.cs.

EF9 mendukung spesifikasi faktor pengisian SQL Server saat menggunakan Migrasi Inti EF untuk membuat kunci dan indeks. Dari dokumen SQL Server, "Ketika indeks dibuat atau dibangun kembali, nilai faktor pengisian menentukan persentase ruang pada setiap halaman tingkat daun yang akan diisi dengan data, menyimpan sisanya di setiap halaman sebagai ruang kosong untuk pertumbuhan di masa mendatang."

Faktor pengisian dapat diatur pada kunci dan indeks utama dan alternatif tunggal atau komposit. Contohnya:

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

Saat diterapkan ke tabel yang ada, ini akan mengubah tabel ke faktor pengisian ke batasan:

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

Peningkatan ini dikontribusikan oleh @deano-hunter. Terima kasih banyak!

Membuat konvensi pembuatan model yang ada lebih dapat diperluas

Tip

Kode yang ditampilkan di sini berasal dari CustomConventionsSample.cs.

Konvensi pembangunan model publik untuk aplikasi diperkenalkan di EF7. Di EF9, kami telah mempermudah perluasan beberapa konvensi yang ada. Misalnya, kode untuk memetakan properti berdasarkan atribut di EF7 adalah ini:

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

Di EF9, ini dapat disederhanakan ke hal berikut:

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

Memperbarui ApplyConfigurationsFromAssembly untuk memanggil konstruktor non-publik

Dalam versi EF Core sebelumnya, ApplyConfigurationsFromAssembly metode ini hanya membuat jenis konfigurasi dengan konstruktor publik dan tanpa parameter. Di EF9, kami telah meningkatkan pesan kesalahan yang dihasilkan ketika ini gagal, dan juga mengaktifkan instansiasi oleh konstruktor non-publik. Ini berguna ketika menemukan konfigurasi bersama di kelas berlapis privat yang tidak boleh dibuat oleh kode aplikasi. Contohnya:

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

Selain itu, beberapa orang berpikir pola ini adalah kelalaian karena menggabungkan jenis entitas ke konfigurasi. Orang lain berpikir itu sangat berguna karena menemukan konfigurasi bersama dengan jenis entitas. Jangan perdebatkan ini di sini. :-)

HierarkiId SQL Server

Tip

Kode yang ditampilkan di sini berasal dari HierarchyIdSample.cs.

Gula untuk pembuatan jalur HierarkiId

Dukungan kelas satu untuk jenis SQL Server HierarchyId ditambahkan di EF8. Dalam EF9, metode gula telah ditambahkan untuk mempermudah pembuatan simpul anak baru dalam struktur pohon. Misalnya, kueri kode berikut untuk entitas yang sudah ada dengan HierarchyId properti:

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

Properti ini HierarchyId kemudian dapat digunakan untuk membuat simpul anak tanpa manipulasi string eksplisit. Contohnya:

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

Jika daisy memiliki HierarchyId , /4/1/3/1/maka, child1 akan mendapatkan HierarchyId "/4/1/3/1/1/", dan child2 akan mendapatkan HierarchyId "/4/1/3/1/2/".

Untuk membuat simpul antara kedua anak ini, sub-tingkat tambahan dapat digunakan. Contohnya:

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

Ini membuat simpul dengan HierarchyId , /4/1/3/1/1.5/menempatkannya bteween child1 dan child2.

Peningkatan ini dikontribusikan oleh @Rezakazemi890. Terima kasih banyak!

Alat

Lebih sedikit pembangunan ulang

Alat dotnet ef baris perintah secara default membangun proyek Anda sebelum menjalankan alat. Ini karena tidak membangun kembali sebelum menjalankan alat adalah sumber umum kebingungan ketika hal-hal tidak berfungsi. Pengembang berpengalaman dapat menggunakan --no-build opsi untuk menghindari build ini, yang mungkin lambat. Namun, bahkan opsi dapat --no-build menyebabkan proyek dibangun kembali saat berikutnya dibangun di luar alat EF.

Kami percaya kontribusi komunitas dari @Suchiman telah memperbaiki ini. Namun, kami juga sadar bahwa tweak di sekitar perilaku MSBuild memiliki kecenderungan untuk memiliki konsekuensi yang tidak diinginkan, jadi kami meminta orang-orang seperti Anda untuk mencoba ini dan melaporkan kembali pengalaman negatif apa pun yang Anda miliki.