Преобразования значений

Преобразователи значений позволяют преобразовывать значения свойств при чтении или записи в базу данных. Это преобразование может быть от одного значения к другому из того же типа (например, шифрование строк) или из значения одного типа в значение другого типа (например, преобразование значений перечисления в строки в базе данных и из них).

Совет

Вы можете запустить и отладить весь код, используемый в этой документации, скачав пример кода из GitHub.

Обзор

Преобразователи значений указываются в терминах a ModelClrType и a ProviderClrType. Тип модели — это тип .NET свойства в типе сущности. Тип поставщика — это тип .NET, понятный поставщиком базы данных. Например, чтобы сохранить перечисления в виде строк в базе данных, тип модели — это тип перечисления, а тип поставщика — String. Эти два типа могут быть одинаковыми.

Преобразования определяются с помощью двух Func деревьев выражений: от ModelClrType одного до ProviderClrType и другого из ProviderClrTypeModelClrTypeних. Деревья выражений используются, чтобы их можно было скомпилировать в делегат доступа к базе данных для эффективного преобразования. Дерево выражений может содержать простой вызов метода преобразования для сложных преобразований.

Примечание.

Свойство, настроенное для преобразования значений, может также потребоваться указать 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 хранения значений перечисления в виде строк, таких как "Доски", "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 и не допускающих значение 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 выбирает преобразование для использования на основе типа свойства в модели и запрошенного типа поставщика базы данных.

Например, перечисление в строковые преобразования используется в качестве примера выше, но 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 в числовой нуль и одно значение:

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, byteuintulongushortsbyteshortcharlong, decimalfloatили .double

Тип модели или свойства Тип поставщика или базы данных Преобразование Использование
bool any_numeric_type False/true до 0/1 .HasConversion<any_numeric_type>()
any_numeric_type False/true для двух чисел Используйте BoolToTwoValuesConverter<TProvider>.
строка False/true для "N"/"Y" .HasConversion<string>()
строка False/true для всех двух строк Используйте BoolToStringConverter.
any_numeric_type bool Значение 0/1 до false/true .HasConversion<bool>()
any_numeric_type Простой приведение .HasConversion<any_numeric_type>()
строка Число в виде строки .HasConversion<string>()
Перечисление any_numeric_type Числовое значение перечисления .HasConversion<any_numeric_type>()
строка Строковое представление значения перечисления .HasConversion<string>()
строка bool Анализирует строку как логическое значение .HasConversion<bool>()
any_numeric_type Анализирует строку как заданный числовый тип .HasConversion<any_numeric_type>()
char Первый символ строки .HasConversion<char>()
Дата и время Синтаксический анализ строки в качестве даты и времени .HasConversion<DateTime>()
DateTimeOffset Анализирует строку как DateTimeOffset .HasConversion<DateTimeOffset>()
TimeSpan Анализ строки в виде интервала timeSpan .HasConversion<TimeSpan>()
GUID Анализ строки в виде GUID .HasConversion<Guid>()
byte[] Строка в виде байтов UTF8 .HasConversion<byte[]>()
char строка Одна символьная строка .HasConversion<string>()
Дата и время длинный Закодированное значение даты и времени, сохраняющее DateTime.Kind .HasConversion<long>()
длинный Ticks Используйте DateTimeToTicksConverter.
строка Строка даты и времени инвариантного языка и региональных параметров .HasConversion<string>()
DateTimeOffset длинный Закодированная дата и время со смещением .HasConversion<long>()
строка Строка даты и времени инвариантного языка и региональных параметров с смещением .HasConversion<string>()
TimeSpan длинный Ticks .HasConversion<long>()
строка Строка интервала времени инвариантного языка и региональных параметров .HasConversion<string>()
URI-адрес строка Универсальный код ресурса (URI) в виде строки .HasConversion<string>()
PhysicalAddress строка Адрес в виде строки .HasConversion<string>()
byte[] Байты в порядке сети большого плана .HasConversion<byte[]>()
IPAddress строка Адрес в виде строки .HasConversion<string>()
byte[] Байты в порядке сети большого плана .HasConversion<byte[]>()
GUID строка GUID в формате dd-dd .HasConversion<string>()
byte[] Байты в порядке двоичной сериализации .NET .HasConversion<byte[]>()

Обратите внимание, что эти преобразования предполагают, что формат значения подходит для преобразования. Например, преобразование строк в числа завершится ошибкой, если строковые значения не могут быть проанализированы как числа.

Полный список встроенных преобразователей:

Обратите внимание, что все встроенные преобразователи без отслеживания состояния, поэтому один экземпляр можно безопасно совместно использовать несколькими свойствами.

Аспекты столбцов и указания по сопоставлению

Некоторые типы баз данных имеют аспекты, которые изменяют способ хранения данных. Например:

  • Точность и масштабирование для десятичных и даты и времени столбцов
  • Размер и длина двоичных и строковых столбцов
  • Юникод для строковых столбцов

Эти аспекты можно настроить обычным образом для свойства, использующего преобразователь значений, и будет применяться к преобразованному типу базы данных. Например, при преобразовании перечисления в строки можно указать, что столбец базы данных должен быть не юникодом и хранить до 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);
}

Это приводит к тому, что при использовании миграции EF Core с SQL Server происходит следующее varchar(20) :

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);
}

Теперь при использовании этого преобразователя столбец базы данных будет не юникодом с максимальной длиной 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 Аналогичным образом Post.BlogId необходимо назначить BlogKeyсвойство внешнего ключа.

Примечание.

Отображение этого шаблона не означает, что мы рекомендуем его. Внимательно рассмотрим, помогает ли этот уровень абстракции или мешает вашему опыту разработки. Кроме того, рекомендуется использовать навигации и созданные ключи вместо прямого обращения к значениям ключей.

Затем эти ключевые свойства можно сопоставить с помощью преобразователей значений:

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 для метки времени или rowversion

SQL Server поддерживает автоматическую оптимистическую параллелизм с помощью 8-байтовых двоичныхtimestamprowversion/столбцов. Они всегда считываются из базы данных и записываются с помощью 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 , если это то, что вам нужно.

Удаление этих ограничений рассматривается для будущих выпусков.