Entity Framework - Code First - Can't Store List <String>

106

Escribí tal clase:

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

y

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

Después de ejecutar el código:

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

mis datos se están guardando pero solo el Id. No tengo tablas ni relaciones que se apliquen a la lista de cadenas .

¿Qué estoy haciendo mal? Intenté también hacer Strings virtual pero no cambió nada.

Gracias por tu ayuda.

Pablo
fuente
3
¿Cómo espera que List <sting> se almacene en la base de datos? Eso no funcionará. Cámbielo a cuerda.
Wiktor Zychla
4
Si tiene una lista, debe apuntar a alguna entidad. Para que EF almacene la lista, necesita una segunda tabla. En la segunda tabla, pondrá todo lo de su lista y usará una clave externa para apuntar a su Testentidad. Así que crea una nueva entidad con Idpropiedad y MyStringpropiedad, luego haz una lista de eso.
Daniel Gabriel
1
Correcto ... No se puede almacenar en la base de datos directamente, pero esperaba que Entity Framework creara una nueva entidad para hacerlo por sí mismo. Gracias por tus comentarios.
Paul

Respuestas:

161

Entity Framework no admite colecciones de tipos primitivos. Puede crear una entidad (que se guardará en una tabla diferente) o realizar algún procesamiento de cadena para guardar su lista como una cadena y completar la lista después de que la entidad se materialice.

Pawel
fuente
¿Qué pasa si una entidad contiene una lista de entidades? ¿Cómo se guardará el mapeo?
A_Arnold
Depende, lo más probable es que en una mesa separada.
Pawel
puede intentar serializar y luego comprimir y guardar el texto con formato json, o encriptarlo y guardarlo si es necesario. de cualquier manera, no puede hacer que el marco haga el mapeo de tablas de tipo complejo por usted.
Niklas
89

EF Core 2.1+:

Propiedad:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Sasan
fuente
5
Gran solución para EF Core. Aunque parece tener un problema con la conversión de caracteres a cadenas. Tuve que implementarlo así: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller
8
Esta es la única respuesta realmente correcta en mi humilde opinión. Todos los demás requieren que cambie su modelo, y eso viola el principio de que los modelos de dominio deben ignorar la persistencia. (Está bien si está utilizando modelos de dominio y persistencia separados, pero pocas personas realmente lo hacen).
Marcell Toth
2
Debería aceptar mi solicitud de edición porque no puede utilizar char como primer argumento de string.Join y debe proporcionar un char [] como primer argumento de string.Split si también desea proporcionar StringSplitOptions.
Dominik
2
En .NET Core puede hacerlo. Estoy usando este código exacto en uno de mis proyectos.
Sasan
2
No disponible en .NET Standard
Sasan
54

Esta respuesta se basa en las proporcionadas por @Sasan y @CAD bloke .

Funciona solo con EF Core 2.1+ (no compatible con .NET Standard) (Newtonsoft JsonConvert)

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

Usando la configuración fluida de EF Core, serializamos / deserializamos el Lista / desde JSON.

Por qué este código es la combinación perfecta de todo lo que puede esforzarse:

  • El problema con la respuesta original de Sasn es que se convertirá en un gran lío si las cadenas de la lista contienen comas (o cualquier carácter elegido como delimitador) porque convertirá una sola entrada en múltiples entradas, pero es la más fácil de leer y más conciso.
  • El problema con la respuesta del tipo CAD es que es feo y requiere que se modifique el modelo, lo cual es una mala práctica de diseño (consulte el comentario de Marcell Toth sobre la respuesta de Sasan ). Pero es la única respuesta segura para los datos.
Mathieu VIALES
fuente
7
bravo, esta probablemente debería ser la respuesta aceptada
Shirkan
1
Ojalá esto funcionara en .NET Framework y EF 6, es una solución realmente elegante.
CAD bloke
Ésta es una solución asombrosa. Gracias
Marlon
¿Eres capaz de realizar consultas en ese campo? Mis intentos han fracasado estrepitosamente: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();no encuentra nada.
Nicola Iarocci
3
Para responder a mi propia pregunta, cito los documentos : "El uso de conversiones de valor puede afectar la capacidad de EF Core para traducir expresiones a SQL. Se registrará una advertencia para tales casos. Se está considerando la eliminación de estas limitaciones para una versión futura". - Aunque aún estaría bien.
Nicola Iarocci
44

Sé que esta es una pregunta antigua, y Pawel ha dado la respuesta correcta , solo quería mostrar un ejemplo de código de cómo hacer un procesamiento de cadenas y evitar una clase adicional para la lista de un tipo primitivo.

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(); }
    }
}
randoms
fuente
1
¿Por qué no utilizar métodos estáticos en lugar de utilizar propiedades públicas? (¿O estoy mostrando mi sesgo de programación procedimental?)
Duston
@randoms ¿por qué es necesario definir 2 listas? uno como propiedad y otro como la lista real? Le agradecería que también pudiera explicar cómo funciona el enlace aquí, porque esta solución no me funciona bien y no puedo entender el enlace aquí. Gracias
LiranBo
2
hay una lista privada, que tiene dos propiedades públicas asociadas, Strings, que usará en su aplicación para agregar y eliminar cadenas, y StringsAsString, que es el valor que se guardará en la base de datos, como una lista separada por comas. Sin embargo, no estoy realmente seguro de lo que está preguntando, el enlace es la lista privada _strings, que conecta las dos propiedades públicas.
randoms
1
Tenga en cuenta que esta respuesta no se escapa ,(coma) en cadenas. Si una cadena de la lista contiene una o más ,(coma), la cadena se divide en varias cadenas.
Jogge
2
En string.Joinla coma debe ser rodeado por comillas dobles (por una cadena), y no comillas simples (para un char). Ver msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris
29

JSON.NET al rescate.

Lo serializa en JSON para persistir en la base de datos y lo deserializa para reconstituir la colección .NET. Esto parece funcionar mejor de lo que esperaba con Entity Framework 6 y SQLite. Sé que lo pidió, List<string>pero aquí hay un ejemplo de una colección aún más compleja que funciona bien.

Etiqueté la propiedad persistente con [Obsolete]para que fuera muy obvio para mí que "esta no es la propiedad que está buscando" en el curso normal de la codificación. La propiedad "real" está etiquetada con, [NotMapped]por lo que Entity framework la ignora.

(tangente no relacionada): podría hacer lo mismo con tipos más complejos, pero debe preguntarse si hizo que consultar las propiedades de ese objeto sea demasiado difícil para usted. (sí, en mi caso).

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);
    }
}
Tipo CAD
fuente
Encuentro esta solución bastante fea, pero en realidad es la única sana. Todas las opciones que ofrecen unirse a la lista usando cualquier carácter y luego dividirla pueden convertirse en un desastre si el carácter de división está incluido en las cadenas. Json debería estar mucho más cuerdo.
Mathieu VIALES
1
Terminé haciendo una respuesta que es una "fusión" de esta y otra para solucionar cada problema de respuesta (fealdad / seguridad de datos) utilizando los puntos fuertes del otro.
Mathieu VIALES
13

Solo para simplificar:

Entity framework no admite primitivas. Puede crear una clase para envolverla o agregar otra propiedad para formatear la lista como una cadena:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Adam Tal
fuente
1
Esto es en caso de que un elemento de la lista no pueda contener una cadena. De lo contrario, tendrás que escapar. O serializar / deserializar la lista para situaciones más complejas.
Adam Tal
3
Además, no olvide utilizar [NotMapped] en la propiedad ICollection
Ben Petersen
7

Por supuesto, Pawel ha dado la respuesta correcta . Pero encontré en esta publicación que desde EF 6+ es posible guardar propiedades privadas. Así que preferiría este código, porque no puede guardar las cadenas de forma incorrecta.

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
fuente
6
¿Qué pasa si la cadena contiene una coma?
Chalky
4
No recomendaría hacerlo de esta manera. StringsAsStringssolo se actualizará cuando se cambie la Strings referencia , y la única vez en su ejemplo que ocurre es en la asignación. Agregar o eliminar elementos de su Stringslista después de la asignación no actualizará la StringsAsStringsvariable de respaldo. La forma correcta de implementar esto sería exponer StringsAsStringscomo una vista de la Stringslista, en lugar de al revés. Una los valores en el getdescriptor de acceso de la StringsAsStringspropiedad y divídalos en el setdescriptor de acceso.
jduncanator
Para evitar agregar propiedades privadas (que no están libres de efectos secundarios), haga privado el establecedor de la propiedad serializada. jduncanator tiene razón, por supuesto: si no detecta las manipulaciones de la lista (¿usa un ObservableCollection?), EF no notará los cambios.
Leonidas
Como mencionó @jduncanator, esta solución no funciona cuando se realiza una modificación en la Lista (vinculante en MVVM, por ejemplo)
Ihab Hajj
7

Modificando ligeramente la respuesta de @Mathieu Viales , aquí hay un fragmento de código compatible con .NET Standard que utiliza el nuevo serializador System.Text.Json eliminando así la dependencia de 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));

Tenga en cuenta que si bien el segundo argumento en ambos Serialize()y Deserialize()suele ser opcional, obtendrá un error:

Un árbol de expresión no puede contener una llamada o invocación que utilice argumentos opcionales

Establecer explícitamente eso en el valor predeterminado (nulo) para cada uno aclara eso.

Xaniff
fuente
3

Puede usar este ScalarCollectioncontenedor que limita una matriz y proporciona algunas opciones de manipulación ( Gist ):

Uso:

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

Código:

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
fuente
8
parece un poco sobre ingeniería?
Falco Alexander
1
@FalcoAlexander He actualizado mi publicación ... Tal vez un poco detallado, pero hace el trabajo. Asegúrese de reemplazarlo NET462con el entorno apropiado o agregarlo.
Shimmy Weitzhandler
1
+1 por el esfuerzo de armar esto. La solución es un poco excesiva para almacenar una variedad de cadenas :)
GETah