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:
- Konvertieren von Bool-Eigenschaften:
- BoolToStringConverter - Bool in Zeichenfolgen wie "N" und "J"
- BoolToTwoValuesConverter<TProvider> - Bool in beliebige zwei Werte
- BoolToZeroOneConverter<TProvider> - Bool in null und eins
- Konvertieren von Bytearrayeigenschaften:
- BytesToStringConverter - Bytearray mit Base64-codierter Zeichenfolge
- Jede Konvertierung, die nur eine Typumwandlung erfordert
- CastingConverter<TModel,TProvider> - Konvertierungen, die nur eine Typumwandlung erfordern
- Konvertieren von Zeicheneigenschaften:
- CharToStringConverter - Zeichen an einstellige Zeichenfolge
- Konvertieren von DateTimeOffset-Eigenschaften:
- DateTimeOffsetToBinaryConverter - DateTimeOffset in binärcodierten 64-Bit-Wert
- DateTimeOffsetToBytesConverter - DateTimeOffset in Bytearray
- DateTimeOffsetToStringConverter - DateTimeOffset in Zeichenfolge
- Konvertieren von DateTime-Eigenschaften:
- DateTimeToBinaryConverter - DateTime bis 64-Bit-Wert einschließlich DateTimeArt
- DateTimeToStringConverter - DateTime in Zeichenfolge
- DateTimeToTicksConverter - DateTime in Teilstriche
- Konvertieren von Enumerationseigenschaften:
- EnumToNumberConverter<TEnum,TNumber> - Enumeration zur zugrunde liegenden Zahl
- EnumToStringConverter<TEnum> - Enumeration in Zeichenfolge
- Konvertieren von Guid Eigenschaften:
- GuidToBytesConverter - Guid in Bytearray
- GuidToStringConverter - Guid in Zeichenfolge
- Konvertieren von IPAddress-Eigenschaften:
- IPAddressToBytesConverter - IPAddress in Bytearray
- IPAddressToStringConverter - IPAddress in Zeichenfolge
- Konvertieren numerischer Eigenschaften (Int, Double, Dezimalzahl usw.):
- NumberToBytesConverter<TNumber> - Ein beliebiger numerischer Wert in Bytearray
- NumberToStringConverter<TNumber> - Ein beliebiger numerischer Wert in Zeichenfolge
- Konvertieren von PhysicalAddress-Eigenschaften:
- PhysicalAddressToBytesConverter - PhysicalAddress in Bytearray
- PhysicalAddressToStringConverter - PhysicalAddress in Zeichenfolge
- Konvertieren von Zeichenfolgeneigenschaften:
- StringToBoolConverter - Zeichenfolgen wie "N" und "J" in Bool
- StringToBytesConverter - Zeichenfolge in UTF8 Bytes
- StringToCharConverter - Zeichenfolge in Zeichen
- StringToDateTimeConverter - Zeichenfolge in DateTime
- StringToDateTimeOffsetConverter - Zeichenfolge in DateTimeOffset
- StringToEnumConverter<TEnum> - Zeichenfolge in Enumeration
- StringToGuidConverter - Zeichenfolge in Guid
- StringToNumberConverter<TNumber> - Zeichenfolge in numerischen Typ
- StringToTimeSpanConverter - Zeichenfolge in TimeSpan
- StringToUriConverter - Zeichenfolge in Uri
- Konvertieren von TimeSpan-Eigenschaften:
- TimeSpanToStringConverter - TimeSpan in Zeichenfolge
- TimeSpanToTicksConverter - TimeSpan in Teilstriche
- Konvertieren von Uri-Eigenschaften:
- UriToStringConverter - Uri in Zeichenfolge
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 BlogKey
zugewiesen 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.