Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Преобразователи значений позволяют преобразовывать значения свойств при чтении или записи в базу данных. Это преобразование может быть от одного значения к другому того же типа (например, шифрование строк) или из значения одного типа в значение другого типа (например, преобразование значений перечисления в строки в базе данных и в обратном направлении).
Совет
Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.
Обзор
Преобразователи значений указываются в терминах a ModelClrType и a ProviderClrType. Тип модели — это тип .NET свойства в типе сущности. Тип поставщика — это тип .NET, понятный поставщиком базы данных. Например, чтобы сохранить перечисления в виде строк в базе данных, тип модели — это тип перечисления, а тип поставщика — String. Эти два типа могут быть одинаковыми.
Преобразования определяются с помощью двух деревьев выражений: одного от ModelClrType до ProviderClrType, и другого от ProviderClrType до ModelClrType. Деревья выражений используются, чтобы их можно было скомпилировать в делегат доступа к базе данных для эффективного преобразования. Дерево выражений может содержать простой вызов метода преобразования для сложных преобразований.
Примечание.
Возможно, свойству, настроенному для преобразования значений, также потребуется указать ValueComparer<T>. Дополнительные сведения см. в приведенных ниже примерах и документации по сравнениям значений.
Настройка преобразователя значений
Преобразования значений настраиваются в DbContext.OnModelCreating. Например, рассмотрим перечисление и тип сущности, определенный следующим образом:
public class Rider
{
public int Id { get; set; }
public EquineBeast Mount { get; set; }
}
public enum EquineBeast
{
Donkey,
Mule,
Horse,
Unicorn
}
Преобразования можно настроить для OnModelCreating хранения значений перечисления в виде строк, таких как "Donkey", "Mule" и т. д. в базе данных; необходимо просто предоставить одну функцию, которая преобразует из ModelClrType в другое ProviderClrType для противоположного преобразования:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
Примечание.
Значение null никогда не передается преобразователю значений. Значение NULL в столбце базы данных всегда равно null в экземпляре сущности и наоборот. Это упрощает реализацию преобразований и позволяет совместно использовать их между свойствами, поддерживающими и не поддерживающими значение NULL. См. проблему GitHub №13850 для получения дополнительной информации.
Массовая настройка преобразователя значений
Для каждого свойства, использующего соответствующий тип CLR, обычно настраивается один и тот же преобразователь значений. Вместо того чтобы делать это вручную для каждого свойства, можно использовать предварительную конфигурацию модели по соглашению, чтобы сделать это один раз для всей модели. Для этого определите преобразователь значений как класс:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
Затем переопределите ConfigureConventions в вашем типе контекста и настройте преобразователь как следующим образом:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Предварительно определенные преобразования
EF Core содержит множество предварительно определенных преобразований, которые позволяют избежать необходимости вручную записывать функции преобразования. Вместо этого EF Core выбирает преобразование для использования на основе типа свойства в модели и запрошенного типа поставщика базы данных.
Примером вышеупомянуты преобразования из enum в строку, но EF Core на самом деле выполняет это автоматически, если тип поставщика настроен как string с использованием универсального типа HasConversion.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
}
То же самое можно достичь, явно указав тип столбца базы данных. Например, если тип сущности определен следующим образом:
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
Затем значения перечислений будут сохранены в базе данных в виде строк без дополнительной настройки OnModelCreating.
Класс ValueConverter
Вызов HasConversion, как показано выше, создаст экземпляр ValueConverter<TModel,TProvider> и установит его в свойство. Вместо этого ValueConverter можно создать явно. Например:
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);
}
Это может быть полезно, если несколько свойств используют одно и то же преобразование.
Встроенные преобразователи
Как упоминалось выше, EF Core поставляется с набором предварительно определенных ValueConverter<TModel,TProvider> классов, найденных в Microsoft.EntityFrameworkCore.Storage.ValueConversion пространстве имен. Во многих случаях EF выбирает соответствующий встроенный преобразователь на основе типа свойства в модели и типа, запрошенного в базе данных, как показано выше для перечислений. Например, использование .HasConversion<int>() на свойстве bool приведет к преобразованию логических значений типа bool в числовые значения ноль и один.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion<int>();
}
Это функционально то же самое, что создание экземпляра встроенного BoolToZeroOneConverter<TProvider> и его явной установки.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new BoolToZeroOneConverter<int>();
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion(converter);
}
В следующей таблице перечислены часто используемые предварительно определенные преобразования из типов моделей или свойств в типы поставщиков баз данных. В таблице any_numeric_type означает один из int, shortlongbyteuintushortulongsbytechar, decimalfloatили .double
| Тип модели или свойства | Тип поставщика или базы данных | Преобразование | Использование |
|---|---|---|---|
| булевая переменная (bool) | любой_числовой_тип | Замена False/true на 0/1 | .HasConversion<any_numeric_type>() |
| любой_числовой_тип | Ложь/Истина для любых двух чисел | Используйте BoolToTwoValuesConverter<TProvider> | |
| строка | Ложь/истина для "N"/"Y" | .HasConversion<string>() |
|
| строка | False/true для всех двух строк | Используйте BoolToStringConverter | |
| любой_числовой_тип | булевая переменная (bool) | Заменить 0/1 на false/true | .HasConversion<bool>() |
| любой_числовой_тип | Простой каст | .HasConversion<any_numeric_type>() |
|
| строка | Число в виде строки | .HasConversion<string>() |
|
| Энам | любой_числовой_тип | Числовое значение перечисления | .HasConversion<any_numeric_type>() |
| строка | Строковое представление значения перечисления | .HasConversion<string>() |
|
| строка | булевая переменная (bool) | Интерпретирует строку как логическое значение | .HasConversion<bool>() |
| любой_числовой_тип | Парсит строку в указанный числовой тип | .HasConversion<any_numeric_type>() |
|
| символ | Первый символ строки | .HasConversion<char>() |
|
| Дата/время | Парсит строку как объект DateTime | .HasConversion<DateTime>() |
|
| DateTimeOffset (смещение даты и времени) | Анализирует строку как DateTimeOffset | .HasConversion<DateTimeOffset>() |
|
| Интервал времени | Производит разбор строки как TimeSpan. | .HasConversion<TimeSpan>() |
|
| GUID | Преобразует строку в GUID | .HasConversion<Guid>() |
|
| байт[] | Строка в виде байтов UTF8 | .HasConversion<byte[]>() |
|
| символ | строка | Одна символьная строка | .HasConversion<string>() |
| Дата/время | длинный | Закодированное значение даты и времени, сохраняющее DateTime.Kind | .HasConversion<long>() |
| длинный | Клещи | Используйте DateTimeToTicksConverter | |
| строка | Строка даты и времени для инвариантной культуры | .HasConversion<string>() |
|
| DateTimeOffset (смещение даты и времени) | длинный | Закодированная дата и время со смещением | .HasConversion<long>() |
| строка | Строка даты и времени для инвариантной культуры со смещением | .HasConversion<string>() |
|
| Интервал времени | длинный | Клещи | .HasConversion<long>() |
| строка | Строка интервала времени инвариантной культуры | .HasConversion<string>() |
|
| URI | строка | Универсальный код ресурса (URI) в виде строки | .HasConversion<string>() |
| Физический адрес | строка | Адрес в виде строки | .HasConversion<string>() |
| байт[] | Байты в порядке сети большого плана | .HasConversion<byte[]>() |
|
| IP-адрес | строка | Адрес в виде строки | .HasConversion<string>() |
| байт[] | Байты в порядке сети большого плана | .HasConversion<byte[]>() |
|
| GUID | строка | GUID в формате 'dddddddd-dddd-dddd-dddd-dddddddddddd' | .HasConversion<string>() |
| байт[] | Порядок байтов при двоичной сериализации в .NET | .HasConversion<byte[]>() |
Обратите внимание, что эти преобразования предполагают, что формат значения подходит для преобразования. Например, преобразование строк в числа завершится ошибкой, если строковые значения не могут быть проанализированы как числа.
Полный список встроенных преобразователей:
- Преобразование булевых свойств:
- BoolToStringConverter — Bool к строкам, таким как "N" и "Y"
- BoolToTwoValuesConverter<TProvider> — Bool к любым двум значениям
- BoolToZeroOneConverter<TProvider> - Преобразование Bool в ноль и один
- Преобразование свойств массива байтов:
- BytesToStringConverter — массив байтов в виде строки в кодировке Base64
- Любое преобразование, требующее только приведения типов
- CastingConverter<TModel,TProvider> — преобразования, для которых требуется только приведение типов
- Преобразование свойств char:
- CharToStringConverter — Преобразование символа в строку, состоящую из одного символа
- Преобразование свойств DateTimeOffset:
- DateTimeOffsetToBinaryConverter - DateTimeOffset в двоично закодированное 64-разрядное значение
- DateTimeOffsetToBytesConverter - DateTimeOffset в массив байтов
- DateTimeOffsetToStringConverter - DateTimeOffset в строку
- Преобразование свойств DateTime:
- DateTimeToBinaryConverter - DateTime до 64-разрядного значения, включая DateTimeKind
- DateTimeToStringConverter - DateTime в строку
- DateTimeToTicksConverter - DateTime для галок
- Преобразование свойств перечисления:
- EnumToNumberConverter<TEnum,TNumber> — перечисление в базовое число
- EnumToStringConverter<TEnum> — перечисление в строку
- Преобразование свойств Guid:
- GuidToBytesConverter - Guid в массив байтов
- GuidToStringConverter - Guid в строку
- Преобразование свойств IPAddress:
- IPAddressToBytesConverter - IPAddress в массив байтов
- IPAddressToStringConverter - IPAddress в строку
- Преобразование числовых (int, double, десятичных и т. д.) свойств:
- NumberToBytesConverter<TNumber> — любое числовое значение для массива байтов
- NumberToStringConverter<TNumber> — любое числовое значение строки
-
PhysicalAddress: Преобразование свойств
- PhysicalAddressToBytesConverter - PhysicalAddress в массив байтов
- PhysicalAddressToStringConverter - PhysicalAddress в строку
- Преобразование строковых свойств:
- StringToBoolConverter — Строки, такие как "N" и "Y", для bool
- StringToBytesConverter — Строка в байты UTF8
- StringToCharConverter — Преобразование строки в символ
- StringToDateTimeConverter — Преобразовать в строку DateTime
- StringToDateTimeOffsetConverter — Строка в DateTimeOffset
- StringToEnumConverter<TEnum> — Преобразование строки в перечисление
- StringToGuidConverter — Строка в Guid
- StringToNumberConverter<TNumber> — Преобразование строки в числовой тип
- StringToTimeSpanConverter — Строка в TimeSpan
- StringToUriConverter — Строка в Uri
- Преобразование свойств TimeSpan:
- TimeSpanToStringConverter - TimeSpan в строку
- TimeSpanToTicksConverter - TimeSpan для галок
- Преобразование свойств Uri:
- UriToStringConverter - Uri в строку
Обратите внимание, что все встроенные преобразователи без состояния, поэтому одним экземпляром можно безопасно пользоваться в нескольких свойствах.
Фасеты столбцов и указания по сопоставлению
Некоторые типы баз данных имеют аспекты, которые изменяют способ хранения данных. Например:
- Точность и масштаб для десятичных столбцов и столбцов для данных типа дата/время.
- Размер и длина двоичных и строковых столбцов
- Юникод для строковых столбцов
Эти аспекты можно настроить обычным образом для свойства, использующего преобразователь значений, и будет применяться к преобразованному типу базы данных. Например, при преобразовании перечисления в строки можно указать, что столбец базы данных должен быть не юникодом и хранить до 20 символов:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>()
.HasMaxLength(20)
.IsUnicode(false);
}
Или, когда вы создаёте преобразователь явно:
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);
}
Это приводит к образованию столбца varchar(20) при использовании миграций EF Core с SQL Server.
CREATE TABLE [Rider] (
[Id] int NOT NULL IDENTITY,
[Mount] varchar(20) NOT NULL,
CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));
Однако если по умолчанию все EquineBeast столбцы должны быть varchar(20), эти сведения необходимо предоставить преобразователю значений в виде ConverterMappingHints. Например:
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);
}
Теперь при использовании этого преобразователя столбец базы данных будет не поддерживающим Unicode и длиной до 20 символов. Однако это только подсказки, так как они переопределяются любыми аспектами явно заданными в сопоставленном свойстве.
Примеры
Простые объекты значений
В этом примере используется простой тип для упаковки примитивного типа. Это может быть полезно, если вы хотите, чтобы тип в модели был более конкретным (и, следовательно, более типобезопасный), чем примитивный тип. В этом примере это тип Dollars, который упаковывает десятичный примитив:
public readonly struct Dollars
{
public Dollars(decimal amount)
=> Amount = amount;
public decimal Amount { get; }
public override string ToString()
=> $"${Amount}";
}
Это можно использовать в типе сущности:
public class Order
{
public int Id { get; set; }
public Dollars Price { get; set; }
}
И преобразуется в базовый decimal объект при хранении в базе данных:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => v.Amount,
v => new Dollars(v));
Примечание.
Этот объект данных реализован как структура только для чтения. Это означает, что EF Core может создавать моментальные снимки и сравнивать значения без проблем. Дополнительные сведения см. в разделе "Сравнения значений ".
Составные объекты значений
В предыдущем примере тип объекта value содержал только одно свойство. Чаще всего для типа объекта значения создается несколько свойств, которые вместе образуют концепцию домена. Например, общий Money тип, содержащий как сумму, так и валюту:
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
}
Этот объект значения можно использовать в типе сущности, как и раньше:
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
Преобразователи значений в настоящее время могут преобразовывать значения только в один столбец базы данных и из нее. Это ограничение означает, что все значения свойств из объекта должны быть закодированы в одно значение столбца. Обычно это обрабатывается путем сериализации объекта при записи в базу данных, а затем десериализации его снова на выходе. Например, использование System.Text.Json:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));
Примечание.
Мы планируем разрешить сопоставление объекта с несколькими столбцами в будущей версии EF Core, удаляя необходимость использовать сериализацию здесь. Это отслеживается проблемой GitHub #13947.
Примечание.
Как и в предыдущем примере, этот объект значения реализуется как структура только для чтения. Это означает, что EF Core может создавать моментальные снимки и сравнивать значения без проблем. Дополнительные сведения см. в разделе "Сравнения значений ".
Коллекции примитивов
Сериализация также может использоваться для хранения коллекции примитивных значений. Например:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public ICollection<string> Tags { get; set; }
}
Использование 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> представляет изменяемый тип ссылок. Это означает, что ValueComparer<T> требуется, чтобы EF Core правильно отслеживал и обнаруживал изменения. Дополнительные сведения см. в разделе "Сравнения значений ".
Коллекции объектов значений
Объединение предыдущих двух примеров позволяет создать коллекцию объектов значений. Например, рассмотрим AnnualFinance тип, который моделирует финансы блога в течение одного года:
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);
}
Этот тип составляется из нескольких созданных нами ранее типов Money.
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
}
Затем мы можем добавить в тип сущности коллекцию элементов AnnualFinance.
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
Повторно используйте сериализацию для хранения этого.
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()));
Примечание.
Как и раньше, для этого преобразования требуется ValueComparer<T>. Дополнительные сведения см. в разделе "Сравнения значений ".
Объекты value в качестве ключей
Иногда примитивные свойства ключей могут быть упакованы в объекты значений, чтобы добавить дополнительный уровень безопасности типов при назначении значений. Например, можно реализовать тип ключа для блогов и тип ключа для записей:
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; }
}
Затем их можно использовать в модели домена:
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; }
}
Обратите внимание, что Blog.Id не может быть случайно назначен PostKey, и Post.Id не может быть случайно назначен BlogKey. Аналогичным образом, свойству внешнего ключа BlogKey необходимо назначить Post.BlogId.
Примечание.
Отображение этого шаблона не означает, что мы рекомендуем его. Внимательно рассмотрим, помогает ли этот уровень абстракции или мешает вашему опыту разработки. Кроме того, рекомендуется использовать средства навигации и созданные ключи вместо непосредственного обращения к значениям ключей.
Затем эти ключевые свойства можно сопоставить с помощью преобразователей значений:
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);
});
}
Примечание.
Ключевые свойства с преобразованиями могут использовать только созданные ключевые значения, начиная с EF Core 7.0.
Используйте ulong для timestamp/rowversion
SQL Server поддерживает автоматическую оптимистическую конкурентность с помощью 8-байтовых двоичных rowversion/timestamp столбцов. Они всегда считываются из базы данных и записываются с помощью 8-байтового массива. Однако массивы байтов являются изменяемым ссылочным типом, что делает работу с ними несколько сложной. Преобразователи значений позволяют rowversion вместо этого сопоставляться со свойством ulong , что гораздо удобнее и проще использовать, чем массив байтов. Например, рассмотрим Blog сущность с маркером параллелизма ulong:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
Это можно сопоставить со столбцом SQL Server rowversion с помощью преобразователя значений:
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Укажите dateTime.Kind при чтении дат
SQL Server отменяет DateTime.Kind флаг при сохранении DateTime в качестве datetime или datetime2. Это означает, что значения DateTime, возвращаемые из базы данных, всегда имеют значение DateTimeKindUnspecified.
Преобразователи значений можно использовать двумя способами для решения этой проблемы. Во-первых, EF Core имеет преобразователь значений, который создает 8-байтовое закрытое значение с сохранением флага Kind. Например:
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
Это позволяет использовать значения DateTime с различными Kind флагами в базе данных.
Проблема с этим подходом заключается в том, что база данных больше не имеет распознаваемых datetime или datetime2 столбцов. Поэтому вместо этого обычно всегда хранить время UTC (или, менее часто, всегда локальное время), а затем либо игнорировать Kind флаг или задать его соответствующим значением с помощью преобразователя значений. Например, приведенный ниже преобразователь гарантирует, что DateTime значение, прочитанное из базы данных, будет иметь DateTimeKindUTCследующие значения:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Если набор локальных и UTC-значений устанавливается в экземплярах сущностей, преобразователь можно использовать для правильного преобразования перед вставкой. Например:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Примечание.
Тщательно рассмотрите возможность объединения всего кода доступа к базе данных для использования времени в формате UTC постоянно, работая с локальным временем только при отображении данных пользователю.
Используйте строковые ключи без учета регистра
Некоторые базы данных, включая SQL Server, выполняют сравнение строк без учета регистра по умолчанию. С другой стороны , .NET выполняет сравнения строк с учетом регистра по умолчанию. Это означает, что значение внешнего ключа, например DotNet, будет соответствовать значению первичного ключа dotnet на SQL Server, но не будет соответствовать этому значению в EF Core. Сравнение значений для ключей можно использовать, чтобы заставить EF Core выполнять сравнение строк без учета регистра, как это происходит в базе данных. Например, рассмотрим модель блога и записи с помощью строковых ключей:
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; }
}
Это не будет работать должным образом, если некоторые из значений Post.BlogId имеют разные регистры. Ошибки, вызванные этим, зависят от того, что делает приложение, но обычно включают графы объектов, которые не исправлены правильно, и /или обновления, которые завершаются ошибкой, так как значение FK неправильно. Для исправления этого можно использовать средство сравнения значений:
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);
});
}
Примечание.
Сравнения строк в .NET и в базах данных могут различаться не только в плане чувствительности к регистру. Этот шаблон работает для простых ключей ASCII, но может завершиться сбоем для ключей с любым типом символов, специфичных для культуры. Дополнительные сведения см. в разделе "Параметры сортировки и регистрозависимость".
Обработка строк базы данных фиксированной длины
В предыдущем примере не требуется преобразователь значений. Однако преобразователь может быть полезен для типов строк базы данных фиксированной длины, например char(20) или nchar(20). Строки фиксированной длины заполняются до полной длины при вставке значения в базу данных. Это означает, что ключевое значение "dotnet" будет считываться обратно из базы данных как "dotnet..............", где . представляет пробел. Затем это не будет правильно сравниваться со значениями ключей, которые не заполнены.
Преобразователь значений можно использовать для уменьшения отступов при чтении значений ключа. Это можно объединить с сравнивателем значений в предыдущем примере, чтобы правильно сравнить ключи ASCII с фиксированной длиной без учета регистра. Например:
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);
});
}
Шифрование значений свойств
Преобразователи значений можно использовать для шифрования значений свойств перед отправкой их в базу данных, а затем расшифровывать их на выходе. Например, использование разворота строки в качестве замены реального алгоритма шифрования:
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Примечание.
В настоящее время нет способа получить ссылку на текущее состояние DbContext или другое состояние сеанса из преобразователя значений. Это ограничивает типы шифрования, которые можно использовать. Проголосуйте за вопрос GitHub No 11597 , чтобы это ограничение было удалено.
Предупреждение
Обязательно понимать все последствия, если вы разрабатываете собственное шифрование для защиты конфиденциальных данных. Вместо этого следует использовать предварительно созданные механизмы шифрования, такие как Always Encrypted на SQL Server.
Ограничения
Существует несколько известных текущих ограничений системы преобразования значений:
- Как отмечалось выше,
nullнельзя преобразовать. Проголосуйте (👍) за вопрос GitHub #13850 , если это то, что вам нужно. - Невозможно выполнять запросы к свойствам, преобразованным с использованием преобразователя значений, например, к ссылочным элементам в типе .NET, преобразованном с помощью этого метода, в запросах LINQ. Проголосуйте (👍) за задачу GitHub #10434, если это необходимо, но рассмотрите возможность использования столбца JSON вместо этого.
- В настоящее время нет способа распространить преобразование одного свойства в несколько столбцов или наоборот. Проголосуйте (👍) за вопрос GitHub #13947 , если это то, что вам нужно.
- Создание значений не поддерживается для большинства ключей, к которым применяются преобразователи значений. Проголосуйте (👍) за вопрос GitHub #11597 , если это то, что вам нужно.
- Преобразования значений не могут ссылаться на текущий экземпляр DbContext. Проголосуйте (👍) за вопрос GitHub #12205 , если это то, что вам нужно.
- Параметры типов с преобразованным значением в настоящее время нельзя использовать в сырых API SQL. Проголосуйте (👍) за вопрос GitHub #27534 , если это то, что вам нужно.
Удаление этих ограничений рассматривается для будущих выпусков.