값 변환

값 변환기를 사용하면 데이터베이스에서 읽거나 데이터베이스에 쓸 때 속성 값을 변환할 수 있습니다. 이 변환은 한 값에서 동일한 형식의 다른 값(예: 문자열 암호화) 또는 한 형식의 값에서 다른 형식의 값으로 변환할 수 있습니다(예: 열거형 값을 데이터베이스의 문자열로 변환).

GitHub에서 샘플 코드를 다운로드하여 이 문서의 모든 코드를 실행하고 디버그할 수 있습니다.

개요

값 변환기는 ModelClrTypeProviderClrType 측면에서 지정됩니다. 모델 형식은 엔터티 형식에 있는 속성의 .NET 형식입니다. 공급자 유형은 데이터베이스 공급자가 이해하는 .NET 형식입니다. 예를 들어 열거형을 데이터베이스에 문자열로 저장하려면 모델 형식이 열거형의 형식이고 공급자 형식은 String입니다. 이러한 두 형식은 동일할 수 있습니다.

변환은 두 개의 Func 식 트리를 사용하여 정의됩니다. 하나는 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
}

열거형 값을 데이터베이스에 "Donkey", "Mule" 등의 문자열로 저장하도록 OnModelCreating에서 변환을 구성할 수 있습니다. 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이고 그 반대의 경우도 마찬가지입니다. 이렇게 하면 변환 구현이 더 쉬워지고 nullable 및 nullable이 아닌 속성 간에 변환을 공유할 수 있습니다. 자세한 내용은 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는 모델의 속성 형식 및 요청된 데이터베이스 공급자 유형에 따라 사용할 변환을 선택합니다.

예를 들어 열거형에서 문자열로의 변환은 위의 예제로 사용되지만 공급자 형식이 HasConversion의 제네릭 형식을 사용하여 string으로 구성된 경우 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는 열거형에 대해 위에 표시된 것처럼 모델의 속성 형식과 데이터베이스에서 요청된 형식에 따라 적절한 기본 제공 변환기를 선택합니다. 예를 들어 bool 속성에서 .HasConversion<int>()을 사용하면 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_typeint, short, long, byte, uint, ushort, ulong, sbyte, char, decimal, float 또는 double 중 하나를 의미합니다.

모델/속성 형식 공급자/데이터베이스 유형 전환 사용
부울 any_numeric_type False/true를 0/1로 .HasConversion<any_numeric_type>()
any_numeric_type False/true를 두 숫자로 BoolToTwoValuesConverter<TProvider> 사용
string False/true를 "N"/"Y"로 .HasConversion<string>()
string False/true를 두 문자열로 BoolToStringConverter 사용
any_numeric_type 부울 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 부울 문자열을 부울로 구문 분석 .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 단일 문자열 .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[] big-endian 네트워크 순서의 바이트 .HasConversion<byte[]>()
IPAddress string 문자열인 주소 .HasConversion<string>()
byte[] big-endian 네트워크 순서의 바이트 .HasConversion<byte[]>()
GUID string 'dddddddd-dddd-dddd-dddd-dddddddddddd' 형식의 GUID .HasConversion<string>()
byte[] .NET 이진 serialization 순서의 바이트 .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);
}

그러면 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인 유니코드가 아닌 열이 됩니다. 그러나 매핑된 속성에 명시적으로 설정된 모든 패싯에 의해 재정의되므로 힌트일 뿐입니다.

예제

단순 값 개체

이 예제에서는 단순 형식을 사용하여 기본 형식을 래핑합니다. 이는 모델의 형식이 기본 형식보다 더 구체적이고 형식이 안전하도록 하려는 경우에 유용할 수 있습니다. 이 예제에서 해당 형식은 10진수 기본 형식을 래핑하는 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는 문제 없이 값을 스냅샷과 비교할 수 있습니다. 자세한 내용은 값 비교자를 참조하세요.

복합 값 개체

이전 예제에서 값 개체 형식에는 단일 속성만 포함되었습니다. 값 개체 형식이 도메인 개념을 구성하는 여러 속성을 구성하는 것이 더 일반적입니다. 예를 들어 금액과 통화를 모두 포함하는 일반 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에서 개체를 여러 열에 매핑할 수 있도록 허용하여 여기서 serialization을 사용할 필요가 없습니다. 이는 GitHub 문제 #13947에서 찾을 수 있습니다.

참고 항목

이전 예제와 마찬가지로 이 값 개체는 읽기 전용 구조체로 구현됩니다. 즉, EF Core는 문제 없이 값을 스냅샷과 비교할 수 있습니다. 자세한 내용은 값 비교자를 참조하세요.

기본 형식의 컬렉션

serialization을 사용하여 기본 값 컬렉션을 저장할 수도 있습니다. 예시:

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>가 필요합니다. 자세한 내용은 값 비교자를 참조하세요.

값 개체의 컬렉션

이전 두 예제를 함께 결합하여 값 개체의 컬렉션을 만들 수 있습니다. 예를 들어 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; }
}

그리고 다시 serialization을 사용하여 다음을 저장합니다.

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>가 필요합니다. 자세한 내용은 값 비교자를 참조하세요.

개체를 키로 값 지정

경우에 따라 기본 키 속성을 값 개체에 래핑하여 값을 할당할 때 형식 안전 수준을 추가할 수 있습니다. 예를 들어 블로그의 키 형식과 게시물의 키 형식을 구현할 수 있습니다.

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부터 생성된 키 값만 사용할 수 있습니다.

타임스탬프/rowversion에 ulong 사용

SQL Server는 8바이트 이진 rowversion/timestamp을 사용하여 자동 낙관적 동시성을 지원합니다. 이러한 열은 항상 8바이트 배열을 사용하여 데이터베이스에서 읽어지고 데이터베이스에 쓰여집니다. 그러나 바이트 배열은 변경 가능한 참조 형식이므로 처리하기가 다소 어려워집니다. 값 변환기를 사용하면 ulong 속성에 rowversion을 대신 매핑할 수 있습니다. 이 속성은 바이트 배열보다 훨씬 더 적절하고 사용하기 쉽습니다. 예를 들어 ulong 동시성 토큰이 있는 Blog 엔터티를 고려하세요.

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 지정

DateTimedatetime 또는 datetime2로 저장할 때 DateTime.Kind 플래그를 삭제합니다. 즉, 데이터베이스에서 돌아오는 DateTime 값에는 항상 UnspecifiedDateTimeKind가 있습니다.

값 변환기는 이를 처리하는 두 가지 방법으로 사용할 수 있습니다. 먼저 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));

참고 항목

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

참고 항목

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

속성 값 암호화

값 변환기를 사용하여 속성 값을 데이터베이스로 보내기 전에 암호화한 다음, 나가는 길에 암호를 해독할 수 있습니다. 예를 들어 실제 암호화 알고리즘 대신 문자열 반전을 사용합니다. 그러나 #PasswordDigest는 UsernameToken 암호화를 대체할 수 없습니다.

modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
    v => new string(v.Reverse().ToArray()),
    v => new string(v.Reverse().ToArray()));

참고 항목

현재 값 변환기 내에서 현재 DbContext 또는 기타 세션 상태에 대한 참조를 가져올 수 있는 방법은 없습니다. 이렇게 하면 사용할 수 있는 암호화 종류가 제한됩니다. 이 제한을 제거하려면 GitHub 문제 #11597에 투표하세요.

Warning

중요한 데이터를 보호하기 위해 자체 암호화를 롤하는 경우 모든 의미를 이해해야 합니다. 대신 SQL Server에서 상시 암호화와 같은 미리 빌드된 암호화 메커니즘을 사용하는 것이 좋습니다.

제한 사항

값 변환 시스템의 몇 가지 알려진 현재 제한 사항이 있습니다.

  • 위에서 설명한 대로 null은 변환할 수 없습니다. 필요한 경우 GitHub 문제 #13850에 투표하세요(👍).
  • 값 변환 속성(예: LINQ 쿼리의 값 변환 .NET 형식에 대한 참조 멤버)으로 쿼리할 수 없습니다. 필요한 경우GitHub 문제 #10434에 투표하세요(👍). 대신 JSON 열을 사용하는 것이 좋습니다.
  • 현재 한 속성의 변환을 여러 열로 분산하거나 그 반대로 분산할 수 있는 방법은 없습니다. 필요한 경우 GitHub 문제 #13947에 투표하세요(👍).
  • 값 변환기를 통해 매핑된 대부분의 키에는 값 생성이 지원되지 않습니다. 필요한 경우 GitHub 문제 #11597에 투표하세요(👍).
  • 값 변환은 현재 DbContext 인스턴스를 참조할 수 없습니다. 필요한 경우 GitHub 문제 #12205에 투표하세요(👍).
  • 값 변환 형식을 사용하는 매개 변수는 현재 원시 SQL API에서 사용할 수 없습니다. 필요한 경우 GitHub 문제 #27534에 투표하세요(👍).

이러한 제한 사항은 향후 릴리스에서 제거될 예정입니다.