Udostępnij za pośrednictwem


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

Wskazówka

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 odniesieniu do ModelClrType i ProviderClrType. Typ modelu to typ .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, co pozwala na wydajne konwersje. 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>. 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 enum i typ jednostki, które zostały zdefiniowane 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ć w OnModelCreating do przechowywania wartości wyliczenia jako ciągów znaków, takich jak "Donkey", "Mule", itp. w bazie danych; wystarczy podać jedną funkcję, która konwertuje z ModelClrType 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 w swoim typie kontekstu przesłoń/zastąp ConfigureConventions 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 konwersje z wyliczenia na ciąg znaków są 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 ogólnego typu HasConversion.

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 znaków w bazie danych bez dalszej konfiguracji w OnModelCreating.

Klasa ValueConverter

Wywołanie HasConversion, jak pokazano powyżej, spowoduje utworzenie wystąpienia ValueConverter<TModel,TProvider> i ustawienie go w właściwości. ValueConverter Zamiast tego można je utworzyć jawnie. Na 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 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 typów wyliczeniowych. Na przykład użycie .HasConversion<int>() na właściwości bool spowoduje, że EF Core przekonwertuje wartości logiczne na wartości liczbowe zero i jeden.

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, longbyteuintushortulongsbytechardecimalfloatlub .double

Typ modelu/typ właściwości Dostawca/typ bazy danych Konwersja Użycie
Bool dowolny_typ_numeryczny Wartość false/true do 0/1 .HasConversion<any_numeric_type>()
dowolny_typ_numeryczny Fałsz/prawda dla dowolnych dwóch liczb Użyj BoolToTwoValuesConverter<TProvider>
ciąg Fałsz/prawda na "N"/"Y" .HasConversion<string>()
ciąg Fałsz/prawda dla dowolnych dwóch ciągów Użyj BoolToStringConverter
dowolny_typ_numeryczny Bool Od 0/1 na false/true .HasConversion<bool>()
dowolny_typ_numeryczny Rzutowanie proste .HasConversion<any_numeric_type>()
ciąg Liczba jako ciąg .HasConversion<string>()
Wyliczenie dowolny_typ_numeryczny Wartość liczbowa wyliczenia .HasConversion<any_numeric_type>()
ciąg Reprezentacja wartości wyliczenia jako ciąg znaków .HasConversion<string>()
ciąg Bool Analizuje ciąg jako wartość logiczną .HasConversion<bool>()
dowolny_typ_numeryczny Analizuje ciąg jako typ liczbowy zdefiniowany przez użytkownika .HasConversion<any_numeric_type>()
char Pierwszy znak ciągu .HasConversion<char>()
Data i Czas Analizuje ciąg jako data/godzina .HasConversion<DateTime>()
PrzesunięcieDatyICzasu 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 znaków jako bajty UTF8 .HasConversion<byte[]>()
char ciąg Ciąg z pojedynczym znakiem .HasConversion<string>()
Data i Czas długi Zakodowana data/godzina zachowująca atrybut DateTime.Kind .HasConversion<long>()
długi Kleszcze Użyj DateTimeToTicksConverter
ciąg Niezmienny ciąg daty/godziny kultury .HasConversion<string>()
PrzesunięcieDatyICzasu długi Zakodowana data/godzina z przesunięciem .HasConversion<long>()
ciąg Niezmienny ciąg daty/godziny kultury z przesunięciem .HasConversion<string>()
przedział_czasu długi Kleszcze .HasConversion<long>()
ciąg Niezmienny ciąg przedziału czasu kultury .HasConversion<string>()
Identyfikator URI ciąg Identyfikator URI jako ciąg .HasConversion<string>()
adres fizyczny ciąg Adres jako ciąg .HasConversion<string>()
byte[] Bajty w kolejności sieci big-endian .HasConversion<byte[]>()
Adres IP ciąg Adres jako ciąg .HasConversion<string>()
byte[] Bajty w kolejności sieci big-endian .HasConversion<byte[]>()
Identyfikator GUID ciąg Identyfikator GUID w formacie "dddddddd-dddd-dddd-dddd-dddddddddddd" .HasConversion<string>()
byte[] Kolejność bajtów w binarnej serializacji .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 tekstowych

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 konwersji z typu enum na ciągi znaków 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ć wartość varchar(20), należy przekazać te informacje do konwertera wartości jako ConverterMappingHints. Na 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);
}

Za każdym razem, gdy używany jest ten konwerter, kolumna bazy danych będzie bez 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 podstawowy typ 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 podstawowe decimal podczas zapisywania 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 readonly struct. Oznacza to, że EF Core może tworzyć migawkę 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. Zwykle odbywa się to poprzez serializowanie obiektu przy zapisie do bazy danych, a następnie deserializowanie przy odczycie. Na przykład używając 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 zgłoszenie GitHub #13947.

Uwaga

Podobnie jak w poprzednim przykładzie, ten obiekt wartości jest implementowany jako readonly struktura. Oznacza to, że EF Core może tworzyć migawki 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. Na 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 ValueComparer<T> jest potrzebny, 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 składa się z kilku typów Money utworzonych wcześniej:

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ą BlogKey właściwość.

Uwaga

Pokazanie tego wzorca nie oznacza, że jest to zalecane. Dokładnie zastanów się, czy ten poziom abstrakcji pomaga czy utrudnia twoje doświadczenie programistyczne. 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 binarnychrowversion/timestamp. 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 trudne do ich obsługi. Konwertery wartości pozwalają mapować rowversion na właściwość ulong, co jest znacznie bardziej odpowiednie i łatwiejsze 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 jako datetime lub datetime2. Oznacza to, że wartości DateTime pochodzące z bazy danych zawsze mają DateTimeKindUnspecified.

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 . Na 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 instancjach jednostek jest ustawiana kombinacja wartości lokalnych i UTC, można użyć konwertera, aby odpowiednio je skonwertować przed wstawieniem. Na przykład:

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

Uwaga

Rozważ dokładnie ujednolicenie kodu dostępu do baz danych w celu używania czasu UTC, przestawiając się na czas lokalny wyłącznie podczas prezentowania danych użytkownikom.

Używaj kluczy tekstowych ignorujących wielkość liter

Niektóre bazy danych, w tym program SQL Server, domyślnie wykonują porównania ciągów bez uwzględniania wielkości liter. Platforma .NET z kolei domyślnie wykonuje porównania ciągów uwzględniające wielkość 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ównywarka wartości dla kluczy może być używana do wymuszenia w EF Core porównań ciągów bez uwzględniania wielkości liter, tak jak w bazie danych. Weźmy pod uwagę na przykład model bloga/postów z kluczami typu string.

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ścią 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 wrażliwość na wielkość 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 być użyty do usuwania 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. Na 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 odszyfrowywania ich na wyjściu. Na przykład użycie odwrócenia ciągu znaków 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 zgłoszenie 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 konwertowanych na wartości, takich jak członkowie referencyjni na typach .NET konwertowanych na wartości w zapytaniach LINQ. Zagłosuj (👍) na problem na GitHubie #10434, jeśli jest to coś, czego potrzebujesz, ale rozważ użycie kolumny JSON zamiast tego.
  • Obecnie nie ma możliwości przeniesienia przekształcenia jednej właściwości na wiele kolumn ani odwrotnie. Proszę zagłosować (👍) na zagadnienie 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 👍 zgłoszenie GitHub #11597, jeśli tego potrzebujesz.
  • Konwersje wartości nie mogą odwoływać się do bieżącego wystąpienia DbContext. Zagłosuj (👍) na kwestię 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.