Ich erstelle eine wiederverwendbare Bibliothek mit .NET Core (für .NETStandard 1.4) und verwende Entity Framework Core (und für beide neu). Ich habe eine Entitätsklasse, die wie folgt aussieht:
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
und ich habe eine DbContext-Klasse, die das DbSet definiert:
public DbSet<Campaign> Campaigns { get; set; }
(Ich verwende auch das Repository-Muster mit DI, aber ich denke nicht, dass dies relevant ist.)
Meine Unit-Tests geben mir folgenden Fehler:
System.InvalidOperationException: Die durch die Navigationseigenschaft 'JToken.Parent' vom Typ 'JContainer' dargestellte Beziehung kann nicht ermittelt werden. Konfigurieren Sie die Beziehung entweder manuell oder ignorieren Sie diese Eigenschaft im Modell.
Gibt es eine Möglichkeit anzuzeigen, dass dies keine Beziehung ist, sondern als große Zeichenfolge gespeichert werden sollte?
ExtendedData
zustring
und speichern Sie dann die Zeichenfolge JSONAntworten:
Ich werde das anders beantworten.
Idealerweise sollte das Domänenmodell keine Ahnung haben, wie Daten gespeichert werden. Durch das Hinzufügen von Sicherungsfeldern und zusätzlichen
[NotMapped]
Eigenschaften wird Ihr Domänenmodell tatsächlich an Ihre Infrastruktur gekoppelt.Denken Sie daran - Ihre Domain ist König und nicht die Datenbank. Die Datenbank wird nur zum Speichern von Teilen Ihrer Domain verwendet.
Stattdessen können Sie die EF Core-
HasConversion()
Methode auf dem verwendenEntityTypeBuilder
Objekt verwenden, um zwischen Ihrem Typ und JSON zu konvertieren.Angesichts dieser 2 Domänenmodelle:
public class Person { public int Id { get; set; } [Required] [MaxLength(50)] public string FirstName { get; set; } [Required] [MaxLength(50)] public string LastName { get; set; } [Required] public DateTime DateOfBirth { get; set; } public IList<Address> Addresses { get; set; } } public class Address { public string Type { get; set; } public string Company { get; set; } public string Number { get; set; } public string Street { get; set; } public string City { get; set; } }
Ich habe nur Attribute hinzugefügt, an denen die Domain interessiert ist - und keine Details, an denen die Datenbank interessiert wäre. IE gibt es keine
[Key]
.Mein DbContext hat Folgendes
IEntityTypeConfiguration
fürPerson
:public class PersonsConfiguration : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { // This Converter will perform the conversion to and from Json to the desired type builder.Property(e => e.Addresses).HasConversion( v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }), v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore })); } }
Mit dieser Methode können Sie Ihre Domain vollständig von Ihrer Infrastruktur entkoppeln. Keine Notwendigkeit für alle Hintergrundfelder und zusätzlichen Eigenschaften.
quelle
person.Addresses.Add
, wird die Entität nicht als aktualisiert gekennzeichnet. Sie müssen den Property Setter aufrufenperson.Addresses = updatedAddresses
.@ Michaels Antwort hat mich auf den richtigen Weg gebracht, aber ich habe es etwas anders umgesetzt. Am Ende habe ich den Wert als Zeichenfolge in einer privaten Eigenschaft gespeichert und als "Hintergrundfeld" verwendet. Die ExtendedData-Eigenschaft konvertierte dann JObject in eine Zeichenfolge am Set und umgekehrt bei get:
public class Campaign { // https://docs.microsoft.com/en-us/ef/core/modeling/backing-field private string _extendedData; [Key] public Guid Id { get; set; } [Required] [MaxLength(50)] public string Name { get; set; } [NotMapped] public JObject ExtendedData { get { return JsonConvert.DeserializeObject<JObject>(string.IsNullOrEmpty(_extendedData) ? "{}" : _extendedData); } set { _extendedData = value.ToString(); } } }
Um dies
_extendedData
als Hintergrundfeld festzulegen, habe ich dies meinem Kontext hinzugefügt:protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Campaign>() .Property<string>("ExtendedDataStr") .HasField("_extendedData"); }
Update: Darrens Antwort auf die Verwendung von EF Core Value Conversions (neu in EF Core 2.1 - die zum Zeitpunkt dieser Antwort noch nicht vorhanden war) scheint an dieser Stelle der beste Weg zu sein.
quelle
Der Schlüssel zur korrekten Funktion des Change Tracker ist die Implementierung eines ValueComparer sowie eines ValueConverter. Unten finden Sie eine Erweiterung, um Folgendes zu implementieren:
public static class ValueConversionExtensions { public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new() { ValueConverter<T, string> converter = new ValueConverter<T, string> ( v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject<T>(v) ?? new T() ); ValueComparer<T> comparer = new ValueComparer<T> ( (l, r) => JsonConvert.SerializeObject(l) == JsonConvert.SerializeObject(r), v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(), v => JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(v)) ); propertyBuilder.HasConversion(converter); propertyBuilder.Metadata.SetValueConverter(converter); propertyBuilder.Metadata.SetValueComparer(comparer); propertyBuilder.HasColumnType("jsonb"); return propertyBuilder; } }
Beispiel, wie das funktioniert.
public class Person { public int Id { get; set; } [Required] [MaxLength(50)] public string FirstName { get; set; } [Required] [MaxLength(50)] public string LastName { get; set; } [Required] public DateTime DateOfBirth { get; set; } public List<Address> Addresses { get; set; } } public class Address { public string Type { get; set; } public string Company { get; set; } public string Number { get; set; } public string Street { get; set; } public string City { get; set; } } public class PersonsConfiguration : IEntityTypeConfiguration<Person> { public void Configure(EntityTypeBuilder<Person> builder) { // This Converter will perform the conversion to and from Json to the desired type builder.Property(e => e.Addresses).HasJsonConversion<IList<Address>>(); } }
Dadurch funktioniert der ChangeTracker korrekt.
quelle
class
sodass Sie sie nicht verwenden könnenIList<Address>
. Es muss ein konkreter Typ seinIList<Address>
. Eine wichtige Sache, die Sie hier beachten sollten, ist, dass Sie die JSON-Daten nur mit handgeschriebenem SQL abfragen können, was zu ziemlich komplexem SQL mit CTEs und dergleichen führt.jsonb
zunvarchar(max)
. Wie von MicrosoftFür diejenigen, die EF 2.1 verwenden, gibt es ein nettes kleines NuGet-Paket EfCoreJsonValueConverter , das es ziemlich einfach macht.
using Innofactor.EfCoreJsonValueConverter; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; public class Campaign { [Key] public Guid Id { get; set; } [Required] [MaxLength(50)] public string Name { get; set; } public JObject ExtendedData { get; set; } } public class CampaignConfiguration : IEntityTypeConfiguration<Campaign> { public void Configure(EntityTypeBuilder<Campaign> builder) { builder .Property(application => application.ExtendedData) .HasJsonValueConversion(); } }
quelle
Könnten Sie so etwas versuchen?
[NotMapped] private JObject extraData; [NotMapped] public JObject ExtraData { get { return extraData; } set { extraData = value; } } [Column("ExtraData")] public string ExtraDataStr { get { return this.extraData.ToString(); } set { this.extraData = JsonConvert.DeserializeObject<JObject>(value); } }
Hier ist die Migrationsausgabe:
ExtraData = table.Column<string>(nullable: true),
quelle
[BackingField(...)]
Attribut ist jedoch nicht Teil der hier beschriebenen üblichen Attribute: docs.microsoft.com/en-us/ef/core/ Modellierung /…// DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder) { var entityTypes = modelBuilder.Model.GetEntityTypes(); foreach (var entityType in entityTypes) { foreach (var property in entityType.ClrType.GetProperties().Where(x => x != null && x.GetCustomAttribute<HasJsonConversionAttribute>() != null)) { modelBuilder.Entity(entityType.ClrType) .Property(property.PropertyType, property.Name) .HasJsonConversion(); } } base.OnModelCreating(modelBuilder); }
Erstellen Sie ein Attribut, um die Eigenschaften der Entitäten zu behandeln.
public class HasJsonConversionAttribute : System.Attribute { }
Erstellen Sie eine Erweiterungsklasse, um Josn-Eigenschaften zu finden
public static class ValueConversionExtensions { public static PropertyBuilder HasJsonConversion(this PropertyBuilder propertyBuilder) { ParameterExpression parameter1 = Expression.Parameter(propertyBuilder.Metadata.ClrType, "v"); MethodInfo methodInfo1 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("SerializeObject", types: new Type[] { typeof(object) }); MethodCallExpression expression1 = Expression.Call(methodInfo1 ?? throw new Exception("Method not found"), parameter1); ParameterExpression parameter2 = Expression.Parameter(typeof(string), "v"); MethodInfo methodInfo2 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("DeserializeObject", 1, BindingFlags.Static | BindingFlags.Public, Type.DefaultBinder, CallingConventions.Any, types: new Type[] { typeof(string) }, null)?.MakeGenericMethod(propertyBuilder.Metadata.ClrType) ?? throw new Exception("Method not found"); MethodCallExpression expression2 = Expression.Call(methodInfo2, parameter2); var converter = Activator.CreateInstance(typeof(ValueConverter<,>).MakeGenericType(typeof(List<AttributeValue>), typeof(string)), new object[] { Expression.Lambda( expression1,parameter1), Expression.Lambda( expression2,parameter2), (ConverterMappingHints) null }); propertyBuilder.HasConversion(converter as ValueConverter); return propertyBuilder; } }
Entitätsbeispiel
public class Attribute { [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)] public int Id { get; set; } public string Name { get; set; } [HasJsonConversion] public List<AttributeValue> Values { get; set; } } public class AttributeValue { public string Value { get; set; } public IList<AttributeValueTranslation> Translations { get; set; } } public class AttributeValueTranslation { public string Translation { get; set; } public string CultureName { get; set; } }
Quelle herunterladen
quelle
Hier ist etwas, das ich benutzt habe
Modell
public class FacilityModel { public string Name { get; set; } public JObject Values { get; set; } }
Entität
[Table("facility", Schema = "public")] public class Facility { public string Name { get; set; } public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>(); }
Kartierung
this.CreateMap<Facility, FacilityModel>().ReverseMap();
DBContext
base.OnModelCreating(builder); builder.Entity<Facility>() .Property(b => b.Values) .HasColumnType("jsonb") .HasConversion( v => JsonConvert.SerializeObject(v), v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
quelle
Der Kommentar von @ Métoule :
Ich habe einen anderen Ansatz gewählt, damit diese Tatsache offensichtlich ist: Verwenden Sie Getter- und Setter- Methoden anstelle einer Eigenschaft.
public void SetExtendedData(JObject extendedData) { ExtendedData = JsonConvert.SerializeObject(extendedData); _deserializedExtendedData = extendedData; } //just to prevent deserializing more than once unnecessarily private JObject _deserializedExtendedData; public JObject GetExtendedData() { if (_extendedData != null) return _deserializedExtendedData; _deserializedExtendedData = string.IsNullOrEmpty(ExtendedData) ? null : JsonConvert.DeserializeObject<JObject>(ExtendedData); return _deserializedExtendedData; }
Sie könnten dies theoretisch tun:
Aber es ist viel klarer, dass das nicht das tut, was Sie denken, dass es tut ™.
Wenn Sie zuerst die Datenbank verwenden und eine Art Klassenautomatiker für EF verwenden, werden die Klassen normalerweise als deklariert
partial
, sodass Sie dieses Zeug in einer separaten Datei hinzufügen können, die beim nächsten Mal nicht weggeblasen wird Aktualisieren Sie Ihre Klassen aus Ihrer Datenbank.quelle
Für Entwickler, die mit EF Core 3.1 arbeiten und auf einen solchen Fehler stoßen ("Für den Entitätstyp 'XXX' muss ein Primärschlüssel definiert werden. Wenn Sie einen schlüssellosen Entitätstyp verwenden möchten, rufen Sie 'HasNoKey ()' auf."), Ist die Lösung Nur um die .HasConversion () -Methode mit ihrem Lambda von: public class OrderConfiguration: IEntityTypeConfiguration nach: protected override void OnModelCreating (ModelBuilder modelBuilder) // in Ihrer YourModelContext: DbContext-Klasse zu verschieben.
quelle