Konwersje wartości
Konwertery wartości umożliwiają konwertowanie wartości właściwości podczas odczytywania z bazy danych lub zapisywania ich w bazie danych. Ta konwersja może pochodzić z jednej wartości do innego tego samego typu (na przykład szyfrowania ciągów) lub wartości jednego typu do wartości innego typu (na przykład konwertowania wartości wyliczeniowych na i z ciągów w bazie danych).
Napiwek
Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.
Omówienie
Konwertery wartości są określane w kategoriach a ModelClrType
i ProviderClrType
. Typ modelu to typ platformy .NET właściwości w typie jednostki. Typ dostawcy to typ platformy .NET rozumiany przez dostawcę bazy danych. Na przykład aby zapisać wyliczenia jako ciągi w bazie danych, typ modelu jest typem wyliczenia, a typ dostawcy to String
. Te dwa typy mogą być takie same.
Konwersje są definiowane przy użyciu dwóch Func
drzew wyrażeń: jeden z ModelClrType
do ProviderClrType
i drugi z ProviderClrType
do ModelClrType
. Drzewa wyrażeń są używane, aby można je było skompilować do delegata dostępu do bazy danych w celu wydajnej konwersji. Drzewo wyrażeń może zawierać proste wywołanie metody konwersji dla złożonych konwersji.
Uwaga
Właściwość skonfigurowana do konwersji wartości może również wymagać określenia ValueComparer<T>wartości . Aby uzyskać więcej informacji, zapoznaj się z poniższymi przykładami i dokumentacją funkcji porównywania wartości.
Konfigurowanie konwertera wartości
Konwersje wartości są konfigurowane w programie DbContext.OnModelCreating. Rozważmy na przykład wyliczenie i typ jednostki zdefiniowany jako:
public class Rider
{
public int Id { get; set; }
public EquineBeast Mount { get; set; }
}
public enum EquineBeast
{
Donkey,
Mule,
Horse,
Unicorn
}
Konwersje można skonfigurować do OnModelCreating przechowywania wartości wyliczenia jako ciągów, takich jak "Donkey", "", itp. w bazie danych; wystarczy po prostu podać jedną funkcję, która konwertuje z ModelClrType
elementu na ProviderClrType
, a drugą na odwrotną konwersję:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
Uwaga
null
Wartość nigdy nie zostanie przekazana do konwertera wartości. Wartość null w kolumnie bazy danych jest zawsze wartością null w wystąpieniu jednostki i odwrotnie. Dzięki temu implementacja konwersji jest łatwiejsza i umożliwia udostępnianie ich między właściwościami dopuszczanymi do wartości null i niepustymi. Aby uzyskać więcej informacji, zobacz Problem z usługą GitHub #13850 .
Zbiorcze konfigurowanie konwertera wartości
Ten sam konwerter wartości jest często konfigurowany dla każdej właściwości, która używa odpowiedniego typu CLR. Zamiast ręcznie wykonywać te czynności dla każdej właściwości, możesz użyć konfiguracji modelu przed konwencją, aby to zrobić raz dla całego modelu. W tym celu zdefiniuj konwerter wartości jako klasę:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
Następnie przesłoń ConfigureConventions typ kontekstu i skonfiguruj konwerter w następujący sposób:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Wstępnie zdefiniowane konwersje
Program EF Core zawiera wiele wstępnie zdefiniowanych konwersji, które unikają ręcznego zapisywania funkcji konwersji. Zamiast tego program EF Core wybierze konwersję do użycia na podstawie typu właściwości w modelu i żądanego typu dostawcy bazy danych.
Na przykład wyliczenie do konwersji ciągów jest używane jako przykład powyżej, ale program EF Core faktycznie zrobi to automatycznie, gdy typ dostawcy jest skonfigurowany jako string
przy użyciu typu HasConversionogólnego :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
}
To samo można osiągnąć, jawnie określając typ kolumny bazy danych. Jeśli na przykład typ jednostki jest zdefiniowany w następujący sposób:
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
Następnie wartości wyliczenia zostaną zapisane jako ciągi w bazie danych bez dalszej konfiguracji w pliku OnModelCreating.
Klasa ValueConverter
Wywołanie HasConversion , jak pokazano powyżej, spowoduje utworzenie ValueConverter<TModel,TProvider> wystąpienia i ustawienie go we właściwości . ValueConverter
Zamiast tego można je utworzyć jawnie. Na przykład:
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);
}
Może to być przydatne, gdy wiele właściwości używa tej samej konwersji.
Wbudowane konwertery
Jak wspomniano powyżej, program EF Core jest dostarczany z zestawem wstępnie zdefiniowanych ValueConverter<TModel,TProvider> klas znajdujących się w Microsoft.EntityFrameworkCore.Storage.ValueConversion przestrzeni nazw. W wielu przypadkach program EF wybierze odpowiedni wbudowany konwerter na podstawie typu właściwości w modelu i typu żądanego w bazie danych, jak pokazano powyżej dla wyliczenia. Na przykład użycie właściwości .HasConversion<int>()
bool
spowoduje, że program EF Core przekonwertuje wartości logiczne na wartości liczbowe zero i jedną wartość:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion<int>();
}
Jest to funkcjonalnie takie samo, jak utworzenie wystąpienia wbudowanego BoolToZeroOneConverter<TProvider> i jawne ustawienie go:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new BoolToZeroOneConverter<int>();
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion(converter);
}
W poniższej tabeli przedstawiono podsumowanie często używanych wstępnie zdefiniowanych konwersji z typów modelu/właściwości na typy dostawców baz danych. W tabeli any_numeric_type
oznacza jedną z wartości int
, short
, long
ushort
ulong
uint
byte
char
decimal
sbyte
float
lub .double
Typ modelu/właściwości | Dostawca/typ bazy danych | Konwersja | Użycie |
---|---|---|---|
bool | any_numeric_type | Wartość false/true do 0/1 | .HasConversion<any_numeric_type>() |
any_numeric_type | Fałsz/prawda do dwóch liczb | Korzystanie z polecenia BoolToTwoValuesConverter<TProvider> | |
string | Fałsz/true do "N"/"Y" | .HasConversion<string>() |
|
string | Wartość false/true dla dwóch ciągów | Korzystanie z polecenia BoolToStringConverter | |
any_numeric_type | bool | Od 0/1 do wartości false/true | .HasConversion<bool>() |
any_numeric_type | Rzutowanie proste | .HasConversion<any_numeric_type>() |
|
string | Liczba jako ciąg | .HasConversion<string>() |
|
Wyliczenie | any_numeric_type | Wartość liczbowa wyliczenia | .HasConversion<any_numeric_type>() |
string | Reprezentacja ciągu wartości wyliczenia | .HasConversion<string>() |
|
string | bool | Analizuje ciąg jako wartość logiczną | .HasConversion<bool>() |
any_numeric_type | Analizuje ciąg jako podany typ liczbowy | .HasConversion<any_numeric_type>() |
|
char | Pierwszy znak ciągu | .HasConversion<char>() |
|
DateTime | Analizuje ciąg jako data/godzina | .HasConversion<DateTime>() |
|
DateTimeOffset | Analizuje ciąg jako element DateTimeOffset | .HasConversion<DateTimeOffset>() |
|
przedział_czasu | Analizuje ciąg jako przedział czasu | .HasConversion<TimeSpan>() |
|
Identyfikator GUID | Analizuje ciąg jako identyfikator GUID | .HasConversion<Guid>() |
|
byte[] | Ciąg jako bajty UTF8 | .HasConversion<byte[]>() |
|
char | string | Ciąg z pojedynczym znakiem | .HasConversion<string>() |
DateTime | długi | Zakodowana data/godzina zachowująca wartość DateTime.Kind | .HasConversion<long>() |
długi | Kleszcze | Korzystanie z polecenia DateTimeToTicksConverter | |
string | Niezmienny ciąg daty/godziny kultury | .HasConversion<string>() |
|
DateTimeOffset | długi | Zakodowana data/godzina z przesunięciem | .HasConversion<long>() |
string | Niezmienny ciąg daty/godziny kultury z przesunięciem | .HasConversion<string>() |
|
przedział_czasu | długi | Kleszcze | .HasConversion<long>() |
string | Niezmienny ciąg przedziału czasu kultury | .HasConversion<string>() |
|
Identyfikator URI | string | Identyfikator URI jako ciąg | .HasConversion<string>() |
PhysicalAddress | string | Adres jako ciąg | .HasConversion<string>() |
byte[] | Bajty w kolejności sieci big-endian | .HasConversion<byte[]>() |
|
IPAddress | string | Adres jako ciąg | .HasConversion<string>() |
byte[] | Bajty w kolejności sieci big-endian | .HasConversion<byte[]>() |
|
Identyfikator GUID | string | Identyfikator GUID w formacie "dd-d-d-d-d" | .HasConversion<string>() |
byte[] | Bajty w kolejności serializacji binarnej platformy .NET | .HasConversion<byte[]>() |
Należy pamiętać, że te konwersje zakładają, że format wartości jest odpowiedni dla konwersji. Na przykład konwertowanie ciągów na liczby zakończy się niepowodzeniem, jeśli wartości ciągu nie mogą być analizowane jako liczby.
Pełna lista wbudowanych konwerterów to:
- Konwertowanie właściwości wartości logicznej:
- BoolToStringConverter - Wartość logiczna do ciągów, takich jak "N" i "Y"
- BoolToTwoValuesConverter<TProvider> - Wartość logiczna do dwóch wartości
- BoolToZeroOneConverter<TProvider> - Wartość logiczna do zera i jedna
- Konwertowanie właściwości tablicy bajtów:
- BytesToStringConverter - Tablica bajtów do ciągu zakodowanego w formacie Base64
- Każda konwersja, która wymaga tylko rzutu typu
- CastingConverter<TModel,TProvider> - Konwersje, które wymagają tylko rzutu typu
- Konwertowanie właściwości znaków:
- CharToStringConverter - Znak do ciągu pojedynczego znaku
- Konwertowanie DateTimeOffset właściwości:
- DateTimeOffsetToBinaryConverter - DateTimeOffset do wartości 64-bitowej zakodowanej binarnie
- DateTimeOffsetToBytesConverter - DateTimeOffset do tablicy bajtów
- DateTimeOffsetToStringConverter - DateTimeOffset do ciągu
- Konwertowanie DateTime właściwości:
- DateTimeToBinaryConverter - DateTime do 64-bitowej wartości, w tym DateTimeKind
- DateTimeToStringConverter - DateTime do ciągu
- DateTimeToTicksConverter - DateTime do kleszczy
- Konwertowanie właściwości wyliczenia:
- EnumToNumberConverter<TEnum,TNumber> - Wyliczenie do bazowej liczby
- EnumToStringConverter<TEnum> - Wyliczenie do ciągu
- Konwertowanie Guid właściwości:
- GuidToBytesConverter - Guid do tablicy bajtów
- GuidToStringConverter - Guid do ciągu
- Konwertowanie IPAddress właściwości:
- IPAddressToBytesConverter - IPAddress do tablicy bajtów
- IPAddressToStringConverter - IPAddress do ciągu
- Konwertowanie właściwości liczbowych (int, double, decimal itp.):
- NumberToBytesConverter<TNumber> - Dowolna wartość liczbowa do tablicy bajtów
- NumberToStringConverter<TNumber> - Dowolna wartość liczbowa do ciągu
- Konwertowanie PhysicalAddress właściwości:
- PhysicalAddressToBytesConverter - PhysicalAddress do tablicy bajtów
- PhysicalAddressToStringConverter - PhysicalAddress do ciągu
- Konwertowanie właściwości ciągu:
- StringToBoolConverter - Ciągi, takie jak "N" i "Y" do wartości logicznej
- StringToBytesConverter - Ciąg do UTF8 bajtów
- StringToCharConverter - Ciąg do znaku
- StringToDateTimeConverter - Ciąg do DateTime
- StringToDateTimeOffsetConverter - Ciąg do DateTimeOffset
- StringToEnumConverter<TEnum> - Ciąg do wyliczenia
- StringToGuidConverter - Ciąg do Guid
- StringToNumberConverter<TNumber> - Ciąg do typu liczbowego
- StringToTimeSpanConverter - Ciąg do TimeSpan
- StringToUriConverter - Ciąg do Uri
- Konwertowanie TimeSpan właściwości:
- TimeSpanToStringConverter - TimeSpan do ciągu
- TimeSpanToTicksConverter - TimeSpan do kleszczy
- Konwertowanie Uri właściwości:
- UriToStringConverter - Uri do ciągu
Należy pamiętać, że wszystkie wbudowane konwertery są bezstanowe i dlatego pojedyncze wystąpienie może być bezpiecznie współużytkowane przez wiele właściwości.
Aspekty kolumn i wskazówki mapowania
Niektóre typy baz danych mają aspekty modyfikujące sposób przechowywania danych. Są to:
- Precyzja i skala dla kolumn dziesiętnych i daty/godziny
- Rozmiar/długość kolumn binarnych i ciągów
- Unicode dla kolumn ciągu
Te aspekty można skonfigurować w normalny sposób dla właściwości używającej konwertera wartości i będą stosowane do przekonwertowanego typu bazy danych. Na przykład podczas konwertowania z wyliczenia na ciągi możemy określić, że kolumna bazy danych powinna być nie unicode i przechowywać maksymalnie 20 znaków:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>()
.HasMaxLength(20)
.IsUnicode(false);
}
Lub podczas jawnego tworzenia konwertera:
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);
}
Spowoduje to wyświetlenie varchar(20)
kolumny podczas korzystania z migracji programu EF Core do programu SQL Server:
CREATE TABLE [Rider] (
[Id] int NOT NULL IDENTITY,
[Mount] varchar(20) NOT NULL,
CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));
Jeśli jednak domyślnie wszystkie EquineBeast
kolumny powinny mieć varchar(20)
wartość , te informacje mogą być przekazywane do konwertera ConverterMappingHintswartości jako . Na przykład:
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);
}
Teraz w dowolnym momencie użycia tego konwertera kolumna bazy danych będzie nie unicode o maksymalnej długości 20. Są to jednak tylko wskazówki, ponieważ są one zastępowane przez wszystkie aspekty jawnie ustawione we właściwości mapowanej.
Przykłady
Proste obiekty wartości
W tym przykładzie użyto prostego typu do opakowania typu pierwotnego. Może to być przydatne, gdy chcesz, aby typ w modelu był bardziej szczegółowy (a tym samym bardziej bezpieczny dla typu) niż typ pierwotny. W tym przykładzie ten typ to Dollars
, który opakowuje typ pierwotny dziesiętny:
public readonly struct Dollars
{
public Dollars(decimal amount)
=> Amount = amount;
public decimal Amount { get; }
public override string ToString()
=> $"${Amount}";
}
Może to być używane w typie jednostki:
public class Order
{
public int Id { get; set; }
public Dollars Price { get; set; }
}
I przekonwertowane na bazowe decimal
podczas przechowywania w bazie danych:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => v.Amount,
v => new Dollars(v));
Uwaga
Ten obiekt wartości jest implementowany jako struktura readonly. Oznacza to, że program EF Core może migawek i porównywać wartości bez problemu. Aby uzyskać więcej informacji, zobacz Porównanie wartości.
Obiekty wartości złożonej
W poprzednim przykładzie typ obiektu wartości zawierał tylko jedną właściwość. Typ obiektu wartości jest częściej używany do tworzenia wielu właściwości, które razem tworzą koncepcję domeny. Na przykład ogólny Money
typ, który zawiera zarówno kwotę, jak i walutę:
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
}
Ten obiekt wartości może być używany w typie jednostki, tak jak poprzednio:
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
Konwertery wartości mogą obecnie konwertować wartości tylko na i z jednej kolumny bazy danych. To ograniczenie oznacza, że wszystkie wartości właściwości z obiektu muszą być zakodowane w jedną wartość kolumny. Jest to zwykle obsługiwane przez serializowanie obiektu w miarę przechodzenia do bazy danych, a następnie deserializacji go ponownie w drodze. Na przykład przy użyciu polecenia System.Text.Json:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));
Uwaga
Planujemy zezwolić na mapowanie obiektu na wiele kolumn w przyszłej wersji programu EF Core, co pozwala usunąć konieczność użycia serializacji tutaj. Jest to śledzone przez problem z usługą GitHub #13947.
Uwaga
Podobnie jak w poprzednim przykładzie, ten obiekt wartości jest implementowany jako struktura readonly. Oznacza to, że program EF Core może migawek i porównywać wartości bez problemu. Aby uzyskać więcej informacji, zobacz Porównanie wartości.
Kolekcje elementów pierwotnych
Serializacji można również używać do przechowywania kolekcji wartości pierwotnych. Na przykład:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public ICollection<string> Tags { get; set; }
}
Użyj System.Text.Json ponownie:
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>
reprezentuje modyfikowalny typ odwołania. Oznacza to, że element jest potrzebny, ValueComparer<T> aby program EF Core mógł prawidłowo śledzić i wykrywać zmiany. Aby uzyskać więcej informacji, zobacz Porównanie wartości.
Kolekcje obiektów wartości
Łącząc dwa poprzednie przykłady, możemy utworzyć kolekcję obiektów wartości. Rozważmy na przykład typ, AnnualFinance
który modeluje finanse bloga przez jeden rok:
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);
}
Ten typ komponuje kilka utworzonych Money
wcześniej typów:
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
}
Następnie możemy dodać kolekcję AnnualFinance
do typu jednostki:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
I ponownie użyj serializacji do przechowywania tego:
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()));
Uwaga
Tak jak wcześniej ta konwersja wymaga ValueComparer<T>. Aby uzyskać więcej informacji, zobacz Porównanie wartości.
Obiekty wartości jako klucze
Czasami właściwości klucza pierwotnego mogą być opakowane w obiekty wartości, aby dodać dodatkowy poziom bezpieczeństwa typu w przypisywaniu wartości. Na przykład możemy zaimplementować typ klucza dla blogów i typ klucza dla wpisów:
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; }
}
Można ich następnie użyć w modelu domeny:
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; }
}
Zwróć uwagę, że Blog.Id
nie można przypadkowo przypisać elementu PostKey
i Post.Id
nie można go przypadkowo przypisać BlogKey
. Podobnie właściwość klucza obcego Post.BlogId
musi mieć przypisaną BlogKey
właściwość .
Uwaga
Pokazanie tego wzorca nie oznacza, że jest to zalecane. Dokładnie zastanów się, czy ten poziom abstrakcji pomaga lub utrudnia środowisko deweloperskie. Należy również rozważyć użycie nawigacji i wygenerowanych kluczy zamiast bezpośrednio radzić sobie z wartościami kluczy.
Te właściwości klucza można następnie mapować przy użyciu konwerterów wartości:
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);
});
}
Uwaga
Właściwości klucza z konwersjami mogą używać tylko wygenerowanych wartości kluczy, począwszy od programu EF Core 7.0.
Użyj ulong dla znacznika czasu/elementu rowversion
Program SQL Server obsługuje automatyczną optymistyczną współbieżność przy użyciu 8-bajtowych kolumn binarnychtimestamp
/rowversion
. Są one zawsze odczytywane i zapisywane w bazie danych przy użyciu tablicy 8-bajtowej. Jednak tablice bajtów są modyfikowalnym typem odwołania, co sprawia, że są one nieco bolesne do radzenia sobie z. Konwertery wartości umożliwiają rowversion
mapowanie elementu na ulong
właściwość, która jest znacznie bardziej odpowiednia i łatwa w użyciu niż tablica bajtów. Rozważmy na przykład Blog
jednostkę z tokenem współbieżności ulong:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
Można to zamapować na kolumnę programu SQL Server rowversion
przy użyciu konwertera wartości:
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Określ typ DateTime.Kind podczas odczytywania dat
Program SQL Server odrzuca flagę DateTime.Kind podczas przechowywania DateTime elementu jako lub datetime
datetime2
. Oznacza to, że wartości DateTime pochodzące z bazy danych zawsze mają DateTimeKind wartość Unspecified
.
Konwertery wartości mogą być używane na dwa sposoby, aby sobie z tym poradzić. Po pierwsze, program EF Core ma konwerter wartości, który tworzy nieprzezroczystą wartość 8-bajtową, która zachowuje flagę Kind
. Na przykład:
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
Umożliwia to mieszanie wartości DateTime z różnymi Kind
flagami w bazie danych.
Problem z tym podejściem polega na tym, że baza danych nie ma już rozpoznawalnych datetime
ani datetime2
kolumn. Zamiast tego często zdarza się przechowywać czas UTC (lub rzadziej zawsze czas lokalny), a następnie zignorować flagę Kind
lub ustawić ją na odpowiednią wartość przy użyciu konwertera wartości. Na przykład poniższy konwerter gwarantuje, że DateTime
wartość odczytana z bazy danych będzie miała wartość DateTimeKind UTC
:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Jeśli w wystąpieniach jednostek jest ustawiana kombinacja wartości lokalnych i UTC, konwerter może służyć do odpowiedniego konwertowania przed wstawieniem. Na przykład:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Uwaga
Starannie rozważ ujednolicenie całego kodu dostępu do bazy danych, aby używać czasu UTC przez cały czas, tylko do czynienia z czasem lokalnym podczas prezentowania danych użytkownikom.
Używanie kluczy ciągów bez uwzględniania wielkości liter
Niektóre bazy danych, w tym program SQL Server, domyślnie wykonują porównania ciągów bez uwzględniania wielkości liter. Z drugiej strony platforma .NET domyślnie wykonuje porównania ciągów z uwzględnieniem wielkości liter. Oznacza to, że wartość klucza obcego, taka jak "DotNet", będzie zgodna z wartością klucza podstawowego "dotnet" w programie SQL Server, ale nie będzie zgodna z nią w programie EF Core. Porównanie wartości dla kluczy może służyć do wymuszenia programu EF Core w porównaniach ciągów bez uwzględniania wielkości liter, takich jak w bazie danych. Rozważmy na przykład model bloga/postów z kluczami ciągów:
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; }
}
Nie będzie to działać zgodnie z oczekiwaniami, jeśli niektóre wartości Post.BlogId
mają inną wielkość liter. Błędy spowodowane przez to będą zależeć od tego, co robi aplikacja, ale zazwyczaj obejmują grafy obiektów, które nie są poprawnie naprawione , i/lub aktualizacje, które kończą się niepowodzeniem, ponieważ wartość FK jest nieprawidłowa. Aby rozwiązać ten błąd, można użyć porównania wartości:
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);
});
}
Uwaga
Porównania ciągów platformy .NET i porównania ciągów bazy danych mogą się różnić bardziej niż tylko wielkość liter. Ten wzorzec działa w przypadku prostych kluczy ASCII, ale może zakończyć się niepowodzeniem dla kluczy z dowolnym rodzajem znaków specyficznych dla kultury. Aby uzyskać więcej informacji, zobacz Sortowanie i ważność wielkości liter.
Obsługa ciągów bazy danych o stałej długości
Poprzedni przykład nie potrzebował konwertera wartości. Jednak konwerter może być przydatny w przypadku typów ciągów bazy danych o stałej długości, takich jak char(20)
lub nchar(20)
. Ciągi o stałej długości są dopełniane do pełnej długości za każdym razem, gdy wartość zostanie wstawiona do bazy danych. Oznacza to, że wartość klucza "dotnet
" będzie odczytywana z bazy danych jako "dotnet..............
", gdzie .
reprezentuje znak spacji. Nie spowoduje to poprawnego porównania z wartościami kluczy, które nie są dopełnione.
Konwerter wartości może służyć do przycinania wypełnienia podczas odczytywania wartości klucza. Można to połączyć z modułem porównującym wartości w poprzednim przykładzie, aby poprawnie porównać klucze ASCII o stałej długości. Na przykład:
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);
});
}
Szyfrowanie wartości właściwości
Konwertery wartości mogą służyć do szyfrowania wartości właściwości przed wysłaniem ich do bazy danych, a następnie odszyfrować je w drodze. Na przykład użycie odwrócenia ciągu jako zamiennika rzeczywistego algorytmu szyfrowania:
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Uwaga
Obecnie nie ma możliwości pobrania odwołania do bieżącego elementu DbContext lub innego stanu sesji z poziomu konwertera wartości. Ogranicza to rodzaje szyfrowania, których można użyć. Zagłosuj na problem z usługą GitHub #11597 , aby to ograniczenie zostało usunięte.
Ostrzeżenie
Pamiętaj, aby zrozumieć wszystkie implikacje, jeśli wdrożysz własne szyfrowanie w celu ochrony poufnych danych. Rozważ użycie wstępnie utworzonych mechanizmów szyfrowania, takich jak Always Encrypted w programie SQL Server.
Ograniczenia
Istnieje kilka znanych bieżących ograniczeń systemu konwersji wartości:
- Jak wspomniano powyżej,
null
nie można przekonwertować. Zagłosuj (👍) na problem z usługą GitHub #13850 , jeśli jest to coś, czego potrzebujesz. - Nie można wykonywać zapytań dotyczących właściwości przekonwertowanych na wartość, np. elementów członkowskich odwołań w typie platformy .NET przekonwertowanym na wartość w zapytaniach LINQ. Zagłosuj (👍) na problem z usługą GitHub #10434 , jeśli jest to coś potrzebnego , ale zamiast tego rozważ użycie kolumny JSON.
- Obecnie nie ma możliwości rozłożenia konwersji jednej właściwości na wiele kolumn lub na odwrót. Zagłosuj na problem z usługą GitHub #13947,👍 jeśli jest to coś, czego potrzebujesz.
- Generowanie wartości nie jest obsługiwane w przypadku większości kluczy mapowanych za pomocą konwerterów wartości. Zagłosuj na👍 problem z usługą GitHub #11597, jeśli jest to coś, czego potrzebujesz.
- Konwersje wartości nie mogą odwoływać się do bieżącego wystąpienia DbContext. Zagłosuj na problem z usługą GitHub #12205,👍 jeśli jest to coś, czego potrzebujesz.
- Parametry używające typów przekonwertowanych na wartość nie mogą być obecnie używane w nieprzetworzonych interfejsach API SQL. Zagłosuj (👍) na problem z usługą GitHub #27534 , jeśli jest to coś, czego potrzebujesz.
Usunięcie tych ograniczeń jest brane pod uwagę w przyszłych wersjach.