Wertkonvertierungen

Wertkonverter ermöglichen es, Eigenschaftswerte beim Lesen oder Schreiben in die Datenbank zu konvertieren. Diese Konvertierung kann von einem Wert in einen anderen des gleichen Typs (z. B. Verschlüsseln von Zeichenfolgen) oder von einen Wert eines Typs in den Wert eines anderen Typs erfolgen (z. B. Konvertieren von Enumerationswerten in und aus Zeichenfolgen in der Datenbank).

Tipp

Sie können den gesamten Code in dieser Dokumentation ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Übersicht

Wertkonverter werden in Bezug auf ModelClrType und ProviderClrType angegeben. Der Modelltyp ist der .NET-Typ der Eigenschaft im Entitätstyp. Der Anbietertyp ist der .NET-Typ, der vom Datenbankanbieter verstanden wird. Um z. B. Enumerationen als Zeichenfolgen in der Datenbank zu speichern, ist der Modelltyp Enumeration und der Anbietertyp String. Diese beiden Typen können identisch sein.

Konvertierungen werden mithilfe von zwei Func Ausdrucksstrukturen definiert: Eine von ModelClrType zu ProviderClrType und die andere von ProviderClrType zu ModelClrType. Ausdrucksstrukturen werden verwendet, damit sie für effiziente Konvertierungen in den Datenbankzugriffsdelegat kompiliert werden können. Die Ausdrucksstruktur kann einen einfachen Aufruf einer Konvertierungsmethode für komplexe Konvertierungen enthalten.

Hinweis

Eine Eigenschaft, die für die Wertkonvertierung konfiguriert wurde, muss möglicherweise auch eine ValueComparer<T> angeben. Weitere Informationen finden Sie in den folgenden Beispielen und in der Dokumentation zu Wertabgleichen.

Konfigurieren eines Wertkonverters

Wertkonvertierungen werden in DbContext.OnModelCreating konfiguriert. Nehmen Sie z. B. einen Enumerations- und Entitätstyp, der wie folgt definiert ist:

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

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

Konvertierungen können so in OnModelCreating konfiguriert werden, dass die Enumerationswerte als Zeichenfolgen wie "Esel", "Maultier" usw. in der Datenbank gespeichert werden. Sie müssen lediglich eine Funktion bereitstellen, die von ModelClrType zu ProviderClrType konvertiert und eine andere für die entgegengesetzte Konvertierung:

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

Hinweis

Ein null-Wert wird nie an einen Wertkonverter übergeben. Eine NULL in einer Datenbankspalte ist immer eine NULL in der Entitätsinstanz und umgekehrt. Dies erleichtert die Implementierung von Konvertierungen und ermöglicht die gemeinsame Nutzung von nullfähigen und nicht nullfähigen Eigenschaften. Weitere Informationen finden Sie unter GitHub-Problem #13850.

Massenkonfiguration eines Wertkonverters

Es ist üblich, dass derselbe Wertkonverter für jede Eigenschaft konfiguriert wird, die den relevanten CLR-Typ verwendet. Anstatt es manuell für jede Eigenschaft durchzuführen, können Sie es einmal für das gesamte Modell mithilfe der Vorkonventionsmodellkonfiguration ausführen. Definieren Sie dazu den Wertkonverter als Klasse:

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

Überschreiben Sie ConfigureConventions dann den Kontexttyp, und konfigurieren Sie den Konverter wie folgt:

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

Benutzerdefinierte Konvertierungen

EF Core enthält viele vordefinierte Konvertierungen, die das manuelle Schreiben von Konvertierungsfunktionen verringern. Stattdessen wählt EF Core die Konvertierung aus, die basierend auf dem Eigenschaftentyp im Modell und dem angeforderten Datenbankanbietertyp verwendet werden soll.

Zum Beispiel werden oberhalb Enumerationen zu Zeichenfolgen konvertiert, aber EF Core führt diese tatsächlich automatisch aus, wenn der Anbietertyp so konfiguriert ist, dass als string der generische Typ von HasConversion verwendet wird:

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

Das gleiche kann durch explizites Angeben des Datenbankspaltentyps erreicht werden. Wenn zum Beispiel der Entitätstyp wie folgt definiert ist:

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

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

Anschließend werden die Enumerationswerte ohne weitere Konfiguration in OnModelCreating als Zeichenfolgen in der Datenbank gespeichert.

Die Wertkonverter-Klasse

Wenn Sie wie oben gezeigt HasConversion aufrufen, wird eine Instanz ValueConverter<TModel,TProvider> erstellt und für die Eigenschaft festgelegt. Die ValueConverter kann stattdessen explizit erstellt werden. Beispiel:

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

Dies kann nützlich sein, wenn mehrere Eigenschaften dieselbe Konvertierung verwenden.

Integrierte Konverter

Wie bereits erwähnt, wird EF Core mit einer Reihe vordefinierter ValueConverter<TModel,TProvider>-Klassen ausgeliefert, die im Namespace Microsoft.EntityFrameworkCore.Storage.ValueConversion enthalten sind. In vielen Fällen wählt EF den entsprechenden integrierten Konverter basierend auf dem Typ der Eigenschaft im Modell und dem Typ, der in der Datenbank angefordert wurde, wie oben für Enumerationen dargestellt. Die Verwendung von .HasConversion<int>() in einer bool-Eigenschaft bewirkt beispielsweise, dass EF Core Boolwerte in numerische Nullen und Einsen konvertiert:

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

Dies ist funktional das gleiche wie das Erstellen einer Instanz der integrierten und expliziten Einstellung von BoolToZeroOneConverter<TProvider>:

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

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

In der folgenden Tabelle werden häufig verwendete vordefinierte Konvertierungen von Modell-/Eigenschaftstypen in Datenbankanbietertypen zusammengefasst. In der Tabelle any_numeric_type bedeutet eine von int, short, long, byte, uint, ushort, ulong, sbyte, char, decimal, float oder double.

Modell-/Eigenschaftstyp Anbieter-/Datenbanktyp Konvertierung Verwendung
bool numerischer_Typ falsch/wahr zu 0/1 .HasConversion<any_numeric_type>()
numerischer_Typ Falsch/wahr für jede zwei Zahlen Verwenden Sie BoolToTwoValuesConverter<TProvider>
Zeichenfolge Falsch/wahr zu "N"/"J" .HasConversion<string>()
Zeichenfolge Falsch/wahr für jede zwei Zeichenfolgen Verwenden Sie BoolToStringConverter
numerischer_Typ bool 0/1 zu falsch, wahr .HasConversion<bool>()
numerischer_Typ Einfache Umwandlung .HasConversion<any_numeric_type>()
Zeichenfolge Die Zahl als Zeichenfolge .HasConversion<string>()
Enum numerischer_Typ Der numerische Wert der Enumeration .HasConversion<any_numeric_type>()
Zeichenfolge Die Zeichenfolgendarstellung des Enumerationswerts .HasConversion<string>()
Zeichenfolge bool Analysiert die Zeichenfolge als Bool .HasConversion<bool>()
numerischer_Typ Analysiert die Zeichenfolge als den angegebenen numerischen Typ .HasConversion<any_numeric_type>()
char Das erste Zeichen der Zeichenfolge .HasConversion<char>()
Datetime Analysiert die Zeichenfolge als DateTime .HasConversion<DateTime>()
DateTimeOffset Analysiert die Zeichenfolge als DateTimeOffset .HasConversion<DateTimeOffset>()
TimeSpan Analysiert die Zeichenfolge als TimeSpan .HasConversion<TimeSpan>()
GUID Analysiert die Zeichenfolge als GUID .HasConversion<Guid>()
byte[] Die Zeichenfolge als UTF8 Bytes .HasConversion<byte[]>()
char Zeichenfolge Eine einzelne Zeichenfolge .HasConversion<string>()
Datetime long Codiertes Datum/Uhrzeit, das DateTime.Art beibehält .HasConversion<long>()
long Ticks Verwenden Sie DateTimeToTicksConverter
Zeichenfolge Invariante Kulturzeichenfolge für Datums-/Uhrzeit .HasConversion<string>()
DateTimeOffset long Codiertes Datum/Uhrzeit mit Offset .HasConversion<long>()
Zeichenfolge Invariante Kulturzeichenfolge für Datums-/Uhrzeit mit Offset .HasConversion<string>()
TimeSpan long Ticks .HasConversion<long>()
Zeichenfolge Invariante Kulturzeichenfolge für Zeitspanne .HasConversion<string>()
Uri Zeichenfolge Der URI als Zeichenfolge .HasConversion<string>()
PhysischeAdresse Zeichenfolge Die Adresse als Zeichenfolge .HasConversion<string>()
byte[] Bytes in Big-End-Netzwerkreihenfolge .HasConversion<byte[]>()
IP-Adresse Zeichenfolge Die Adresse als Zeichenfolge .HasConversion<string>()
byte[] Bytes in Big-End-Netzwerkreihenfolge .HasConversion<byte[]>()
GUID Zeichenfolge Die GUID im ‚dddddddd-dddd-dddd-dddd-dddddddddddd‘-Format .HasConversion<string>()
byte[] Byte in binärer Serialisierungsreihenfolge von .NET .HasConversion<byte[]>()

Beachten Sie, dass bei diesen Konvertierungen davon ausgegangen wird, dass das Format des Werts für die Konvertierung geeignet ist. Das Konvertieren von Zeichenfolgen in Zahlen schlägt z. B. fehl, wenn die Zeichenfolgenwerte nicht als Zahlen analysiert werden können.

Die vollständige Liste der integrierten Konverter lautet:

Beachten Sie, dass alle integrierten Konverter zustandslos sind und eine einzelne Instanz sicher von mehreren Eigenschaften gemeinsam genutzt werden kann.

Hinweise zu Spalten-Facets und -Zuordnungen

Einige Datenbanktypen weisen Facets auf, welche die Speicherung der Daten ändern. Dazu gehören:

  • Genauigkeit und Skalierung für Dezimal- und Datums-/Uhrzeitspalten
  • Größe/Länge für Binär- und Zeichenfolgenspalten
  • Unicode für Zeichenfolgenspalten

Diese Facetten können auf normale Weise für eine Eigenschaft, die einen Wertkonverter verwendet, konfiguriert werden und gelten für den konvertierten Datenbanktyp. Wenn z. B. eine Enumeration in eine Zeichenfolge konvertiert werden soll, können wir festlegen, dass die Datenbankspalte nicht Unicode sein und bis zu 20 Zeichen speichern soll:

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

Oder beim expliziten Erstellen des Konverters:

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

Wenn EF Core-Migrationen für SQL Server verwendet werden, führt es zu einer Spalte varchar(20):

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

Wenn jedoch standardmäßig alle EquineBeast-Spalten varchar(20) sein sollen, können diese Informationen dem Wertkonverter als ConverterMappingHints zugewiesen werden. Beispiel:

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

Wenn dieser Konverter jetzt verwendet wird, ist die Datenbankspalte nicht Unicode hat eine maximale Länge von 20. Dies sind jedoch nur Hinweise, da sie von allen der Eigenschaft explizit zugeordneten Facetten überschrieben werden.

Beispiele

Einfache Wertobjekte

In diesem Beispiel wird ein einfacher Typ verwendet, um einen Grundtyp umzuschließen. Dies kann nützlich sein, wenn der Typ in Ihrem Modell spezifischer (und daher typsicherer) als ein Grundtyp sine soll. In diesem Beispiel ist der Typ, der den Dezimalgrundtyp umschließt, Typ Dollars:

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

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

Dies kann in einem Entitätstyp verwendet werden:

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

    public Dollars Price { get; set; }
}

Und in den zugrunde liegenden decimal konvertiert werden, wenn er in der Datenbank gespeichert wird:

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

Hinweis

Dieses Wertobjekt wird als Readonly-Struktur implementiert. Das bedeutet, dass EF Core ohne Probleme eine Momentaufnahme von den Werten machen und sie vergleichen kann. Weitere Informationen finden Sie unter Wertabgleiche.

Zusammengesetzte Wertobjekte

Im vorherigen Beispiel enthielt der Wertobjekttyp nur eine einzelne Eigenschaft. Es ist häufiger, dass ein Wertobjekttyp mehrere Eigenschaften umfasst, die zusammen ein Domänenkonzept bilden. Ein allgemeiner Typ Money, der sowohl den Betrag als auch die Währung enthält:

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
}

Dieses Wertobjekt kann wie zuvor in einem Entitätstyp verwendet werden:

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

    public Money Price { get; set; }
}

Wertkonverter können derzeit nur Werte in und aus einer einzelnen Datenbankspalte konvertieren. Diese Einschränkung bedeutet, dass alle Eigenschaftswerte aus dem Objekt in einem einzelnen Spaltenwert codiert werden müssen. Dies wird in der Regel umgesetzt, indem das Objekt serialisiert wird, wenn es in die Datenbank hineinwechselt, und dann wird es auf dem Weg hinaus wieder deserialisiert. Beispielsweise gilt bei Verwendung von System.Text.Json:

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

Hinweis

Wir planen das Zuordnen eines Objekts zu mehreren Spalten in einer zukünftigen Version von EF Core verfügbar zu machen, was die Serialisierung unnötig machen würde. Der Fortschritt hierzu ist in GitHub-Problem Nr. 13947 einsehbar.

Hinweis

Wie im vorherigen Beispiel wird dieses Wertobjekt als Readonly-Struktur implementiert. Das bedeutet, dass EF Core ohne Probleme eine Momentaufnahme von den Werten machen und sie vergleichen kann. Weitere Informationen finden Sie unter Wertabgleiche.

Sammlungen von Grundtypen

Serialisierung kann auch verwendet werden, um eine Sammlung von Grundtypwerten zu speichern. Beispiel:

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

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

Erneut System.Text.Json verwenden:

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> stellt einen veränderbaren Verweistyp dar. Dies bedeutet, dass eine ValueComparer<T> erforderlich ist, damit EF Core Änderungen richtig nachverfolgen und erkennen kann. Weitere Informationen finden Sie unter Wertabgleiche.

Sammlungen von Wertobjekten

Durch die Kombination der beiden vorherigen Beispiele können wir eine Sammlung von Wertobjekten erstellen. Ziehen Sie beispielsweise einen AnnualFinance-Typ in Betracht, der Blogfinanzierung für ein einzelnes Jahr modelliert:

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

Dieser Typ verfasst mehrere der zuvor erstellten Money-Typen:

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
}

Anschließend können wir eine Sammlung unseres Entitätstyps AnnualFinance hinzufügen:

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

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

Und erneut die Serialisierung verwenden, um Folgendes zu speichern:

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

Hinweis

Wie zuvor erfordert diese Konvertierung eine ValueComparer<T>. Weitere Informationen finden Sie unter Wertabgleiche.

Wertobjekte als Schlüssel

Manchmal können primitive Schlüsseleigenschaften in Wertobjekte eingeschlossen werden, um eine zusätzliche Ebene der Typsicherheit beim Zuweisen von Werten hinzuzufügen. Beispielsweise könnten wir einen Schlüsseltyp für Blogs und einen Schlüsseltyp für Beiträge implementieren:

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

Diese können dann im Domänenmodell verwendet werden:

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

Beachten Sie, dass Blog.Id nicht versehentlich einer PostKey und Post.Id nicht versehentlich einer BlogKey zugewiesen werden kann. Ebenso muss der Fremdschlüsseleigenschaft Post.BlogId eine BlogKeyzugewiesen werden.

Hinweis

Dass wir dieses Musters zeigen, bedeutet nicht, dass wir es empfehlen. Überlegen Sie sorgfältig, ob diese Abstraktionsstufe Ihre Entwicklungserfahrung fördert oder beeinträchtigt. Erwägen Sie auch die Verwendung von Navigationen und generierten Schlüsseln, anstatt direkt mit Schlüsselwerten zu arbeiten.

Diese Schlüsseleigenschaften können dann mithilfe von Wertkonvertern zugeordnet werden:

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

Hinweis

Schlüsseleigenschaften mit Konvertierungen können ab EF Core 7.0 nur generierte Schlüsselwerte verwenden.

Verwenden von ulong für Zeitstempel/Zeilenversion

SQL Server unterstützen die automatische optimistische Parallelität mithilfe von 8-Byte-Binärspaltenrowversion/timestamp. Diese werden immer mit einem 8-Byte-Array aus der Datenbank gelesen und in die Datenbank geschrieben. Bytearrays sind jedoch ein änderbarer Verweistyptyp, wodurch sie etwas schwierig in der Handhabung sind. Wertkonverter ermöglichen stattdessen die Zuordnung von rowversion zu einer Eigenschaft ulong, die wesentlich geeigneter und einfacher zu verwenden ist als das Bytearray. Betrachten Sie beispielsweise eine Entität Blog mit einem ulong-Parallelitätstoken:

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

Sie kann einer SQL Server-Spalte rowversion mithilfe eines Wertkonverters zugeordnet werden:

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

Angeben der DateTime.Art beim Lesen von Datumsangaben

Der SQL Server verwirft die DateTime.Kind-Flag beim Speichern eines DateTime als datetime oder datetime2. Dies bedeutet, dass DateTime-Werte, die aus der Datenbank stammen, immer eine DateTimeKind von Unspecified haben.

Wertkonverter können auf zwei Arten verwendet werden, um dies zu bewältigen. Zuerst verfügt EF Core über einen Wertkonverter, der einen undurchsichtigen 8-Byte-Wert erstellt, der die Kind-Flag behält. Beispiel:

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

Dadurch können DateTime-Werte mit unterschiedlichen Kind-Flags in der Datenbank gemischt werden.

Das Problem mit diesem Ansatz besteht darin, dass die Datenbank keine erkennbaren datetime- oder datetime2-Spalten mehr hat. Stattdessen ist es üblich, UTC-Zeit (oder seltener immer die lokale Zeit) zu speichern und dann entweder die Kind-Flag zu ignorieren oder sie mit einem Wertkonverter auf den entsprechenden Wert festzulegen. Der folgende Konverter stellt z. B. sicher, dass der DateTime-Wert, der aus der Datenbank gelesen wird, DateTimeKind UTC hat:

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

Wenn eine Mischung aus lokalen und UTC-Werten in Entitätsinstanzen festgelegt wird, kann der Konverter verwendet werden, um vor dem Einfügen entsprechend zu konvertieren. Beispiel:

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

Hinweis

Erwägen Sie sorgfältig, den gesamten Datenbankzugriffscode zu vereinheitlichen und immer UTC-Zeit zu verwenden und nur lokale Zeit zu verwenden, wenn Daten Benutzern präsentiert werden.

Verwendung von Zeichenfolgenschlüsseln mit Groß- und Kleinschreibung

Einige Datenbanken, einschließlich SQL Server, führen standardmäßig Zeichenfolgenvergleiche durch, bei denen die Groß- und Kleinschreibung nicht beachtet wird. .NET führt dagegen standardmäßig Zeichenfolgenvergleiche mit Groß- und Kleinschreibung durch. Das bedeutet, dass ein Fremdschlüsselwert wie "DotNet" mit dem Primärschlüsselwert "dotnet" auf SQL Server, aber nicht in EF Core übereinstimmt. EF Core kann mit Hilfe eines Wertabgleichs für Schlüssel gezwungen werden, die Groß-und Kleinschreibung in Vergleichen zu ignorieren, wie in der Datenbank. Nehmen Sie z. B. ein Blog-/Beitragsmodell mit Zeichenfolgenschlüsseln:

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

Es funktioniert nicht wie erwartet, wenn einige der Post.BlogId-Werte unterschiedliche Groß- und Kleinschreibung aufweisen. Die Fehler, die hiervon verursacht werden, hängen davon ab, was die Anwendung tut, aber in der Regel treten sie bei Graphen von Objekten auf, die nicht ordnungsgemäß korrigiert wurden und/oder Aktualisierungen, die fehlschlagen, weil der FK-Wert falsch ist. Ein Wertvergleicher kann verwendet werden, um folgendes zu korrigieren:

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

Hinweis

.NET-Zeichenfolgenvergleiche und Datenbankzeichenfolgenvergleiche können sich in mehr als nur der Groß- und Kleinschreibung unterscheiden. Dieses Muster funktioniert für einfache ASCII-Schlüssel, kann jedoch für Schlüssel kulturspezifischen Zeichen fehlschlagen. Weitere Informationen finden Sie unter Sortierungen und Groß- und Kleinschreibung.

Behandlung von Datenbankzeichenfolgen mit fester Länge

Im vorherigen Beispiel wurde kein Wertkonverter benötigt. Ein Konverter kann jedoch für Zeichenfolgentypen mit fester Länge wie char(20) oder nchar(20) nützlich sein. Zeichenfolgen mit fester Länge werden bei jedem Einfügen eines Werts in die Datenbank auf die gesamte Länge aufgefüllt. Das bedeutet, dass ein Schlüsselwert von "dotnet" aus der Datenbank als "dotnet.............." gelesen wird, wobei . für Leerzeichen steht. Das wird dann nicht ordnungsgemäß mit Schlüsselwerten verglichen, die nicht aufgefüllt werden.

Ein Wertkonverter kann zum Kürzen des Abstands beim Lesen von Schlüsselwerten verwendet werden. Dies kann mit dem Wertvergleicher im vorherigen Beispiel kombiniert werden, um die Groß- und Kleinschreibung von ASCII-Schlüsseln mit fester Länge korrekt zu vergleichen. Beispiel:

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

Verschlüsseln von Eigenschaftswerten

Wertkonverter können verwendet werden, um Eigenschaftswerte zu verschlüsseln, bevor sie an die Datenbank gesendet werden, und sie dann zu entschlüsseln wenn sie wieder nach außen gesendet werden. Verwenden Sie zum Beispiel der Zeichenfolgenumkehr als Ersatz für einen echten Verschlüsselungsalgorithmus:

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

Hinweis

Es gibt derzeit keine Möglichkeit, einen Verweis auf den aktuellen DbContext oder einen anderen Sitzungszustand aus einem Wertkonverter abzurufen. Dadurch werden die Verschlüsselungsarten, die verwendet werden können, beschränkt. Stimmen Sie für das GitHub-Problem #11597, damit diese Einschränkung entfernt wird.

Warnung

Achten Sie darauf, alle Auswirkungen zu verstehen, wenn Sie ihre eigene Verschlüsselung zum Schutz vertraulicher Daten bereitstellen. Erwägen Sie stattdessen die Verwendung vordefinierter Verschlüsselungsmechanismen auf SQL Servern, wie z. B. Always Encrypted.

Begrenzungen

Es gibt einige bekannte aktuelle Einschränkungen des Wertkonvertierungssystems:

  • Wie oben erwähnt, kann null nicht konvertiert werden. Bitte stimmen Sie (👍) für GitHub-Problem #13850, wenn dies etwas ist, das Sie benötigen.
  • Es ist nicht möglich, Abfragen in wertkonvertierten Eigenschaften durchzuführen, z. B. Verweiselemente in dem wertkonvertierten .NET-Typ in Ihren LINQ-Abfragen. Bitte stimmen Sie (👍) für GitHub-Problem #10434, wenn das etwas ist, das Sie benötigen - aber erwägen Sie stattdessen auch die Verwendung einer JSON-Spalte.
  • Es gibt derzeit keine Möglichkeit, die Konvertierung einer Eigenschaft in mehrere Spalten zu verteilen oder umgekehrt. Bitte stimmen Sie (👍) für GitHub-Problem #13947, wenn das etwas ist, was Sie benötigen.
  • Die Wertgenerierung wird für die meisten Schlüssel, die über Wertkonverter zugeordnet sind, nicht unterstützt. Bitte stimmen Sie (👍) für GitHub-Problem #11597, wenn das etwas ist, was Sie benötigen.
  • Wertkonvertierungen können nicht auf die aktuelle DbContext-Instanz verweisen. Bitte stimmen Sie (👍) für GitHub-Problem #12205, wenn das etwas ist, was Sie benötigen.
  • Parameter, die wertkonvertierte Typen verwenden, können derzeit nicht in unformatierten SQL-APIs verwendet werden. Bitte stimmen Sie (👍) für GitHub-Problem #27534, wenn das etwas ist, was Sie benötigen.

Das Entfernen dieser Einschränkungen wird für zukünftige Versionen in Betracht gezogen.