Notatka
Dostęp do tej strony wymaga autoryzacji. Może spróbować zalogować się lub zmienić katalogi.
Dostęp do tej strony wymaga autoryzacji. Możesz spróbować zmienić katalogi.
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).
Wskazówka
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 odniesieniu do ModelClrType i ProviderClrType. Typ modelu to typ .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, co pozwala na wydajne konwersje. 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>. 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 enum i typ jednostki, które zostały zdefiniowane 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ć w OnModelCreating do przechowywania wartości wyliczenia jako ciągów znaków, takich jak "Donkey", "Mule", itp. w bazie danych; wystarczy podać jedną funkcję, która konwertuje z ModelClrType 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 w swoim typie kontekstu przesłoń/zastąp ConfigureConventions 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 konwersje z wyliczenia na ciąg znaków są 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 ogólnego typu HasConversion.
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 znaków w bazie danych bez dalszej konfiguracji w OnModelCreating.
Klasa ValueConverter
Wywołanie HasConversion, jak pokazano powyżej, spowoduje utworzenie wystąpienia ValueConverter<TModel,TProvider> i ustawienie go w 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 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 typów wyliczeniowych. Na przykład użycie .HasConversion<int>() na właściwości bool spowoduje, że EF Core przekonwertuje wartości logiczne na wartości liczbowe zero i jeden.
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, longbyteuintushortulongsbytechardecimalfloatlub .double
| Typ modelu/typ właściwości | Dostawca/typ bazy danych | Konwersja | Użycie |
|---|---|---|---|
| Bool | dowolny_typ_numeryczny | Wartość false/true do 0/1 | .HasConversion<any_numeric_type>() |
| dowolny_typ_numeryczny | Fałsz/prawda dla dowolnych dwóch liczb | Użyj BoolToTwoValuesConverter<TProvider> | |
| ciąg | Fałsz/prawda na "N"/"Y" | .HasConversion<string>() |
|
| ciąg | Fałsz/prawda dla dowolnych dwóch ciągów | Użyj BoolToStringConverter | |
| dowolny_typ_numeryczny | Bool | Od 0/1 na false/true | .HasConversion<bool>() |
| dowolny_typ_numeryczny | Rzutowanie proste | .HasConversion<any_numeric_type>() |
|
| ciąg | Liczba jako ciąg | .HasConversion<string>() |
|
| Wyliczenie | dowolny_typ_numeryczny | Wartość liczbowa wyliczenia | .HasConversion<any_numeric_type>() |
| ciąg | Reprezentacja wartości wyliczenia jako ciąg znaków | .HasConversion<string>() |
|
| ciąg | Bool | Analizuje ciąg jako wartość logiczną | .HasConversion<bool>() |
| dowolny_typ_numeryczny | Analizuje ciąg jako typ liczbowy zdefiniowany przez użytkownika | .HasConversion<any_numeric_type>() |
|
| char | Pierwszy znak ciągu | .HasConversion<char>() |
|
| Data i Czas | Analizuje ciąg jako data/godzina | .HasConversion<DateTime>() |
|
| PrzesunięcieDatyICzasu | 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 znaków jako bajty UTF8 | .HasConversion<byte[]>() |
|
| char | ciąg | Ciąg z pojedynczym znakiem | .HasConversion<string>() |
| Data i Czas | długi | Zakodowana data/godzina zachowująca atrybut DateTime.Kind | .HasConversion<long>() |
| długi | Kleszcze | Użyj DateTimeToTicksConverter | |
| ciąg | Niezmienny ciąg daty/godziny kultury | .HasConversion<string>() |
|
| PrzesunięcieDatyICzasu | długi | Zakodowana data/godzina z przesunięciem | .HasConversion<long>() |
| ciąg | Niezmienny ciąg daty/godziny kultury z przesunięciem | .HasConversion<string>() |
|
| przedział_czasu | długi | Kleszcze | .HasConversion<long>() |
| ciąg | Niezmienny ciąg przedziału czasu kultury | .HasConversion<string>() |
|
| Identyfikator URI | ciąg | Identyfikator URI jako ciąg | .HasConversion<string>() |
| adres fizyczny | ciąg | Adres jako ciąg | .HasConversion<string>() |
| byte[] | Bajty w kolejności sieci big-endian | .HasConversion<byte[]>() |
|
| Adres IP | ciąg | Adres jako ciąg | .HasConversion<string>() |
| byte[] | Bajty w kolejności sieci big-endian | .HasConversion<byte[]>() |
|
| Identyfikator GUID | ciąg | Identyfikator GUID w formacie "dddddddd-dddd-dddd-dddd-dddddddddddd" | .HasConversion<string>() |
| byte[] | Kolejność bajtów w binarnej serializacji .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 boolowskich:
- BoolToStringConverter - Bool na ciągi znaków, takie jak "N" i "Y"
- BoolToTwoValuesConverter<TProvider> - Przekształcenie wartości Bool na dowolne dwie wartości
- BoolToZeroOneConverter<TProvider> - Konwersja wartości logicznej na zero i jeden
- 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 jedynie rzutowania typu.
- CastingConverter<TModel,TProvider> - Konwersje, które wymagają jedynie rzutowania typu
- Konwertowanie właściwości znaków:
- CharToStringConverter - Przekształcenie znaku na ciąg jednoliterowy
- Konwertowanie DateTimeOffset właściwości:
- DateTimeOffsetToBinaryConverter - DateTimeOffset do wartości 64-bitowej zakodowanej binarnie
- DateTimeOffsetToBytesConverter - DateTimeOffset do tablicy bajtów
- DateTimeOffsetToStringConverter - DateTimeOffset na łańcuch
- Konwertowanie DateTime właściwości:
- DateTimeToBinaryConverter - DateTime do wartości 64-bitowej, obejmującej DateTimeKind
- DateTimeToStringConverter - DateTime na ciąg znaków
- DateTimeToTicksConverter - DateTime do kleszczy
- Konwertowanie właściwości wyliczeniowych:
- EnumToNumberConverter<TEnum,TNumber> - Wyliczenie do bazowej liczby
- EnumToStringConverter<TEnum> - Enum 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 łańcucha
- Konwertowanie właściwości liczbowych (int, double, decimal itp.):
- NumberToBytesConverter<TNumber> - Konwersja dowolnej wartości liczbowej 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 znaków
- Konwertowanie właściwości ciągu:
- StringToBoolConverter - Ciągi takie jak "N" i "Y" na wartości logiczne
- StringToBytesConverter - Ciąg na bajty UTF8
- StringToCharConverter - Ciąg do znaku
- StringToDateTimeConverter - Ciąg do DateTime
- StringToDateTimeOffsetConverter - Ciąg do DateTimeOffset
- StringToEnumConverter<TEnum> - Ciąg do enum
- StringToGuidConverter - Ciąg do Guid
- StringToNumberConverter<TNumber> - Ciąg do typu danych liczbowego
- StringToTimeSpanConverter - Ciąg do TimeSpan
- StringToUriConverter - Ciąg do Uri
- Konwertowanie TimeSpan właściwości:
- TimeSpanToStringConverter - TimeSpan do ciągu znaków
- TimeSpanToTicksConverter - TimeSpan do kleszczy
- Konwertowanie Uri właściwości:
- UriToStringConverter - Uri do ciągu znaków
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 tekstowych
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 konwersji z typu enum na ciągi znaków 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ć wartość varchar(20), należy przekazać te informacje do konwertera wartości jako ConverterMappingHints. 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);
}
Za każdym razem, gdy używany jest ten konwerter, kolumna bazy danych będzie bez 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 podstawowy typ 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 podstawowe decimal podczas zapisywania 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 readonly struct. Oznacza to, że EF Core może tworzyć migawkę 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. Zwykle odbywa się to poprzez serializowanie obiektu przy zapisie do bazy danych, a następnie deserializowanie przy odczycie. Na przykład używając 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 zgłoszenie GitHub #13947.
Uwaga
Podobnie jak w poprzednim przykładzie, ten obiekt wartości jest implementowany jako readonly struktura. Oznacza to, że EF Core może tworzyć migawki 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 ValueComparer<T> jest potrzebny, 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 składa się z kilku typów Money utworzonych wcześniej:
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 PostKeyi 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 czy utrudnia twoje doświadczenie programistyczne. 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 binarnychrowversion/timestamp. 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 trudne do ich obsługi. Konwertery wartości pozwalają mapować rowversion na właściwość ulong, co jest znacznie bardziej odpowiednie i łatwiejsze 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 jako datetime lub datetime2. Oznacza to, że wartości DateTime pochodzące z bazy danych zawsze mają DateTimeKindUnspecified.
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ść DateTimeKindUTC:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Jeśli w instancjach jednostek jest ustawiana kombinacja wartości lokalnych i UTC, można użyć konwertera, aby odpowiednio je skonwertować przed wstawieniem. Na przykład:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Uwaga
Rozważ dokładnie ujednolicenie kodu dostępu do baz danych w celu używania czasu UTC, przestawiając się na czas lokalny wyłącznie podczas prezentowania danych użytkownikom.
Używaj kluczy tekstowych ignorujących wielkość liter
Niektóre bazy danych, w tym program SQL Server, domyślnie wykonują porównania ciągów bez uwzględniania wielkości liter. Platforma .NET z kolei domyślnie wykonuje porównania ciągów uwzględniające wielkość 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ównywarka wartości dla kluczy może być używana do wymuszenia w EF Core porównań ciągów bez uwzględniania wielkości liter, tak jak w bazie danych. Weźmy pod uwagę na przykład model bloga/postów z kluczami typu 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; }
}
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ścią 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 wrażliwość na wielkość 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 być użyty do usuwania 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 odszyfrowywania ich na wyjściu. Na przykład użycie odwrócenia ciągu znaków 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 zgłoszenie 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,
nullnie 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 konwertowanych na wartości, takich jak członkowie referencyjni na typach .NET konwertowanych na wartości w zapytaniach LINQ. Zagłosuj (👍) na problem na GitHubie #10434, jeśli jest to coś, czego potrzebujesz, ale rozważ użycie kolumny JSON zamiast tego.
- Obecnie nie ma możliwości przeniesienia przekształcenia jednej właściwości na wiele kolumn ani odwrotnie. Proszę zagłosować (👍) na zagadnienie 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 👍 zgłoszenie GitHub #11597, jeśli tego potrzebujesz.
- Konwersje wartości nie mogą odwoływać się do bieżącego wystąpienia DbContext. Zagłosuj (👍) na kwestię 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.