Konversi Nilai
Pengonversi nilai memungkinkan nilai properti dikonversi saat membaca dari atau menulis ke database. Konversi ini bisa dari satu nilai ke nilai lain dengan jenis yang sama (misalnya, mengenkripsi string) atau dari nilai satu jenis ke nilai jenis lain (misalnya, mengonversi nilai enum ke dan dari string dalam database.)
Tip
Anda dapat menjalankan dan men-debug ke semua kode dalam dokumen ini dengan mengunduh kode sampel dari GitHub.
Gambaran Umum
Pengonversi nilai ditentukan dalam hal ModelClrType
dan ProviderClrType
. Jenis model adalah jenis .NET properti dalam jenis entitas. Jenis penyedia adalah jenis .NET yang dipahami oleh penyedia database. Misalnya, untuk menyimpan enum sebagai string dalam database, jenis model adalah jenis enum, dan jenis penyedianya adalah String
. Kedua jenis ini bisa sama.
Konversi didefinisikan menggunakan dua Func
pohon ekspresi: satu dari ModelClrType
ke ProviderClrType
dan yang lainnya dari ProviderClrType
ke ModelClrType
. Pohon ekspresi digunakan sehingga dapat dikompilasi ke dalam delegasi akses database untuk konversi yang efisien. Pohon ekspresi mungkin berisi panggilan sederhana ke metode konversi untuk konversi yang kompleks.
Catatan
Properti yang telah dikonfigurasi untuk konversi nilai mungkin juga perlu menentukan ValueComparer<T>. Lihat contoh di bawah ini, dan dokumentasi Value Comparers untuk informasi selengkapnya.
Mengonfigurasi pengonversi nilai
Konversi nilai dikonfigurasi dalam DbContext.OnModelCreating. Misalnya, pertimbangkan enum dan jenis entitas yang didefinisikan sebagai:
public class Rider
{
public int Id { get; set; }
public EquineBeast Mount { get; set; }
}
public enum EquineBeast
{
Donkey,
Mule,
Horse,
Unicorn
}
Konversi dapat dikonfigurasi OnModelCreating untuk menyimpan nilai enum sebagai string seperti "Keledai", "Mule", dll. dalam database; Anda hanya perlu menyediakan satu fungsi yang mengonversi dari ModelClrType
ke ProviderClrType
, dan yang lain untuk konversi yang berlawanan:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
Catatan
Nilai null
tidak akan pernah diteruskan ke pengonversi nilai. Null dalam kolom database selalu null dalam instans entitas, dan sebaliknya. Ini membuat implementasi konversi lebih mudah dan memungkinkannya dibagikan di antara properti nullable dan non-nullable. Lihat Masalah GitHub #13850 untuk informasi selengkapnya.
Mengonfigurasi pengonversi nilai secara massal
Umum untuk pengonversi nilai yang sama dikonfigurasi untuk setiap properti yang menggunakan jenis CLR yang relevan. Daripada melakukan ini secara manual untuk setiap properti, Anda dapat menggunakan konfigurasi model pra-konvensi untuk melakukan ini sekali untuk seluruh model Anda. Untuk melakukan ini, tentukan pengonversi nilai Anda sebagai kelas:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
Kemudian, ambil alih ConfigureConventions dalam jenis konteks Anda dan konfigurasikan pengonversi sebagai berikut:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Konversi yang telah ditentukan sebelumnya
EF Core berisi banyak konversi yang telah ditentukan sebelumnya yang menghindari kebutuhan untuk menulis fungsi konversi secara manual. Sebagai gantinya, EF Core akan memilih konversi yang akan digunakan berdasarkan jenis properti dalam model dan jenis penyedia database yang diminta.
Misalnya, enum ke konversi string digunakan sebagai contoh di atas, tetapi EF Core akan benar-benar melakukan ini secara otomatis ketika jenis penyedia dikonfigurasi sebagai string
menggunakan jenis generik dari HasConversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
}
Hal yang sama dapat dicapai dengan secara eksplisit menentukan jenis kolom database. Misalnya, jika jenis entitas didefinisikan seperti:
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
Kemudian nilai enum akan disimpan sebagai string dalam database tanpa konfigurasi lebih lanjut di OnModelCreating.
Kelas ValueConverter
Panggilan seperti yang HasConversion ditunjukkan di atas akan membuat ValueConverter<TModel,TProvider> instans dan mengaturnya pada properti . sebagai gantinya ValueConverter
dapat dibuat secara eksplisit. Contohnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new ValueConverter<EquineBeast, string>(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(converter);
}
Ini dapat berguna ketika beberapa properti menggunakan konversi yang sama.
Konverter bawaan
Seperti disebutkan di atas, EF Core dikirim dengan sekumpulan kelas yang telah ditentukan ValueConverter<TModel,TProvider> sebelumnya, yang ditemukan di Microsoft.EntityFrameworkCore.Storage.ValueConversion namespace layanan. Dalam banyak kasus EF akan memilih konverter bawaan yang sesuai berdasarkan jenis properti dalam model dan jenis yang diminta dalam database, seperti yang ditunjukkan di atas untuk enum. Misalnya, menggunakan .HasConversion<int>()
pada bool
properti akan menyebabkan EF Core mengonversi nilai bool menjadi nol numerik dan satu nilai:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion<int>();
}
Ini secara fungsional sama dengan membuat instans bawaan BoolToZeroOneConverter<TProvider> dan mengaturnya secara eksplisit:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new BoolToZeroOneConverter<int>();
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion(converter);
}
Tabel berikut ini merangkum konversi yang umum digunakan sebelumnya dari jenis model/properti ke jenis penyedia database. Dalam tabel any_numeric_type
berarti salah satu dari int
, , long
short
, byte
, uint
, ushort
, ulong
, sbyte
, char
, decimal
, float
, atau double
.
Jenis model/properti | Jenis penyedia/database | Konversi | Penggunaan |
---|---|---|---|
bool | any_numeric_type | False/true ke 0/1 | .HasConversion<any_numeric_type>() |
any_numeric_type | False/true ke dua angka apa pun | Menggunakan BoolToTwoValuesConverter<TProvider> | |
string | False/true ke "N"/"Y" | .HasConversion<string>() |
|
string | False/true ke dua string apa pun | Menggunakan BoolToStringConverter | |
any_numeric_type | bool | 0/1 ke false/true | .HasConversion<bool>() |
any_numeric_type | Pemeran sederhana | .HasConversion<any_numeric_type>() |
|
string | Angka sebagai string | .HasConversion<string>() |
|
Enum | any_numeric_type | Nilai numerik enum | .HasConversion<any_numeric_type>() |
string | Representasi string dari nilai enum | .HasConversion<string>() |
|
string | bool | Mengurai string sebagai bool | .HasConversion<bool>() |
any_numeric_type | Mengurai string sebagai jenis numerik yang diberikan | .HasConversion<any_numeric_type>() |
|
char | Karakter pertama string | .HasConversion<char>() |
|
DateTime | Mengurai string sebagai DateTime | .HasConversion<DateTime>() |
|
DateTimeOffset | Mengurai string sebagai DateTimeOffset | .HasConversion<DateTimeOffset>() |
|
TimeSpan | Mengurai string sebagai TimeSpan | .HasConversion<TimeSpan>() |
|
Guid | Mengurai string sebagai Guid | .HasConversion<Guid>() |
|
byte[] | String sebagai byte UTF8 | .HasConversion<byte[]>() |
|
char | string | String karakter tunggal | .HasConversion<string>() |
DateTime | long | Tanggal/waktu yang dikodekan mempertahankan DateTime.Kind | .HasConversion<long>() |
long | Tanda Waktu | Menggunakan DateTimeToTicksConverter | |
string | String tanggal/waktu budaya invarian | .HasConversion<string>() |
|
DateTimeOffset | long | Tanggal/waktu yang dikodekan dengan offset | .HasConversion<long>() |
string | String tanggal/waktu budaya yang invarian dengan offset | .HasConversion<string>() |
|
TimeSpan | long | Tanda Waktu | .HasConversion<long>() |
string | String rentang waktu budaya invariant | .HasConversion<string>() |
|
Uri | string | URI sebagai string | .HasConversion<string>() |
PhysicalAddress | string | Alamat sebagai string | .HasConversion<string>() |
byte[] | Byte dalam urutan jaringan big-endian | .HasConversion<byte[]>() |
|
IPAddress | string | Alamat sebagai string | .HasConversion<string>() |
byte[] | Byte dalam urutan jaringan big-endian | .HasConversion<byte[]>() |
|
Guid | string | GUID dalam format 'ddddddd-dddd-dddd-dddd-ddddddddddddd' | .HasConversion<string>() |
byte[] | Byte dalam urutan serialisasi biner .NET | .HasConversion<byte[]>() |
Perhatikan bahwa konversi ini mengasumsikan bahwa format nilai sesuai untuk konversi. Misalnya, mengonversi string menjadi angka akan gagal jika nilai string tidak dapat diurai sebagai angka.
Daftar lengkap konverter bawaan adalah:
- Mengonversi properti bool:
- BoolToStringConverter - Bool ke string seperti "N" dan "Y"
- BoolToTwoValuesConverter<TProvider> - Bool ke dua nilai apa pun
- BoolToZeroOneConverter<TProvider> - Bool ke nol dan satu
- Mengonversi properti array byte:
- BytesToStringConverter - Array byte ke string yang dikodekan Base64
- Konversi apa pun yang hanya memerlukan type-cast
- CastingConverter<TModel,TProvider> - Konversi yang hanya memerlukan jenis cast
- Mengonversi properti karakter:
- CharToStringConverter - Karakter ke string karakter tunggal
- Mengonversi DateTimeOffset properti:
- DateTimeOffsetToBinaryConverter - DateTimeOffset ke nilai 64-bit yang dikodekan biner
- DateTimeOffsetToBytesConverter - DateTimeOffset ke array byte
- DateTimeOffsetToStringConverter - DateTimeOffset ke string
- Mengonversi DateTime properti:
- DateTimeToBinaryConverter - DateTime ke nilai 64-bit termasuk DateTimeKind
- DateTimeToStringConverter - DateTime ke string
- DateTimeToTicksConverter - DateTime untuk mencentang
- Mengonversi properti enum:
- EnumToNumberConverter<TEnum,TNumber> - Enum ke nomor yang mendasar
- EnumToStringConverter<TEnum> - Enum ke string
- Mengonversi Guid properti:
- GuidToBytesConverter - Guid ke array byte
- GuidToStringConverter - Guid ke string
- Mengonversi IPAddress properti:
- IPAddressToBytesConverter - IPAddress ke array byte
- IPAddressToStringConverter - IPAddress ke string
- Mengonversi properti numerik (int, double, desimal, dll.):
- NumberToBytesConverter<TNumber> - Setiap nilai numerik ke array byte
- NumberToStringConverter<TNumber> - Setiap nilai numerik untuk string
- Mengonversi PhysicalAddress properti:
- PhysicalAddressToBytesConverter - PhysicalAddress ke array byte
- PhysicalAddressToStringConverter - PhysicalAddress ke string
- Mengonversi properti string:
- StringToBoolConverter - String seperti "N" dan "Y" ke bool
- StringToBytesConverter - String ke byte UTF8
- StringToCharConverter - String ke karakter
- StringToDateTimeConverter - String ke DateTime
- StringToDateTimeOffsetConverter - String ke DateTimeOffset
- StringToEnumConverter<TEnum> - String untuk enum
- StringToGuidConverter - String ke Guid
- StringToNumberConverter<TNumber> - String ke jenis numerik
- StringToTimeSpanConverter - String ke TimeSpan
- StringToUriConverter - String ke Uri
- Mengonversi TimeSpan properti:
- TimeSpanToStringConverter - TimeSpan ke string
- TimeSpanToTicksConverter - TimeSpan untuk mencentang
- Mengonversi Uri properti:
- UriToStringConverter - Uri ke string
Perhatikan bahwa semua konverter bawaan tanpa status dan sehingga satu instans dapat dibagikan dengan aman oleh beberapa properti.
Faset kolom dan petunjuk pemetaan
Beberapa jenis database memiliki faset yang mengubah cara data disimpan. Ini termasuk:
- Presisi dan skala untuk kolom desimal dan tanggal/waktu
- Ukuran/panjang untuk kolom biner dan string
- Unicode untuk kolom string
Faset ini dapat dikonfigurasi dengan cara normal untuk properti yang menggunakan pengonversi nilai, dan akan berlaku untuk jenis database yang dikonversi. Misalnya, saat mengonversi dari enum ke string, kita dapat menentukan bahwa kolom database harus non-Unicode dan menyimpan hingga 20 karakter:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>()
.HasMaxLength(20)
.IsUnicode(false);
}
Atau, saat membuat konverter secara eksplisit:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new ValueConverter<EquineBeast, string>(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(converter)
.HasMaxLength(20)
.IsUnicode(false);
}
Ini menghasilkan varchar(20)
kolom saat menggunakan migrasi EF Core terhadap SQL Server:
CREATE TABLE [Rider] (
[Id] int NOT NULL IDENTITY,
[Mount] varchar(20) NOT NULL,
CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));
Namun, jika secara default semua EquineBeast
kolom harus varchar(20)
, maka informasi ini dapat diberikan kepada pengonversi nilai sebagai ConverterMappingHints. Contohnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new ValueConverter<EquineBeast, string>(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v),
new ConverterMappingHints(size: 20, unicode: false));
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(converter);
}
Sekarang setiap kali konverter ini digunakan, kolom database akan menjadi non-unicode dengan panjang maksimum 20. Namun, ini hanya petunjuk karena mereka ditimpa oleh faset apa pun yang secara eksplisit diatur pada properti yang dipetakan.
Contoh
Objek nilai sederhana
Contoh ini menggunakan jenis sederhana untuk membungkus jenis primitif. Ini dapat berguna ketika Anda ingin jenis dalam model Anda lebih spesifik (dan karenanya lebih aman jenis) daripada jenis primitif. Dalam contoh ini, jenis tersebut adalah Dollars
, yang membungkus primitif desimal:
public readonly struct Dollars
{
public Dollars(decimal amount)
=> Amount = amount;
public decimal Amount { get; }
public override string ToString()
=> $"${Amount}";
}
Ini dapat digunakan dalam jenis entitas:
public class Order
{
public int Id { get; set; }
public Dollars Price { get; set; }
}
Dan dikonversi ke yang mendasar decimal
saat disimpan dalam database:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => v.Amount,
v => new Dollars(v));
Catatan
Objek nilai ini diimplementasikan sebagai struktur baca-saja. Ini berarti bahwa EF Core dapat mengambil rekam jepret dan membandingkan nilai tanpa masalah. Lihat Pembanding Nilai untuk informasi selengkapnya.
Objek nilai komposit
Dalam contoh sebelumnya, jenis objek nilai hanya berisi satu properti. Lebih umum bagi jenis objek nilai untuk menyusun beberapa properti yang bersama-sama membentuk konsep domain. Misalnya, jenis umum Money
yang berisi jumlah dan mata uang:
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
}
Objek nilai ini dapat digunakan dalam jenis entitas seperti sebelumnya:
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
Pengonversi nilai saat ini hanya dapat mengonversi nilai ke dan dari satu kolom database. Batasan ini berarti bahwa semua nilai properti dari objek harus dikodekan ke dalam satu nilai kolom. Ini biasanya ditangani dengan menserialisasikan objek saat masuk ke database, dan kemudian mendeserialisasinya lagi saat keluar. Misalnya, menggunakan System.Text.Json:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));
Catatan
Kami berencana untuk mengizinkan pemetaan objek ke beberapa kolom dalam versi EF Core yang akan datang, menghapus kebutuhan untuk menggunakan serialisasi di sini. Ini dilacak oleh masalah GitHub #13947.
Catatan
Seperti contoh sebelumnya, objek nilai ini diimplementasikan sebagai struktur baca-saja. Ini berarti bahwa EF Core dapat mengambil rekam jepret dan membandingkan nilai tanpa masalah. Lihat Pembanding Nilai untuk informasi selengkapnya.
Kumpulan primitif
Serialisasi juga dapat digunakan untuk menyimpan kumpulan nilai primitif. Contohnya:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public ICollection<string> Tags { get; set; }
}
Menggunakan System.Text.Json lagi:
modelBuilder.Entity<Post>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (ICollection<string>)c.ToList()));
ICollection<string>
mewakili jenis referensi yang dapat diubah. Ini berarti bahwa ValueComparer<T> diperlukan agar EF Core dapat melacak dan mendeteksi perubahan dengan benar. Lihat Pembanding Nilai untuk informasi selengkapnya.
Kumpulan objek nilai
Menggabungkan dua contoh sebelumnya bersama-sama kita dapat membuat kumpulan objek nilai. Misalnya, pertimbangkan AnnualFinance
jenis yang memodelkan keuangan blog selama satu tahun:
public readonly struct AnnualFinance
{
[JsonConstructor]
public AnnualFinance(int year, Money income, Money expenses)
{
Year = year;
Income = income;
Expenses = expenses;
}
public int Year { get; }
public Money Income { get; }
public Money Expenses { get; }
public Money Revenue => new Money(Income.Amount - Expenses.Amount, Income.Currency);
}
Jenis ini menyusun beberapa jenis yang Money
kami buat sebelumnya:
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
}
Kita kemudian dapat menambahkan kumpulan AnnualFinance
ke jenis entitas kita:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
Dan sekali lagi gunakan serialisasi untuk menyimpan ini:
modelBuilder.Entity<Blog>()
.Property(e => e.Finances)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<AnnualFinance>>(v, (JsonSerializerOptions)null),
new ValueComparer<IList<AnnualFinance>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (IList<AnnualFinance>)c.ToList()));
Catatan
Seperti sebelumnya, konversi ini memerlukan ValueComparer<T>. Lihat Pembanding Nilai untuk informasi selengkapnya.
Objek nilai sebagai kunci
Terkadang properti kunci primitif dapat dibungkus dalam objek nilai untuk menambahkan tingkat keamanan jenis tambahan dalam menetapkan nilai. Misalnya, kita dapat menerapkan jenis kunci untuk blog, dan jenis kunci untuk posting:
public readonly struct BlogKey
{
public BlogKey(int id) => Id = id;
public int Id { get; }
}
public readonly struct PostKey
{
public PostKey(int id) => Id = id;
public int Id { get; }
}
Ini kemudian dapat digunakan dalam model domain:
public class Blog
{
public BlogKey Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
}
public class Post
{
public PostKey Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public BlogKey? BlogId { get; set; }
public Blog Blog { get; set; }
}
Perhatikan bahwa Blog.Id
tidak dapat secara tidak sengaja ditetapkan PostKey
, dan Post.Id
tidak dapat secara tidak sengaja ditetapkan .BlogKey
Demikian pula, Post.BlogId
properti kunci asing harus diberi BlogKey
.
Catatan
Menampilkan pola ini tidak berarti kami merekomendasikannya. Pertimbangkan dengan cermat apakah tingkat abstraksi ini membantu atau menghambat pengalaman pengembangan Anda. Selain itu, pertimbangkan untuk menggunakan navigasi dan kunci yang dihasilkan alih-alih berurusan dengan nilai kunci secara langsung.
Properti kunci ini kemudian dapat dipetakan menggunakan pengonversi nilai:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var blogKeyConverter = new ValueConverter<BlogKey, int>(
v => v.Id,
v => new BlogKey(v));
modelBuilder.Entity<Blog>().Property(e => e.Id).HasConversion(blogKeyConverter);
modelBuilder.Entity<Post>(
b =>
{
b.Property(e => e.Id).HasConversion(v => v.Id, v => new PostKey(v));
b.Property(e => e.BlogId).HasConversion(blogKeyConverter);
});
}
Catatan
Properti utama dengan konversi hanya dapat menggunakan nilai kunci yang dihasilkan yang dimulai dengan EF Core 7.0.
Gunakan ulong untuk tanda waktu/rowversion
SQL Server mendukung konkurensi optimis otomatis menggunakan kolom biner rowversion
/timestamp
8-byte. Ini selalu dibaca dari dan ditulis ke database menggunakan array 8-byte. Namun, array byte adalah jenis referensi yang dapat diubah, yang membuatnya agak menyakitkan untuk ditangani. Pengonversi nilai rowversion
memungkinkan untuk dipetakan ke ulong
properti, yang jauh lebih tepat dan mudah digunakan daripada array byte. Misalnya, pertimbangkan Blog
entitas dengan token konkurensi ulong:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
Ini dapat dipetakan ke kolom server rowversion
SQL menggunakan pengonversi nilai:
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Tentukan DateTime.Kind saat membaca tanggal
SQL Server membuang DateTime.Kind bendera saat menyimpan DateTime sebagai datetime
atau datetime2
. Ini berarti bahwa nilai DateTime yang kembali dari database selalu memiliki DateTimeKind .Unspecified
Pengonversi nilai dapat digunakan dalam dua cara untuk menangani hal ini. Pertama, EF Core memiliki pengonversi nilai yang membuat nilai buram 8 byte yang mempertahankan Kind
bendera. Contohnya:
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
Ini memungkinkan nilai DateTime dengan bendera yang berbeda Kind
untuk dicampur dalam database.
Masalah dengan pendekatan ini adalah database tidak lagi dapat dikenali datetime
atau datetime2
kolom. Jadi sebaliknya, biasanya selalu menyimpan waktu UTC (atau, kurang umum, selalu waktu lokal) dan kemudian mengabaikan Kind
bendera atau mengaturnya ke nilai yang sesuai menggunakan pengonversi nilai. Misalnya, konverter di bawah ini memastikan bahwa nilai yang DateTime
dibaca dari database akan memiliki DateTimeKind UTC
:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Jika campuran nilai lokal dan UTC diatur dalam instans entitas, maka konverter dapat digunakan untuk mengonversi dengan tepat sebelum menyisipkan. Contoh:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Catatan
Pertimbangkan dengan cermat untuk menyatukan semua kode akses database untuk menggunakan waktu UTC sepanjang waktu, hanya berurusan dengan waktu lokal saat menyajikan data kepada pengguna.
Menggunakan kunci string yang tidak peka huruf besar/kecil
Beberapa database, termasuk SQL Server, melakukan perbandingan string yang tidak peka huruf besar/kecil secara default. .NET, di sisi lain, melakukan perbandingan string peka huruf besar/kecil secara default. Ini berarti bahwa nilai kunci asing seperti "DotNet" akan cocok dengan nilai kunci utama "dotnet" di SQL Server, tetapi tidak akan cocok dengannya di EF Core. Pembanding nilai untuk kunci dapat digunakan untuk memaksa EF Core ke dalam perbandingan string yang tidak peka huruf besar/kecil seperti dalam database. Misalnya, pertimbangkan model blog/posting dengan kunci string:
public class Blog
{
public string Id { get; set; }
public string Name { get; set; }
public ICollection<Post> Posts { get; set; }
}
public class Post
{
public string Id { get; set; }
public string Title { get; set; }
public string Content { get; set; }
public string BlogId { get; set; }
public Blog Blog { get; set; }
}
Ini tidak akan berfungsi seperti yang Post.BlogId
diharapkan jika beberapa nilai memiliki casing yang berbeda. Kesalahan yang disebabkan oleh ini akan tergantung pada apa yang dilakukan aplikasi, tetapi biasanya melibatkan grafik objek yang tidak diperbaiki dengan benar, dan/atau pembaruan yang gagal karena nilai FK salah. Perbandingan nilai dapat digunakan untuk memperbaiki ini:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var comparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
v => v.ToUpper().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);
});
}
Catatan
Perbandingan string .NET dan perbandingan string database dapat berbeda dalam lebih dari sekadar sensitivitas kasus. Pola ini berfungsi untuk kunci ASCII sederhana, tetapi mungkin gagal untuk kunci dengan segala jenis karakter khusus budaya. Lihat Kolate dan Sensitivitas Kasus untuk informasi selengkapnya.
Menangani string database dengan panjang tetap
Contoh sebelumnya tidak memerlukan pengonversi nilai. Namun, pengonversi dapat berguna untuk jenis string database dengan panjang tetap seperti char(20)
atau nchar(20)
. String panjang tetap diisi dengan panjang penuh setiap kali nilai dimasukkan ke dalam database. Ini berarti bahwa nilai kunci "dotnet
" akan dibaca kembali dari database sebagai "dotnet..............
", di mana .
mewakili karakter spasi. Ini kemudian tidak akan membandingkan dengan benar dengan nilai kunci yang tidak diisi.
Pengonversi nilai dapat digunakan untuk memangkas padding saat membaca nilai kunci. Ini dapat dikombinasikan dengan pembanding nilai dalam contoh sebelumnya untuk membandingkan kunci ASCII yang tidak peka huruf besar/kecil dengan panjang tetap dengan benar. Contohnya:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new ValueConverter<string, string>(
v => v,
v => v.Trim());
var comparer = new ValueComparer<string>(
(l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
v => v.ToUpper().GetHashCode(),
v => v);
modelBuilder.Entity<Blog>()
.Property(e => e.Id)
.HasColumnType("char(20)")
.HasConversion(converter, comparer);
modelBuilder.Entity<Post>(
b =>
{
b.Property(e => e.Id).HasColumnType("char(20)").HasConversion(converter, comparer);
b.Property(e => e.BlogId).HasColumnType("char(20)").HasConversion(converter, comparer);
});
}
Mengenkripsi nilai properti
Pengonversi nilai dapat digunakan untuk mengenkripsi nilai properti sebelum mengirimkannya ke database, lalu mendekripsinya saat keluar. Misalnya, menggunakan pembalikan string sebagai pengganti algoritma enkripsi nyata:
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Catatan
Saat ini tidak ada cara untuk mendapatkan referensi ke DbContext saat ini, atau status sesi lainnya, dari dalam pengonversi nilai. Ini membatasi jenis enkripsi yang dapat digunakan. Pilih masalah GitHub #11597 agar batasan ini dihapus.
Peringatan
Pastikan untuk memahami semua implikasi jika Anda menggulung enkripsi Anda sendiri untuk melindungi data sensitif. Pertimbangkan untuk menggunakan mekanisme enkripsi bawaan, seperti Always Encrypted di SQL Server.
Batasan
Ada beberapa batasan yang diketahui saat ini dari sistem konversi nilai:
- Seperti disebutkan di atas,
null
tidak dapat dikonversi. Silakan pilih (👍) untuk masalah GitHub #13850 jika ini adalah sesuatu yang Anda butuhkan. - Tidak dimungkinkan untuk mengkueri ke properti yang dikonversi nilai, misalnya anggota referensi pada jenis .NET yang dikonversi nilai dalam kueri LINQ Anda. Silakan pilih (👍) untuk masalah GitHub #10434 jika ini adalah sesuatu yang Anda butuhkan - tetapi mempertimbangkan untuk menggunakan kolom JSON sebagai gantinya.
- Saat ini tidak ada cara untuk menyebarkan konversi satu properti ke beberapa kolom atau sebaliknya. Silakan pilih (👍) untuk masalah GitHub #13947 jika ini adalah sesuatu yang Anda butuhkan.
- Pembuatan nilai tidak didukung untuk sebagian besar kunci yang dipetakan melalui pengonversi nilai. Silakan pilih (👍) untuk masalah GitHub #11597 jika ini adalah sesuatu yang Anda butuhkan.
- Konversi nilai tidak dapat mereferensikan instans DbContext saat ini. Silakan pilih (👍) untuk masalah GitHub #12205 jika ini adalah sesuatu yang Anda butuhkan.
- Parameter yang menggunakan jenis yang dikonversi nilai saat ini tidak dapat digunakan dalam API SQL mentah. Silakan pilih (👍) untuk masalah GitHub #27534 jika ini adalah sesuatu yang Anda butuhkan.
Penghapusan batasan ini sedang dipertimbangkan untuk rilis mendatang.