Wie speichere ich JSON in einem Entitätsfeld mit EF Core?

74

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?

Alex
quelle
Ich denke , Sie sollten die Art der Änderung ExtendedDatazu stringund speichern Sie dann die Zeichenfolge JSON
Michael
1
@ Michael Ich habe darüber nachgedacht, aber ich möchte sicherstellen, dass es immer gültiges JSON ist.
Alex
@Alex - Wenn dies das einzige Problem ist, das überprüft werden muss, ob es sich um gültiges JSON handelt, können Sie der Einfachheit halber der Set-Methode Ihrer Eigenschaft eine Analyse hinzufügen (dh versuchen, sie zu deserialisieren) - und gegebenenfalls eine InvalidDataException oder eine JsonSerializationException auslösen ungültig.
Matt

Antworten:

149

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 IEntityTypeConfigurationfür Person:

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.

Darren Wainwright
quelle
46
Seien Sie bei diesem Ansatz vorsichtig: EF Core markiert eine Entität nur dann als geändert, wenn das Feld zugewiesen ist. Wenn Sie also verwenden person.Addresses.Add, wird die Entität nicht als aktualisiert gekennzeichnet. Sie müssen den Property Setter aufrufen person.Addresses = updatedAddresses.
Métoule
1
@DarrenWainwright Ich bin damit einverstanden, dass das Modell nichts darüber wissen sollte, wie es gespeichert ist, aber leider verwenden wir [NotMapped] nicht, wenn wir die Route IEntityTypeConfiguration noch verwenden, da kein Äquivalent vorhanden ist. Es sei denn, ich sehe es mit dem EntityTypeBuilder nirgendwo.
Kilhoffer
6
Diese Methode hat bei mir funktioniert, aber Sie müssen diese Konfiguration auch anwenden! modelBuilder.ApplyConfiguration (neue PersonsConfiguration ());
CodeThief
1
@ Métoule - Meine Lösung Behebt dieses Problem. Sie benötigen einen ValueComparer. Siehe meine Antwort unten.
Robert Raboud
3
Wenn Sie dies auf ef core 3.1 versuchen, wird der folgende Fehler angezeigt. Irgendeine Idee, wie man das behebt? "Für den Entitätstyp 'Adresse' muss ein Primärschlüssel definiert werden. Wenn Sie einen schlüssellosen Entitätstyp verwenden möchten, rufen Sie 'HasNoKey ()' auf."
Nairooz NIlafdeen
34

@ 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 _extendedDataals 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.

Alex
quelle
das könnte auch eine Option sein. Beide Ansätze sind in der EF-Kerndokumentation enthalten. Das Hintergrundfeld sieht sauberer aus, kann aber später nicht so einfach zu verstehen sein :)
Michael
2
Beide sind der richtige Ansatz. Der einzige Unterschied besteht darin, dass der Hintergrundfeldansatz eine Schatteneigenschaft verwenden würde, da der EF-Kern Schatteneigenschaften unterstützt, und zusätzliche Eigenschaften in Ihrem Domänenmodell vermeiden würde. :)
Smit
Wie benutzt man das mit jquery?
Kavin404
3
@ Kavin404 - Sie verwenden dies nicht mit JQuery. EF Core ist eine .NET-Datenzugriffstechnologie und JQuery ist ein Javascript-Framework für das Front-End (Browser).
Alex
25

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.

Robert Raboud
quelle
Schöne Lösung! Versuchte es und es funktioniert. Ein Ausrutscher im Code; Der Konverter hat eine Typbeschränkung für, classsodass Sie sie nicht verwenden können IList<Address>. Es muss ein konkreter Typ sein IList<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.
Marnix van Valen
2
Netter Fang, ich habe den Code korrigiert. Dies ist tatsächlich Teil eines neuen Artikels, den ich schreibe, und eines neuen Nuget-Pakets, um dies zu handhaben. Ich arbeite an einer generischen Lösung zum Hinzufügen von Metadaten, ohne dass die Entität kontinuierlich geändert werden muss. Ich verwende ein Wörterbuch <Zeichenfolge, Objekt>, um die Metadaten über Name-Wert-Paare zu speichern. Und ja, diese Lösung eignet sich nicht zum einfachen Abfragen der Metadaten.
Robert Raboud
@RobertRaboud: Gibt es Neuigkeiten zu diesem Artikel oder dem Nuget-Paket? Würde mich sehr freuen
Max R.
@ RobertRaboud die Lösung ist super! Artikel wäre wunderbar als Ressource für alle - aber ich bin nicht verkauft und denke nicht, dass der allgemeine JSON-Blob ein guter Weg ist. Sie möchten die Felder immer noch klein halten und denken Sie daran, dass EF Core noch keine JSON-Zuordnungen hat, SQL Server selbst jedoch (und Sie könnten Ihre eigenen schreiben). Daher ist Zukunftssicherheit besser, wenn Sie keine leichteren JSON-Felder haben ein großes <string, object> Wörterbuch. Natürlich gibt es eine Ausnahme, aber wahrscheinlich mehr für die 5-10% der Szenarien als für die + 90%
Marchy
4
Für SQL Server, musste ich ändern jsonbzu nvarchar(max). Wie von Microsoft
Learner
6

Fü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();
    }
}
sjclark76
quelle
5

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),
Michael
quelle
Ich denke, dies ist die richtige Richtung - anscheinend kann ich "Backing Fields" verwenden, um dies zu erreichen ( docs.microsoft.com/en-us/ef/core/modeling/backing-field ).
Alex
Müssen wir extraData als Hintergrundfeld mit Anmerkungen versehen? (gemäß docs.microsoft.com/en-us/ef/core/modeling/… ) Das [BackingField(...)]Attribut ist jedoch nicht Teil der hier beschriebenen üblichen Attribute: docs.microsoft.com/en-us/ef/core/ Modellierung /…
Matt
Diese Lösung wurde für ältere EF Core entwickelt. Jetzt können Sie extraData als Hintergrundfeld mit Anmerkungen versehen (die Logik muss jedoch geringfügig geändert werden) oder sogar Schatteneigenschaften verwenden ( docs.microsoft.com/en-us/ef/core/modeling/shadow-properties ). Oder Sie können verwenden, wie es ist, wenn es noch funktioniert.
Michael
2

// 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

Zekaee Esmaeel
quelle
1

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));
Micheal Berhanu
quelle
0

Der Kommentar von @ Métoule :

Seien Sie bei diesem Ansatz vorsichtig: EF Core markiert eine Entität nur dann als geändert, wenn das Feld zugewiesen ist . Wenn Sie also person.Addresses.Add verwenden, wird die Entität nicht als aktualisiert gekennzeichnet. Sie müssen den Property Setter person.Addresses = updatedAddresses aufrufen.

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:

campaign.GetExtendedData().Add(something);

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.

Gabriel Luci
quelle
0

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.

Jack
quelle