Entity Framework - Code First - Liste <String> kann nicht gespeichert werden

106

Ich habe eine solche Klasse geschrieben:

class Test
{
    [Key]
    [DatabaseGeneratedAttribute(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    [Required]
    public List<String> Strings { get; set; }

    public Test()
    {
        Strings = new List<string>
        {
            "test",
            "test2",
            "test3",
            "test4"
        };
    }
}

und

internal class DataContext : DbContext
{
    public DbSet<Test> Tests { get; set; }
}

Nach dem Ausführen von Code:

var db = new DataContext();
db.Tests.Add(new Test());
db.SaveChanges();

Meine Daten werden gespeichert, aber nur die Id. Ich habe keine Tabellen oder Beziehungen, die für die Strings- Liste gelten.

Was mache ich falsch? Ich habe auch versucht, Strings zu machen, virtualaber es hat nichts geändert.

Danke für Ihre Hilfe.

Paul
quelle
3
Wie erwarten Sie, dass die Liste <Sting> in der Datenbank gespeichert wird? Das wird nicht funktionieren. Ändern Sie es in Zeichenfolge.
Wiktor Zychla
4
Wenn Sie eine Liste haben, muss diese auf eine Entität verweisen. Damit EF die Liste speichern kann, benötigt es eine zweite Tabelle. In der zweiten Tabelle wird alles aus Ihrer Liste eingefügt und mit einem Fremdschlüssel auf Ihre TestEntität verwiesen. Erstellen Sie also eine neue Entität mit IdEigenschaft und MyStringEigenschaft und erstellen Sie eine Liste davon.
Daniel Gabriel
1
Richtig ... Es kann nicht direkt in der Datenbank gespeichert werden, aber ich hoffte, dass Entity Framework eine neue Entität erstellt, um dies selbst zu tun. Vielen Dank für Ihre Kommentare.
Paul

Antworten:

161

Entity Framework unterstützt keine Sammlungen primitiver Typen. Sie können entweder eine Entität erstellen (die in einer anderen Tabelle gespeichert wird) oder eine Zeichenfolgenverarbeitung durchführen, um Ihre Liste als Zeichenfolge zu speichern und die Liste zu füllen, nachdem die Entität materialisiert wurde.

Pawel
quelle
Was ist, wenn eine Entität eine Liste von Entitäten enthält? Wie wird das Mapping gespeichert?
A_Arnold
Kommt darauf an - höchstwahrscheinlich auf eine separate Tabelle.
Pawel
Sie können versuchen, den json-formatierten Text zu serialisieren und anschließend zu komprimieren und zu speichern oder ihn bei Bedarf zu verschlüsseln und zu speichern. In beiden Fällen kann das Framework die komplexe Typentabellenzuordnung nicht für Sie durchführen.
Niklas
89

EF Core 2.1+:

Eigentum:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Sasan
quelle
5
Tolle Lösung für EF Core. Obwohl es ein Problem mit der Konvertierung von Zeichen in Zeichenfolgen zu geben scheint. Ich musste es wie folgt implementieren: .HasConversion (v => string.Join (";", v), v => v.Split (neues Zeichen [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller
8
Dies ist meiner Meinung nach die einzig wirklich richtige Antwort. Bei allen anderen müssen Sie Ihr Modell ändern. Dies verstößt gegen das Prinzip, dass Domänenmodelle persistenzunabhängig sein sollten. (Es ist in Ordnung, wenn Sie separate Persistenz- und Domänenmodelle verwenden, aber nur wenige Leute tun dies tatsächlich.)
Marcell Toth
2
Sie sollten meine Bearbeitungsanforderung akzeptieren, da Sie char nicht als erstes Argument von string.Join verwenden können und ein char [] als erstes Argument von string.Split angeben müssen, wenn Sie auch StringSplitOptions bereitstellen möchten.
Dominik
2
In .NET Core können Sie. Ich verwende genau diesen Code in einem meiner Projekte.
Sasan
2
Nicht verfügbar in .NET Standard
Sasan
53

Diese Antwort basiert auf denen von @Sasan und @CAD .

Funktioniert nur mit EF Core 2.1+ (nicht .NET Standard-kompatibel) (Newtonsoft JsonConvert)

builder.Entity<YourEntity>().Property(p => p.Strings)
    .HasConversion(
        v => JsonConvert.SerializeObject(v),
        v => JsonConvert.DeserializeObject<List<string>>(v));

Unter Verwendung der fließenden EF Core-Konfiguration serialisieren / deserialisieren wir das Listzu / von JSON.

Warum dieser Code die perfekte Mischung aus allem ist, was Sie anstreben könnten:

  • Das Problem mit Sasns ursprünglicher Antwort ist, dass es zu einem großen Durcheinander wird, wenn die Zeichenfolgen in der Liste Kommas (oder ein beliebiges als Trennzeichen ausgewähltes Zeichen) enthalten, da ein einzelner Eintrag in mehrere Einträge umgewandelt wird, aber am einfachsten zu lesen ist am prägnantesten.
  • Das Problem mit der Antwort von CAD-Typen ist, dass sie hässlich ist und eine Änderung des Modells erfordert, was eine schlechte Konstruktionspraxis darstellt (siehe Marcell Toths Kommentar zu Sasans Antwort ). Aber es ist die einzige Antwort, die datensicher ist.
Mathieu VIALES
quelle
7
Bravo, dies sollte wahrscheinlich die akzeptierte Antwort sein
Shirkan
1
Ich wünschte, dies würde in .NET Framework & EF 6 funktionieren, es ist eine wirklich elegante Lösung.
CAD Kerl
Dies ist eine erstaunliche Lösung. Vielen Dank
Marlon
Können Sie auf diesem Feld nachfragen? Meine Versuche sind kläglich gescheitert: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();findet nichts.
Nicola Iarocci
2
Um meine eigene Frage zu beantworten, zitieren Sie die Dokumente : "Die Verwendung von Wertekonvertierungen kann sich auf die Fähigkeit von EF Core auswirken, Ausdrücke in SQL zu übersetzen. In solchen Fällen wird eine Warnung protokolliert. Die Beseitigung dieser Einschränkungen wird für eine zukünftige Version in Betracht gezogen." - Wäre trotzdem schön.
Nicola Iarocci
44

Ich weiß, dass dies eine alte Frage ist, und Pawel hat die richtige Antwort gegeben . Ich wollte nur ein Codebeispiel für die Verarbeitung von Zeichenfolgen zeigen und eine zusätzliche Klasse für die Liste eines primitiven Typs vermeiden.

public class Test
{
    public Test()
    {
        _strings = new List<string>
        {
            "test",
            "test2",
            "test3",
            "test4"
        };
    }

    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    private List<String> _strings { get; set; }

    public List<string> Strings
    {
        get { return _strings; }
        set { _strings = value; }
    }

    [Required]
    public string StringsAsString
    {
        get { return String.Join(',', _strings); }
        set { _strings = value.Split(',').ToList(); }
    }
}
Zufälligkeiten
quelle
1
Warum nicht statische Methoden anstelle von öffentlichen Eigenschaften? (Oder zeige ich meine prozedurale Programmierverzerrung?)
Duston
@randoms warum ist es notwendig 2 Listen zu definieren? eine als Eigenschaft und eine als aktuelle Liste? Ich würde mich freuen, wenn Sie auch erklären können, wie die Bindung hier funktioniert, da diese Lösung für mich nicht gut funktioniert und ich die Bindung hier nicht herausfinden kann. Vielen Dank
LiranBo
2
Es gibt eine private Liste mit zwei öffentlichen Eigenschaften: Strings, die Sie in Ihrer Anwendung zum Hinzufügen und Entfernen von Strings verwenden, und StringsAsString, den Wert, der in der Datenbank als durch Kommas getrennte Liste gespeichert wird. Ich bin mir nicht sicher, was Sie fragen, die Bindung ist die private Liste _strings, die die beiden öffentlichen Eigenschaften miteinander verbindet.
Zufällige
1
Bitte beachten Sie, dass diese Antwort nicht ,in Zeichenfolgen (Komma) steht. Wenn eine Zeichenfolge in der Liste eine oder mehrere ,Zeichenfolgen (Komma) enthält, wird die Zeichenfolge in mehrere Zeichenfolgen aufgeteilt.
Jogge
2
Im string.JoinKomma sollten doppelte Anführungszeichen (für eine Zeichenfolge) und keine einfachen Anführungszeichen (für ein Zeichen) stehen. Siehe msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris
28

JSON.NET zur Rettung.

Sie serialisieren es in JSON, um in der Datenbank zu bleiben, und deserialisieren es, um die .NET-Sammlung wiederherzustellen. Dies scheint mit Entity Framework 6 und SQLite besser zu funktionieren, als ich erwartet hatte. Ich weiß, dass Sie danach gefragt haben, List<string>aber hier ist ein Beispiel für eine noch komplexere Sammlung, die einwandfrei funktioniert.

Ich habe die persistierte Eigenschaft mit markiert, [Obsolete]sodass es für mich sehr offensichtlich ist, dass "dies nicht die Eigenschaft ist, nach der Sie suchen" im normalen Verlauf der Codierung. Die "echte" Eigenschaft ist mit gekennzeichnet, [NotMapped]sodass das Entity-Framework sie ignoriert.

(nicht verwandte Tangente): Sie könnten dasselbe mit komplexeren Typen tun, aber Sie müssen sich fragen, haben Sie es sich nur zu schwer gemacht, die Eigenschaften dieses Objekts abzufragen? (Ja, in meinem Fall).

using Newtonsoft.Json;
....
[NotMapped]
public Dictionary<string, string> MetaData { get; set; } = new Dictionary<string, string>();

/// <summary> <see cref="MetaData"/> for database persistence. </summary>
[Obsolete("Only for Persistence by EntityFramework")]
public string MetaDataJsonForDb
{
    get
    {
        return MetaData == null || !MetaData.Any()
                   ? null
                   : JsonConvert.SerializeObject(MetaData);
    }

    set
    {
        if (string.IsNullOrWhiteSpace(value))
           MetaData.Clear();
        else
           MetaData = JsonConvert.DeserializeObject<Dictionary<string, string>>(value);
    }
}
CAD-Typ
quelle
Ich finde diese Lösung ziemlich hässlich, aber es ist tatsächlich die einzig vernünftige. Alle Optionen, die anbieten, der Liste mit einem beliebigen Zeichen beizutreten und es dann wieder aufzuteilen, können zu einem wilden Durcheinander führen, wenn das Aufteilungszeichen in den Zeichenfolgen enthalten ist. Json sollte viel vernünftiger sein.
Mathieu VIALES
1
Am Ende habe ich eine Antwort gegeben , die eine "Verschmelzung" dieser und einer anderen ist, um jedes Antwortproblem (Hässlichkeit / Datensicherheit) unter Verwendung der Stärken des anderen zu beheben.
Mathieu VIALES
12

Nur zur Vereinfachung -

Das Entity Framework unterstützt keine Grundelemente. Sie erstellen entweder eine Klasse zum Umschließen oder fügen eine weitere Eigenschaft hinzu, um die Liste als Zeichenfolge zu formatieren:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Adam Tal
quelle
1
Dies ist der Fall, wenn ein Listenelement keine Zeichenfolge enthalten kann. Andernfalls müssen Sie ihm entkommen. Oder um die Liste für komplexere Situationen zu serialisieren / deserialisieren.
Adam Tal
3
Vergessen Sie auch nicht, [NotMapped] für die ICollection-Eigenschaft zu verwenden
Ben Petersen
7

Natürlich hat Pawel die richtige Antwort gegeben . Aber ich habe in diesem Beitrag festgestellt, dass es seit EF 6+ möglich ist, private Immobilien zu speichern. Daher würde ich diesen Code bevorzugen, da Sie die Zeichenfolgen nicht falsch speichern können.

public class Test
{
    [Key]
    [DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }

    [Column]
    [Required]
    private String StringsAsStrings { get; set; }

    public List<String> Strings
    {
        get { return StringsAsStrings.Split(',').ToList(); }
        set
        {
            StringsAsStrings = String.Join(",", value);
        }
    }
    public Test()
    {
        Strings = new List<string>
        {
            "test",
            "test2",
            "test3",
            "test4"
        };
    }
}
Plumpssack
quelle
6
Was ist, wenn die Zeichenfolge ein Komma enthält?
Chalky
4
Ich würde es nicht empfehlen, es so zu machen. StringsAsStringswird nur aktualisiert, wenn die Strings Referenz geändert wird, und das einzige Mal in Ihrem Beispiel, das passiert, ist die Zuweisung. Durch Hinzufügen oder Entfernen von Elementen zu Ihrer StringsListe nach der Zuweisung wird die Sicherungsvariable nicht aktualisiert StringsAsStrings. Der richtige Weg, dies zu implementieren, besteht darin, es StringsAsStringsals Ansicht der StringsListe anzuzeigen und nicht umgekehrt. Verbinden Sie die Werte im getAccessor der StringsAsStringsEigenschaft und teilen Sie sie im setAccessor auf.
jduncanator
Um das Hinzufügen privater Eigenschaften (die nicht nebenwirkungsfrei sind) zu vermeiden, machen Sie den Setter der serialisierten Eigenschaft privat. jduncanator ist natürlich richtig: Wenn Sie die Listenmanipulationen nicht erfassen (eine ObservableCollection verwenden?), werden die Änderungen von EF nicht bemerkt.
Leonidas
Wie @jduncanator erwähnte, funktioniert diese Lösung nicht, wenn eine Änderung an der Liste vorgenommen wird (Bindung in MVVM zum Beispiel)
Ihab Hajj
6

Ein wenig zwicken @Mathieu Viales ‚s Antwort , hier ist ein .NET - Standard kompatibel Schnipsel des neuen System.Text.Json Serializer mit damit die Abhängigkeit von Newtonsoft.Json beseitigen.

using System.Text.Json;

builder.Entity<YourEntity>().Property(p => p.Strings)
    .HasConversion(
        v => JsonSerializer.Serialize(v, default),
        v => JsonSerializer.Deserialize<List<string>>(v, default));

Beachten Sie, dass das zweite Argument in beiden Serialize()und Deserialize()normalerweise optional ist, Sie jedoch eine Fehlermeldung erhalten:

Ein Ausdrucksbaum darf keinen Aufruf oder Aufruf enthalten, der optionale Argumente verwendet

Wenn Sie dies explizit auf den Standardwert (null) setzen, wird dies gelöscht.

Xaniff
quelle
2

Sie können diesen ScalarCollectionContainer verwenden, der ein Array begrenzt und einige Manipulationsoptionen bietet ( Gist ):

Verwendung:

public class Person
{
    public int Id { get; set; }
    //will be stored in database as single string.
    public SaclarStringCollection Phones { get; set; } = new ScalarStringCollection();
}

Code:

using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;

namespace System.Collections.Specialized
{
#if NET462
  [ComplexType]
#endif
  public abstract class ScalarCollectionBase<T> :
#if NET462
    Collection<T>,
#else
    ObservableCollection<T>
#endif
  {
    public virtual string Separator { get; } = "\n";
    public virtual string ReplacementChar { get; } = " ";
    public ScalarCollectionBase(params T[] values)
    {
      if (values != null)
        foreach (var item in Items)
          Items.Add(item);
    }

#if NET462
    [Browsable(false)]
#endif
    [EditorBrowsable(EditorBrowsableState.Never)]
    [Obsolete("Not to be used directly by user, use Items property instead.")]
    public string Data
    {
      get
      {
        var data = Items.Select(item => Serialize(item)
          .Replace(Separator, ReplacementChar.ToString()));
        return string.Join(Separator, data.Where(s => s?.Length > 0));
      }
      set
      {
        Items.Clear();
        if (string.IsNullOrWhiteSpace(value))
          return;

        foreach (var item in value
            .Split(new[] { Separator }, 
              StringSplitOptions.RemoveEmptyEntries).Select(item => Deserialize(item)))
          Items.Add(item);
      }
    }

    public void AddRange(params T[] items)
    {
      if (items != null)
        foreach (var item in items)
          Add(item);
    }

    protected abstract string Serialize(T item);
    protected abstract T Deserialize(string item);
  }

  public class ScalarStringCollection : ScalarCollectionBase<string>
  {
    protected override string Deserialize(string item) => item;
    protected override string Serialize(string item) => item;
  }

  public class ScalarCollection<T> : ScalarCollectionBase<T>
    where T : IConvertible
  {
    protected override T Deserialize(string item) =>
      (T)Convert.ChangeType(item, typeof(T));
    protected override string Serialize(T item) => Convert.ToString(item);
  }
}
Shimmy Weitzhandler
quelle
8
sieht ein bisschen überarbeitet aus?!
Falco Alexander
1
@ FalcoAlexander Ich habe meinen Beitrag aktualisiert ... Vielleicht etwas ausführlich, macht aber den Job. Stellen Sie sicher, dass Sie durch NET462die entsprechende Umgebung ersetzen oder hinzufügen.
Shimmy Weitzhandler
1
+1 für die Mühe, dies zusammenzustellen. Die Lösung ist ein wenig übertrieben für das Speichern einer Reihe von Zeichenfolgen :)
GETah