値変換
値コンバーターを使用すると、データベースとの読み取りや書き込みの際にプロパティ値を変換できます。 ある値から同じ型の別の値への変換 (文字列の暗号化など) や、ある型の値から別の型の値への変換 (データベース内の文字列に対する列挙値の変換など) が可能です。
ヒント
このドキュメントに含まれているすべてのコードは、GitHub からサンプル コードをダウンロードすることで実行およびデバッグできます。
概要
値コンバーターは、ModelClrType
と ProviderClrType
に関して指定されます。 モデル型は、エンティティ型における、.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_type
は int
、short
、long
、byte
、uint
、ushort
、ulong
、sbyte
、char
、decimal
、float
、double
のいずれかを意味します。
モデル/プロパティの型 | プロバイダー/データベースの型 | 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[]>() |
これらの変換では、その変換とって値の形式が適切であることが前提になっています。 たとえば、文字列値を数値として解析できない場合は、文字列から数値への変換は失敗することになります。
すべての組み込みコンバーターの一覧は次のとおりです。
- bool プロパティの変換:
- BoolToStringConverter - bool から "N" や "Y" などの文字列
- BoolToTwoValuesConverter<TProvider> - bool から任意の 2 つの値
- BoolToZeroOneConverter<TProvider> - bool から 0 および 1
- バイト配列プロパティの変換:
- BytesToStringConverter - バイト配列から Base64 エンコードされた文字列
- 型キャストのみが必要なすべての変換
- CastingConverter<TModel,TProvider> - 型キャストのみが必要な変換
- char プロパティの変換:
- CharToStringConverter - char から 1 文字の文字列
- DateTimeOffset プロパティの変換:
- DateTimeOffsetToBinaryConverter - DateTimeOffset からバイナリ エンコードされた 64 ビット値
- DateTimeOffsetToBytesConverter - DateTimeOffset からバイト配列
- DateTimeOffsetToStringConverter - DateTimeOffset から文字列
- DateTime プロパティの変換:
- DateTimeToBinaryConverter - DateTime から DateTimeKind を含む 64 ビット値
- DateTimeToStringConverter - DateTime から文字列
- DateTimeToTicksConverter - DateTime からティック
- 列挙型プロパティの変換:
- EnumToNumberConverter<TEnum,TNumber> - 列挙型から基になる数値
- EnumToStringConverter<TEnum> - 列挙型から文字列
- Guid プロパティの変換:
- GuidToBytesConverter - Guid からバイト配列
- GuidToStringConverter - Guid から文字列
- IPAddress プロパティの変換:
- IPAddressToBytesConverter - IPAddress からバイト配列
- IPAddressToStringConverter - IPAddress から文字列
- 数値 (int、double、decimal など) プロパティの変換:
- NumberToBytesConverter<TNumber> - バイト配列から任意の数値
- NumberToStringConverter<TNumber> - 任意の数値から文字列
- 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 から文字列
すべての組み込みコンバーターはステートレスなので、複数のプロパティで 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.Id
を PostKey
に割り当てることはできず、誤って Post.Id
を BlogKey
に割り当てることはできないことに注目してください。 同様に、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 バイトの配列を使用してデータベースに対する読み書きが行われます。 ただし、バイト配列は変更可能な参照型なので、その処理がいくらか難しくなります。 値コンバーターを使用すると、代わりに rowversion
を ulong
プロパティに割り当てることができます。これは、バイト配列よりも適切で使いやすい方法です。 たとえば、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 は、DateTime を datetime
または 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
値に DateTimeKind UTC
があることが保証されます。
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 に (👍) を投票してください。
これらの制限の解消については、今後のリリースのために検討中です。
.NET