Konwersje wartości

Konwertery wartości umożliwiają konwertowanie wartości właściwości podczas odczytywania z bazy danych lub zapisywania ich w bazie danych. Ta konwersja może pochodzić z jednej wartości do innego tego samego typu (na przykład szyfrowania ciągów) lub wartości jednego typu do wartości innego typu (na przykład konwertowania wartości wyliczeniowych na i z ciągów w bazie danych).

Napiwek

Możesz uruchomić i debugować cały kod podany w tym dokumencie, pobierając przykładowy kod z serwisu GitHub.

Omówienie

Konwertery wartości są określane w kategoriach a ModelClrType i ProviderClrType. Typ modelu to typ platformy .NET właściwości w typie jednostki. Typ dostawcy to typ platformy .NET rozumiany przez dostawcę bazy danych. Na przykład aby zapisać wyliczenia jako ciągi w bazie danych, typ modelu jest typem wyliczenia, a typ dostawcy to String. Te dwa typy mogą być takie same.

Konwersje są definiowane przy użyciu dwóch Func drzew wyrażeń: jeden z ModelClrType do ProviderClrType i drugi z ProviderClrType do ModelClrType. Drzewa wyrażeń są używane, aby można je było skompilować do delegata dostępu do bazy danych w celu wydajnej konwersji. Drzewo wyrażeń może zawierać proste wywołanie metody konwersji dla złożonych konwersji.

Uwaga

Właściwość skonfigurowana do konwersji wartości może również wymagać określenia ValueComparer<T>wartości . Aby uzyskać więcej informacji, zapoznaj się z poniższymi przykładami i dokumentacją funkcji porównywania wartości.

Konfigurowanie konwertera wartości

Konwersje wartości są konfigurowane w programie DbContext.OnModelCreating. Rozważmy na przykład wyliczenie i typ jednostki zdefiniowany jako:

public class Rider
{
    public int Id { get; set; }
    public EquineBeast Mount { get; set; }
}

public enum EquineBeast
{
    Donkey,
    Mule,
    Horse,
    Unicorn
}

Konwersje można skonfigurować do OnModelCreating przechowywania wartości wyliczenia jako ciągów, takich jak "Donkey", "Mule", itp. w bazie danych; wystarczy po prostu podać jedną funkcję, która konwertuje z ModelClrType elementu na ProviderClrType, a drugą na odwrotną konwersję:

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

Uwaga

null Wartość nigdy nie zostanie przekazana do konwertera wartości. Wartość null w kolumnie bazy danych jest zawsze wartością null w wystąpieniu jednostki i odwrotnie. Dzięki temu implementacja konwersji jest łatwiejsza i umożliwia udostępnianie ich między właściwościami dopuszczanymi do wartości null i niepustymi. Aby uzyskać więcej informacji, zobacz Problem z usługą GitHub #13850 .

Zbiorcze konfigurowanie konwertera wartości

Ten sam konwerter wartości jest często konfigurowany dla każdej właściwości, która używa odpowiedniego typu CLR. Zamiast ręcznie wykonywać te czynności dla każdej właściwości, możesz użyć konfiguracji modelu przed konwencją, aby to zrobić raz dla całego modelu. W tym celu zdefiniuj konwerter wartości jako klasę:

public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

Następnie przesłoń ConfigureConventions typ kontekstu i skonfiguruj konwerter w następujący sposób:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

Wstępnie zdefiniowane konwersje

Program EF Core zawiera wiele wstępnie zdefiniowanych konwersji, które unikają ręcznego zapisywania funkcji konwersji. Zamiast tego program EF Core wybierze konwersję do użycia na podstawie typu właściwości w modelu i żądanego typu dostawcy bazy danych.

Na przykład wyliczenie do konwersji ciągów jest używane jako przykład powyżej, ale program EF Core faktycznie zrobi to automatycznie, gdy typ dostawcy jest skonfigurowany jako string przy użyciu typu HasConversionogólnego :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>();
}

To samo można osiągnąć, jawnie określając typ kolumny bazy danych. Jeśli na przykład typ jednostki jest zdefiniowany w następujący sposób:

public class Rider2
{
    public int Id { get; set; }

    [Column(TypeName = "nvarchar(24)")]
    public EquineBeast Mount { get; set; }
}

Następnie wartości wyliczenia zostaną zapisane jako ciągi w bazie danych bez dalszej konfiguracji w pliku OnModelCreating.

Klasa ValueConverter

Wywołanie HasConversion , jak pokazano powyżej, spowoduje utworzenie ValueConverter<TModel,TProvider> wystąpienia i ustawienie go we właściwości . ValueConverter Zamiast tego można je utworzyć jawnie. Przykład:

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

Może to być przydatne, gdy wiele właściwości używa tej samej konwersji.

Wbudowane konwertery

Jak wspomniano powyżej, program EF Core jest dostarczany z zestawem wstępnie zdefiniowanych ValueConverter<TModel,TProvider> klas znajdujących się w Microsoft.EntityFrameworkCore.Storage.ValueConversion przestrzeni nazw. W wielu przypadkach program EF wybierze odpowiedni wbudowany konwerter na podstawie typu właściwości w modelu i typu żądanego w bazie danych, jak pokazano powyżej dla wyliczenia. Na przykład użycie właściwości .HasConversion<int>()bool spowoduje, że program EF Core przekonwertuje wartości logiczne na wartości liczbowe zero i jedną wartość:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<User>()
        .Property(e => e.IsActive)
        .HasConversion<int>();
}

Jest to funkcjonalnie takie samo, jak utworzenie wystąpienia wbudowanego BoolToZeroOneConverter<TProvider> i jawne ustawienie go:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new BoolToZeroOneConverter<int>();

    modelBuilder
        .Entity<User>()
        .Property(e => e.IsActive)
        .HasConversion(converter);
}

W poniższej tabeli przedstawiono podsumowanie często używanych wstępnie zdefiniowanych konwersji z typów modelu/właściwości na typy dostawców baz danych. W tabeli any_numeric_type oznacza jedną z wartości int, short, longushortulonguintbytechardecimalsbytefloatlub .double

Typ modelu/właściwości Dostawca/typ bazy danych Konwersja Użycie
bool any_numeric_type Wartość false/true do 0/1 .HasConversion<any_numeric_type>()
any_numeric_type Fałsz/prawda do dwóch liczb Korzystanie z polecenia BoolToTwoValuesConverter<TProvider>
string Fałsz/true do "N"/"Y" .HasConversion<string>()
string Wartość false/true dla dwóch ciągów Korzystanie z polecenia BoolToStringConverter
any_numeric_type bool Od 0/1 do wartości false/true .HasConversion<bool>()
any_numeric_type Rzutowanie proste .HasConversion<any_numeric_type>()
string Liczba jako ciąg .HasConversion<string>()
Wyliczenie any_numeric_type Wartość liczbowa wyliczenia .HasConversion<any_numeric_type>()
string Reprezentacja ciągu wartości wyliczenia .HasConversion<string>()
string bool Analizuje ciąg jako wartość logiczną .HasConversion<bool>()
any_numeric_type Analizuje ciąg jako podany typ liczbowy .HasConversion<any_numeric_type>()
char Pierwszy znak ciągu .HasConversion<char>()
Data/godzina Analizuje ciąg jako data/godzina .HasConversion<DateTime>()
DateTimeOffset Analizuje ciąg jako element DateTimeOffset .HasConversion<DateTimeOffset>()
przedział_czasu Analizuje ciąg jako przedział czasu .HasConversion<TimeSpan>()
Identyfikator GUID Analizuje ciąg jako identyfikator GUID .HasConversion<Guid>()
byte[] Ciąg jako bajty UTF8 .HasConversion<byte[]>()
char string Ciąg z pojedynczym znakiem .HasConversion<string>()
Data/godzina długi Zakodowana data/godzina zachowująca wartość DateTime.Kind .HasConversion<long>()
długi Kleszcze Korzystanie z polecenia DateTimeToTicksConverter
string Niezmienny ciąg daty/godziny kultury .HasConversion<string>()
DateTimeOffset długi Zakodowana data/godzina z przesunięciem .HasConversion<long>()
string Niezmienny ciąg daty/godziny kultury z przesunięciem .HasConversion<string>()
przedział_czasu długi Kleszcze .HasConversion<long>()
string Niezmienny ciąg przedziału czasu kultury .HasConversion<string>()
Identyfikator URI string Identyfikator URI jako ciąg .HasConversion<string>()
Physicaladdress string Adres jako ciąg .HasConversion<string>()
byte[] Bajty w kolejności sieci big-endian .HasConversion<byte[]>()
IPAddress string Adres jako ciąg .HasConversion<string>()
byte[] Bajty w kolejności sieci big-endian .HasConversion<byte[]>()
Identyfikator GUID string Identyfikator GUID w formacie "dd-d-d-d-d" .HasConversion<string>()
byte[] Bajty w kolejności serializacji binarnej platformy .NET .HasConversion<byte[]>()

Należy pamiętać, że te konwersje zakładają, że format wartości jest odpowiedni dla konwersji. Na przykład konwertowanie ciągów na liczby zakończy się niepowodzeniem, jeśli wartości ciągu nie mogą być analizowane jako liczby.

Pełna lista wbudowanych konwerterów to:

Należy pamiętać, że wszystkie wbudowane konwertery są bezstanowe i dlatego pojedyncze wystąpienie może być bezpiecznie współużytkowane przez wiele właściwości.

Aspekty kolumn i wskazówki mapowania

Niektóre typy baz danych mają aspekty modyfikujące sposób przechowywania danych. Są to:

  • Precyzja i skala dla kolumn dziesiętnych i daty/godziny
  • Rozmiar/długość kolumn binarnych i ciągów
  • Unicode dla kolumn ciągu

Te aspekty można skonfigurować w normalny sposób dla właściwości używającej konwertera wartości i będą stosowane do przekonwertowanego typu bazy danych. Na przykład podczas konwertowania z wyliczenia na ciągi możemy określić, że kolumna bazy danych powinna być nie unicode i przechowywać maksymalnie 20 znaków:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>()
        .HasMaxLength(20)
        .IsUnicode(false);
}

Lub podczas jawnego tworzenia konwertera:

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

Spowoduje to wyświetlenie varchar(20) kolumny podczas korzystania z migracji programu EF Core do programu SQL Server:

CREATE TABLE [Rider] (
    [Id] int NOT NULL IDENTITY,
    [Mount] varchar(20) NOT NULL,
    CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));

Jeśli jednak domyślnie wszystkie EquineBeast kolumny powinny mieć varchar(20)wartość , te informacje mogą być przekazywane do konwertera ConverterMappingHintswartości jako . Przykład:

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

Teraz w dowolnym momencie użycia tego konwertera kolumna bazy danych będzie nie unicode o maksymalnej długości 20. Są to jednak tylko wskazówki, ponieważ są one zastępowane przez wszystkie aspekty jawnie ustawione we właściwości mapowanej.

Przykłady

Proste obiekty wartości

W tym przykładzie użyto prostego typu do opakowania typu pierwotnego. Może to być przydatne, gdy chcesz, aby typ w modelu był bardziej szczegółowy (a tym samym bardziej bezpieczny dla typu) niż typ pierwotny. W tym przykładzie ten typ to Dollars, który opakowuje typ pierwotny dziesiętny:

public readonly struct Dollars
{
    public Dollars(decimal amount) 
        => Amount = amount;
        
    public decimal Amount { get; }

    public override string ToString() 
        => $"${Amount}";
}

Może to być używane w typie jednostki:

public class Order
{
    public int Id { get; set; }

    public Dollars Price { get; set; }
}

I przekonwertowane na bazowe decimal podczas przechowywania w bazie danych:

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => v.Amount,
        v => new Dollars(v));

Uwaga

Ten obiekt wartości jest implementowany jako struktura readonly. Oznacza to, że program EF Core może migawek i porównywać wartości bez problemu. Aby uzyskać więcej informacji, zobacz Porównanie wartości.

Obiekty wartości złożonej

W poprzednim przykładzie typ obiektu wartości zawierał tylko jedną właściwość. Typ obiektu wartości jest częściej używany do tworzenia wielu właściwości, które razem tworzą koncepcję domeny. Na przykład ogólny Money typ, który zawiera zarówno kwotę, jak i walutę:

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
}

Ten obiekt wartości może być używany w typie jednostki, tak jak poprzednio:

public class Order
{
    public int Id { get; set; }

    public Money Price { get; set; }
}

Konwertery wartości mogą obecnie konwertować wartości tylko na i z jednej kolumny bazy danych. To ograniczenie oznacza, że wszystkie wartości właściwości z obiektu muszą być zakodowane w jedną wartość kolumny. Jest to zwykle obsługiwane przez serializowanie obiektu w miarę przechodzenia do bazy danych, a następnie deserializacji go ponownie w drodze. Na przykład przy użyciu polecenia System.Text.Json:

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

Uwaga

Planujemy zezwolić na mapowanie obiektu na wiele kolumn w przyszłej wersji programu EF Core, co pozwala usunąć konieczność użycia serializacji tutaj. Jest to śledzone przez problem z usługą GitHub #13947.

Uwaga

Podobnie jak w poprzednim przykładzie, ten obiekt wartości jest implementowany jako struktura readonly. Oznacza to, że program EF Core może migawek i porównywać wartości bez problemu. Aby uzyskać więcej informacji, zobacz Porównanie wartości.

Kolekcje elementów pierwotnych

Serializacji można również używać do przechowywania kolekcji wartości pierwotnych. Przykład:

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Contents { get; set; }

    public ICollection<string> Tags { get; set; }
}

Użyj System.Text.Json ponownie:

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> reprezentuje modyfikowalny typ odwołania. Oznacza to, że element jest potrzebny, ValueComparer<T> aby program EF Core mógł prawidłowo śledzić i wykrywać zmiany. Aby uzyskać więcej informacji, zobacz Porównanie wartości.

Kolekcje obiektów wartości

Łącząc dwa poprzednie przykłady, możemy utworzyć kolekcję obiektów wartości. Rozważmy na przykład typ, AnnualFinance który modeluje finanse bloga przez jeden rok:

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

Ten typ komponuje kilka utworzonych Money wcześniej typów:

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
}

Następnie możemy dodać kolekcję AnnualFinance do typu jednostki:

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

    public IList<AnnualFinance> Finances { get; set; }
}

I ponownie użyj serializacji do przechowywania tego:

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

Uwaga

Tak jak wcześniej ta konwersja wymaga ValueComparer<T>. Aby uzyskać więcej informacji, zobacz Porównanie wartości.

Obiekty wartości jako klucze

Czasami właściwości klucza pierwotnego mogą być opakowane w obiekty wartości, aby dodać dodatkowy poziom bezpieczeństwa typu w przypisywaniu wartości. Na przykład możemy zaimplementować typ klucza dla blogów i typ klucza dla wpisów:

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

Można ich następnie użyć w modelu domeny:

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

Zwróć uwagę, że Blog.Id nie można przypadkowo przypisać elementu PostKeyi Post.Id nie można go przypadkowo przypisać BlogKey. Podobnie właściwość klucza obcego Post.BlogId musi mieć przypisaną BlogKeywłaściwość .

Uwaga

Pokazanie tego wzorca nie oznacza, że jest to zalecane. Dokładnie zastanów się, czy ten poziom abstrakcji pomaga lub utrudnia środowisko deweloperskie. Należy również rozważyć użycie nawigacji i wygenerowanych kluczy zamiast bezpośrednio radzić sobie z wartościami kluczy.

Te właściwości klucza można następnie mapować przy użyciu konwerterów wartości:

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

Uwaga

Właściwości klucza z konwersjami mogą używać tylko wygenerowanych wartości kluczy, począwszy od programu EF Core 7.0.

Użyj ulong dla znacznika czasu/elementu rowversion

Program SQL Server obsługuje automatyczną optymistyczną współbieżność przy użyciu 8-bajtowych kolumn binarnychtimestamp/rowversion. Są one zawsze odczytywane i zapisywane w bazie danych przy użyciu tablicy 8-bajtowej. Jednak tablice bajtów są modyfikowalnym typem odwołania, co sprawia, że są one nieco bolesne do radzenia sobie z. Konwertery wartości umożliwiają rowversion mapowanie elementu na ulong właściwość, która jest znacznie bardziej odpowiednia i łatwa w użyciu niż tablica bajtów. Rozważmy na przykład Blog jednostkę z tokenem współbieżności ulong:

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

Można to zamapować na kolumnę programu SQL Server rowversion przy użyciu konwertera wartości:

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

Określ typ DateTime.Kind podczas odczytywania dat

Program SQL Server odrzuca flagę DateTime.Kind podczas przechowywania DateTime elementu jako lub datetimedatetime2. Oznacza to, że wartości DateTime pochodzące z bazy danych zawsze mają DateTimeKind wartość Unspecified.

Konwertery wartości mogą być używane na dwa sposoby, aby sobie z tym poradzić. Po pierwsze, program EF Core ma konwerter wartości, który tworzy nieprzezroczystą wartość 8-bajtową, która zachowuje flagę Kind . Przykład:

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

Umożliwia to mieszanie wartości DateTime z różnymi Kind flagami w bazie danych.

Problem z tym podejściem polega na tym, że baza danych nie ma już rozpoznawalnych datetime ani datetime2 kolumn. Zamiast tego często zdarza się przechowywać czas UTC (lub rzadziej zawsze czas lokalny), a następnie zignorować flagę Kind lub ustawić ją na odpowiednią wartość przy użyciu konwertera wartości. Na przykład poniższy konwerter gwarantuje, że DateTime wartość odczytana z bazy danych będzie miała wartość DateTimeKindUTC:

modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v,
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Jeśli w wystąpieniach jednostek jest ustawiana kombinacja wartości lokalnych i UTC, konwerter może służyć do odpowiedniego konwertowania przed wstawieniem. Na przykład:

modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v.ToUniversalTime(),
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Uwaga

Starannie rozważ ujednolicenie całego kodu dostępu do bazy danych, aby używać czasu UTC przez cały czas, tylko do czynienia z czasem lokalnym podczas prezentowania danych użytkownikom.

Używanie kluczy ciągów bez uwzględniania wielkości liter

Niektóre bazy danych, w tym program SQL Server, domyślnie wykonują porównania ciągów bez uwzględniania wielkości liter. Z drugiej strony platforma .NET domyślnie wykonuje porównania ciągów z uwzględnieniem wielkości liter. Oznacza to, że wartość klucza obcego, taka jak "DotNet", będzie zgodna z wartością klucza podstawowego "dotnet" w programie SQL Server, ale nie będzie zgodna z nią w programie EF Core. Porównanie wartości dla kluczy może służyć do wymuszenia programu EF Core w porównaniach ciągów bez uwzględniania wielkości liter, takich jak w bazie danych. Rozważmy na przykład model bloga/postów z kluczami ciągów:

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

Nie będzie to działać zgodnie z oczekiwaniami, jeśli niektóre wartości Post.BlogId mają inną wielkość liter. Błędy spowodowane przez to będą zależeć od tego, co robi aplikacja, ale zazwyczaj obejmują grafy obiektów, które nie są poprawnie naprawione , i/lub aktualizacje, które kończą się niepowodzeniem, ponieważ wartość FK jest nieprawidłowa. Aby rozwiązać ten błąd, można użyć porównania wartości:

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

Uwaga

Porównania ciągów platformy .NET i porównania ciągów bazy danych mogą się różnić bardziej niż tylko wielkość liter. Ten wzorzec działa w przypadku prostych kluczy ASCII, ale może zakończyć się niepowodzeniem dla kluczy z dowolnym rodzajem znaków specyficznych dla kultury. Aby uzyskać więcej informacji, zobacz Sortowanie i ważność wielkości liter.

Obsługa ciągów bazy danych o stałej długości

Poprzedni przykład nie potrzebował konwertera wartości. Jednak konwerter może być przydatny w przypadku typów ciągów bazy danych o stałej długości, takich jak char(20) lub nchar(20). Ciągi o stałej długości są dopełniane do pełnej długości za każdym razem, gdy wartość zostanie wstawiona do bazy danych. Oznacza to, że wartość klucza "dotnet" będzie odczytywana z bazy danych jako "dotnet..............", gdzie . reprezentuje znak spacji. Nie spowoduje to poprawnego porównania z wartościami kluczy, które nie są dopełnione.

Konwerter wartości może służyć do przycinania wypełnienia podczas odczytywania wartości klucza. Można to połączyć z modułem porównującym wartości w poprzednim przykładzie, aby poprawnie porównać klucze ASCII o stałej długości. Przykład:

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

Szyfrowanie wartości właściwości

Konwertery wartości mogą służyć do szyfrowania wartości właściwości przed wysłaniem ich do bazy danych, a następnie odszyfrować je w drodze. Na przykład użycie odwrócenia ciągu jako zamiennika rzeczywistego algorytmu szyfrowania:

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

Uwaga

Obecnie nie ma możliwości pobrania odwołania do bieżącego elementu DbContext lub innego stanu sesji z poziomu konwertera wartości. Ogranicza to rodzaje szyfrowania, których można użyć. Zagłosuj na problem z usługą GitHub #11597 , aby to ograniczenie zostało usunięte.

Ostrzeżenie

Pamiętaj, aby zrozumieć wszystkie implikacje, jeśli wdrożysz własne szyfrowanie w celu ochrony poufnych danych. Rozważ użycie wstępnie utworzonych mechanizmów szyfrowania, takich jak Always Encrypted w programie SQL Server.

Ograniczenia

Istnieje kilka znanych bieżących ograniczeń systemu konwersji wartości:

  • Jak wspomniano powyżej, null nie można przekonwertować. Zagłosuj (👍) na problem z usługą GitHub #13850 , jeśli jest to coś, czego potrzebujesz.
  • Nie można wykonywać zapytań dotyczących właściwości przekonwertowanych na wartość, np. elementów członkowskich odwołań w typie platformy .NET przekonwertowanym na wartość w zapytaniach LINQ. Zagłosuj (👍) na problem z usługą GitHub #10434 , jeśli jest to coś potrzebnego , ale zamiast tego rozważ użycie kolumny JSON.
  • Obecnie nie ma możliwości rozłożenia konwersji jednej właściwości na wiele kolumn lub na odwrót. Zagłosuj na problem z usługą GitHub #13947,👍 jeśli jest to coś, czego potrzebujesz.
  • Generowanie wartości nie jest obsługiwane w przypadku większości kluczy mapowanych za pomocą konwerterów wartości. Zagłosuj na👍 problem z usługą GitHub #11597, jeśli jest to coś, czego potrzebujesz.
  • Konwersje wartości nie mogą odwoływać się do bieżącego wystąpienia DbContext. Zagłosuj na problem z usługą GitHub #12205,👍 jeśli jest to coś, czego potrzebujesz.
  • Parametry używające typów przekonwertowanych na wartość nie mogą być obecnie używane w nieprzetworzonych interfejsach API SQL. Zagłosuj (👍) na problem z usługą GitHub #27534 , jeśli jest to coś, czego potrzebujesz.

Usunięcie tych ograniczeń jest brane pod uwagę w przyszłych wersjach.