Entity Framework - Code First - Impossible de stocker la liste <String>

106

J'ai écrit une telle classe:

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

et

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

Après l'exécution du code:

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

mes données sont enregistrées mais juste le fichier Id. Je n'ai pas de tables ni de relations applicables à la liste des chaînes .

Qu'est-ce que je fais mal? J'ai aussi essayé de faire des cordes virtual mais cela n'a rien changé.

Merci de votre aide.

Paul
la source
3
Comment pensez-vous que la liste <sting> est stockée dans la base de données? Cela ne fonctionnera pas. Changez-le en chaîne.
Wiktor Zychla
4
Si vous avez une liste, elle doit pointer vers une entité. Pour que EF stocke la liste, il a besoin d'une deuxième table. Dans le deuxième tableau, il mettra tout de votre liste et utilisera une clé étrangère pour pointer vers votre Testentité. Alors créez une nouvelle entité avec Idpropriété et MyStringpropriété, puis faites une liste de cela.
Daniel Gabriel
1
Bien ... Il ne peut pas être stocké directement dans la base de données, mais j'espérais qu'Entity Framework créerait une nouvelle entité pour le faire par lui-même. Merci pour vos commentaires.
Paul

Réponses:

161

Entity Framework ne prend pas en charge les collections de types primitifs. Vous pouvez créer une entité (qui sera enregistrée dans une table différente) ou effectuer un traitement de chaîne pour enregistrer votre liste sous forme de chaîne et remplir la liste une fois l'entité matérialisée.

Pawel
la source
Et si une entité contient une liste d'entités? comment la cartographie sera-t-elle sauvegardée?
A_Arnold
Dépend - très probablement à une table séparée.
Pawel
peut essayer de sérialiser, puis de compresser et d'enregistrer le texte au format json, ou de le chiffrer et de l'enregistrer si nécessaire. de toute façon, vous ne pouvez pas laisser le framework faire le mappage de table de type complexe pour vous.
Niklas
90

EF Core 2.1+:

Propriété:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Sasan
la source
5
Excellente solution pour EF Core. Bien qu'il semble y avoir un problème avec la conversion de caractères en chaînes. J'ai dû l'implémenter comme tel: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller
8
C'est la seule réponse vraiment correcte à mon humble avis. Tous les autres nécessitent que vous changiez votre modèle, et cela viole le principe selon lequel les modèles de domaine doivent être ignorants de la persistance. (C'est bien si vous utilisez des modèles de persistance et de domaine séparés, mais peu de gens le font réellement.)
Marcell Toth
2
Vous devriez accepter ma demande de modification car vous ne pouvez pas utiliser char comme premier argument de string.Join et vous devez fournir un char [] comme premier argument de string.Split si vous souhaitez également fournir StringSplitOptions.
Dominik
2
Dans .NET Core, vous pouvez. J'utilise ce morceau de code exact dans l'un de mes projets.
Sasan
2
Non disponible dans .NET Standard
Sasan
54

Cette réponse est basée sur celles fournies par @Sasan et @CAD bloke .

Fonctionne uniquement avec EF Core 2.1+ (non compatible avec .NET Standard) (Newtonsoft JsonConvert )

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

En utilisant la configuration fluide EF Core, nous sérialisons / désérialisons le List vers / depuis JSON.

Pourquoi ce code est le mélange parfait de tout ce que vous pouvez rechercher:

  • Le problème avec la réponse originale de Sasn est qu'elle se transformera en gros désordre si les chaînes de la liste contiennent des virgules (ou tout caractère choisi comme délimiteur) car cela transformera une seule entrée en plusieurs entrées, mais c'est la plus facile à lire et le plus concis.
  • Le problème avec la réponse de type CAO est qu'elle est moche et nécessite que le modèle soit modifié, ce qui est une mauvaise pratique de conception (voir le commentaire de Marcell Toth sur la réponse de Sasan ). Mais c'est la seule réponse qui soit sûre pour les données.
Mathieu VIALES
la source
7
bravo, cela devrait probablement être la réponse acceptée
Shirkan
1
Je souhaite que cela fonctionne dans .NET Framework & EF 6, c'est une solution vraiment élégante.
Bloke CAD
C'est une solution incroyable. Merci
Marlon le
Êtes-vous capable d'interroger sur ce champ? Mes tentatives ont lamentablement échoué: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();ne trouve rien.
Nicola Iarocci
3
Pour répondre à ma propre question, citant la documentation : "L'utilisation de conversions de valeurs peut avoir un impact sur la capacité d'EF Core à traduire des expressions en SQL. Un avertissement sera consigné dans de tels cas. La suppression de ces limitations est envisagée pour une version ultérieure." - Ce serait toujours bien.
Nicola Iarocci
44

Je sais que c'est une vieille question, et Pawel a donné la bonne réponse , je voulais juste montrer un exemple de code sur la façon de traiter des chaînes et éviter une classe supplémentaire pour la liste d'un type primitif.

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(); }
    }
}
aléatoires
la source
1
Pourquoi pas des méthodes statiques au lieu d'utiliser des propriétés publiques? (Ou est-ce que je montre mon biais de programmation procédurale?)
Duston
@randoms pourquoi est-il nécessaire de définir 2 listes? un comme propriété et un comme liste réelle? J'apprécierais que vous puissiez également expliquer comment fonctionne la liaison ici, car cette solution ne fonctionne pas bien pour moi, et je ne peux pas comprendre la liaison ici. Merci
LiranBo
2
il existe une liste privée, qui a deux propriétés publiques associées, Strings, que vous utiliserez dans votre application pour ajouter et supprimer des chaînes, et StringsAsString qui est la valeur qui sera enregistrée dans la base de données, sous forme de liste séparée par des virgules. Je ne suis pas vraiment sûr de ce que vous demandez, la liaison est la liste privée _strings, qui relie les deux propriétés publiques ensemble.
randoms
1
Veuillez garder à l'esprit que cette réponse ne s'échappe pas ,(virgule) dans les chaînes. Si une chaîne de la liste contient une ou plusieurs ,(virgules), la chaîne est divisée en plusieurs chaînes.
Jogge
2
Dans string.Joinla virgule devrait être entouré par des guillemets doubles (pour une chaîne), pas apostrophes (pour un char). Voir msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris
29

JSON.NET à la rescousse.

Vous le sérialisez en JSON pour qu'il persiste dans la base de données et le désérialisez pour reconstituer la collection .NET. Cela semble fonctionner mieux que ce à quoi je m'attendais avec Entity Framework 6 et SQLite. Je sais que vous avez demandé List<string>mais voici un exemple de collection encore plus complexe qui fonctionne très bien.

J'ai marqué la propriété persistante avec [Obsolete]donc il serait très évident pour moi que "ce n'est pas la propriété que vous recherchez" dans le cours normal du codage. La propriété "real" est étiquetée avec [NotMapped]donc Entity Framework l'ignore.

(tangente non liée): Vous pourriez faire la même chose avec des types plus complexes, mais vous devez vous demander si vous avez simplement rendu l'interrogation des propriétés de cet objet trop difficile pour vous? (oui, dans mon cas).

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);
    }
}
Type CAD
la source
Je trouve cette solution assez moche, mais c'est en fait la seule saine d'esprit. Toutes les options proposant de rejoindre la liste en utilisant n'importe quel caractère, puis de la diviser en arrière, peuvent se transformer en un désordre sauvage si le caractère de division est inclus dans les chaînes. Json devrait être beaucoup plus sain d'esprit.
Mathieu VIALES
1
J'ai fini par faire une réponse qui est une "fusion" de celle-ci et d'une autre pour résoudre chaque problème de réponse (laideur / sécurité des données) en utilisant les points forts de l'autre.
Mathieu VIALES
13

Juste pour simplifier -

Le cadre d'entité ne prend pas en charge les primitives. Vous créez une classe pour l'encapsuler ou ajoutez une autre propriété pour mettre en forme la liste sous forme de chaîne:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Adam Tal
la source
1
C'est dans le cas où un élément de liste ne peut pas contenir de chaîne. Sinon, vous devrez y échapper. Ou pour sérialiser / désérialiser la liste pour des situations plus complexes.
Adam Tal
3
N'oubliez pas non plus d'utiliser [NotMapped] sur la propriété ICollection
Ben Petersen
7

Bien sûr, Pawel a donné la bonne réponse . Mais j'ai trouvé dans cet article que depuis EF 6+, il est possible de sauvegarder des propriétés privées. Je préférerais donc ce code, car vous ne pouvez pas enregistrer les chaînes de manière incorrecte.

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
la source
6
Que faire si la chaîne contient une virgule?
Chalky
4
Je ne recommanderais pas de le faire de cette façon. StringsAsStringsne sera mis à jour que lorsque la Strings référence est modifiée, et le seul moment dans votre exemple qui se produit est lors de l'affectation. L'ajout ou la suppression d'éléments de votre Stringsliste après l'attribution ne mettra pas à jour la StringsAsStringsvariable de sauvegarde. La bonne façon d'implémenter cela serait d'exposer StringsAsStringscomme une vue de la Stringsliste, au lieu de l'inverse. Joignez les valeurs ensemble dans l' getaccesseur de la StringsAsStringspropriété et divisez-les dans l' setaccesseur.
jduncanator
Pour éviter d'ajouter des propriétés privées (ce qui n'est pas sans effet secondaire), rendez privé le setter de la propriété sérialisée. jduncanator a bien sûr raison: si vous n'attrapez pas les manipulations de liste (utilisez un ObservableCollection?), les changements ne seront pas remarqués par EF.
Leonidas
Comme @jduncanator l'a mentionné, cette solution ne fonctionne pas lorsqu'une modification de la liste est effectuée (liaison dans MVVM par exemple)
Ihab Hajj
7

Tordant légèrement @Mathieu Viales de » la réponse , voici un extrait standard .NET compatible à l' aide de la nouvelle sérialiseur System.Text.Json éliminant ainsi la dépendance à l' égard Newtonsoft.Json.

using System.Text.Json;

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

Notez que bien que le deuxième argument dans les deux Serialize()et Deserialize()soit généralement facultatif, vous obtiendrez une erreur:

Une arborescence d'expression ne peut pas contenir un appel ou une invocation qui utilise des arguments facultatifs

Définir explicitement cela sur la valeur par défaut (null) pour chacun efface cela.

Xaniff
la source
3

Vous pouvez utiliser ce ScalarCollectionconteneur qui limite un tableau et fournit des options de manipulation ( Gist ):

Usage:

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
la source
8
a l'air un peu trop machiné?!
Falco Alexander
1
@FalcoAlexander J'ai mis à jour mon message ... Peut-être un peu bavard mais fait le travail. Assurez-vous de le remplacer NET462par l'environnement approprié ou de l'ajouter à celui-ci.
Shimmy Weitzhandler
1
+1 pour l'effort de mettre cela ensemble. La solution est un peu exagérée pour stocker un tableau de chaînes :)
GETah