Převody hodnot
Převaděče hodnot umožňují převod hodnot vlastností při čtení z databáze nebo zápisu do databáze. Tento převod může být z jedné hodnoty na jinou ze stejného typu (například šifrování řetězců) nebo z hodnoty jednoho typu na hodnotu jiného typu (například převod hodnot výčtu do a z řetězců v databázi.)
Tip
Celý kód v tomto dokumentu můžete spustit a ladit tak, že si stáhnete ukázkový kód z GitHubu.
Přehled
Převaděče hodnot jsou určeny z hlediska a ModelClrType
ProviderClrType
. Typ modelu je typ .NET vlastnosti v typu entity. Typ zprostředkovatele je typ .NET, kterému poskytovatel databáze rozumí. Pokud chcete například uložit výčty jako řetězce v databázi, typ modelu je typ výčtu a typ zprostředkovatele je String
. Tyto dva typy můžou být stejné.
Převody jsou definovány pomocí dvou Func
stromů výrazů: jeden z ModelClrType
do ProviderClrType
a druhý z ProviderClrType
do ModelClrType
. Stromy výrazů se používají, aby je bylo možné zkompilovat do delegáta přístupu k databázi pro efektivní převody. Strom výrazů může obsahovat jednoduché volání metody převodu pro složité převody.
Poznámka:
Vlastnost, která byla nakonfigurována pro převod hodnoty, může také potřebovat zadat ValueComparer<T>. Další informace najdete v následujících příkladech a dokumentaci k porovnání hodnot.
Konfigurace převaděče hodnot
Převody hodnot jsou nakonfigurovány v DbContext.OnModelCreating. Představte si například výčet a typ entity definovaný jako:
public class Rider
{
public int Id { get; set; }
public EquineBeast Mount { get; set; }
}
public enum EquineBeast
{
Donkey,
Mule,
Horse,
Unicorn
}
Převody lze nakonfigurovat OnModelCreating tak, aby ukládaly hodnoty výčtu jako řetězce, jako jsou "Donkey", "Mule" atd. V databázi. Stačí zadat jednu funkci, která se převede z ModelClrType
hodnoty na ProviderClrType
a druhou pro opačný převod:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
Poznámka:
Hodnota null
se nikdy nepředá převaděči hodnot. Hodnota null ve sloupci databáze je vždy null v instanci entity a naopak. Díky tomu je implementace převodů jednodušší a umožňuje je sdílet mezi vlastnostmi s možnou hodnotou null a nenulovou. Další informace najdete v tématu Problém GitHubu č. 13850 .
Hromadná konfigurace převaděče hodnot
Je běžné, že pro každou vlastnost, která používá příslušný typ CLR, se běžně konfiguruje stejný převaděč hodnot. Místo toho, abyste to udělali ručně pro každou vlastnost, můžete použít konfiguraci modelu před konvencí, abyste to udělali jednou pro celý model. Chcete-li to provést, definujte převaděč hodnot jako třídu:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
Potom přepište ConfigureConventions v kontextu typ a nakonfigurujte převaděč následujícím způsobem:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Předdefinované převody
EF Core obsahuje mnoho předdefinovaných převodů, které zabraňují ručnímu zápisu funkcí převodu. Místo toho EF Core vybere převod, který se použije na základě typu vlastnosti v modelu a požadovaného typu poskytovatele databáze.
Například výčty na převody řetězců se používají jako příklad výše, ale EF Core to ve skutečnosti provede automaticky, když je typ zprostředkovatele nakonfigurovaný jako string
použití obecného typu HasConversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
}
Totéž lze dosáhnout explicitním zadáním typu sloupce databáze. Pokud je například typ entity definovaný takto:
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
Pak budou hodnoty výčtu uloženy jako řetězce v databázi bez jakékoli další konfigurace v OnModelCreating.
ValueConverter – třída
Volání HasConversion , jak je znázorněno výše, vytvoří ValueConverter<TModel,TProvider> instanci a nastaví ji pro vlastnost. Místo toho je možné vytvořit ValueConverter
explicitně. Příklad:
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);
}
To může být užitečné, když více vlastností používá stejný převod.
Integrované převaděče
Jak už bylo zmíněno výše, EF Core se dodává se sadou předdefinovaných ValueConverter<TModel,TProvider> tříd, které se nacházejí v Microsoft.EntityFrameworkCore.Storage.ValueConversion oboru názvů. V mnoha případech EF zvolí vhodný integrovaný převaděč na základě typu vlastnosti v modelu a typu požadovaného v databázi, jak je znázorněno výše pro výčty. Například použití .HasConversion<int>()
u bool
vlastnosti způsobí, že EF Core převede logické hodnoty na číselnou nulu a jednu hodnotu:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion<int>();
}
To je funkčně stejné jako vytvoření instance integrované BoolToZeroOneConverter<TProvider> a explicitní nastavení:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new BoolToZeroOneConverter<int>();
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion(converter);
}
Následující tabulka shrnuje běžně používané předdefinované převody z typů modelu nebo vlastností na typy zprostředkovatele databáze. V tabulce any_numeric_type
se rozumí jeden z int
, , byte
ushort
short
sbyte
uint
long
ulong
, char
, , decimal
, , float
nebo .double
Typ modelu nebo vlastnosti | Poskytovatel/typ databáze | Převod | Využití |
---|---|---|---|
bool | any_numeric_type | False/true až 0/1 | .HasConversion<any_numeric_type>() |
any_numeric_type | False/true pro libovolná dvě čísla | Použití BoolToTwoValuesConverter<TProvider> | |
string | False/true na "N"/"Y" | .HasConversion<string>() |
|
string | False/true pro všechny dva řetězce | Použití BoolToStringConverter | |
any_numeric_type | bool | 0/1 až false/true | .HasConversion<bool>() |
any_numeric_type | Jednoduché přetypování | .HasConversion<any_numeric_type>() |
|
string | Číslo jako řetězec | .HasConversion<string>() |
|
Výčet | any_numeric_type | Číselná hodnota výčtu | .HasConversion<any_numeric_type>() |
string | Řetězcová reprezentace hodnoty výčtu | .HasConversion<string>() |
|
string | bool | Parsuje řetězec jako logickou hodnotu. | .HasConversion<bool>() |
any_numeric_type | Parsuje řetězec jako daný číselný typ. | .HasConversion<any_numeric_type>() |
|
char | První znak řetězce | .HasConversion<char>() |
|
DateTime | Parsuje řetězec jako DateTime. | .HasConversion<DateTime>() |
|
DateTimeOffset | Parsuje řetězec jako DateTimeOffset. | .HasConversion<DateTimeOffset>() |
|
TimeSpan | Parsuje řetězec jako timeSpan. | .HasConversion<TimeSpan>() |
|
Guid | Parsuje řetězec jako identifikátor GUID. | .HasConversion<Guid>() |
|
byte[] | Řetězec jako bajty UTF8 | .HasConversion<byte[]>() |
|
char | string | Řetězec s jedním znakem | .HasConversion<string>() |
DateTime | long | Zakódované datum a čas zachování dateTime.Kind | .HasConversion<long>() |
long | Tiká | Použití DateTimeToTicksConverter | |
string | Invariantní řetězec data a času jazykové verze | .HasConversion<string>() |
|
DateTimeOffset | long | Kódované datum a čas s posunem | .HasConversion<long>() |
string | Invariantní řetězec data a času jazykové verze s posunem | .HasConversion<string>() |
|
TimeSpan | long | Tiká | .HasConversion<long>() |
string | Řetězec invariantní jazykové verze | .HasConversion<string>() |
|
Identifikátor URI | string | Identifikátor URI jako řetězec | .HasConversion<string>() |
PhysicalAddress | string | Adresa jako řetězec | .HasConversion<string>() |
byte[] | Bajty v pořadí velkých koncových sítí | .HasConversion<byte[]>() |
|
IPAddress | string | Adresa jako řetězec | .HasConversion<string>() |
byte[] | Bajty v pořadí velkých koncových sítí | .HasConversion<byte[]>() |
|
Guid | string | Identifikátor GUID ve formátu ddd-dddd-d-d-dd | .HasConversion<string>() |
byte[] | Bajty v pořadí binární serializace .NET | .HasConversion<byte[]>() |
Všimněte si, že tyto převody předpokládají, že formát hodnoty je vhodný pro převod. Například převod řetězců na čísla selže, pokud řetězcové hodnoty nelze analyzovat jako čísla.
Úplný seznam předdefinovaných převaděčů je:
- Převod logických vlastností:
- BoolToStringConverter – Logická hodnota pro řetězce, jako jsou "N" a "Y".
- BoolToTwoValuesConverter<TProvider> - Bool na jakékoli dvě hodnoty
- BoolToZeroOneConverter<TProvider> - Logická hodnota na nulu a jedna
- Převod vlastností pole bajtů:
- BytesToStringConverter – Bajtové pole na řetězec kódovaný v base64
- Jakýkoli převod, který vyžaduje pouze přetypování typu
- CastingConverter<TModel,TProvider> - Převody, které vyžadují pouze přetypování typu
- Převod vlastností znaku:
- CharToStringConverter – Znak na řetězec s jedním znakem
- Převod DateTimeOffset vlastností:
- DateTimeOffsetToBinaryConverter - DateTimeOffset na binární kódovanou 64bitovou hodnotu
- DateTimeOffsetToBytesConverter - DateTimeOffset na bajtové pole
- DateTimeOffsetToStringConverter - DateTimeOffset do řetězce
- Převod DateTime vlastností:
- DateTimeToBinaryConverter - DateTime na 64bitovou hodnotu včetně DateTimeKind
- DateTimeToStringConverter - DateTime do řetězce
- DateTimeToTicksConverter - DateTime na záškrty
- Převod vlastností výčtu:
- EnumToNumberConverter<TEnum,TNumber> - Výčet k podkladovému číslu
- EnumToStringConverter<TEnum> - Výčet k řetězci
- Převod Guid vlastností:
- GuidToBytesConverter - Guid na bajtové pole
- GuidToStringConverter - Guid do řetězce
- Převod IPAddress vlastností:
- IPAddressToBytesConverter - IPAddress na bajtové pole
- IPAddressToStringConverter - IPAddress do řetězce
- Převod číselných vlastností (int, double, decimal atd.):
- NumberToBytesConverter<TNumber> - Libovolná číselná hodnota k bajtové matici
- NumberToStringConverter<TNumber> – Libovolná číselná hodnota k řetězci
- Převod PhysicalAddress vlastností:
- PhysicalAddressToBytesConverter - PhysicalAddress na bajtové pole
- PhysicalAddressToStringConverter - PhysicalAddress do řetězce
- Převod vlastností řetězce:
- StringToBoolConverter – Řetězce jako "N" a "Y" k bool
- StringToBytesConverter - String to UTF8 bytes
- StringToCharConverter - Řetězec na znak
- StringToDateTimeConverter - Řetězec na DateTime
- StringToDateTimeOffsetConverter - Řetězec na DateTimeOffset
- StringToEnumConverter<TEnum> - String to enum
- StringToGuidConverter - Řetězec na Guid
- StringToNumberConverter<TNumber> - Řetězec na číselný typ
- StringToTimeSpanConverter - Řetězec na TimeSpan
- StringToUriConverter - Řetězec na Uri
- Převod TimeSpan vlastností:
- TimeSpanToStringConverter - TimeSpan do řetězce
- TimeSpanToTicksConverter - TimeSpan na záškrty
- Převod Uri vlastností:
- UriToStringConverter - Uri do řetězce
Všimněte si, že všechny předdefinované převaděče jsou bezstavové, takže jednu instanci může bezpečně sdílet více vlastností.
Fasety sloupců a rady pro mapování
Některé typy databází mají omezující vlastnosti, které upravují způsob ukládání dat. Tady jsou některé z nich:
- Přesnost a měřítko pro desetinné čárky a sloupce s datem a časem
- Velikost a délka binárních a řetězcových sloupců
- Unicode pro sloupce řetězců
Tyto omezující vlastnosti lze nakonfigurovat běžným způsobem pro vlastnost, která používá převaděč hodnot, a použije se na převedený typ databáze. Například při převodu z výčtu na řetězce můžeme určit, že sloupec databáze by měl být jiný než Unicode, a uložit až 20 znaků:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>()
.HasMaxLength(20)
.IsUnicode(false);
}
Nebo při vytváření převaděče explicitně:
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);
}
Výsledkem je varchar(20)
sloupec při použití migrací EF Core na SQL Server:
CREATE TABLE [Rider] (
[Id] int NOT NULL IDENTITY,
[Mount] varchar(20) NOT NULL,
CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));
Pokud by však měly být varchar(20)
ve výchozím nastavení všechny EquineBeast
sloupce , pak mohou být tyto informace předány převaděči hodnot jako ConverterMappingHints. Příklad:
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);
}
Teď, když se tento převaděč použije, bude sloupec databáze bez kódování Unicode s maximální délkou 20. Jedná se však pouze o rady, protože jsou přepsány všemi omezujícími vlastnostmi explicitně nastavenými na mapované vlastnosti.
Příklady
Jednoduché objekty hodnot
Tento příklad používá jednoduchý typ k zabalení primitivního typu. To může být užitečné v případě, že chcete, aby byl typ v modelu konkrétnější (a proto bezpečnější než primitivní typ). V tomto příkladu je Dollars
tento typ , který zabalí desítkové primitivy:
public readonly struct Dollars
{
public Dollars(decimal amount)
=> Amount = amount;
public decimal Amount { get; }
public override string ToString()
=> $"${Amount}";
}
To lze použít v typu entity:
public class Order
{
public int Id { get; set; }
public Dollars Price { get; set; }
}
A při uložení v databázi se převede na podklad decimal
:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => v.Amount,
v => new Dollars(v));
Poznámka:
Tento objekt hodnoty je implementován jako struktura jen pro čtení. To znamená, že EF Core může pořizovat snímky a porovnávat hodnoty bez problému. Další informace najdete v tématu Porovnání hodnot.
Složené objekty hodnot
V předchozím příkladu typ objektu hodnoty obsahoval pouze jednu vlastnost. Běžnější je vytvoření více vlastností, které společně tvoří koncept domény. Například obecný Money
typ, který obsahuje částku i měnu:
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
}
Tento objekt hodnoty lze použít v typu entity jako předtím:
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
Převaděče hodnot můžou v současné době převádět pouze hodnoty do a z jednoho sloupce databáze. Toto omezení znamená, že všechny hodnoty vlastností z objektu musí být kódovány do jedné hodnoty sloupce. Obvykle se to zpracovává serializací objektu, protože jde do databáze, a pak deserializaci znovu na cestě ven. Například pomocí :System.Text.Json
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));
Poznámka:
Plánujeme povolit mapování objektu na více sloupců v budoucí verzi EF Core, což odstraňuje nutnost použití serializace zde. Tento problém sleduje GitHub #13947.
Poznámka:
Stejně jako v předchozím příkladu se tento objekt hodnoty implementuje jako struktura jen pro čtení. To znamená, že EF Core může pořizovat snímky a porovnávat hodnoty bez problému. Další informace najdete v tématu Porovnání hodnot.
Kolekce primitiv
Serializace se dá použít také k uložení kolekce primitivních hodnot. Příklad:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public ICollection<string> Tags { get; set; }
}
Znovu použijte System.Text.Json :
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>
představuje proměnlivý typ odkazu. To znamená, že je potřeba, ValueComparer<T> aby EF Core mohl správně sledovat a zjišťovat změny. Další informace najdete v tématu Porovnání hodnot.
Kolekce objektů hodnot
Kombinací předchozích dvou příkladů můžeme vytvořit kolekci hodnotových objektů. Představte si AnnualFinance
například typ, který modeluje finance blogu za 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);
}
Tento typ tvoří několik typů, které Money
jsme vytvořili dříve:
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
}
Pak můžeme do našeho typu entity přidat kolekci AnnualFinance
:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
A znovu použijte serializaci k uložení:
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()));
Poznámka:
Stejně jako předtím tento převod vyžaduje ValueComparer<T>. Další informace najdete v tématu Porovnání hodnot.
Objekty hodnot jako klíče
Někdy můžou být primitivní vlastnosti klíče zabalené do objektů hodnot, aby se při přiřazování hodnot přidala další úroveň zabezpečení typu. Mohli bychom například implementovat typ klíče pro blogy a typ klíče pro příspěvky:
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; }
}
Ty se pak dají použít v doménovém modelu:
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; }
}
Všimněte si, že Blog.Id
nelze omylem přiřadit PostKey
, a Post.Id
nelze omylem přiřadit BlogKey
. Podobně musí být vlastnost cizího Post.BlogId
klíče přiřazena .BlogKey
Poznámka:
Zobrazení tohoto vzoru neznamená, že ho doporučujeme. Pečlivě zvažte, jestli tato úroveň abstrakce pomáhá nebo brání vašemu vývojovému prostředí. Zvažte také použití navigace a vygenerovaných klíčů místo přímé práce s hodnotami klíčů.
Tyto klíčové vlastnosti je pak možné mapovat pomocí převaděčů hodnot:
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);
});
}
Poznámka:
Klíčové vlastnosti s převody můžou používat pouze vygenerované hodnoty klíče od EF Core 7.0.
Použití příkazu ulong pro časové razítko nebo rowversion
SQL Server podporuje automatickou optimistickou souběžnost pomocí binárních sloupců s 8 bajtyrowversion
/timestamp
. Ty se vždy čtou a zapisují do databáze pomocí pole 8 bajtů. Bajtová pole jsou však proměnlivým referenčním typem, což je poněkud bolestné při řešení. Převaděče hodnot umožňují rowversion
místo toho namapovat na ulong
vlastnost, která je mnohem vhodnější a snadno použitelná než pole bajtů. Představte si například entitu Blog
s tokenem ulong concurrency:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
To lze namapovat na sloupec SERVERU SQL rowversion
pomocí převaděče hodnot:
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Určení dateTime.Kind při čtení kalendářních dat
SQL Server zahodí DateTime.Kind příznak při ukládání příznaku DateTime jako nebo datetime2
datetime
. To znamená, že hodnoty DateTime vracející se z databáze mají vždy hodnotu DateTimeKind Unspecified
.
Převaděče hodnot lze použít dvěma způsoby, jak to vyřešit. Za prvé, EF Core má převaděč hodnot, který vytvoří 8bajtů neprůhlenou hodnotu, která zachovává Kind
příznak. Příklad:
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
To umožňuje v databázi kombinovat hodnoty DateTime s různými Kind
příznaky.
Problém s tímto přístupem spočívá v tom, že databáze již nemá rozpoznatelné datetime
sloupce nebo datetime2
sloupce. Místo toho je běžné vždy ukládat čas UTC (nebo méně často místní čas) a pak buď ignorovat Kind
příznak, nebo ho nastavit na odpovídající hodnotu pomocí převaděče hodnot. Následující převaděč například zajistí, že DateTime
hodnota načtená z databáze bude obsahovat DateTimeKind UTC
:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Pokud se v instancích entit nastavuje kombinace místních hodnot a hodnot UTC, lze převaděč před vložením použít k převodu odpovídajícím způsobem. Příklad:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Poznámka:
Pečlivě zvažte sjednocení veškerého přístupového kódu databáze, aby se vždy používal čas UTC, a to pouze při prezentování dat uživatelům v místním čase.
Použití řetězcových klíčů bez rozlišování velkých a velkých písmen
Některé databáze, včetně SQL Serveru, ve výchozím nastavení provádějí porovnání řetězců bez rozlišování malých a velkých písmen. .NET naopak ve výchozím nastavení provádí porovnávání řetězců s rozlišováním velkých a malých písmen. To znamená, že hodnota cizího klíče, jako je DotNet, bude odpovídat hodnotě primárního klíče "dotnet" na SQL Serveru, ale nebude se shodovat s hodnotou primárního klíče na SQL Serveru, ale nebude se shodovat s hodnotou primárního klíče v EF Core. Porovnávací nástroj pro hodnoty pro klíče lze použít k vynucení porovnání řetězců bez rozlišování velkých a malých písmen, jako je v databázi. Představte si například model blogů a příspěvků s řetězcovými klíči:
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; }
}
To nebude fungovat podle očekávání, pokud některé hodnoty Post.BlogId
mají různá velikost velikostí. Chyby způsobené tím budou záviset na tom, co aplikace dělá, ale obvykle zahrnují grafy objektů, které nejsou správně opravené , a/nebo aktualizace, které selžou, protože hodnota FK je nesprávná. K opravě tohoto parametru je možné použít porovnávač hodnot:
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);
});
}
Poznámka:
Porovnání řetězců .NET a porovnání databázových řetězců se můžou lišit více než jen citlivostí velkých a malých písmen. Tento vzor funguje pro jednoduché klíče ASCII, ale u klíčů s libovolným typem znaků specifických pro jazykovou verzi může selhat. Další informace najdete v tématu Kolace a citlivost písmen.
Zpracování databázových řetězců s pevnou délkou
Předchozí příklad nepotřeboval převaděč hodnot. Převaděč však může být užitečný pro typy řetězců databáze s pevnou délkou, jako char(20)
nebo nchar(20)
. Řetězce s pevnou délkou jsou vycpané na celou délku při každém vložení hodnoty do databáze. To znamená, že hodnota klíče "dotnet
" se načte z databáze jako "dotnet..............
", kde .
představuje znak mezery. To pak nebude správně porovnávat s hodnotami klíčů, které nejsou vycpané.
Převaděč hodnot lze použít k oříznutí odsazení při čtení hodnot klíče. To lze zkombinovat s porovnávačem hodnot v předchozím příkladu, aby bylo možné správně porovnat klíče ASCII bez rozlišování velkých a velkých písmen. Příklad:
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);
});
}
Šifrování hodnot vlastností
Převaděče hodnot lze použít k šifrování hodnot vlastností před jejich odesláním do databáze a pak je dešifrovat na cestě. Například použití návratu řetězce jako náhrady skutečného šifrovacího algoritmu:
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Poznámka:
V současné době neexistuje způsob, jak získat odkaz na aktuální DbContext nebo jiný stav relace z převaděče hodnot. Tím se omezí druhy šifrování, které je možné použít. Hlasujte pro problém GitHubu č. 11597 , abyste toto omezení odebrali.
Upozorňující
Pokud za účelem ochrany citlivých dat zahrnete vlastní šifrování, ujistěte se, že rozumíte všem důsledkům. Zvažte místo toho použití předem připravených mechanismů šifrování, jako je funkce Always Encrypted na SQL Serveru.
Omezení
Existuje několik známých aktuálních omezení systému převodu hodnot:
- Jak je uvedeno výše,
null
nelze převést. Pokud je to něco, co potřebujete, hlasujte (👍) pro problém GitHubu #13850 . - V dotazech LINQ není možné dotazovat na vlastnosti převedené na hodnoty, například odkazy na členy typu .NET převedené na hodnotu. Pokud je to něco, co potřebujete, hlasujte (👍) pro problém GitHubu #10434 , ale zvažte použití sloupce JSON.
- V současné době neexistuje způsob, jak rozložit převod jedné vlastnosti na více sloupců nebo naopak. Pokud je to něco, co potřebujete, hlasujte (👍) pro problém GitHubu č. 13947 .
- Generování hodnot není podporováno pro většinu klíčů mapovaných prostřednictvím převaděčů hodnot. Pokud je to něco, co potřebujete, hlasujte prosím (👍) pro problém GitHubu č. 11597 .
- Převody hodnot nemohou odkazovat na aktuální instanci DbContext. Pokud je to něco, co potřebujete, hlasujte (👍) pro problém GitHubu č. 12205 .
- Parametry používající typy převedené hodnotou se v současné době nedají použít v nezpracovaných rozhraních SQL API. Pokud je to něco, co potřebujete, hlasujte (👍) pro problém GitHubu #27534 .
Odebrání těchto omezení se považuje za budoucí verze.