Memutus perubahan di EF Core 8 (EF8)
Halaman ini mendokuensikan PERUBAHAN API dan perilaku yang berpotensi memutus pembaruan aplikasi yang ada dari EF Core 7 ke EF Core 8. Pastikan untuk meninjau perubahan yang melanggar sebelumnya jika memperbarui dari versi EF Core yang lebih lama:
Kerangka Target
EF Core 8 menargetkan .NET 8. Aplikasi yang menargetkan versi .NET, .NET Core, dan .NET Framework yang lebih lama perlu diperbarui untuk menargetkan .NET 8.
Ringkasan
Perubahan berdampak tinggi
Contains
dalam kueri LINQ mungkin berhenti bekerja pada versi SQL Server yang lebih lama
Perilaku yang lama
Sebelumnya, ketika Contains
operator digunakan dalam kueri LINQ dengan daftar nilai berparameter, EF menghasilkan SQL yang tidak efisien tetapi bekerja pada semua versi SQL Server.
Perilaku yang baru
Dimulai dengan EF Core 8.0, EF sekarang menghasilkan SQL yang lebih efisien, tetapi tidak didukung pada SQL Server 2014 ke bawah.
Perhatikan bahwa versi SQL Server yang lebih baru dapat dikonfigurasi dengan tingkat kompatibilitas yang lebih lama, juga membuatnya tidak kompatibel dengan SQL baru. Ini juga dapat terjadi dengan database Azure SQL yang dimigrasikan dari instans SQL Server lokal sebelumnya, membawa tingkat kompatibilitas lama.
Mengapa
SQL sebelumnya yang dihasilkan oleh EF Core untuk Contains
menyisipkan nilai berparameter sebagai konstanta di SQL. Misalnya, kueri LINQ berikut:
var names = new[] { "Blog1", "Blog2" };
var blogs = await context.Blogs
.Where(b => names.Contains(b.Name))
.ToArrayAsync();
... akan diterjemahkan ke SQL berikut:
SELECT [b].[Id], [b].[Name]
FROM [Blogs] AS [b]
WHERE [b].[Name] IN (N'Blog1', N'Blog2')
Penyisipan nilai konstanta tersebut ke dalam SQL menciptakan banyak masalah performa, mengalahkan penembolokan rencana kueri dan menyebabkan pengeluaran kueri lain yang tidak diperlukan. Terjemahan EF Core 8.0 baru menggunakan fungsi SQL Server OPENJSON
untuk mentransfer nilai sebagai array JSON. Ini memecahkan masalah performa yang melekat pada teknik sebelumnya; namun, OPENJSON
fungsi ini tidak tersedia di SQL Server 2014 ke bawah.
Untuk informasi selengkapnya tentang perubahan ini, lihat posting blog ini.
Mitigasi
Jika database Anda adalah SQL Server 2016 (13.x) atau yang lebih baru, atau jika Anda menggunakan Azure SQL, periksa tingkat kompatibilitas database Anda yang dikonfigurasi melalui perintah berikut:
SELECT name, compatibility_level FROM sys.databases;
Jika tingkat kompatibilitas di bawah 130 (SQL Server 2016), pertimbangkan untuk memodifikasinya ke nilai yang lebih baru (dokumentasi).
Jika tidak, jika versi database Anda benar-benar lebih lama dari SQL Server 2016, atau diatur ke tingkat kompatibilitas lama yang tidak dapat Anda ubah karena beberapa alasan, konfigurasikan EF Core untuk kembali ke SQL yang lebih lama dan kurang efisien sebagai berikut:
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseSqlServer(@"<CONNECTION STRING>", o => o.UseCompatibilityLevel(120));
Enum di JSON disimpan sebagai ints alih-alih string secara default
Perilaku yang lama
Dalam EF7, enum yang dipetakan ke JSON adalah, secara default, disimpan sebagai nilai string dalam dokumen JSON.
Perilaku yang baru
Dimulai dengan EF Core 8.0, EF sekarang, secara default, memetakan enum ke nilai bilangan bulat dalam dokumen JSON.
Mengapa
EF selalu, secara default, enum yang dipetakan ke kolom numerik dalam database relasional. Karena EF mendukung kueri di mana nilai dari JSON berinteraksi dengan nilai dari kolom dan parameter, penting bahwa nilai dalam JSON cocok dengan nilai di kolom non-JSON.
Mitigasi
Untuk terus menggunakan string, konfigurasikan properti enum dengan konversi. Contohnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<User>().Property(e => e.Status).HasConversion<string>();
}
Atau, untuk semua properti jenis enum::
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder.Properties<StatusEnum>().HaveConversion<string>();
}
Perubahan dampak sedang
SQL Server date
dan time
sekarang perancah ke .NET DateOnly
dan TimeOnly
Perilaku yang lama
Sebelumnya, saat membuat perancah database SQL Server dengan date
atau time
kolom, EF akan menghasilkan properti entitas dengan jenis DateTime dan TimeSpan.
Perilaku yang baru
Dimulai dengan EF Core 8.0, date
dan time
di-scaffold sebagai DateOnly dan TimeOnly.
Mengapa
DateOnly dan TimeOnly diperkenalkan dalam .NET 6.0, dan sangat cocok untuk memetakan jenis tanggal dan waktu database. DateTime terutama berisi komponen waktu yang tidak digunakan dan dapat menyebabkan kebingungan saat memetakannya ke date
, dan TimeSpan mewakili interval waktu - mungkin termasuk hari - daripada waktu hari di mana suatu peristiwa terjadi. Menggunakan jenis baru mencegah bug dan kebingungan, dan memberikan kejelasan niat.
Mitigasi
Perubahan ini hanya memengaruhi pengguna yang secara teratur menyusun ulang database mereka ke dalam model kode EF (alur "database-first").
Disarankan untuk bereaksi terhadap perubahan ini dengan memodifikasi kode Anda untuk menggunakan perancah DateOnly dan TimeOnly jenis yang baru. Namun, jika itu tidak memungkinkan, Anda dapat mengedit templat perancah untuk kembali ke pemetaan sebelumnya. Untuk melakukan ini, siapkan templat seperti yang dijelaskan di halaman ini. Kemudian, edit EntityType.t4
file, temukan di mana properti entitas dibuat (cari property.ClrType
), dan ubah kode menjadi berikut:
var clrType = property.GetColumnType() switch
{
"date" when property.ClrType == typeof(DateOnly) => typeof(DateTime),
"date" when property.ClrType == typeof(DateOnly?) => typeof(DateTime?),
"time" when property.ClrType == typeof(TimeOnly) => typeof(TimeSpan),
"time" when property.ClrType == typeof(TimeOnly?) => typeof(TimeSpan?),
_ => property.ClrType
};
usings.AddRange(code.GetRequiredUsings(clrType));
var needsNullable = Options.UseNullableReferenceTypes && property.IsNullable && !clrType.IsValueType;
var needsInitializer = Options.UseNullableReferenceTypes && !property.IsNullable && !clrType.IsValueType;
#>
public <#= code.Reference(clrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
Kolom Boolean dengan nilai yang dihasilkan database tidak lagi di-scaffolded sebagai nullable
Perilaku yang lama
Sebelumnya, kolom yang tidak dapat bool
diubah ke null dengan batasan default database dibuat sebagai properti nullable bool?
.
Perilaku yang baru
Dimulai dengan EF Core 8.0, kolom yang tidak dapat bool
diubah ke null selalu diacak sebagai properti yang tidak dapat diubah ke null.
Mengapa
Properti bool
tidak akan memiliki nilainya yang dikirim ke database jika nilai tersebut adalah false
, yang merupakan default CLR. Jika database memiliki nilai true
default untuk kolom , maka meskipun nilai properti adalah false
, nilai dalam database berakhir sebagai true
. Namun, di EF8, sentinel yang digunakan untuk menentukan apakah properti memiliki nilai dapat diubah. Ini dilakukan secara otomatis untuk bool
properti dengan nilai yang dihasilkan database , true
yang berarti bahwa tidak lagi diperlukan untuk membuat perancah properti sebagai nullable.
Mitigasi
Perubahan ini hanya memengaruhi pengguna yang secara teratur menyusun ulang database mereka ke dalam model kode EF (alur "database-first").
Disarankan untuk bereaksi terhadap perubahan ini dengan memodifikasi kode Anda untuk menggunakan properti bool yang tidak dapat diubah ke null. Namun, jika itu tidak memungkinkan, Anda dapat mengedit templat perancah untuk kembali ke pemetaan sebelumnya. Untuk melakukan ini, siapkan templat seperti yang dijelaskan di halaman ini. Kemudian, edit EntityType.t4
file, temukan di mana properti entitas dibuat (cari property.ClrType
), dan ubah kode menjadi berikut:
#>
var propertyClrType = property.ClrType != typeof(bool)
|| (property.GetDefaultValueSql() == null && property.GetDefaultValue() != null)
? property.ClrType
: typeof(bool?);
#>
public <#= code.Reference(propertyClrType) #><#= needsNullable ? "?" : "" #> <#= property.Name #> { get; set; }<#= needsInitializer ? " = null!;" : "" #>
<#
<#
Perubahan berdampak rendah
Metode SQLite Math
sekarang diterjemahkan ke SQL
Perilaku Lama
Sebelumnya hanya metode Abs, Max, Min, dan Round yang diterjemahkan Math
ke SQL. Semua anggota lain akan dievaluasi pada klien jika mereka muncul di ekspresi Pilih kueri akhir.
Perilaku yang baru
Dalam EF Core 8.0, semua Math
metode dengan fungsi matematika SQLite yang sesuai diterjemahkan ke SQL.
Fungsi matematika ini telah diaktifkan di pustaka SQLite asli yang kami sediakan secara default (melalui dependensi kami pada paket nuGet SQLitePCLRaw.bundle_e_sqlite3). Mereka juga telah diaktifkan di pustaka yang disediakan oleh SQLitePCLRaw.bundle_e_sqlcipher. Jika Anda menggunakan salah satu pustaka ini, aplikasi Anda tidak boleh terpengaruh oleh perubahan ini.
Namun, ada kemungkinan bahwa aplikasi termasuk pustaka SQLite asli dengan cara lain mungkin tidak mengaktifkan fungsi matematika. Dalam kasus ini, Math
metode akan diterjemahkan ke SQL dan tidak mengalami kesalahan fungsi seperti itu saat dijalankan.
Mengapa
SQLite menambahkan fungsi matematika bawaan dalam versi 3.35.0. Meskipun dinonaktifkan secara default, mereka telah menjadi cukup pervasif sehingga kami memutuskan untuk memberikan terjemahan default untuk mereka di penyedia EF Core SQLite kami.
Kami juga berkolaborasi dengan Eric Sink pada proyek SQLitePCLRaw untuk mengaktifkan fungsi matematika di semua pustaka SQLite asli yang disediakan sebagai bagian dari proyek tersebut.
Mitigasi
Cara paling sederhana untuk memperbaiki jeda adalah, jika memungkinkan, untuk mengaktifkan fungsi matematika adalah pustaka SQLite asli dengan menentukan opsi waktu kompilasi SQLITE_ENABLE_MATH_FUNCTIONS .
Jika Anda tidak mengontrol kompilasi pustaka asli, Anda juga dapat memperbaiki jeda dengan membuat fungsi sendiri saat runtime menggunakan API Microsoft.Data.Sqlite .
sqliteConnection
.CreateFunction<double, double, double>(
"pow",
Math.Pow,
isDeterministic: true);
Atau, Anda dapat memaksa evaluasi klien dengan membagi ekspresi Pilih menjadi dua bagian yang dipisahkan oleh AsEnumerable
.
// Before
var query = dbContext.Cylinders
.Select(
c => new
{
Id = c.Id
// May throw "no such function: pow"
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
// After
var query = dbContext.Cylinders
// Select the properties you'll need from the database
.Select(
c => new
{
c.Id,
c.Radius,
c.Height
})
// Switch to client-eval
.AsEnumerable()
// Select the final results
.Select(
c => new
{
Id = c.Id,
Volume = Math.PI * Math.Pow(c.Radius, 2) * c.Height
});
ITypeBase menggantikan IEntityType di beberapa API
Perilaku yang lama
Sebelumnya, semua jenis struktural yang dipetakan adalah jenis entitas.
Perilaku yang baru
Dengan pengenalan jenis kompleks di EF8, beberapa API yang sebelumnya menggunakan IEntityType
sekarang ITypeBase
sehingga API dapat digunakan dengan jenis entitas atau kompleks. Drive ini termasuk:
IProperty.DeclaringEntityType
sekarang usang danIProperty.DeclaringType
harus digunakan sebagai gantinya.IEntityTypeIgnoredConvention
sekarang usang danITypeIgnoredConvention
harus digunakan sebagai gantinya.IValueGeneratorSelector.Select
sekarang menerima yangITypeBase
mungkin, tetapi tidak harus menjadi .IEntityType
Mengapa
Dengan pengenalan jenis kompleks di EF8, API ini dapat digunakan dengan IEntityType
atau IComplexType
.
Mitigasi
API lama usang, tetapi tidak akan dihapus hingga EF10. Kode harus diperbarui untuk menggunakan API baru ASAP.
Ekspresi ValueConverter dan ValueComparer harus menggunakan API publik untuk model yang dikompilasi
Perilaku yang lama
Sebelumnya, ValueConverter
ValueComparer
dan definisi tidak disertakan dalam model yang dikompilasi, sehingga dapat berisi kode arbitrer.
Perilaku yang baru
EF sekarang mengekstrak ekspresi dari ValueConverter
objek dan ValueComparer
dan menyertakan C# ini dalam model yang dikompilasi. Ini berarti bahwa ekspresi ini hanya boleh menggunakan API publik.
Mengapa
Tim EF secara bertahap memindahkan lebih banyak konstruksi ke dalam model yang dikompilasi untuk mendukung penggunaan EF Core dengan AOT di masa depan.
Mitigasi
Buat API yang digunakan oleh perbandingan publik. Misalnya, pertimbangkan pengonversi sederhana ini:
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
private static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
private static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
Untuk menggunakan pengonversi ini dalam model yang dikompilasi dengan EF8, ConvertToString
metode dan ConvertToBytes
harus dibuat publik. Contohnya:
public class MyValueConverter : ValueConverter<string, byte[]>
{
public MyValueConverter()
: base(v => ConvertToBytes(v), v => ConvertToString(v))
{
}
public static string ConvertToString(byte[] bytes)
=> ""; // ... TODO: Conversion code
public static byte[] ConvertToBytes(string chars)
=> Array.Empty<byte>(); // ... TODO: Conversion code
}
ExcludeFromMigrations tidak lagi mengecualikan tabel lain dalam hierarki TPC
Perilaku yang lama
Sebelumnya, menggunakan ExcludeFromMigrations
pada tabel dalam hierarki TPC juga akan mengecualikan tabel lain dalam hierarki.
Perilaku yang baru
Dimulai dengan EF Core 8.0, ExcludeFromMigrations
tidak berdampak pada tabel lain.
Mengapa
Perilaku lama adalah bug dan mencegah migrasi digunakan untuk mengelola hierarki di seluruh proyek.
Mitigasi
Gunakan ExcludeFromMigrations
secara eksplisit pada tabel lain yang harus dikecualikan.
Kunci bilangan bulat non-bayangan dipertahankan ke dokumen Cosmos
Perilaku yang lama
Sebelumnya, properti bilangan bulat non-bayangan yang cocok dengan kriteria untuk menjadi properti kunci yang disintesis tidak akan disimpan ke dalam dokumen JSON, tetapi sebaliknya disintesis ulang saat keluar.
Perilaku yang baru
Dimulai dengan EF Core 8.0, properti ini sekarang bertahan.
Mengapa
Perilaku lama adalah bug dan mencegah properti yang cocok dengan kriteria kunci yang disintesis agar tidak dipertahankan ke Cosmos.
Mitigasi
Kecualikan properti dari model jika nilainya tidak boleh dipertahankan.
Selain itu, Anda dapat menonaktifkan perilaku ini sepenuhnya dengan mengatur Microsoft.EntityFrameworkCore.Issue31664
appContext beralih ke true
, lihat AppContext untuk konsumen pustaka untuk detail selengkapnya.
AppContext.SetSwitch("Microsoft.EntityFrameworkCore.Issue31664", isEnabled: true);
Model relasional dihasilkan dalam model yang dikompilasi
Perilaku yang lama
Sebelumnya, model relasional dihitung pada run-time bahkan saat menggunakan model yang dikompilasi.
Perilaku yang baru
Dimulai dengan EF Core 8.0, model relasional adalah bagian dari model yang dikompilasi yang dihasilkan. Namun, untuk model yang sangat besar, file yang dihasilkan mungkin gagal dikompilasi.
Mengapa
Hal ini dilakukan untuk lebih meningkatkan waktu startup.
Mitigasi
Edit file yang dihasilkan *ModelBuilder.cs
dan hapus baris AddRuntimeAnnotation("Relational:RelationalModel", CreateRelationalModel());
serta metode CreateRelationalModel()
.
Perancah dapat menghasilkan nama navigasi yang berbeda
Perilaku yang lama
Sebelumnya ketika perancah DbContext
jenis entitas dan dari database yang ada, nama navigasi untuk hubungan terkadang berasal dari awalan umum dari beberapa nama kolom kunci asing.
Perilaku yang baru
Dimulai dengan EF Core 8.0, awalan umum nama kolom dari kunci asing komposit tidak lagi digunakan untuk menghasilkan nama navigasi.
Mengapa
Ini adalah aturan penamaan yang tidak jelas yang kadang-kadang menghasilkan nama yang sangat buruk seperti, , S
Student_
, atau bahkan hanya _
. Tanpa aturan ini, nama-nama aneh tidak lagi dihasilkan, dan konvensi penamaan untuk navigasi juga dibuat lebih sederhana, sehingga membuatnya lebih mudah untuk memahami dan memprediksi nama mana yang akan dihasilkan.
Mitigasi
EF Core Power Tools memiliki opsi untuk terus menghasilkan navigasi dengan cara lama. Atau, kode yang dihasilkan dapat sepenuhnya disesuaikan menggunakan templat T4. Ini dapat digunakan untuk contoh properti kunci asing dari hubungan perancah dan menggunakan aturan apa pun yang sesuai untuk kode Anda untuk menghasilkan nama navigasi yang Anda butuhkan.
Diskriminator sekarang memiliki panjang maksimum
Perilaku yang lama
Sebelumnya, kolom diskriminator yang dibuat untuk pemetaan warisan TPH dikonfigurasi seperti nvarchar(max)
pada SQL Server/Azure SQL, atau jenis string tidak terbatas yang setara pada database lain.
Perilaku yang baru
Dimulai dengan EF Core 8.0, kolom diskriminator dibuat dengan panjang maksimum yang mencakup semua nilai diskriminator yang diketahui. EF akan menghasilkan migrasi untuk melakukan perubahan ini. Namun, jika kolom diskriminator dibatasi dalam beberapa cara -- misalnya, sebagai bagian dari indeks -- maka AlterColumn
yang dibuat oleh Migrasi mungkin gagal.
Mengapa
nvarchar(max)
kolom tidak efisien dan tidak perlu ketika panjang semua nilai yang mungkin diketahui.
Mitigasi
Ukuran kolom dapat dibuat secara eksplisit tidak terbatas:
modelBuilder.Entity<Foo>()
.Property<string>("Discriminator")
.HasMaxLength(-1);
Nilai kunci SQL Server dibandingkan secara tidak peka huruf besar/kecil
Perilaku yang lama
Sebelumnya, saat melacak entitas dengan kunci string dengan penyedia database SQL Server/Azure SQL, nilai kunci dibandingkan menggunakan perbandingan ordinal peka huruf besar/kecil .NET default.
Perilaku yang baru
Dimulai dengan EF Core 8.0, nilai kunci string SQL Server/Azure SQL dibandingkan menggunakan perbandingan ordinal yang tidak peka huruf besar/kecil .NET default.
Mengapa
Secara default, SQL Server menggunakan perbandingan yang tidak peka huruf besar/kecil saat membandingkan nilai kunci asing untuk kecocokan dengan nilai kunci utama. Ini berarti ketika EF menggunakan perbandingan peka huruf besar/kecil, EF mungkin tidak menghubungkan kunci asing ke kunci utama kapan seharusnya.
Mitigasi
Perbandingan peka huruf besar/kecil dapat digunakan dengan mengatur kustom ValueComparer
. Contohnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var comparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.Ordinal),
v => v.GetHashCode(),
v => v);
modelBuilder.Entity<Blog>()
.Property(e => e.Id)
.Metadata.SetValueComparer(comparer);
modelBuilder.Entity<Post>(
b =>
{
b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
});
}