Bagikan melalui


Yang Baru di EF Core 6.0

EF Core 6.0 telah dikirim ke NuGet. Halaman ini berisi gambaran umum perubahan menarik yang diperkenalkan dalam rilis ini.

Tip

Anda dapat menjalankan dan men-debug ke dalam sampel yang ditunjukkan di bawah ini dengan mengunduh kode sampel dari GitHub.

Tabel temporal SQL Server

Masalah GitHub: #4693.

Tabel temporal SQL Server secara otomatis melacak semua data yang pernah disimpan dalam tabel, bahkan setelah data tersebut diperbarui atau dihapus. Ini dicapai dengan membuat "tabel riwayat" paralel tempat data historis bertanda waktu disimpan setiap kali perubahan dilakukan pada tabel utama. Ini memungkinkan data historis untuk dikueri, seperti untuk audit, atau dipulihkan, seperti untuk pemulihan setelah mutasi atau penghapusan yang tidak disengaja.

EF Core sekarang mendukung:

  • Pembuatan tabel temporal menggunakan Migrasi
  • Transformasi tabel yang ada menjadi tabel temporal, sekali lagi menggunakan Migrasi
  • Mengkueri data historis
  • Memulihkan data dari beberapa titik di masa lalu

Mengonfigurasi tabel temporal

Pembuat model dapat digunakan untuk mengonfigurasi tabel sebagai temporal. Contohnya:

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

Saat menggunakan EF Core untuk membuat database, tabel baru akan dikonfigurasi sebagai tabel temporal dengan default SQL Server untuk tanda waktu dan tabel riwayat. Misalnya, pertimbangkan Employee jenis entitas:

public class Employee
{
    public Guid EmployeeId { get; set; }
    public string Name { get; set; }
    public string Position { get; set; }
    public string Department { get; set; }
    public string Address { get; set; }
    public decimal AnnualSalary { get; set; }
}

Tabel temporal yang dibuat akan terlihat seperti ini:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [PeriodEnd] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    [PeriodStart] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([PeriodStart], [PeriodEnd])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistory]))');

Perhatikan bahwa SQL Server membuat dua kolom tersembunyi datetime2 yang disebut PeriodEnd dan PeriodStart. "Kolom titik" ini mewakili rentang waktu di mana data dalam baris ada. Kolom ini dipetakan ke properti bayangan dalam model EF Core, memungkinkannya digunakan dalam kueri seperti yang ditunjukkan nanti.

Penting

Waktu dalam kolom ini selalu waktu UTC yang dihasilkan oleh SQL Server. Waktu UTC digunakan untuk semua operasi yang melibatkan tabel temporal, seperti dalam kueri yang ditunjukkan di bawah ini.

Perhatikan juga bahwa tabel riwayat terkait yang disebut EmployeeHistory dibuat secara otomatis. Nama kolom periode dan tabel riwayat dapat diubah dengan konfigurasi tambahan ke penyusun model. Contohnya:

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

Ini tercermin dalam tabel yang dibuat oleh SQL Server:

DECLARE @historyTableSchema sysname = SCHEMA_NAME()
EXEC(N'CREATE TABLE [Employees] (
    [EmployeeId] uniqueidentifier NOT NULL,
    [Name] nvarchar(100) NULL,
    [Position] nvarchar(100) NULL,
    [Department] nvarchar(100) NULL,
    [Address] nvarchar(1024) NULL,
    [AnnualSalary] decimal(10,2) NOT NULL,
    [ValidFrom] datetime2 GENERATED ALWAYS AS ROW START NOT NULL,
    [ValidTo] datetime2 GENERATED ALWAYS AS ROW END NOT NULL,
    CONSTRAINT [PK_Employees] PRIMARY KEY ([EmployeeId]),
    PERIOD FOR SYSTEM_TIME([ValidFrom], [ValidTo])
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = [' + @historyTableSchema + N'].[EmployeeHistoricalData]))');

Menggunakan tabel temporal

Sebagian besar waktu, tabel temporal digunakan sama seperti tabel lainnya. Artinya, kolom periode dan data historis ditangani secara transparan oleh SQL Server sehingga aplikasi dapat mengabaikannya. Misalnya, entitas baru dapat disimpan ke database dengan cara normal:

context.AddRange(
    new Employee
    {
        Name = "Pinky Pie",
        Address = "Sugarcube Corner, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Party Organizer",
        AnnualSalary = 100.0m
    },
    new Employee
    {
        Name = "Rainbow Dash",
        Address = "Cloudominium, Ponyville, Equestria",
        Department = "DevDiv",
        Position = "Ponyville weather patrol",
        AnnualSalary = 900.0m
    },
    new Employee
    {
        Name = "Fluttershy",
        Address = "Everfree Forest, Equestria",
        Department = "DevDiv",
        Position = "Animal caretaker",
        AnnualSalary = 30.0m
    });

context.SaveChanges();

Data ini kemudian dapat dikueri, diperbarui, dan dihapus dengan cara normal. Contohnya:

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

Selain itu, setelah kueri pelacakan normal, nilai dari kolom periode data saat ini dapat diakses dari entitas yang dilacak. Contohnya:

var employees = context.Employees.ToList();
foreach (var employee in employees)
{
    var employeeEntry = context.Entry(employee);
    var validFrom = employeeEntry.Property<DateTime>("ValidFrom").CurrentValue;
    var validTo = employeeEntry.Property<DateTime>("ValidTo").CurrentValue;

    Console.WriteLine($"  Employee {employee.Name} valid from {validFrom} to {validTo}");
}

Cetakan ini:

Starting data:
  Employee Pinky Pie valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Rainbow Dash valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM
  Employee Fluttershy valid from 8/26/2021 4:38:58 PM to 12/31/9999 11:59:59 PM

Perhatikan bahwa ValidTo kolom (secara default disebut PeriodEnd) berisi datetime2 nilai maks. Ini selalu terjadi untuk baris saat ini dalam tabel. Kolom ValidFrom (secara default disebut PeriodStart) berisi waktu UTC yang disisipkan baris.

Mengkueri data historis

EF Core mendukung kueri yang menyertakan data historis melalui beberapa operator kueri baru:

  • TemporalAsOf: Mengembalikan baris yang aktif (saat ini) pada waktu UTC yang diberikan. Ini adalah satu baris dari tabel atau tabel riwayat saat ini untuk kunci primer tertentu.
  • TemporalAll: Mengembalikan semua baris dalam data historis. Ini biasanya banyak baris dari tabel riwayat dan/atau tabel saat ini untuk kunci primer tertentu.
  • TemporalFromTo: Mengembalikan semua baris yang aktif di antara dua waktu UTC tertentu. Ini mungkin banyak baris dari tabel riwayat dan/atau tabel saat ini untuk kunci primer tertentu.
  • TemporalBetween: Sama seperti TemporalFromTo, kecuali bahwa baris disertakan yang menjadi aktif di batas atas.
  • TemporalContainedIn: Mengembalikan semua baris yang mulai aktif dan berakhir aktif di antara dua waktu UTC yang diberikan. Ini mungkin banyak baris dari tabel riwayat dan/atau tabel saat ini untuk kunci primer tertentu.

Catatan

Lihat dokumentasi tabel temporal SQL Server untuk informasi selengkapnya tentang baris mana yang disertakan untuk masing-masing operator ini.

Misalnya, setelah membuat beberapa pembaruan dan menghapus data kami, kita dapat menjalankan kueri menggunakan TemporalAll untuk melihat data historis:

var history = context
    .Employees
    .TemporalAll()
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

foreach (var pointInTime in history)
{
    Console.WriteLine(
        $"  Employee {pointInTime.Employee.Name} was '{pointInTime.Employee.Position}' from {pointInTime.ValidFrom} to {pointInTime.ValidTo}");
}

Perhatikan bagaimana EF. Metode properti dapat digunakan untuk mengakses nilai dari kolom periode. Ini digunakan dalam OrderBy klausul untuk mengurutkan data, lalu dalam proyeksi untuk menyertakan nilai-nilai ini dalam data yang dikembalikan.

Kueri ini mengembalikan data berikut:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM

Perhatikan bahwa baris terakhir yang dikembalikan berhenti aktif pada 26/8/2021 16:44:59 PM. Ini karena baris untuk Rainbow Dash dihapus dari tabel utama pada saat itu. Kita akan melihat nanti bagaimana data ini dapat dipulihkan.

Kueri serupa dapat ditulis menggunakan TemporalFromTo, , TemporalBetweenatau TemporalContainedIn. Contohnya:

var history = context
    .Employees
    .TemporalBetween(timeStamp2, timeStamp3)
    .Where(e => e.Name == "Rainbow Dash")
    .OrderBy(e => EF.Property<DateTime>(e, "ValidFrom"))
    .Select(
        e => new
        {
            Employee = e,
            ValidFrom = EF.Property<DateTime>(e, "ValidFrom"),
            ValidTo = EF.Property<DateTime>(e, "ValidTo")
        })
    .ToList();

Kueri ini mengembalikan baris berikut:

Historical data for Rainbow Dash between 8/26/2021 4:41:14 PM and 8/26/2021 4:42:44 PM:
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM

Memulihkan data historis

Seperti disebutkan di atas, Rainbow Dash dihapus dari Employees tabel. Ini jelas merupakan kesalahan, jadi mari kita kembali ke titik waktu dan memulihkan baris yang hilang dari waktu itu.

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

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

Kueri ini mengembalikan satu baris untuk Rainbow Dash seperti pada waktu UTC yang diberikan. Semua kueri yang menggunakan operator temporal tidak dilacak secara default, sehingga entitas yang dikembalikan di sini tidak dilacak. Ini masuk akal, karena saat ini tidak ada di tabel utama. Untuk memasukkan kembali entitas ke dalam tabel utama, kita cukup menandainya sebagai Added dan kemudian memanggil SaveChanges.

Setelah menyisipkan ulang baris Rainbow Dash, mengkueri data historis menunjukkan bahwa baris dipulihkan seperti pada waktu UTC yang diberikan:

Historical data for Rainbow Dash:
  Employee Rainbow Dash was 'Ponyville weather patrol' from 8/26/2021 4:38:58 PM to 8/26/2021 4:40:29 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:40:29 PM to 8/26/2021 4:41:59 PM
  Employee Rainbow Dash was 'Wonderbolt Reservist' from 8/26/2021 4:41:59 PM to 8/26/2021 4:43:29 PM
  Employee Rainbow Dash was 'Wonderbolt' from 8/26/2021 4:43:29 PM to 8/26/2021 4:44:59 PM
  Employee Rainbow Dash was 'Wonderbolt Trainee' from 8/26/2021 4:44:59 PM to 12/31/9999 11:59:59 PM

Bundel Migrasi

Masalah GitHub: #19693.

Migrasi EF Core digunakan untuk menghasilkan pembaruan skema database berdasarkan perubahan pada model EF. Pembaruan skema ini harus diterapkan pada waktu penyebaran aplikasi, seringkali sebagai bagian dari sistem integrasi berkelanjutan/penyebaran berkelanjutan (C.I./C.D.).

EF Core sekarang menyertakan cara baru untuk menerapkan pembaruan skema ini: bundel migrasi. Bundel migrasi adalah migrasi kecil yang dapat dieksekusi yang berisi migrasi dan kode yang diperlukan untuk menerapkan migrasi ini ke database.

Catatan

Lihat Memperkenalkan Bundel Migrasi Inti EF yang ramah DevOps di Blog .NET untuk diskusi yang lebih mendalam tentang migrasi, bundel, dan penyebaran.

Bundel migrasi dibuat menggunakan dotnet ef alat baris perintah. Pastikan Anda telah menginstal versi terbaru alat sebelum melanjutkan.

Bundel memerlukan migrasi untuk disertakan. Ini dibuat menggunakan dotnet ef migrations add seperti yang dijelaskan dalam dokumentasi migrasi. Setelah Anda memiliki migrasi yang siap disebarkan, buat bundel menggunakan dotnet ef migrations bundle. Contohnya:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Output adalah executable yang cocok untuk sistem operasi target Anda. Dalam kasus saya ini adalah Windows x64, jadi saya mendapatkan dihilangkan efbundle.exe di folder lokal saya. Menjalankan executable ini menerapkan migrasi yang terkandung di dalamnya:

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

Migrasi diterapkan ke database hanya jika belum diterapkan. Misalnya, menjalankan bundel yang sama lagi tidak melakukan apa-apa, karena tidak ada migrasi baru untuk diterapkan:

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

Namun, jika perubahan dilakukan pada model dan lebih banyak migrasi dihasilkan dengan dotnet ef migrations add, maka ini dapat dibundel ke dalam executable baru yang siap diterapkan. Contohnya:

PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add SecondMigration
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations add Number3
Build started...
Build succeeded.
Done. To undo this action, use 'ef migrations remove'
PS C:\local\AllTogetherNow\SixOh> dotnet ef migrations bundle --force
Build started...
Build succeeded.
Building bundle...
Done. Migrations Bundle: C:\local\AllTogetherNow\SixOh\efbundle.exe
PS C:\local\AllTogetherNow\SixOh>

Perhatikan bahwa --force opsi dapat digunakan untuk menimpa bundel yang ada dengan yang baru.

Menjalankan bundel baru ini menerapkan dua migrasi baru ini ke database:

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

Secara default, bundel menggunakan database string koneksi dari konfigurasi aplikasi Anda. Namun, database yang berbeda dapat dimigrasikan dengan melewati string koneksi pada baris perintah. Contohnya:

PS C:\local\AllTogetherNow\SixOh> .\efbundle.exe --connection "Data Source=(LocalDb)\MSSQLLocalDB;Database=SixOhProduction"
Applying migration '20210903083845_MyMigration'.
Applying migration '20210903084526_SecondMigration'.
Applying migration '20210903084538_Number3'.
Done.
PS C:\local\AllTogetherNow\SixOh>

Perhatikan bahwa kali ini ketiga migrasi diterapkan, karena belum ada yang diterapkan ke database produksi.

Opsi lain dapat diteruskan ke baris perintah. Beberapa opsi umum adalah:

  • --output untuk menentukan jalur file yang dapat dieksekusi untuk dibuat.
  • --context untuk menentukan jenis DbContext yang akan digunakan saat proyek berisi beberapa jenis konteks.
  • --project untuk menentukan proyek yang akan digunakan. Default ke direktori kerja saat ini.
  • --startup-project untuk menentukan proyek startup yang akan digunakan. Default ke direktori kerja saat ini.
  • --no-build untuk mencegah proyek dibangun sebelum menjalankan perintah. Ini hanya boleh digunakan jika proyek diketahui sudah diperbarui.
  • --verbose untuk melihat informasi terperinci tentang apa yang dilakukan perintah. Gunakan opsi ini saat menyertakan informasi dalam laporan bug.

Gunakan dotnet ef migrations bundle --help untuk melihat semua opsi yang tersedia.

Perhatikan bahwa secara default setiap migrasi diterapkan dalam transaksinya sendiri. Lihat masalah GitHub #22616 untuk diskusi tentang kemungkinan peningkatan di masa mendatang di area ini.

Konfigurasi model pra-konvensi

Masalah GitHub: #12229.

Versi EF Core sebelumnya mengharuskan pemetaan untuk setiap properti dari jenis tertentu dikonfigurasi secara eksplisit ketika pemetaan tersebut berbeda dari default. Ini termasuk "faset" seperti panjang maksimum string dan presisi desimal, serta konversi nilai untuk jenis properti.

Hal ini juga harus:

  • Konfigurasi pembuat model untuk setiap properti
  • Atribut pemetaan pada setiap properti
  • Perulangan eksplisit atas semua properti semua jenis entitas dan penggunaan API metadata tingkat rendah saat membangun model.

Perhatikan bahwa iterasi eksplisit rawan kesalahan dan sulit dilakukan dengan kuat karena daftar jenis entitas dan properti yang dipetakan mungkin tidak final pada saat iterasi ini terjadi.

EF Core 6.0 memungkinkan konfigurasi pemetaan ini ditentukan sekali untuk jenis tertentu. Kemudian akan diterapkan ke semua properti jenis tersebut dalam model. Ini disebut "konfigurasi model pra-konvensi", karena mengonfigurasi aspek model yang kemudian digunakan oleh konvensi pembuatan model. Konfigurasi tersebut diterapkan dengan mengambil alih ConfigureConventions pada :DbContext

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

Misalnya, pertimbangkan jenis entitas berikut:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsActive { get; set; }
    public Money AccountValue { get; set; }

    public Session CurrentSession { get; set; }

    public ICollection<Order> Orders { get; } = new List<Order>();
}

public class Order
{
    public int Id { get; set; }
    public string SpecialInstructions { get; set; }
    public DateTime OrderDate { get; set; }
    public bool IsComplete { get; set; }
    public Money Price { get; set; }
    public Money? Discount { get; set; }

    public Customer Customer { get; set; }
}

Semua properti string dapat dikonfigurasi menjadi ANSI (bukan Unicode) dan memiliki panjang maksimum 1024:

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

Semua properti DateTime dapat dikonversi ke bilangan bulat 64-bit dalam database, menggunakan konversi default dari DateTimes ke longs:

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

Semua properti bool dapat dikonversi ke bilangan 0 bulat atau 1 menggunakan salah satu pengonversi nilai bawaan:

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

Dengan asumsi Session adalah properti sementara entitas dan tidak boleh dipertahankan, itu dapat diabaikan di mana-mana dalam model:

configurationBuilder
    .IgnoreAny<Session>();

Konfigurasi model pra-konvensi sangat berguna saat bekerja dengan objek nilai. Misalnya, jenis Money dalam model di atas diwakili oleh struktur baca-saja:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Ini kemudian diserialisasikan ke dan dari JSON menggunakan pengonversi nilai kustom:

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

Pengonversi nilai ini dapat dikonfigurasi sekali untuk semua penggunaan Uang:

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

Perhatikan juga bahwa faset tambahan dapat ditentukan untuk kolom string tempat JSON serial disimpan. Dalam hal ini, kolom dibatasi hingga panjang maksimum 64.

Tabel yang dibuat untuk SQL Server menggunakan migrasi menunjukkan bagaimana konfigurasi telah diterapkan ke semua kolom yang dipetakan:

CREATE TABLE [Customers] (
    [Id] int NOT NULL IDENTITY,
    [Name] varchar(1024) NULL,
    [IsActive] int NOT NULL,
    [AccountValue] nvarchar(64) NOT NULL,
    CONSTRAINT [PK_Customers] PRIMARY KEY ([Id])
);
CREATE TABLE [Order] (
    [Id] int NOT NULL IDENTITY,
    [SpecialInstructions] varchar(1024) NULL,
    [OrderDate] bigint NOT NULL,
    [IsComplete] int NOT NULL,
    [Price] nvarchar(64) NOT NULL,
    [Discount] nvarchar(64) NULL,
    [CustomerId] int NULL,
    CONSTRAINT [PK_Order] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Order_Customers_CustomerId] FOREIGN KEY ([CustomerId]) REFERENCES [Customers] ([Id])
);

Dimungkinkan juga untuk menentukan pemetaan jenis default untuk jenis tertentu. Contohnya:

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

Ini jarang diperlukan, tetapi dapat berguna jika jenis digunakan dalam kueri dengan cara yang tidak terkait dengan properti model yang dipetakan.

Catatan

Lihat Mengumumkan Entity Framework Core 6.0 Preview 6: Mengonfigurasi Konvensi di Blog .NET untuk diskusi dan contoh konfigurasi model pra-konvensi lebih lanjut.

Model yang dikompilasi

Masalah GitHub: #1906.

Model yang dikompilasi dapat meningkatkan waktu mulai EF Core untuk aplikasi dengan model besar. Model besar biasanya berarti 100 hingga 1000-an jenis dan hubungan entitas.

Waktu mulai berarti waktu untuk melakukan operasi pertama pada DbContext ketika jenis DbContext digunakan untuk pertama kalinya dalam aplikasi. Perhatikan bahwa hanya membuat instans DbContext tidak menyebabkan model EF diinisialisasi. Sebagai gantinya, operasi pertama umum yang menyebabkan model diinisialisasi termasuk memanggil DbContext.Add atau menjalankan kueri pertama.

Model yang dikompilasi dibuat menggunakan dotnet ef alat baris perintah. Pastikan Anda telah menginstal versi terbaru alat sebelum melanjutkan.

Perintah baru dbcontext optimize digunakan untuk menghasilkan model yang dikompilasi. Contohnya:

dotnet ef dbcontext optimize

Opsi --output-dir dan --namespace dapat digunakan untuk menentukan direktori dan namespace tempat model yang dikompilasi akan dihasilkan. Contohnya:

PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels> dotnet ef dbcontext optimize --output-dir MyCompiledModels --namespace MyCompiledModels
Build started...
Build succeeded.
Successfully generated a compiled model, to use it call 'options.UseModel(MyCompiledModels.BlogsContextModel.Instance)'. Run this command again when the model is modified.
PS C:\dotnet\efdocs\samples\core\Miscellaneous\CompiledModels>

Output dari menjalankan perintah ini mencakup sepotong kode untuk disalin dan ditempelkan ke konfigurasi DbContext Anda untuk menyebabkan EF Core menggunakan model yang dikompilasi. Contohnya:

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

Bootstrapping model yang dikompilasi

Biasanya tidak perlu melihat kode bootstrapping yang dihasilkan. Namun, terkadang dapat berguna untuk menyesuaikan model atau pemuatannya. Kode bootstrapping terlihat seperti ini:

[DbContext(typeof(BlogsContext))]
partial class BlogsContextModel : RuntimeModel
{
    private static BlogsContextModel _instance;
    public static IModel Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new BlogsContextModel();
                _instance.Initialize();
                _instance.Customize();
            }

            return _instance;
        }
    }

    partial void Initialize();

    partial void Customize();
}

Ini adalah kelas parsial dengan metode parsial yang dapat diimplementasikan untuk menyesuaikan model sesuai kebutuhan.

Selain itu, beberapa model yang dikompilasi dapat dihasilkan untuk jenis DbContext yang dapat menggunakan model yang berbeda tergantung pada beberapa konfigurasi runtime. Ini harus ditempatkan ke dalam folder dan namespace yang berbeda, seperti yang ditunjukkan di atas. Informasi runtime, seperti string koneksi, kemudian dapat diperiksa dan model yang benar dikembalikan sesuai kebutuhan. Contohnya:

public static class RuntimeModelCache
{
    private static readonly ConcurrentDictionary<string, IModel> _runtimeModels
        = new();

    public static IModel GetOrCreateModel(string connectionString)
        => _runtimeModels.GetOrAdd(
            connectionString, cs =>
            {
                if (cs.Contains("X"))
                {
                    return BlogsContextModel1.Instance;
                }

                if (cs.Contains("Y"))
                {
                    return BlogsContextModel2.Instance;
                }

                throw new InvalidOperationException("No appropriate compiled model found.");
            });
}

Batasan

Model yang dikompilasi memiliki beberapa batasan:

Karena keterbatasan ini, Anda hanya boleh menggunakan model yang dikompilasi jika waktu mulai EF Core Anda terlalu lambat. Mengkompilasi model kecil biasanya tidak sepadan.

Jika mendukung salah satu fitur ini sangat penting untuk keberhasilan Anda, pilih masalah yang sesuai yang ditautkan di atas.

Tolak ukur

Tip

Anda dapat mencoba mengompilasi model besar dan menjalankan tolok ukur di atasnya dengan mengunduh kode sampel dari GitHub.

Model dalam repositori GitHub yang direferensikan di atas berisi 449 jenis entitas, 6390 properti, dan 720 hubungan. Ini adalah model yang cukup besar. Menggunakan BenchmarkDotNet untuk mengukur, waktu rata-rata untuk kueri pertama adalah 1,02 detik pada laptop yang cukup kuat. Menggunakan model yang dikompilasi menurunkan ini ke 117 milidetik pada perangkat keras yang sama. Peningkatan 8x hingga 10x seperti ini tetap relatif konstan saat ukuran model meningkat.

Compiled model performance improvement

Catatan

Lihat Mengumumkan Entity Framework Core 6.0 Preview 5: Model yang Dikompilasi di Blog .NET untuk diskusi yang lebih mendalam tentang performa startup EF Core dan model yang dikompilasi.

Peningkatan performa pada TechEmpower Fortunes

Masalah GitHub: #23611.

Kami melakukan peningkatan signifikan pada performa kueri untuk EF Core 6.0. Khususnya:

  • Performa EF Core 6.0 sekarang 70% lebih cepat pada tolok ukur TechEmpower Fortunes standar industri, dibandingkan dengan 5,0.
    • Ini adalah peningkatan perf tumpukan penuh, termasuk peningkatan dalam kode tolok ukur, runtime .NET, dll.
  • EF Core 6.0 sendiri 31% lebih cepat mengeksekusi kueri yang tidak terlacak.
  • Alokasi timbunan telah dikurangi sebesar 43% saat menjalankan kueri.

Setelah peningkatan ini, kesenjangan antara Dapper "micro-ORM" yang populer dan EF Core dalam tolok ukur TechEmpower Fortunes dipersempit dari 55% menjadi sekitar sedikit di bawah 5%.

Catatan

Lihat Mengumumkan Entity Framework Core 6.0 Pratinjau 4: Edisi Performa di Blog .NET untuk diskusi terperinci tentang peningkatan performa kueri di EF Core 6.0.

Penyempurnaan penyedia Azure Cosmos DB

EF Core 6.0 berisi banyak peningkatan pada penyedia database Azure Cosmos DB.

Tip

Anda dapat menjalankan dan men-debug ke semua sampel khusus Cosmos dengan mengunduh kode sampel dari GitHub.

Default ke kepemilikan implisit

Masalah GitHub: #24803.

Saat membangun model untuk penyedia Azure Cosmos DB, EF Core 6.0 akan menandai jenis entitas anak sebagai milik entitas induk mereka secara default. Ini menghapus kebutuhan akan banyak OwnsMany panggilan dan OwnsOne dalam model Azure Cosmos DB. Ini membuatnya lebih mudah untuk menyematkan jenis anak ke dalam dokumen untuk jenis induk, yang biasanya merupakan cara yang tepat untuk memodelkan orang tua dan anak dalam database dokumen.

Misalnya, pertimbangkan jenis entitas ini:

public class Family
{
    [JsonPropertyName("id")]
    public string Id { get; set; }

    public string LastName { get; set; }
    public bool IsRegistered { get; set; }

    public Address Address { get; set; }

    public IList<Parent> Parents { get; } = new List<Parent>();
    public IList<Child> Children { get; } = new List<Child>();
}

public class Parent
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
}

public class Child
{
    public string FamilyName { get; set; }
    public string FirstName { get; set; }
    public int Grade { get; set; }

    public string Gender { get; set; }

    public IList<Pet> Pets { get; } = new List<Pet>();
}

Dalam EF Core 5.0, jenis ini akan dimodelkan untuk Azure Cosmos DB dengan konfigurasi berikut:

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

modelBuilder.Entity<Family>()
    .OwnsMany(f => f.Children)
    .OwnsMany(c => c.Pets);

modelBuilder.Entity<Family>()
    .OwnsOne(f => f.Address);

Dalam EF Core 6.0, kepemilikannya implisit, mengurangi konfigurasi model menjadi:

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

Dokumen Azure Cosmos DB yang dihasilkan memiliki orang tua, anak, hewan peliharaan, dan alamat keluarga yang disematkan dalam dokumen keluarga. Contoh:

{
  "Id": "Wakefield.7",
  "LastName": "Wakefield",
  "Discriminator": "Family",
  "IsRegistered": true,
  "id": "Family|Wakefield.7",
  "Address": {
    "City": "NY",
    "County": "Manhattan",
    "State": "NY"
  },
  "Children": [
    {
      "FamilyName": "Merriam",
      "FirstName": "Jesse",
      "Gender": "female",
      "Grade": 8,
      "Pets": [
        {
          "GivenName": "Goofy"
        },
        {
          "GivenName": "Shadow"
        }
      ]
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Lisa",
      "Gender": "female",
      "Grade": 1,
      "Pets": []
    }
  ],
  "Parents": [
    {
      "FamilyName": "Wakefield",
      "FirstName": "Robin"
    },
    {
      "FamilyName": "Miller",
      "FirstName": "Ben"
    }
  ],
  "_rid": "x918AKh6p20CAAAAAAAAAA==",
  "_self": "dbs/x918AA==/colls/x918AKh6p20=/docs/x918AKh6p20CAAAAAAAAAA==/",
  "_etag": "\"00000000-0000-0000-adee-87f30c8c01d7\"",
  "_attachments": "attachments/",
  "_ts": 1632121802
}

Catatan

Penting untuk diingat OwnsOne/OwnsMany bahwa konfigurasi harus digunakan jika Anda perlu mengonfigurasi lebih lanjut jenis yang dimiliki ini.

Koleksi jenis primitif

Masalah GitHub: #14762.

EF Core 6.0 secara asli memetakan koleksi jenis primitif saat menggunakan penyedia database Azure Cosmos DB. Misalnya, pertimbangkan jenis entitas ini:

public class Book
{
    public Guid Id { get; set; }
    public string Title { get; set; }
    public IList<string> Quotes { get; set; }
    public IDictionary<string, string> Notes { get; set; }
}

Daftar dan kamus dapat diisi dan disisipkan ke dalam database dengan cara konvensional:

using var context = new BooksContext();

var book = new Book
{
    Title = "How It Works: Incredible History",
    Quotes = new List<string>
    {
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    },
    Notes = new Dictionary<string, string>
    {
        { "121", "Fridges" },
        { "144", "Peter Higgs" },
        { "48", "Saint Mark's Basilica" },
        { "36", "The Terracotta Army" }
    }
};

context.Add(book);
await context.SaveChangesAsync();

Ini menghasilkan dokumen JSON berikut:

{
    "Id": "0b32283e-22a8-4103-bb4f-6052604868bd",
    "Discriminator": "Book",
    "Notes": {
        "36": "The Terracotta Army",
        "48": "Saint Mark's Basilica",
        "121": "Fridges",
        "144": "Peter Higgs"
    },
    "Quotes": [
        "Thomas (Tommy) Flowers was the British engineer behind the design of the Colossus computer.",
        "Invented originally for Guinness, plastic widgets are nitrogen-filled spheres.",
        "For 20 years after its introduction in 1979, the Walkman dominated the personal stereo market."
    ],
    "Title": "How It Works: Incredible History",
    "id": "Book|0b32283e-22a8-4103-bb4f-6052604868bd",
    "_rid": "t-E3AIxaencBAAAAAAAAAA==",
    "_self": "dbs/t-E3AA==/colls/t-E3AIxaenc=/docs/t-E3AIxaencBAAAAAAAAAA==/",
    "_etag": "\"00000000-0000-0000-9b50-fc769dc901d7\"",
    "_attachments": "attachments/",
    "_ts": 1630075016
}

Koleksi ini kemudian dapat diperbarui, sekali lagi dengan cara konvensional:

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

await context.SaveChangesAsync();

Keterbatasan:

  • Hanya kamus dengan kunci string yang akan didukung
  • Mengkueri konten koleksi primitif saat ini tidak didukung. Pilih #16926, #25700, dan #25701 jika fitur ini penting bagi Anda.

Terjemahan ke fungsi bawaan

Masalah GitHub: #16143.

Penyedia Azure Cosmos DB sekarang menerjemahkan lebih banyak metode Pustaka Kelas Dasar (BCL) ke fungsi bawaan Azure Cosmos DB. Tabel berikut menunjukkan terjemahan yang baru di EF Core 6.0.

Terjemahan string

Metode BCL Fungsi bawaan Catatan
String.Length LENGTH
String.ToLower LOWER
String.TrimStart LTRIM
String.TrimEnd RTRIM
String.Trim TRIM
String.ToUpper UPPER
String.Substring SUBSTRING
+ operator CONCAT
String.IndexOf INDEX_OF
String.Replace REPLACE
String.Equals STRINGEQUAL Hanya panggilan yang tidak peka huruf besar/kecil

Terjemahan untuk LOWER, , RTRIMLTRIM, TRIM, UPPER, dan SUBSTRING dikontribusikan oleh @Marusyk. Terima kasih banyak!

Misalnya:

var stringResults = await context.Triangles.Where(
        e => e.Name.Length > 4
             && e.Name.Trim().ToLower() != "obtuse"
             && e.Name.TrimStart().Substring(2, 2).Equals("uT", StringComparison.OrdinalIgnoreCase))
    .ToListAsync();

Yang diterjemahkan ke:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((LENGTH(c["Name"]) > 4) AND (LOWER(TRIM(c["Name"])) != "obtuse")) AND STRINGEQUALS(SUBSTRING(LTRIM(c["Name"]), 2, 2), "uT", true)))

Terjemahan matematika

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

Terjemahan ini dikontribusikan oleh @Marusyk. Terima kasih banyak!

Misalnya:

var hypotenuse = 42.42;
var mathResults = await context.Triangles.Where(
        e => (Math.Round(e.Angle1) == 90.0
              || Math.Round(e.Angle2) == 90.0)
             && (hypotenuse * Math.Sin(e.Angle1) > 30.0
                 || hypotenuse * Math.Cos(e.Angle2) > 30.0))
    .ToListAsync();

Yang diterjemahkan ke:

SELECT c
FROM root c
WHERE ((c["Discriminator"] = "Triangle") AND (((ROUND(c["Angle1"]) = 90.0) OR (ROUND(c["Angle2"]) = 90.0)) AND (((@__hypotenuse_0 * SIN(c["Angle1"])) > 30.0) OR ((@__hypotenuse_0 * COS(c["Angle2"])) > 30.0))))

Terjemahan DateTime

Metode BCL Fungsi bawaan
DateTime.UtcNow GetCurrentDateTime

Terjemahan ini dikontribusikan oleh @Marusyk. Terima kasih banyak!

Misalnya:

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

Yang diterjemahkan ke:

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

Kueri SQL mentah dengan FromSql

Masalah GitHub: #17311.

Terkadang perlu untuk menjalankan kueri SQL mentah alih-alih menggunakan LINQ. Ini sekarang didukung dengan penyedia Azure Cosmos DB melalui penggunaan FromSql metode . Ini bekerja dengan cara yang sama seperti yang selalu dilakukan dengan penyedia relasional. Contohnya:

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

Yang dijalankan sebagai:

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

Kueri yang berbeda

Masalah GitHub: #16144.

Kueri sederhana yang menggunakan Distinct sekarang diterjemahkan. Contohnya:

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

Yang diterjemahkan ke:

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

Diagnostik

Masalah GitHub: #17298.

Penyedia Azure Cosmos DB sekarang mencatat lebih banyak informasi diagnostik, termasuk peristiwa untuk menyisipkan, mengkueri, memperbarui, dan menghapus data dari database. Unit permintaan (RU) disertakan dalam peristiwa ini kapan pun sesuai.

Catatan

Log menunjukkan penggunaan EnableSensitiveDataLogging() di sini sehingga nilai ID ditampilkan.

Menyisipkan item ke dalam database Azure Cosmos DB menghasilkan peristiwa tersebut CosmosEventId.ExecutedCreateItem . Misalnya, kode ini:

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

Mencatat peristiwa diagnostik berikut:

info: 8/30/2021 14:41:13.356 CosmosEventId.ExecutedCreateItem[30104] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed CreateItem (5 ms, 7.43 RU) ActivityId='417db46f-fcdd-49d9-a7f0-77210cd06f84', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Mengambil item dari database Azure Cosmos DB menggunakan kueri menghasilkan CosmosEventId.ExecutingSqlQuery peristiwa, lalu satu atau beberapa CosmosEventId.ExecutedReadNext peristiwa untuk item yang dibaca. Misalnya, kode ini:

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

Mencatat peristiwa diagnostik berikut:

info: 8/30/2021 14:41:13.475 CosmosEventId.ExecutingSqlQuery[30100] (Microsoft.EntityFrameworkCore.Database.Command)
      Executing SQL query for container 'Shapes' in partition '(null)' [Parameters=[]]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2
info: 8/30/2021 14:41:13.651 CosmosEventId.ExecutedReadNext[30102] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadNext (169.6126 ms, 2.93 RU) ActivityId='4e465fae-3d49-4c1f-bd04-142bc5d0b0a1', Container='Shapes', Partition='(null)', Parameters=[]
      SELECT c
      FROM root c
      WHERE ((c["Discriminator"] = "Triangle") AND (c["id"] = "Equilateral"))
      OFFSET 0 LIMIT 2

Mengambil satu item dari database Azure Cosmos DB menggunakan Find dengan kunci partisi menghasilkan CosmosEventId.ExecutingReadItem peristiwa dan CosmosEventId.ExecutedReadItem . Misalnya, kode ini:

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

Mencatat peristiwa diagnostik berikut:

info: 8/30/2021 14:53:39.326 CosmosEventId.ExecutingReadItem[30101] (Microsoft.EntityFrameworkCore.Database.Command)
      Reading resource 'Isosceles' item from container 'Shapes' in partition 'TrianglesPartition'.
info: 8/30/2021 14:53:39.330 CosmosEventId.ExecutedReadItem[30103] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReadItem (1 ms, 1 RU) ActivityId='3c278643-4e7f-4bb2-9953-6055b5f1288f', Container='Shapes', Id='Isosceles', Partition='TrianglesPartition'

Menyimpan item yang diperbarui ke database Azure Cosmos DB menghasilkan peristiwa tersebut CosmosEventId.ExecutedReplaceItem . Misalnya, kode ini:

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

Mencatat peristiwa diagnostik berikut:

info: 8/30/2021 14:53:39.343 CosmosEventId.ExecutedReplaceItem[30105] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed ReplaceItem (6 ms, 10.67 RU) ActivityId='1525b958-fea1-49e8-89f9-d429d0351fdb', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Menghapus item dari database Azure Cosmos DB menghasilkan peristiwa tersebut CosmosEventId.ExecutedDeleteItem . Misalnya, kode ini:

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

Mencatat peristiwa diagnostik berikut:

info: 8/30/2021 14:53:39.359 CosmosEventId.ExecutedDeleteItem[30106] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DeleteItem (6 ms, 7.43 RU) ActivityId='cbc54463-405b-48e7-8c32-2c6502a4138f', Container='Shapes', Id='Impossible', Partition='TrianglesPartition'

Mengonfigurasi throughput

Masalah GitHub: #17301.

Model Azure Cosmos DB sekarang dapat dikonfigurasi dengan throughput manual atau skala otomatis. Nilai-nilai ini menyediakan throughput pada database. Contohnya:

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

Selain itu, jenis entitas individual dapat dikonfigurasi untuk menyediakan throughput untuk kontainer yang sesuai. Contohnya:

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

Mengonfigurasi time-to-live

Masalah GitHub: #17307.

Jenis entitas dalam model Azure Cosmos DB sekarang dapat dikonfigurasi dengan time-to-live default dan time-to-live untuk penyimpanan analitik. Contohnya:

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

Mengatasi pabrik klien HTTP

Masalah GitHub: #21274. Fitur ini dikontribusikan oleh @dnperfors. Terima kasih banyak!

Yang HttpClientFactory digunakan oleh penyedia Azure Cosmos DB sekarang dapat diatur secara eksplisit. Ini bisa sangat berguna selama pengujian, misalnya untuk melewati validasi sertifikat saat menggunakan emulator Azure Cosmos DB di Linux:

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

Catatan

Lihat Mengambil Penyedia EF Core Azure Cosmos DB untuk Uji Coba di Blog .NET untuk contoh terperinci tentang menerapkan peningkatan penyedia Azure Cosmos DB ke aplikasi yang ada.

Penyempurnaan perancah dari database yang sudah ada

EF Core 6.0 berisi beberapa peningkatan saat merekayasa balik model EF dari database yang ada.

Perancah hubungan banyak-ke-banyak

Masalah GitHub: #22475.

EF Core 6.0 mendeteksi tabel gabungan sederhana dan secara otomatis menghasilkan pemetaan banyak ke banyak untuk mereka. Misalnya, pertimbangkan tabel untuk Posts dan Tags, dan tabel PostTag gabungan yang menghubungkannya:

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

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]));

CREATE TABLE [PostTag] (
    [PostsId] int NOT NULL,
    [TagsId] int NOT NULL,
    CONSTRAINT [PK_PostTag] PRIMARY KEY ([PostsId], [TagsId]),
    CONSTRAINT [FK_PostTag_Posts_TagsId] FOREIGN KEY ([TagsId]) REFERENCES [Tags] ([Id]) ON DELETE CASCADE,
    CONSTRAINT [FK_PostTag_Tags_PostsId] FOREIGN KEY ([PostsId]) REFERENCES [Posts] ([Id]) ON DELETE CASCADE);

Tabel ini dapat diacak dari baris perintah. Contohnya:

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

Ini menghasilkan kelas untuk Post:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Dan kelas untuk Tag:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Tapi tidak ada kelas untuk PostTag meja. Sebagai gantinya, konfigurasi untuk hubungan banyak ke banyak di-scaffold:

entity.HasMany(d => d.Tags)
    .WithMany(p => p.Posts)
    .UsingEntity<Dictionary<string, object>>(
        "PostTag",
        l => l.HasOne<Tag>().WithMany().HasForeignKey("PostsId"),
        r => r.HasOne<Post>().WithMany().HasForeignKey("TagsId"),
        j =>
            {
                j.HasKey("PostsId", "TagsId");
                j.ToTable("PostTag");
                j.HasIndex(new[] { "TagsId" }, "IX_PostTag_TagsId");
            });

Jenis referensi scaffold C# nullable

Masalah GitHub: #15520.

EF Core 6.0 sekarang membuat perancah model EF dan jenis entitas yang menggunakan jenis referensi nullable C# (NRTs). Penggunaan NRT di-scaffold secara otomatis ketika dukungan NRT diaktifkan dalam proyek C# tempat kode sedang dibuat perancahnya.

Misalnya, tabel berikut berisi Tags kedua kolom string yang tidak dapat diubah ke null:

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

Ini menghasilkan properti string nullable dan non-nullable yang sesuai di kelas yang dihasilkan:

public partial class Tag
{
    public Tag()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;
    public string? Description { get; set; }

    public virtual ICollection<Post> Posts { get; set; }
}

Demikian pula, tabel berikut berisi Posts hubungan yang diperlukan ke Blogs tabel:

CREATE TABLE [Posts] (
    [Id] int NOT NULL IDENTITY,
    [Title] nvarchar(max) NOT NULL,
    [Contents] nvarchar(max) NOT NULL,
    [PostedOn] datetime2 NOT NULL,
    [UpdatedOn] datetime2 NULL,
    [BlogId] int NOT NULL,
    CONSTRAINT [PK_Posts] PRIMARY KEY ([Id]),
    CONSTRAINT [FK_Posts_Blogs_BlogId] FOREIGN KEY ([BlogId]) REFERENCES [Blogs] ([Id]));

Ini menghasilkan perancah hubungan yang tidak dapat diubah ke null (diperlukan) antara blog:

public partial class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }

    public int Id { get; set; }
    public string Name { get; set; } = null!;

    public virtual ICollection<Post> Posts { get; set; }
}

Dan posting:

public partial class Post
{
    public Post()
    {
        Tags = new HashSet<Tag>();
    }

    public int Id { get; set; }
    public string Title { get; set; } = null!;
    public string Contents { get; set; } = null!;
    public DateTime PostedOn { get; set; }
    public DateTime? UpdatedOn { get; set; }
    public int BlogId { get; set; }

    public virtual Blog Blog { get; set; } = null!;

    public virtual ICollection<Tag> Tags { get; set; }
}

Terakhir, properti DbSet di DbContext yang dihasilkan dibuat dengan cara yang ramah NRT. Contohnya:

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

Komentar database di-scaffolded ke komentar kode

Masalah GitHub: #19113. Fitur ini dikontribusikan oleh @ErikEJ. Terima kasih banyak!

Komentar pada tabel dan kolom SQL sekarang dibuat ke dalam jenis entitas yang dibuat saat reverse-engineering model EF Core dari database SQL Server yang ada.

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

Penyempurnaan kueri LINQ

EF Core 6.0 berisi beberapa peningkatan dalam terjemahan dan eksekusi kueri LINQ.

Dukungan GroupBy yang disempurnakan

Masalah GitHub: #12088, #13805, dan #22609.

EF Core 6.0 berisi dukungan yang lebih baik untuk GroupBy kueri. Secara khusus, EF Core sekarang:

  • Menerjemahkan GroupBy diikuti oleh FirstOrDefault (atau serupa) melalui grup
  • Mendukung pemilihan hasil N teratas dari grup
  • Memperluas navigasi setelah GroupBy operator diterapkan

Berikut ini adalah contoh kueri dari laporan pelanggan dan terjemahannya di SQL Server.

Contoh 1:

var people = context.People
    .Include(e => e.Shoes)
    .GroupBy(e => e.FirstName)
    .Select(
        g => g.OrderBy(e => e.FirstName)
            .ThenBy(e => e.LastName)
            .FirstOrDefault())
    .ToList();
SELECT [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t].[FirstName], [s].[Id], [s].[Age], [s].[PersonId], [s].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName], [p0].[LastName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
LEFT JOIN [Shoes] AS [s] ON [t0].[Id] = [s].[PersonId]
ORDER BY [t].[FirstName], [t0].[FirstName]

Contoh 2:

var group = context.People
    .Select(
        p => new
        {
            p.FirstName,
            FullName = p.FirstName + " " + p.MiddleInitial + " " + p.LastName
        })
    .GroupBy(p => p.FirstName)
    .Select(g => g.First())
    .First();
SELECT [t0].[FirstName], [t0].[FullName], [t0].[c]
FROM (
    SELECT TOP(1) [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[FirstName], [t1].[FullName], [t1].[c]
    FROM (
        SELECT [p0].[FirstName], (((COALESCE([p0].[FirstName], N'') + N' ') + COALESCE([p0].[MiddleInitial], N'')) + N' ') + COALESCE([p0].[LastName], N'') AS [FullName], 1 AS [c], ROW_NUMBER() OVER(PARTITION BY [p0].[FirstName] ORDER BY [p0].[FirstName]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]

Contoh 3:

var people = context.People
    .Where(e => e.MiddleInitial == "Q" && e.Age == 20)
    .GroupBy(e => e.LastName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e.Length)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))
FROM [People] AS [p]
WHERE ([p].[MiddleInitial] = N'Q') AND ([p].[Age] = 20)
GROUP BY [p].[LastName]
ORDER BY CAST(LEN((
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE (([p1].[MiddleInitial] = N'Q') AND ([p1].[Age] = 20)) AND (([p].[LastName] = [p1].[LastName]) OR ([p].[LastName] IS NULL AND [p1].[LastName] IS NULL)))) AS int)

Contoh 4:

var results = (from person in context.People
               join shoes in context.Shoes on person.Age equals shoes.Age
               group shoes by shoes.Style
               into people
               select new
               {
                   people.Key,
                   Style = people.Select(p => p.Style).FirstOrDefault(),
                   Count = people.Count()
               })
    .ToList();
SELECT [s].[Style] AS [Key], (
    SELECT TOP(1) [s0].[Style]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
    WHERE ([s].[Style] = [s0].[Style]) OR ([s].[Style] IS NULL AND [s0].[Style] IS NULL)) AS [Style], COUNT(*) AS [Count]
FROM [People] AS [p]
INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
GROUP BY [s].[Style]

Contoh 5:

var results = context.People
    .GroupBy(e => e.FirstName)
    .Select(g => g.First().LastName)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))
FROM [People] AS [p]
GROUP BY [p].[FirstName]
ORDER BY (
    SELECT TOP(1) [p1].[LastName]
    FROM [People] AS [p1]
    WHERE ([p].[FirstName] = [p1].[FirstName]) OR ([p].[FirstName] IS NULL AND [p1].[FirstName] IS NULL))

Contoh 6:

var results = context.People.Where(e => e.Age == 20)
    .GroupBy(e => e.Id)
    .Select(g => g.First().MiddleInitial)
    .OrderBy(e => e)
    .ToList();
SELECT (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))
FROM [People] AS [p]
WHERE [p].[Age] = 20
GROUP BY [p].[Id]
ORDER BY (
    SELECT TOP(1) [p1].[MiddleInitial]
    FROM [People] AS [p1]
    WHERE ([p1].[Age] = 20) AND ([p].[Id] = [p1].[Id]))

Contoh 7:

var size = 11;
var results
    = context.People
        .Where(
            p => p.Feet.Size == size
                 && p.MiddleInitial != null
                 && p.Feet.Id != 1)
        .GroupBy(
            p => new
            {
                p.Feet.Size,
                p.Feet.Person.LastName
            })
        .Select(
            g => new
            {
                g.Key.LastName,
                g.Key.Size,
                Min = g.Min(p => p.Feet.Size),
            })
        .ToList();
Executed DbCommand (12ms) [Parameters=[@__size_0='11'], CommandType='Text', CommandTimeout='30']
SELECT [p0].[LastName], [f].[Size], MIN([f0].[Size]) AS [Min]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
LEFT JOIN [People] AS [p0] ON [f].[Id] = [p0].[Id]
LEFT JOIN [Feet] AS [f0] ON [p].[Id] = [f0].[Id]
WHERE (([f].[Size] = @__size_0) AND [p].[MiddleInitial] IS NOT NULL) AND (([f].[Id] <> 1) OR [f].[Id] IS NULL)
GROUP BY [f].[Size], [p0].[LastName]

Contoh 8:

var result = context.People
    .Include(x => x.Shoes)
    .Include(x => x.Feet)
    .GroupBy(
        x => new
        {
            x.Feet.Id,
            x.Feet.Size
        })
    .Select(
        x => new
        {
            Key = x.Key.Id + x.Key.Size,
            Count = x.Count(),
            Sum = x.Sum(el => el.Id),
            SumOver60 = x.Sum(el => el.Id) / (decimal)60,
            TotalCallOutCharges = x.Sum(el => el.Feet.Size == 11 ? 1 : 0)
        })
    .Count();
SELECT COUNT(*)
FROM (
    SELECT [f].[Id], [f].[Size]
    FROM [People] AS [p]
    LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
    GROUP BY [f].[Id], [f].[Size]
) AS [t]

Contoh 9:

var results = context.People
    .GroupBy(n => n.FirstName)
    .Select(g => new
    {
        Feet = g.Key,
        Total = g.Sum(n => n.Feet.Size)
    })
    .ToList();
SELECT [p].[FirstName] AS [Feet], COALESCE(SUM([f].[Size]), 0) AS [Total]
FROM [People] AS [p]
LEFT JOIN [Feet] AS [f] ON [p].[Id] = [f].[Id]
GROUP BY [p].[FirstName]

Contoh 10:

var results = from Person person1
                  in from Person person2
                         in context.People
                     select person2
              join Shoes shoes
                  in context.Shoes
                  on person1.Age equals shoes.Age
              group shoes by
                  new
                  {
                      person1.Id,
                      shoes.Style,
                      shoes.Age
                  }
              into temp
              select
                  new
                  {
                      temp.Key.Id,
                      temp.Key.Age,
                      temp.Key.Style,
                      Values = from t
                                   in temp
                               select
                                   new
                                   {
                                       t.Id,
                                       t.Style,
                                       t.Age
                                   }
                  };
SELECT [t].[Id], [t].[Age], [t].[Style], [t0].[Id], [t0].[Style], [t0].[Age], [t0].[Id0]
FROM (
    SELECT [p].[Id], [s].[Age], [s].[Style]
    FROM [People] AS [p]
    INNER JOIN [Shoes] AS [s] ON [p].[Age] = [s].[Age]
    GROUP BY [p].[Id], [s].[Style], [s].[Age]
) AS [t]
LEFT JOIN (
    SELECT [s0].[Id], [s0].[Style], [s0].[Age], [p0].[Id] AS [Id0]
    FROM [People] AS [p0]
    INNER JOIN [Shoes] AS [s0] ON [p0].[Age] = [s0].[Age]
) AS [t0] ON (([t].[Id] = [t0].[Id0]) AND (([t].[Style] = [t0].[Style]) OR ([t].[Style] IS NULL AND [t0].[Style] IS NULL))) AND ([t].[Age] = [t0].[Age])
ORDER BY [t].[Id], [t].[Style], [t].[Age], [t0].[Id0]

Contoh 11:

var grouping = context.People
    .GroupBy(i => i.LastName)
    .Select(g => new { LastName = g.Key, Count = g.Count() , First = g.FirstOrDefault(), Take = g.Take(2)})
    .OrderByDescending(e => e.LastName)
    .ToList();
SELECT [t].[LastName], [t].[c], [t0].[Id], [t2].[Id], [t2].[Age], [t2].[FirstName], [t2].[LastName], [t2].[MiddleInitial], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial]
FROM (
    SELECT [p].[LastName], COUNT(*) AS [c]
    FROM [People] AS [p]
    GROUP BY [p].[LastName]
) AS [t]
LEFT JOIN (
    SELECT [t1].[Id], [t1].[Age], [t1].[FirstName], [t1].[LastName], [t1].[MiddleInitial]
    FROM (
        SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p0].[LastName] ORDER BY [p0].[Id]) AS [row]
        FROM [People] AS [p0]
    ) AS [t1]
    WHERE [t1].[row] <= 1
) AS [t0] ON [t].[LastName] = [t0].[LastName]
LEFT JOIN (
    SELECT [t3].[Id], [t3].[Age], [t3].[FirstName], [t3].[LastName], [t3].[MiddleInitial]
    FROM (
        SELECT [p1].[Id], [p1].[Age], [p1].[FirstName], [p1].[LastName], [p1].[MiddleInitial], ROW_NUMBER() OVER(PARTITION BY [p1].[LastName] ORDER BY [p1].[Id]) AS [row]
        FROM [People] AS [p1]
    ) AS [t3]
    WHERE [t3].[row] <= 2
) AS [t2] ON [t].[LastName] = [t2].[LastName]
ORDER BY [t].[LastName] DESC, [t0].[Id], [t2].[LastName], [t2].[Id]

Contoh 12:

var grouping = context.People
    .Include(e => e.Shoes)
    .OrderBy(e => e.FirstName)
    .ThenBy(e => e.LastName)
    .GroupBy(e => e.FirstName)
    .Select(g => new { Name = g.Key, People = g.ToList()})
    .ToList();
SELECT [t].[FirstName], [t0].[Id], [t0].[Age], [t0].[FirstName], [t0].[LastName], [t0].[MiddleInitial], [t0].[Id0], [t0].[Age0], [t0].[PersonId], [t0].[Style]
FROM (
    SELECT [p].[FirstName]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName]
) AS [t]
LEFT JOIN (
    SELECT [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial], [s].[Id] AS [Id0], [s].[Age] AS [Age0], [s].[PersonId], [s].[Style]
    FROM [People] AS [p0]
    LEFT JOIN [Shoes] AS [s] ON [p0].[Id] = [s].[PersonId]
) AS [t0] ON [t].[FirstName] = [t0].[FirstName]
ORDER BY [t].[FirstName], [t0].[Id]

Contoh 13:

var grouping = context.People
    .GroupBy(m => new {m.FirstName, m.MiddleInitial })
    .Select(am => new
    {
        Key = am.Key,
        Items = am.ToList()
    })
    .ToList();
SELECT [t].[FirstName], [t].[MiddleInitial], [p0].[Id], [p0].[Age], [p0].[FirstName], [p0].[LastName], [p0].[MiddleInitial]
FROM (
    SELECT [p].[FirstName], [p].[MiddleInitial]
    FROM [People] AS [p]
    GROUP BY [p].[FirstName], [p].[MiddleInitial]
) AS [t]
LEFT JOIN [People] AS [p0] ON (([t].[FirstName] = [p0].[FirstName]) OR ([t].[FirstName] IS NULL AND [p0].[FirstName] IS NULL)) AND (([t].[MiddleInitial] = [p0].[MiddleInitial]) OR ([t].[MiddleInitial] IS NULL AND [p0].[MiddleInitial] IS NULL))
ORDER BY [t].[FirstName], [t].[MiddleInitial]

Model

Jenis entitas yang digunakan untuk contoh ini adalah:

public class Person
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public string MiddleInitial { get; set; }
    public Feet Feet { get; set; }
    public ICollection<Shoes> Shoes { get; } = new List<Shoes>();
}

public class Shoes
{
    public int Id { get; set; }
    public int Age { get; set; }
    public string Style { get; set; }
    public Person Person { get; set; }
}

public class Feet
{
    public int Id { get; set; }
    public int Size { get; set; }
    public Person Person { get; set; }
}

Menerjemahkan String.Concat dengan beberapa argumen

Masalah GitHub: #23859. Fitur ini dikontribusikan oleh @wmeints. Terima kasih banyak!

Dimulai dengan EF Core 6.0, panggilan ke String.Concat dengan beberapa argumen sekarang diterjemahkan ke SQL. Misalnya, kueri berikut:

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

Akan diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT [s].[Id], [s].[Token1], [s].[Token2], [s].[Token3], [s].[TokensProcessed]
FROM [Shards] AS [s]
WHERE (([s].[Token1] + ([s].[Token2] + [s].[Token3])) <> [s].[TokensProcessed]) OR [s].[TokensProcessed] IS NULL

Integrasi yang lebih lancar dengan System.Linq.Async

Masalah GitHub: #24041.

Paket System.Linq.Async menambahkan pemrosesan LINQ asinkron sisi klien. Menggunakan paket ini dengan versi EF Core sebelumnya rumit karena bentrokan namespace untuk metode LINQ asinkron. Dalam EF Core 6.0 kami telah memanfaatkan pencocokan IAsyncEnumerable<T> pola C# sedemikian sehingga EF Core DbSet<TEntity> yang terekspos tidak perlu mengimplementasikan antarmuka secara langsung.

Perhatikan bahwa sebagian besar aplikasi tidak perlu menggunakan System.Linq.Async karena kueri EF Core biasanya sepenuhnya diterjemahkan di server.

Masalah GitHub: #23921.

Di EF Core 6.0, kami telah melonggarkan persyaratan parameter untuk FreeText(DbFunctions, String, String) dan Contains. Ini memungkinkan fungsi-fungsi ini digunakan dengan kolom biner, atau dengan kolom yang dipetakan menggunakan pengonversi nilai. Misalnya, pertimbangkan jenis entitas dengan properti yang Name didefinisikan sebagai objek nilai:

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

    public Name Name{ get; set; }
}

public class Name
{
    public string First { get; set; }
    public string MiddleInitial { get; set; }
    public string Last { get; set; }
}

Ini dipetakan ke JSON dalam database:

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

Kueri sekarang dapat dijalankan menggunakan Contains atau meskipun jenis properti bukan Namestring.FreeText Contohnya:

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

Ini menghasilkan SQL berikut, saat menggunakan SQL Server:

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

Menerjemahkan ToString di SQLite

Masalah GitHub: #17223. Fitur ini dikontribusikan oleh @ralmsdeveloper. Terima kasih banyak!

Panggilan ke ToString() sekarang diterjemahkan ke SQL saat menggunakan penyedia database SQLite. Ini dapat berguna untuk pencarian teks yang melibatkan kolom non-string. Misalnya, pertimbangkan User jenis entitas yang menyimpan nomor telepon sebagai nilai numerik:

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

ToString dapat digunakan untuk mengonversi angka menjadi string dalam database. Kita kemudian dapat menggunakan string ini dengan fungsi seperti LIKE untuk menemukan angka yang cocok dengan pola. Misalnya, untuk menemukan semua angka yang berisi 555:

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

Ini diterjemahkan ke SQL berikut saat menggunakan database SQLite:

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

Perhatikan bahwa terjemahan ToString() untuk SQL Server sudah didukung di EF Core 5.0, dan mungkin juga didukung oleh penyedia database lainnya.

EF. Functions.Random

Masalah GitHub: #16141. Fitur ini dikontribusikan oleh @RaymondHuy. Terima kasih banyak!

EF.Functions.Random memetakan ke fungsi database yang mengembalikan angka acak semu antara 0 dan 1 eksklusif. Terjemahan telah diterapkan dalam repositori EF Core untuk SQL Server, SQLite, dan Azure Cosmos DB. Misalnya, pertimbangkan User jenis entitas dengan Popularity properti:

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

Popularity dapat memiliki nilai dari 1 hingga 5 inklusif. Menggunakan EF.Functions.Random kita dapat menulis kueri untuk mengembalikan semua pengguna dengan popularitas yang dipilih secara acak:

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

Ini diterjemahkan ke SQL berikut saat menggunakan database SQL Server:

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

Terjemahan SQL Server yang disempurnakan untuk IsNullOrWhitespace

Masalah GitHub: #22916. Fitur ini dikontribusikan oleh @Marusyk. Terima kasih banyak!

Pertimbangkan kueri berikut:

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

Sebelum EF Core 6.0, ini diterjemahkan ke yang berikut ini di SQL Server:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR (LTRIM(RTRIM([u].[FirstName])) = N'')) OR ([u].[LastName] IS NULL OR (LTRIM(RTRIM([u].[LastName])) = N''))

Terjemahan ini telah ditingkatkan untuk EF Core 6.0 menjadi:

SELECT [u].[Id], [u].[FirstName], [u].[LastName]
FROM [Users] AS [u]
WHERE ([u].[FirstName] IS NULL OR ([u].[FirstName] = N'')) OR ([u].[LastName] IS NULL OR ([u].[LastName] = N''))

Menentukan kueri untuk penyedia dalam memori

Masalah GitHub: #24600.

Metode ToInMemoryQuery baru dapat digunakan untuk menulis kueri yang menentukan terhadap database dalam memori untuk jenis entitas tertentu. Ini paling berguna untuk membuat tampilan yang setara pada database dalam memori, terutama ketika tampilan tersebut mengembalikan jenis entitas tanpa kunci. Misalnya, pertimbangkan database pelanggan untuk pelanggan yang berbasis di Inggris Raya. Setiap pelanggan memiliki alamat:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public int Id { get; set; }
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Sekarang, bayangkan kita ingin melihat data ini yang menunjukkan berapa banyak pelanggan yang ada di setiap area kode pos. Kita dapat membuat jenis entitas tanpa kunci untuk mewakili ini:

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

Dan tentukan properti DbSet untuk properti tersebut di DbContext, bersama dengan set untuk jenis entitas tingkat atas lainnya:

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

Kemudian, dalam OnModelCreating, kita dapat menulis kueri LINQ yang menentukan data yang akan dikembalikan untuk CustomerDensities:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<CustomerDensity>()
        .HasNoKey()
        .ToInMemoryQuery(
            () => Customers
                .GroupBy(c => c.Address.Postcode.Substring(0, 3))
                .Select(
                    g =>
                        new CustomerDensity
                        {
                            Postcode = g.Key,
                            CustomerCount = g.Count()
                        }));
}

Ini kemudian dapat dikueri sama seperti properti DbSet lainnya:

var results = context.CustomerDensities.ToList();

Menerjemahkan Substring dengan parameter tunggal

Masalah GitHub: #20173. Fitur ini dikontribusikan oleh @stevendarby. Terima kasih banyak!

EF Core 6.0 sekarang menerjemahkan penggunaan string.Substring dengan satu argumen. Contohnya:

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

Ini diterjemahkan ke SQL berikut saat menggunakan SQL Server:

SELECT TOP(1) SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) AS [Name]
FROM [Customers] AS [c]
WHERE SUBSTRING([c].[Name], 3 + 1, LEN([c].[Name])) = N'hur'

Kueri terpisah untuk koleksi non-navigasi

Masalah GitHub: #21234.

EF Core mendukung pemisahan satu kueri LINQ menjadi beberapa kueri SQL. Di EF Core 6.0, dukungan ini telah diperluas untuk menyertakan kasus di mana koleksi non-navigasi terkandung dalam proyeksi kueri.

Berikut ini adalah contoh kueri yang memperlihatkan terjemahan di SQL Server ke dalam satu kueri atau beberapa kueri.

Contoh 1:

Kueri LINQ:

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

Kueri SQL tunggal:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Beberapa kueri SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Contoh 2:

Kueri LINQ:

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

Kueri SQL tunggal:

SELECT [c].[Id], [t].[OrderDate], [t].[Id]
FROM [Customers] AS [c]
  LEFT JOIN (
  SELECT [o].[OrderDate], [o].[Id], [o].[CustomerId]
  FROM [Order] AS [o]
  WHERE [o].[Id] > 1
  ) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Beberapa kueri SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[Id], [t].[CustomerId], [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
INNER JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] > 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Contoh 3:

Kueri LINQ:

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

Kueri SQL tunggal:

SELECT [c].[Id], [t].[OrderDate]
FROM [Customers] AS [c]
  OUTER APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Beberapa kueri SQL:

SELECT [c].[Id]
FROM [Customers] AS [c]
ORDER BY [c].[Id]

SELECT [t].[OrderDate], [c].[Id]
FROM [Customers] AS [c]
  CROSS APPLY (
  SELECT DISTINCT [o].[OrderDate]
  FROM [Order] AS [o]
  WHERE ([c].[Id] = [o].[CustomerId]) AND ([o].[Id] > 1)
  ) AS [t]
ORDER BY [c].[Id]

Hapus klausa ORDER BY terakhir saat bergabung untuk koleksi

Masalah GitHub: #19828.

Saat memuat entitas satu-ke-banyak terkait, EF Core menambahkan klausul ORDER BY untuk memastikan semua entitas terkait untuk entitas tertentu dikelompokkan bersama- sama. Namun, klausul ORDER BY terakhir tidak diperlukan untuk EF menghasilkan pengelompokan yang diperlukan, dan dapat berdampak pada performa. Oleh karena itu, EF Core 6.0 klausa ini dihapus.

Misalnya, pertimbangkan kueri ini:

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

Dengan EF Core 5.0 di SQL Server, kueri ini diterjemahkan ke:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id], [t].[Id]

Dengan EF Core 6.0, sebaliknya diterjemahkan ke:

SELECT [c].[Id], [t].[Id], [t].[CustomerId], [t].[OrderDate]
FROM [Customers] AS [c]
LEFT JOIN (
    SELECT [o].[Id], [o].[CustomerId], [o].[OrderDate]
    FROM [Order] AS [o]
    WHERE [o].[Id] = 1
) AS [t] ON [c].[Id] = [t].[CustomerId]
ORDER BY [c].[Id]

Kueri tag dengan nama file dan nomor baris

Masalah GitHub: #14176. Fitur ini dikontribusikan oleh @michalczerwinski. Terima kasih banyak!

Tag kueri memungkinkan penambahan tag teksural ke kueri LINQ sehingga kemudian disertakan dalam SQL yang dihasilkan. Di EF Core 6.0, ini dapat digunakan untuk menandai kueri dengan nama file dan nomor baris kode LINQ. Contohnya:

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

Ini menghasilkan SQL yang dihasilkan berikut saat menggunakan SQL Server:

-- file: C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\TagWithFileAndLineSample.cs:21

SELECT [c].[Id], [c].[Name]
FROM [Customers] AS [c]
WHERE [c].[Name] IS NOT NULL AND ([c].[Name] LIKE N'A%')

Perubahan pada penanganan dependen opsional yang dimiliki

Masalah GitHub: #24558.

Menjadi rumit untuk mengetahui apakah entitas dependen opsional ada atau tidak ketika berbagi tabel dengan entitas utamanya. Ini karena ada baris dalam tabel untuk dependen karena prinsipal membutuhkannya, terlepas dari apakah dependen ada atau tidak. Cara untuk menangani ini secara tidak ambigu adalah dengan memastikan bahwa dependen memiliki setidaknya satu properti yang diperlukan. Karena properti yang diperlukan tidak boleh null, itu berarti jika nilai dalam kolom untuk properti tersebut null, maka entitas dependen tidak ada.

Misalnya, pertimbangkan Customer kelas di mana setiap pelanggan memiliki Address:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

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

Alamat bersifat opsional, yang berarti valid untuk menyimpan pelanggan tanpa alamat:

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

Namun, jika pelanggan memang memiliki alamat, alamat tersebut harus memiliki setidaknya kode pos non-null:

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

Ini dipastikan dengan menandai Postcode properti sebagai Required.

Sekarang ketika pelanggan dikueri, jika kolom Kode Pos null, maka ini berarti pelanggan tidak memiliki alamat, dan Customer.Address properti navigasi dibiarkan null. Misalnya, melakukan iterasi melalui pelanggan dan memeriksa apakah Alamat null:

foreach (var customer in context.Customers1)
{
    Console.Write(customer.Name);

    if (customer.Address == null)
    {
        Console.WriteLine(" has no address.");
    }
    else
    {
        Console.WriteLine($" has postcode {customer.Address.Postcode}.");
    }
}

Hasilkan hasil berikut:

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

Pertimbangkan sebagai gantinya kasus di mana tidak ada properti dari alamat yang diperlukan:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Sekarang dimungkinkan untuk menyimpan pelanggan tanpa alamat, dan pelanggan dengan alamat di mana semua properti alamat null:

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

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

Namun, dalam database, kedua kasus ini tidak dapat dibedakan, seperti yang dapat kita lihat dengan langsung mengkueri kolom database:

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

Untuk alasan ini, EF Core 6.0 sekarang akan memperingatkan Anda saat menyimpan dependen opsional di mana semua propertinya null. Contohnya:

peringatan: 27/9/2021 09:25:01.338 RelationalEventId.OptionalDependentWithAllNullPropertiesWarning[20704] (Microsoft.EntityFrameworkCore.Update) Entitas jenis 'Alamat' dengan nilai kunci utama {CustomerId: -2147482646} adalah dependen opsional menggunakan berbagi tabel. Entitas tidak memiliki properti apa pun dengan nilai non-default untuk mengidentifikasi apakah entitas ada. Ini berarti bahwa ketika dikueri, tidak ada instans objek yang akan dibuat alih-alih instans dengan semua properti yang diatur ke nilai default. Setiap dependen berlapis juga akan hilang. Jangan simpan instans apa pun hanya dengan nilai default atau tandai navigasi masuk seperti yang diperlukan dalam model.

Ini menjadi lebih rumit di mana dependen opsional itu sendiri bertindak sebagai prinsipal untuk dependen opsional lebih lanjut, juga dipetakan ke tabel yang sama. Alih-alih hanya peringatan, EF Core 6.0 melarang hanya kasus dependen opsional berlapis. Misalnya, pertimbangkan model berikut, di mana ContactInfo dimiliki oleh Customer dan Address diubah dimiliki oleh ContactInfo:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ContactInfo ContactInfo { get; set; }
}

public class ContactInfo
{
    public string Phone { get; set; }
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Sekarang jika ContactInfo.Phone null, maka EF Core tidak akan membuat instans Address jika hubungan bersifat opsional, meskipun alamat itu sendiri mungkin memiliki data. Untuk model semacam ini, EF Core 6.0 akan memberikan pengecualian berikut:

System.InvalidOperationException: Jenis entitas 'ContactInfo' adalah dependen opsional menggunakan berbagi tabel dan berisi dependen lain tanpa properti non bersama yang diperlukan untuk mengidentifikasi apakah entitas ada. Jika semua properti nullable berisi nilai null dalam database, maka instans objek tidak akan dibuat dalam kueri yang menyebabkan nilai dependen berlapis hilang. Tambahkan properti yang diperlukan untuk membuat instans dengan nilai null untuk properti lain atau tandai navigasi masuk sebagaimana diperlukan untuk selalu membuat instans.

Garis bawah di sini adalah untuk menghindari kasus di mana dependen opsional dapat berisi semua nilai properti yang dapat diubah ke null dan berbagi tabel dengan prinsipalnya. Ada tiga cara mudah untuk menghindari hal ini:

  1. Buat dependen diperlukan. Ini berarti bahwa entitas dependen akan selalu memiliki nilai setelah dikueri, bahkan jika semua propertinya null.
  2. Pastikan dependen berisi setidaknya satu properti yang diperlukan, seperti yang dijelaskan di atas.
  3. Simpan dependen opsional ke tabel mereka sendiri, alih-alih berbagi tabel dengan prinsipal.

Dependen dapat dibuat diperlukan dengan menggunakan Required atribut pada navigasinya:

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; }

    [Required]
    public Address Address { get; set; }
}

public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }
    public string Postcode { get; set; }
}

Atau dengan menentukannya diperlukan dalam OnModelCreating:

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

Dependen dapat disimpan ke tabel lain dengan menentukan tabel yang akan digunakan di OnModelCreating:

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

Lihat OptionalDependentsSample di GitHub untuk contoh dependen opsional lainnya, termasuk kasus dengan dependen opsional berlapis.

Atribut pemetaan baru

EF Core 6.0 berisi beberapa atribut baru yang dapat diterapkan ke kode untuk mengubah cara dipetakan ke database.

UnicodeAttribute

Masalah GitHub: #19794. Fitur ini dikontribusikan oleh @RaymondHuy. Terima kasih banyak!

Dimulai dengan EF Core 6.0, properti string sekarang dapat dipetakan ke kolom non-Unicode menggunakan atribut pemetaan tanpa menentukan jenis database secara langsung. Misalnya, pertimbangkan Book jenis entitas dengan properti untuk Nomor Buku Standar Internasional (ISBN) dalam bentuk "ISBN 978-3-16-148410-0":

public class Book
{
    public int Id { get; set; }
    public string Title { get; set; }

    [Unicode(false)]
    [MaxLength(22)]
    public string Isbn { get; set; }
}

Karena ISBN tidak boleh berisi karakter non-unicode, Unicode atribut akan menyebabkan jenis string non-Unicode digunakan. Selain itu, MaxLength digunakan untuk membatasi ukuran kolom database. Misalnya, saat menggunakan SQL Server, ini menghasilkan kolom varchar(22)database :

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

Catatan

EF Core memetakan properti string ke kolom Unicode secara default. UnicodeAttribute diabaikan ketika sistem database hanya mendukung jenis Unicode.

PrecisionAttribute

Masalah GitHub: #17914. Fitur ini dikontribusikan oleh @RaymondHuy. Terima kasih banyak!

Presisi dan skala kolom database sekarang dapat dikonfigurasi menggunakan atribut pemetaan tanpa menentukan jenis database secara langsung. Misalnya, pertimbangkan Product jenis entitas dengan properti desimal Price :

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

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

EF Core akan memetakan properti ini ke kolom database dengan presisi 10 dan skala 2. Misalnya, di SQL Server:

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

EntityTypeConfigurationAttribute

Masalah GitHub: #23163. Fitur ini dikontribusikan oleh @KaloyanIT. Terima kasih banyak!

IEntityTypeConfiguration<TEntity> instans memungkinkan ModelBuilder konfigurasi untuk setiap jenis entitas terkandung dalam kelas konfigurasinya sendiri. Contohnya:

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

Biasanya, kelas konfigurasi ini harus dibuat dan dipanggil dari DbContext.OnModelCreating. Contohnya:

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

Dimulai dengan EF Core 6.0, dapat EntityTypeConfigurationAttribute ditempatkan pada jenis entitas sehingga EF Core dapat menemukan dan menggunakan konfigurasi yang sesuai. Contohnya:

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

Atribut ini berarti bahwa EF Core akan menggunakan implementasi yang ditentukan IEntityTypeConfiguration setiap kali Book jenis entitas disertakan dalam model. Jenis entitas disertakan dalam model menggunakan salah satu mekanisme normal. Misalnya, dengan membuat DbSet<TEntity> properti untuk jenis entitas:

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

    //...

Atau dengan mendaftarkannya di OnModelCreating:

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

Catatan

EntityTypeConfigurationAttribute jenis tidak akan ditemukan secara otomatis dalam rakitan. Jenis entitas harus ditambahkan ke model sebelum atribut akan ditemukan pada jenis entitas tersebut.

Peningkatan bangunan model

Selain atribut pemetaan baru, EF Core 6.0 berisi beberapa peningkatan lain pada proses pembuatan model.

Dukungan untuk kolom jarang SQL Server

Masalah GitHub: #8023.

Kolom jarang SQL Server adalah kolom biasa yang dioptimalkan untuk menyimpan nilai null. Ini dapat berguna saat menggunakan pemetaan pewarisan TPH di mana properti subjenis yang jarang digunakan akan menghasilkan nilai kolom null untuk sebagian besar baris dalam tabel. Misalnya, pertimbangkan ForumModerator kelas yang diperluas dari ForumUser:

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

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

Mungkin ada jutaan pengguna, dengan hanya beberapa dari mereka yang menjadi moderator. Ini berarti pemetaan ForumName sebagai jarang mungkin masuk akal di sini. Ini sekarang dapat dikonfigurasi menggunakan IsSparse di OnModelCreating. Contohnya:

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

Migrasi EF Core kemudian akan menandai kolom sebagai jarang. Contoh:

CREATE TABLE [ForumUser] (
    [Id] int NOT NULL IDENTITY,
    [Username] nvarchar(max) NULL,
    [Discriminator] nvarchar(max) NOT NULL,
    [ForumName] nvarchar(max) SPARSE NULL,
    CONSTRAINT [PK_ForumUser] PRIMARY KEY ([Id]));

Catatan

Kolom jarang memiliki batasan. Pastikan untuk membaca dokumentasi kolom jarang SQL Server untuk memastikan bahwa kolom jarang adalah pilihan yang tepat untuk skenario Anda.

Penyempurnaan HASConversion API

Masalah GitHub: #25468.

Sebelum EF Core 6.0, kelebihan beban umum metode HasConversion menggunakan parameter generik untuk menentukan jenis yang akan dikonversi. Misalnya, pertimbangkan Currency enum:

public enum Currency
{
    UsDollars,
    PoundsSterling,
    Euros
}

EF Core dapat dikonfigurasi untuk menyimpan nilai enum ini sebagai string "UsDollars", "PoundsStirling", dan "Euros" menggunakan HasConversion<string>. Contohnya:

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

Dimulai dengan EF Core 6.0, jenis generik dapat menentukan jenis pengonversi nilai. Ini bisa menjadi salah satu pengonversi nilai bawaan. Misalnya, untuk menyimpan nilai enum sebagai angka 16-bit dalam database:

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

Atau bisa menjadi jenis pengonversi nilai kustom. Misalnya, pertimbangkan pengonversi yang menyimpan nilai enum sebagai simbol mata uangnya:

public class CurrencyToSymbolConverter : ValueConverter<Currency, string>
{
    public CurrencyToSymbolConverter()
        : base(
            v => v == Currency.PoundsSterling ? "£" : v == Currency.Euros ? "€" : "$",
            v => v == "£" ? Currency.PoundsSterling : v == "€" ? Currency.Euros : Currency.UsDollars)
    {
    }
}

Ini sekarang dapat dikonfigurasi menggunakan metode generik HasConversion :

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

Lebih sedikit konfigurasi untuk hubungan banyak ke banyak

Masalah GitHub: #21535.

Hubungan banyak ke banyak yang tidak ambigu antara dua jenis entitas ditemukan oleh konvensi. Jika perlu atau jika diinginkan, navigasi dapat ditentukan secara eksplisit. Contohnya:

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

Dalam kedua kasus ini, EF Core membuat entitas bersama yang ditik berdasarkan Dictionary<string, object> untuk bertindak sebagai entitas gabungan antara kedua jenis. Dimulai dengan EF Core 6.0, UsingEntity dapat ditambahkan ke konfigurasi untuk hanya mengubah jenis ini, tanpa perlu konfigurasi tambahan. Contohnya:

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

Selain itu, jenis entitas gabungan dapat dikonfigurasi tambahan tanpa perlu menentukan hubungan kiri dan kanan secara eksplisit. Contohnya:

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

Dan akhirnya, konfigurasi lengkap dapat disediakan. Contohnya:

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

Perbolehkan pengonversi nilai untuk mengonversi null

Masalah GitHub: #13850.

Penting

Karena masalah yang diuraikan di bawah ini, konstruktor untuk ValueConverter yang memungkinkan konversi null telah ditandai dengan [EntityFrameworkInternal] untuk rilis EF Core 6.0. Menggunakan konstruktor ini sekarang akan menghasilkan peringatan build.

Pengonversi nilai umumnya tidak mengizinkan konversi null ke beberapa nilai lainnya. Ini karena pengonversi nilai yang sama dapat digunakan untuk jenis nullable dan non-nullable, yang sangat berguna untuk kombinasi PK/FK di mana FK sering nullable dan PK tidak.

Dimulai dengan EF Core 6.0, pengonversi nilai dapat dibuat yang mengonversi null. Namun, validasi fitur ini telah mengungkapkan terbukti sangat bermasalah dalam praktiknya dengan banyak jebakan. Contohnya:

Ini bukan masalah sepele dan untuk masalah kueri yang tidak mudah dideteksi. Oleh karena itu, kami telah menandai fitur ini sebagai internal untuk EF Core 6.0. Anda masih dapat menggunakannya, tetapi Anda akan mendapatkan peringatan kompilator. Peringatan dapat dinonaktifkan menggunakan #pragma warning disable EF1001.

Salah satu contoh di mana mengonversi null dapat berguna adalah ketika database berisi null, tetapi jenis entitas ingin menggunakan beberapa nilai default lainnya untuk properti . Misalnya, pertimbangkan enum di mana nilai defaultnya adalah "Tidak Diketahui":

public enum Breed
{
    Unknown,
    Burmese,
    Tonkinese
}

Namun, database mungkin memiliki nilai null ketika trah tidak diketahui. Dalam EF Core 6.0, pengonversi nilai dapat digunakan untuk memperhitungkan hal ini:

    public class BreedConverter : ValueConverter<Breed, string>
    {
#pragma warning disable EF1001
        public BreedConverter()
            : base(
                v => v == Breed.Unknown ? null : v.ToString(),
                v => v == null ? Breed.Unknown : Enum.Parse<Breed>(v),
                convertsNulls: true)
        {
        }
#pragma warning restore EF1001
    }

Kucing dengan jenis "Tidak Diketahui" akan mengatur kolomnya Breed menjadi null dalam database. Contohnya:

context.AddRange(
    new Cat { Name = "Mac", Breed = Breed.Unknown },
    new Cat { Name = "Clippy", Breed = Breed.Burmese },
    new Cat { Name = "Sid", Breed = Breed.Tonkinese });

context.SaveChanges();

Yang menghasilkan pernyataan sisipan berikut di SQL Server:

info: 9/27/2021 19:43:55.966 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (16ms) [Parameters=[@p0=NULL (Size = 4000), @p1='Mac' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Burmese' (Size = 4000), @p1='Clippy' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
info: 9/27/2021 19:43:55.983 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (0ms) [Parameters=[@p0='Tonkinese' (Size = 4000), @p1='Sid' (Size = 4000)], CommandType='Text', CommandTimeout='30']
      SET NOCOUNT ON;
      INSERT INTO [Cats] ([Breed], [Name])
      VALUES (@p0, @p1);
      SELECT [Id]
      FROM [Cats]
      WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Peningkatan pabrik DbContext

AddDbContextFactory juga mendaftarkan DbContext secara langsung

Masalah GitHub: #25164.

Terkadang berguna untuk memiliki jenis DbContext dan pabrik untuk konteks jenis tersebut yang terdaftar dalam kontainer injeksi dependensi aplikasi (D.I.). Ini memungkinkan, misalnya, instans terlingkup DbContext untuk diselesaikan dari cakupan permintaan, sementara pabrik dapat digunakan untuk membuat beberapa instans independen saat diperlukan.

Untuk mendukung ini, AddDbContextFactory sekarang juga mendaftarkan jenis DbContext sebagai layanan terlingkup. Misalnya, pertimbangkan pendaftaran ini dalam kontainer D.I. aplikasi:

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

Dengan pendaftaran ini, pabrik dapat diselesaikan dari kontainer D.I. root, seperti pada versi sebelumnya:

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

Perhatikan bahwa instans konteks yang dibuat oleh pabrik harus dibuang secara eksplisit.

Selain itu, instans DbContext dapat diselesaikan langsung dari cakupan kontainer:

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

Dalam hal ini instans konteks dibuang ketika cakupan kontainer dibuang; konteks tidak boleh dibuang secara eksplisit.

Pada tingkat yang lebih tinggi, ini berarti bahwa DbContext pabrik dapat disuntikkan ke jenis D.I lainnya. Contohnya:

private class MyController2
{
    private readonly IDbContextFactory<SomeDbContext> _contextFactory;

    public MyController2(IDbContextFactory<SomeDbContext> contextFactory)
    {
        _contextFactory = contextFactory;
    }

    public void DoSomething()
    {
        using var context1 = _contextFactory.CreateDbContext();
        using var context2 = _contextFactory.CreateDbContext();

        var results1 = context1.Blogs.ToList();
        var results2 = context2.Blogs.ToList();

        // Contexts obtained from the factory must be explicitly disposed
    }
}

Atau:

private class MyController1
{
    private readonly SomeDbContext _context;

    public MyController1(SomeDbContext context)
    {
        _context = context;
    }

    public void DoSomething()
    {
        var results = _context.Blogs.ToList();

        // Injected context is disposed when the request scope is disposed
    }
}

DbContextFactory mengabaikan konstruktor tanpa parameter DbContext

Masalah GitHub: #24124.

EF Core 6.0 sekarang memungkinkan konstruktor DbContext tanpa parameter, dan konstruktor yang mengambil DbContextOptions untuk digunakan pada jenis konteks yang sama ketika pabrik didaftarkan melalui AddDbContextFactory. Misalnya, konteks yang digunakan dalam contoh di atas berisi kedua konstruktor:

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

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

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

Pengumpulan DbContext dapat digunakan tanpa injeksi dependensi

Masalah GitHub: #24137.

Jenis PooledDbContextFactory ini telah dipublikasikan sehingga dapat digunakan sebagai kumpulan yang berdiri sendiri untuk instans DbContext, tanpa perlu aplikasi Anda memiliki kontainer injeksi dependensi. Kumpulan dibuat dengan instans DbContextOptions yang akan digunakan untuk membuat instans konteks:

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

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

Pabrik kemudian dapat digunakan untuk membuat dan mengumpulkan instans. Contohnya:

for (var i = 0; i < 2; i++)
{
    using var context1 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context1.ContextId}");

    using var context2 = factory.CreateDbContext();
    Console.WriteLine($"Created DbContext with ID {context2.ContextId}");
}

Instans dikembalikan ke kumpulan saat dibuang.

Perbaikan lain-lain

Dan akhirnya, EF Core berisi beberapa peningkatan di area yang tidak tercakup di atas.

Gunakan [ColumnAttribute.Order] saat membuat tabel

Masalah GitHub: #10059.

Order Properti ColumnAttribute sekarang dapat digunakan untuk mengurutkan kolom saat membuat tabel dengan migrasi. Misalnya, pertimbangkan model berikut:

public class EntityBase
{
    public int Id { get; set; }
    public DateTime UpdatedOn { get; set; }
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    public string Department { get; set; }
    public decimal AnnualSalary { get; set; }
    public Address Address { get; set; }
}

[Owned]
public class Address
{
    public string House { get; set; }
    public string Street { get; set; }
    public string City { get; set; }

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

Secara default, EF Core mengurutkan kolom kunci utama terlebih dahulu, mengikuti properti jenis entitas dan jenis yang dimiliki, dan akhirnya properti dari jenis dasar. Misalnya, tabel berikut dibuat di SQL Server:

CREATE TABLE [EmployeesWithoutOrdering] (
    [Id] int NOT NULL IDENTITY,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [Address_House] nvarchar(max) NULL,
    [Address_Street] nvarchar(max) NULL,
    [Address_City] nvarchar(max) NULL,
    [Address_Postcode] nvarchar(max) NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    CONSTRAINT [PK_EmployeesWithoutOrdering] PRIMARY KEY ([Id]));

Di EF Core 6.0, ColumnAttribute dapat digunakan untuk menentukan urutan kolom yang berbeda. Contohnya:

public class EntityBase
{
    [Column(Order = 1)]
    public int Id { get; set; }

    [Column(Order = 98)]
    public DateTime UpdatedOn { get; set; }

    [Column(Order = 99)]
    public DateTime CreatedOn { get; set; }
}

public class PersonBase : EntityBase
{
    [Column(Order = 2)]
    public string FirstName { get; set; }

    [Column(Order = 3)]
    public string LastName { get; set; }
}

public class Employee : PersonBase
{
    [Column(Order = 20)]
    public string Department { get; set; }

    [Column(Order = 21)]
    public decimal AnnualSalary { get; set; }

    public Address Address { get; set; }
}

[Owned]
public class Address
{
    [Column("House", Order = 10)]
    public string House { get; set; }

    [Column("Street", Order = 11)]
    public string Street { get; set; }

    [Column("City", Order = 12)]
    public string City { get; set; }

    [Required]
    [Column("Postcode", Order = 13)]
    public string Postcode { get; set; }
}

Di SQL Server, tabel yang dihasilkan sekarang:

CREATE TABLE [EmployeesWithOrdering] (
    [Id] int NOT NULL IDENTITY,
    [FirstName] nvarchar(max) NULL,
    [LastName] nvarchar(max) NULL,
    [House] nvarchar(max) NULL,
    [Street] nvarchar(max) NULL,
    [City] nvarchar(max) NULL,
    [Postcode] nvarchar(max) NULL,
    [Department] nvarchar(max) NULL,
    [AnnualSalary] decimal(18,2) NOT NULL,
    [UpdatedOn] datetime2 NOT NULL,
    [CreatedOn] datetime2 NOT NULL,
    CONSTRAINT [PK_EmployeesWithOrdering] PRIMARY KEY ([Id]));

Ini memindahkan FistName kolom dan LastName dipindahkan ke bagian atas, meskipun didefinisikan dalam jenis dasar. Perhatikan bahwa nilai urutan kolom dapat memiliki celah, memungkinkan rentang digunakan untuk selalu menempatkan kolom di akhir, bahkan ketika digunakan oleh beberapa jenis turunan.

Contoh ini juga menunjukkan bagaimana hal yang sama ColumnAttribute dapat digunakan untuk menentukan nama kolom dan urutan.

Pengurutan kolom juga dapat dikonfigurasi menggunakan ModelBuilder API di OnModelCreating. Contohnya:

modelBuilder.Entity<UsingModelBuilder.Employee>(
    entityBuilder =>
    {
        entityBuilder.Property(e => e.Id).HasColumnOrder(1);
        entityBuilder.Property(e => e.FirstName).HasColumnOrder(2);
        entityBuilder.Property(e => e.LastName).HasColumnOrder(3);

        entityBuilder.OwnsOne(
            e => e.Address,
            ownedBuilder =>
            {
                ownedBuilder.Property(e => e.House).HasColumnName("House").HasColumnOrder(4);
                ownedBuilder.Property(e => e.Street).HasColumnName("Street").HasColumnOrder(5);
                ownedBuilder.Property(e => e.City).HasColumnName("City").HasColumnOrder(6);
                ownedBuilder.Property(e => e.Postcode).HasColumnName("Postcode").HasColumnOrder(7).IsRequired();
            });

        entityBuilder.Property(e => e.Department).HasColumnOrder(8);
        entityBuilder.Property(e => e.AnnualSalary).HasColumnOrder(9);
        entityBuilder.Property(e => e.UpdatedOn).HasColumnOrder(10);
        entityBuilder.Property(e => e.CreatedOn).HasColumnOrder(11);
    });

Pemesanan pada pembuat model dengan HasColumnOrder lebih diutamakan daripada urutan apa pun yang ditentukan dengan ColumnAttribute. Ini berarti HasColumnOrder dapat digunakan untuk mengambil alih urutan yang dibuat dengan atribut, termasuk menyelesaikan konflik apa pun saat atribut pada properti yang berbeda menentukan nomor pesanan yang sama.

Penting

Perhatikan bahwa, dalam kasus umum, sebagian besar database hanya mendukung pengurutan kolom saat tabel dibuat. Ini berarti bahwa atribut urutan kolom tidak dapat digunakan untuk mengurutkan ulang kolom dalam tabel yang sudah ada. Satu pengecualian penting untuk ini adalah SQLite, di mana migrasi akan membangun kembali seluruh tabel dengan urutan kolom baru.

API Minimal Inti EF

Masalah GitHub: #25192.

.NET Core 6.0 menyertakan templat yang diperbarui yang menampilkan "API minimal" yang disederhanakan yang menghapus banyak kode boilerplate yang secara tradisional diperlukan dalam aplikasi .NET.

EF Core 6.0 berisi metode ekstensi baru yang mendaftarkan jenis DbContext dan memasok konfigurasi untuk penyedia database dalam satu baris. Contohnya:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlite<MyDbContext>("Data Source=mydatabase.db");
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSqlServer<MyDbContext>(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase");
var builder = WebApplication.CreateBuilder(args);

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

Ini sama persis dengan:

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlite("Data Source=mydatabase.db"));
var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDbContext<MyDbContext>(
    options => options.UseSqlServer(@"Server=(localdb)\mssqllocaldb;Database=MyDatabase;ConnectRetryCount=0"));
var builder = WebApplication.CreateBuilder(args);

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

Catatan

API minimal EF Core hanya mendukung pendaftaran dan konfigurasi DbContext dan penyedia yang sangat dasar. Gunakan AddDbContext, AddDbContextPool, AddDbContextFactory, dll. untuk mengakses semua jenis pendaftaran dan konfigurasi yang tersedia di EF Core.

Lihat sumber daya ini untuk mempelajari selengkapnya tentang API minimal:

Mempertahankan konteks sinkronisasi di SaveChangesAsync

Masalah GitHub: #23971.

Kami mengubah kode EF Core dalam rilis 5.0 untuk diatur Task.ConfigureAwait ke false di semua tempat di mana kami await menyinkronkan kode. Ini umumnya merupakan pilihan yang lebih baik untuk penggunaan EF Core. Namun, SaveChangesAsync adalah kasus khusus karena EF Core akan menetapkan nilai yang dihasilkan ke dalam entitas yang dilacak setelah operasi database asinkron selesai. Perubahan ini kemudian dapat memicu pemberitahuan yang, misalnya, mungkin harus berjalan pada utas U.I. Oleh karena itu, kami mengembalikan perubahan ini dalam EF Core 6.0 hanya untuk metode tersebut SaveChangesAsync .

Database dalam memori: memvalidasi properti yang diperlukan tidak null

Masalah GitHub: #10613. Fitur ini dikontribusikan oleh @fagnercarvalho. Terima kasih banyak!

Database dalam memori EF Core sekarang akan melemparkan pengecualian jika upaya dilakukan untuk menyimpan nilai null untuk properti yang ditandai sebagaimana diperlukan. Misalnya, pertimbangkan User jenis dengan properti yang diperlukan Username :

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

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

Mencoba menyimpan entitas dengan null Username akan menghasilkan pengecualian berikut:

Microsoft.EntityFrameworkCore.DbUpdateException: Properti yang diperlukan '{'Nama Pengguna'}' hilang untuk instans jenis entitas 'Pengguna' dengan nilai kunci '{Id: 1}'.

Validasi ini dapat dinonaktifkan jika perlu. Contohnya:

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

Informasi sumber perintah untuk diagnostik dan pencegat

Masalah GitHub: #23719. Fitur ini dikontribusikan oleh @Giorgi. Terima kasih banyak!

Yang CommandEventData disediakan untuk sumber diagnostik dan pencegat sekarang berisi nilai enum yang menunjukkan bagian mana dari EF yang bertanggung jawab untuk membuat perintah. Ini dapat digunakan sebagai filter dalam diagnostik atau pencegat. Misalnya, kita mungkin menginginkan pencegat yang hanya berlaku untuk perintah yang berasal dari SaveChanges:

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

        return result;
    }
}

Ini memfilter pencegat hanya SaveChanges untuk peristiwa ketika digunakan dalam aplikasi yang juga menghasilkan migrasi dan kueri. Contohnya:

Saving changes for CustomersContext:

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

Penanganan nilai sementara yang lebih baik

Masalah GitHub: #24245.

EF Core tidak mengekspos nilai sementara pada instans jenis entitas. Misalnya, pertimbangkan Blog jenis entitas dengan kunci yang dihasilkan penyimpanan:

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

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

Properti Id kunci akan mendapatkan nilai sementara segera setelah Blog dilacak oleh konteks. Misalnya, saat memanggil DbContext.Add:

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

Nilai sementara dapat diperoleh dari pelacak perubahan konteks, tetapi tidak diatur ke dalam instans entitas. Misalnya, kode ini:

Console.WriteLine($"Blog.Id value on entity instance = {blog.Id}");
Console.WriteLine($"Blog.Id value tracked by EF = {context.Entry(blog).Property(e => e.Id).CurrentValue}");

Menghasilkan output berikut:

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

Ini baik karena mencegah nilai sementara bocor ke dalam kode aplikasi di mana secara tidak sengaja dapat diperlakukan sebagai non-sementara. Namun, terkadang berguna untuk menangani nilai sementara secara langsung. Misalnya, aplikasi mungkin ingin menghasilkan nilai sementaranya sendiri untuk grafik entitas sebelum dilacak sehingga dapat digunakan untuk membentuk hubungan menggunakan kunci asing. Ini dapat dilakukan dengan secara eksplisit menandai nilai sebagai sementara. Contohnya:

var blog = new Blog { Id = -1 };
var post1 = new Post { Id = -1, BlogId = -1 };
var post2 = new Post { Id = -2, BlogId = -1 };

context.Add(blog).Property(e => e.Id).IsTemporary = true;
context.Add(post1).Property(e => e.Id).IsTemporary = true;
context.Add(post2).Property(e => e.Id).IsTemporary = true;

Console.WriteLine($"Blog has explicit temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has explicit temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has explicit temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

Di EF Core 6.0, nilai akan tetap berada pada instans entitas meskipun sekarang ditandai sebagai sementara. Misalnya, kode di atas menghasilkan output berikut:

Blog has explicit temporary ID = -1
Post 1 has explicit temporary ID = -1 and FK to Blog = -1
Post 2 has explicit temporary ID = -2 and FK to Blog = -1

Demikian juga, nilai sementara yang dihasilkan oleh EF Core dapat diatur secara eksplisit ke instans entitas dan ditandai sebagai nilai sementara. Ini dapat digunakan untuk secara eksplisit mengatur hubungan antara entitas baru menggunakan nilai kunci sementara mereka. Contohnya:

var post1 = new Post();
var post2 = new Post();

var blogIdEntry = context.Entry(blog).Property(e => e.Id);
blog.Id = blogIdEntry.CurrentValue;
blogIdEntry.IsTemporary = true;

var post1IdEntry = context.Add(post1).Property(e => e.Id);
post1.Id = post1IdEntry.CurrentValue;
post1IdEntry.IsTemporary = true;
post1.BlogId = blog.Id;

var post2IdEntry = context.Add(post2).Property(e => e.Id);
post2.Id = post2IdEntry.CurrentValue;
post2IdEntry.IsTemporary = true;
post2.BlogId = blog.Id;

Console.WriteLine($"Blog has generated temporary ID = {blog.Id}");
Console.WriteLine($"Post 1 has generated temporary ID = {post1.Id} and FK to Blog = {post1.BlogId}");
Console.WriteLine($"Post 2 has generated temporary ID = {post2.Id} and FK to Blog = {post2.BlogId}");

Menghasilkan:

Blog has generated temporary ID = -2147482647
Post 1 has generated temporary ID = -2147482647 and FK to Blog = -2147482647
Post 2 has generated temporary ID = -2147482646 and FK to Blog = -2147482647

EF Core dianotasi untuk jenis referensi C# nullable

Masalah GitHub: #19007.

Basis kode EF Core sekarang menggunakan jenis referensi nullable C# (NRTs) di seluruh. Ini berarti Anda akan mendapatkan indikasi kompilator yang benar untuk penggunaan null saat menggunakan EF Core 6.0 dari kode Anda sendiri.

Microsoft.Data.Sqlite 6.0

Tip

Anda dapat menjalankan dan men-debug ke semua sampel yang ditunjukkan di bawah ini dengan mengunduh kode sampel dari GitHub.

Pengumpulan Koneksi

Masalah GitHub: #13837.

Adalah praktik umum untuk menjaga koneksi database tetap terbuka untuk waktu sesedikitan mungkin. Ini membantu mencegah pertikaian pada sumber daya koneksi. Inilah sebabnya mengapa pustaka seperti EF Core segera membuka koneksi sebelum melakukan operasi database, dan tutup lagi segera setelahnya. Misalnya, pertimbangkan kode EF Core ini:

Console.WriteLine("Starting query...");
Console.WriteLine();

var users = context.Users.ToList();

Console.WriteLine();
Console.WriteLine("Query finished.");
Console.WriteLine();

foreach (var user in users)
{
    if (user.Username.Contains("microsoft"))
    {
        user.Username = "msft:" + user.Username;

        Console.WriteLine("Starting SaveChanges...");
        Console.WriteLine();

        context.SaveChanges();

        Console.WriteLine();
        Console.WriteLine("SaveChanges finished.");
    }
}

Output dari kode ini, dengan pengelogan untuk koneksi diaktifkan, adalah:

Starting query...

dbug: 8/27/2021 09:26:57.810 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

Query finished.

Starting SaveChanges...

dbug: 8/27/2021 09:26:57.813 RelationalEventId.ConnectionOpened[20001] (Microsoft.EntityFrameworkCore.Database.Connection)
      Opened connection to database 'main' on server 'C:\dotnet\efdocs\samples\core\Miscellaneous\NewInEFCore6\bin\Debug\net6.0\test.db'.
dbug: 8/27/2021 09:26:57.814 RelationalEventId.ConnectionClosed[20003] (Microsoft.EntityFrameworkCore.Database.Connection)
      Closed connection to database 'main' on server 'test.db'.

SaveChanges finished.

Perhatikan bahwa koneksi dibuka dan ditutup dengan cepat untuk setiap operasi.

Namun, untuk sebagian besar sistem database, membuka koneksi fisik ke database adalah operasi yang mahal. Oleh karena itu, sebagian besar penyedia ADO.NET membuat kumpulan koneksi fisik dan menyewakannya ke DbConnection instans sesuai kebutuhan.

SQLite sedikit berbeda karena akses database biasanya hanya mengakses file. Ini berarti membuka koneksi ke database SQLite biasanya sangat cepat. Namun, ini tidak selalu terjadi. Misalnya, membuka koneksi ke database terenkripsi bisa sangat lambat. Oleh karena itu, koneksi SQLite sekarang dikumpulkan saat menggunakan Microsoft.Data.Sqlite 6.0.

Mendukung DateOnly dan TimeOnly

Masalah GitHub: #24506.

Microsoft.Data.Sqlite 6.0 mendukung jenis dan TimeOnly baru DateOnly dari .NET 6. Ini juga dapat digunakan dalam EF Core 6.0 dengan penyedia SQLite. Seperti biasa dengan SQLite, sistem jenis aslinya berarti bahwa nilai dari jenis ini perlu disimpan sebagai salah satu dari empat jenis yang didukung. Microsoft.Data.Sqlite menyimpannya sebagai TEXT. Misalnya, entitas yang menggunakan jenis ini:

public class User
{
    public int Id { get; set; }
    public string Username { get; set; }
    
    public DateOnly Birthday { get; set; }
    public TimeOnly TokensRenewed { get; set; }
}

Peta ke tabel berikut ini di database SQLite:

CREATE TABLE "Users" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Users" PRIMARY KEY AUTOINCREMENT,
    "Username" TEXT NULL,
    "Birthday" TEXT NOT NULL,
    "TokensRenewed" TEXT NOT NULL);

Nilai kemudian dapat disimpan, dikueri, dan diperbarui dengan cara normal. Misalnya, kueri EF Core LINQ ini:

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

Diterjemahkan ke dalam yang berikut ini di SQLite:

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

Dan pengembalian hanya menggunakan dengan ulang tahun sebelum 1900 CE:

Found 'ajcvickers'
Found 'wendy'

Savepoints API

Masalah GitHub: #20228.

Kami telah menstandarkan pada API umum untuk titik penyimpanan di penyedia ADO.NET. Microsoft.Data.Sqlite sekarang mendukung API ini, termasuk:

Menggunakan titik penyimpanan memungkinkan bagian dari transaksi digulung balik tanpa menggulung balik seluruh transaksi. Misalnya, kode di bawah ini:

  • Membuat transaksi
  • Mengirim pembaruan ke database
  • Membuat titik penyimpanan
  • Mengirim pembaruan lain ke database
  • Gulung balik ke titik simpan yang dibuat sebelumnya
  • Menerapkan transaksi
using var connection = new SqliteConnection("Command Timeout=60;DataSource=test.db");
connection.Open();

using var transaction = connection.BeginTransaction();

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'ajcvickers' WHERE Id = 1";
    command.ExecuteNonQuery();
}

transaction.Save("MySavepoint");

using (var command = connection.CreateCommand())
{
    command.CommandText = @"UPDATE Users SET Username = 'wfvickers' WHERE Id = 2";
    command.ExecuteNonQuery();
}

transaction.Rollback("MySavepoint");

transaction.Commit();

Ini akan mengakibatkan pembaruan pertama diterapkan ke database, sementara pembaruan kedua tidak diterapkan karena titik penyimpanan digulung balik sebelum melakukan transaksi.

Batas waktu perintah di string koneksi

Masalah GitHub: #22505. Fitur ini dikontribusikan oleh @nmichels. Terima kasih banyak!

penyedia ADO.NET mendukung dua batas waktu yang berbeda:

  • Batas waktu koneksi, yang menentukan waktu maksimum untuk menunggu saat membuat koneksi ke database.
  • Batas waktu perintah, yang menentukan waktu maksimum untuk menunggu perintah selesai dijalankan.

Batas waktu perintah dapat diatur dari kode menggunakan DbCommand.CommandTimeout. Banyak penyedia sekarang juga mengekspos batas waktu perintah ini di string koneksi. Microsoft.Data.Sqlite mengikuti tren ini dengan Command Timeout kata kunci string koneksi. Misalnya, "Command Timeout=60;DataSource=test.db" akan menggunakan 60 detik sebagai batas waktu default untuk perintah yang dibuat oleh koneksi.

Tip

Sqlite memperlakukan sebagai sinonim untuk Command Timeout dan sebagainya dapat digunakan sebagai gantinya jika disukaiDefault Timeout.