値変換

値コンバーターを使用すると、データベースとの読み取りや書き込みの際にプロパティ値を変換できます。 ある値から同じ型の別の値への変換 (文字列の暗号化など) や、ある型の値から別の型の値への変換 (データベース内の文字列に対する列挙値の変換など) が可能です。

ヒント

このドキュメントに含まれているすべてのコードは、GitHub からサンプル コードをダウンロードすることで実行およびデバッグできます。

概要

値コンバーターは、ModelClrTypeProviderClrType に関して指定されます。 モデル型は、エンティティ型における、.NET 型のプロパティです。 プロバイダー型は、データベース プロバイダーによって認識される .NET 型です。 たとえば、列挙型をデータベースに文字列として保存する場合、モデル型は列挙型で、プロバイダー型は String となります。 これらの 2 つの型が同じであってもかまいません。

変換は、2 つの Func 式ツリーを使用して定義されます。1 つは ModelClrType から ProviderClrType、もう 1 つは ProviderClrType から ModelClrType への変換です。 式ツリーは、効率的な変換のために、データベース アクセスのデリゲートにコンパイルできるように使用されます。 式ツリーには、複雑な変換のために、変換メソッドの単純な呼び出しが含まれる場合があります。

Note

値変換のために構成されたプロパティでも、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 に変換する 1 つの関数と、逆方向の変換のために別の関数を指定する必要があるだけです。

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}

Note

null 値が値コンバーターに渡されることはありません。 データベース列内の null は、エンティティ インスタンス内では常に null であり、その逆も同様です。 これによって、変換の実装がより容易になり、null 許容プロパティと null 非許容プロパティにわたって変換を共有できるようになっています。 詳細については、GitHub イシュー #13850 に関するページを参照してください。

値コンバーターの一括構成

関連する CLR 型を使用するすべてのプロパティに対して同じ値コンバーターが構成されるのが一般的です。 プロパティごとに手動でこれを行うのではなく、規則の前のモデル構成を使用して、モデル全体に対して 1 回これを行うことができます。 これを行うには、値コンバーターをクラスとして定義します。

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 では、代わりに、モデルに含まれるプロパティの型と、要求されたデータベース プロバイダー型に基づいて、使用する変換が選択されます。

たとえば、上記の例として列挙型から文字列型への変換が使用されていますが、プロバイダー型が string として構成されている場合は、実際にはジェネリック型の HasConversion を使用して、EF Core によってこれが自動的に行われます。

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 には、Microsoft.EntityFrameworkCore.Storage.ValueConversion 名前空間にある、定義済みの ValueConverter<TModel,TProvider> クラスのセットが付属しています。 多くの場合、EF では、列挙型について上で示したように、モデル内のプロパティの型と、データベースで必要とされる型に基づいて、適切な組み込みコンバーターが選択されます。 たとえば、.HasConversion<int>() プロパティに対して bool を使用すると、EF Core により、ブール値が数値 0 と 1 つの値に変換されます。

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_typeintshortlongbyteuintushortulongsbytechardecimalfloatdouble のいずれかを意味します。

モデル/プロパティの型 プロバイダー/データベースの型 Conversion 使用法
bool any_numeric_type false/true から 0/1 .HasConversion<any_numeric_type>()
any_numeric_type false/true から任意の 2 つの数字 BoolToTwoValuesConverter<TProvider> を使用します
string false/true から "N"/"Y" .HasConversion<string>()
string false/true から任意の 2 つの文字列 BoolToStringConverter を使用します
any_numeric_type bool 0/1 から false/true .HasConversion<bool>()
any_numeric_type 単純なキャスト .HasConversion<any_numeric_type>()
string 文字列としての数字 .HasConversion<string>()
列挙型 any_numeric_type 列挙型の数値 .HasConversion<any_numeric_type>()
string 列挙値の文字列表現 .HasConversion<string>()
string bool 文字列を bool として解析する .HasConversion<bool>()
any_numeric_type 指定された数値型として文字列を解析する .HasConversion<any_numeric_type>()
char 文字列の最初の文字 .HasConversion<char>()
DateTime 文字列を DateTime として解析する .HasConversion<DateTime>()
DateTimeOffset 文字列を DateTimeOffset として解析する .HasConversion<DateTimeOffset>()
TimeSpan 文字列を TimeSpan として解析する .HasConversion<TimeSpan>()
GUID 文字列を Guid として解析する .HasConversion<Guid>()
byte[] UTF8 バイトとしての文字列 .HasConversion<byte[]>()
char string 1 文字の文字列 .HasConversion<string>()
DateTime long DateTime.Kind を保持するエンコードされた日付/時刻 .HasConversion<long>()
long ティック DateTimeToTicksConverter を使用します
string インバリアント カルチャの日付/時刻の文字列 .HasConversion<string>()
DateTimeOffset long オフセットがあるエンコードされた日付/時刻 .HasConversion<long>()
string オフセットがあるインバリアント カルチャの日付/時刻の文字列 .HasConversion<string>()
TimeSpan long ティック .HasConversion<long>()
string インバリアント カルチャの期間の文字列 .HasConversion<string>()
Uri string 文字列としての URI .HasConversion<string>()
PhysicalAddress string 文字列としてのアドレス .HasConversion<string>()
byte[] ビッグ エンディアン ネットワーク順序でのバイト .HasConversion<byte[]>()
IPAddress string 文字列としてのアドレス .HasConversion<string>()
byte[] ビッグ エンディアン ネットワーク順序でのバイト .HasConversion<byte[]>()
GUID string "dddddddd-dddd-dddd-dddd-dddddddddddd" の形式の GUID .HasConversion<string>()
byte[] .NET バイナリ シリアル化順序でのバイト .HasConversion<byte[]>()

これらの変換では、その変換とって値の形式が適切であることが前提になっています。 たとえば、文字列値を数値として解析できない場合は、文字列から数値への変換は失敗することになります。

すべての組み込みコンバーターの一覧は次のとおりです。

すべての組み込みコンバーターはステートレスなので、複数のプロパティで 1 つのインスタンスを安全に共有できます。

列のファセットとマッピングに関するヒント

一部のデータベース型には、データの格納方法を変更するファセットがあります。 これには以下が含まれます。

  • 10 進数と日付/時刻の列の有効桁数と小数点以下桁数
  • バイナリと文字列の列のサイズと長さ
  • 文字列の列の Unicode

これらのファセットは、値コンバーターを使用するプロパティでの通常の方法で構成でき、変換されたデータベース型に適用されます。 たとえば、列挙型から文字列への変換時には、データベース列を Unicode 以外にして、最大 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);
}

これで、SQL Server に対して EF Core による移行を使用すると 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 の Unicode 以外のデータベース列になります。 ただしこれらは、マップされたプロパティにファセットが明示的に設定されているとオーバーライドされるため、参考にしかなりません。

単純な値のオブジェクト

この例では、単純型を使用してプリミティブ型をラップします。 これは、モデル内の型をプリミティブ型よりも具体的な (つまり、よりタイプ セーフな) ものにしたいときに便利な場合があります。 この例では、その型は Dollars であり、10 進のプリミティブ型をラップするものです。

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

Note

この値オブジェクトは、読み取り専用の構造体として実装されます。 これは、EF Core が問題なくスナップショットを作成して値を比較できることを意味します。 詳細については、値の比較子に関するページを参照してください。

複合値のオブジェクト

前の例では、値オブジェクト型に含まれるプロパティは 1 つだけでした。 値オブジェクト型は、ドメインの概念を形成する複数のプロパティから成るのが一般的です。 たとえば、金額と通貨の両方が含まれる一般的な 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; }
}

値コンバーターで値を変換できるのは、現在のところ、単一のデータベース列との間でのみです。 この制限は、オブジェクトのすべてのプロパティ値を 1 つの列の値にエンコードする必要があることを意味します。 これは通常、データベースに入れるときにオブジェクトをシリアル化し、その後出すときに、もとの状態へ逆シリアル化することで処理されます。たとえば、System.Text.Json を使用する場合は次のようになります。

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));

Note

EF Core の将来のバージョンでは、1 つのオブジェクトを複数の列にマップできるようにして、こうした場合にシリアル化を使用する必要がなくなるようにする計画です。 この件は、GitHub イシュー #13947 で状況が追跡されています。

Note

前の例と同様に、この値オブジェクトは、読み取り専用の構造体として実装されます。 これは、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> は、変更可能な参照型を表します。 これは、EF Core で変更を正しく追跡して検出できるように、ValueComparer<T> が必要であることを意味します。 詳細については、値の比較子に関するページを参照してください。

値オブジェクトのコレクション

前の 2 つの例を組み合わせると、値オブジェクトのコレクションを作成できます。 たとえば、ブログの 1 年間の財政状態をモデル化する 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()));

Note

前と同様に、この変換には ValueComparer<T> が必要です。 詳細については、値の比較子に関するページを参照してください。

キーとしてのオブジェクト値

値の割り当てでタイプセーフのレベルを上げるために、値オブジェクト内にプリミティブ型のキー プロパティをラップすることがあります。 たとえば、ブログ用のキーの型と、投稿用のキーの型を実装できます。

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.IdPostKey に割り当てることはできず、誤って Post.IdBlogKey に割り当てることはできないことに注目してください。 同様に、Post.BlogId 外部キー プロパティには BlogKey が割り当てられる必要があります。

Note

このパターンを紹介していても、それを Microsoft がお勧めしているわけではありません。 このレベルの抽象化が、開発経験の助けになるか妨げになるかを慎重に検討してください。 また、キー値を直接扱うのではなく、ナビゲーションと生成されたキーを使用することを検討してください。

これらのキー プロパティは、値コンバーターを使用して後からマップすることができます。

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

Note

変換を伴うキー プロパティでは、EF Core 7.0 以降で生成されたキー値のみを使用できます。

timestamp/rowversion に ulong を使用する

SQL Server では、8 バイトのバイナリ型 rowversion/timestamp を使用した自動のオプティミスティック同時実行制御がサポートされています。 これらは常に、8 バイトの配列を使用してデータベースに対する読み書きが行われます。 ただし、バイト配列は変更可能な参照型なので、その処理がいくらか難しくなります。 値コンバーターを使用すると、代わりに rowversionulong プロパティに割り当てることができます。これは、バイト配列よりも適切で使いやすい方法です。 たとえば、ulong 型のコンカレンシー トークンを持つ Blog エンティティについて考えてみます。

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ulong Version { get; set; }
}

これは、値コンバーターを使用して SQL サーバーの rowversion 列にマップできます。

modelBuilder.Entity<Blog>()
    .Property(e => e.Version)
    .IsRowVersion()
    .HasConversion<byte[]>();

日付を読み取るときに DateTime.Kind を指定する

SQL Server は、DateTimedatetime または datetime2 として保存するときに、DateTime.Kind フラグを破棄します。 これは、データベースから返される DateTime 値の DateTimeKind は常に Unspecified になることを意味します。

これに対処するために、2 つの方法で値コンバーターを使用できます。 まず、EF Core には、Kind フラグを保持する 8 バイトの非透過的な値を作成する値コンバーターが用意されています。 次に例を示します。

modelBuilder.Entity<Post>()
    .Property(e => e.PostedOn)
    .HasConversion<long>();

これにより、異なる Kind フラグを持つ DateTime 値をデータベース内に混在させることができます。

この方法の問題は、データベースに、認識できる 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));

Note

基になるすべてのデータベース アクセス コードでは常に UTC 時間を使用するよう統一し、ローカル時刻を扱うのはユーザーにデータを表示するときだけにすることを検討してください。

大文字と小文字を区別しない文字列キーを使用する

SQL Server を含む一部のデータベースでは、既定で、大文字と小文字を区別しない文字列比較が実行されます。 一方 .NET では、既定で大文字と小文字の区別がある文字列比較が実行されます。 これは、"DotNet" のような外部キー値は、SQL Server では主キー値 "dotnet" と一致しますが、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);
        });
}

Note

.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()));

Note

現在のところ、値コンバーター内から現在の DbContext または他のセッション状態への参照を取得する方法はありません。 このため、使用できる暗号化の種類に制限があります。 この制限をなくすためには、GitHub イシュー #11597 に投票してください。

警告

機密データを保護するために独自の暗号化を導入する場合は、すべての影響について確実に理解してください。 SQL Server では代わりに、Always Encrypted などの事前構築済みの暗号化メカニズムを使用することを検討してください。

制限事項

値の変換システムには、現在、既知の制限がいくつかあります。

  • 前述したように、null は変換できません、 これが必要なことの場合は、GitHub イシュー #13850 に (👍) を投票してください。
  • 値が変換されたプロパティに対してクエリを実行することはできません (例: LINQ クエリ内で、値が変換された .NET 型のメンバーを参照する)。 これが必要なことの場合は、GitHub イシュー #10434 に (👍) を投票してください。ただし、代わりに JSON 列の使用を検討してください。
  • 現在、1 つのプロパティの変換を複数の列に適用したり、その逆を行う方法はありません。 これが必要なことの場合は、GitHub イシュー #13947 に (👍) を投票してください。
  • 値コンバーターを介してマップされたほとんどのキーでは、値の生成がサポートされていません。 これが必要なことの場合は、GitHub イシュー #11597 に (👍) を投票してください。
  • 値変換では、現在の DbContext インスタンスを参照できません。 これが必要なことの場合は、GitHub イシュー #12205 に (👍) を投票してください。
  • 現在、値が変換された型を使用するパラメータは、生の SQL API では使用できません。 これが必要なことの場合は、GitHub イシュー #27534 に (👍) を投票してください。

これらの制限の解消については、今後のリリースのために検討中です。