Entity Framework - Code First - Não é possível armazenar a lista <>

106

Eu escrevi essa 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"
        };
    }
}

e

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

Depois de executar o código:

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

meus dados estão sendo salvos, mas apenas o Id. Não tenho tabelas nem relacionamentos aplicáveis ​​à lista de Strings .

O que estou fazendo de errado? Também tentei fazer Strings, virtual mas não mudou nada.

Obrigado pela ajuda.

Paulo
fonte
3
Como você espera que List <sting> seja armazenado no banco de dados? Isso não vai funcionar. Mude para string.
Wiktor Zychla
4
Se você tem uma lista, ela deve apontar para alguma entidade. Para EF armazenar a lista, ele precisa de uma segunda tabela. Na segunda tabela, ele colocará tudo de sua lista e usará uma chave estrangeira para apontar de volta para sua Testentidade. Portanto, crie uma nova entidade com Idpropriedade e MyStringpropriedade e , em seguida, faça uma lista disso.
Daniel Gabriel
1
Certo ... Ele não pode ser armazenado no banco de dados diretamente, mas eu esperava que o Entity Framework criasse uma nova entidade para fazer isso por si só. Obrigado por seus comentários.
Paul de

Respostas:

161

O Entity Framework não oferece suporte a coleções de tipos primitivos. Você pode criar uma entidade (que será salva em uma tabela diferente) ou fazer algum processamento de string para salvar sua lista como uma string e preencher a lista após a entidade ser materializada.

Pawel
fonte
e se uma entidade contiver uma lista de entidades? como o mapeamento será salvo?
A_Arnold
Depende - provavelmente para uma mesa separada.
Pawel de
pode tentar serializar e, em seguida, compactar e salvar o texto formatado em json ou criptografar e salvá-lo, se necessário. de qualquer forma, você não pode fazer com que o framework faça o mapeamento de tabela de tipo complexo para você.
Niklas
90

EF Core 2.1+:

Propriedade:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Sasan
fonte
5
Ótima solução para EF Core. Embora pareça haver um problema com a conversão de caractere em string. Tive que implementá-lo assim: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller
8
Esta é a única resposta realmente correta IMHO. Todos os outros exigem que você altere seu modelo, e isso viola o princípio de que os modelos de domínio devem ignorar a persistência. (Tudo bem se você estiver usando modelos separados de persistência e domínio, mas poucas pessoas realmente fazem isso.)
Marcell Toth
2
Você deve aceitar minha solicitação de edição porque você não pode usar char como o primeiro argumento de string.Join e você deve fornecer um char [] como o primeiro argumento de string.Split se você também deseja fornecer StringSplitOptions.
Dominik
2
No .NET Core, você pode. Estou usando esse código exato em um de meus projetos.
Sasan
2
Não disponível no .NET Standard
Sasan
54

Esta resposta é baseada nas fornecidas por @Sasan e @CAD bloke .

Funciona apenas com EF Core 2.1+ (não compatível com .NET Standard) (Newtonsoft JsonConvert)

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

Usando a configuração fluente EF Core, serializamos / desserializamos o Listde / para JSON.

Por que este código é a combinação perfeita de tudo pelo que você pode se esforçar:

  • O problema com a resposta original de Sasn é que ela se tornará uma grande bagunça se as strings na lista contiverem vírgulas (ou qualquer caractere escolhido como delimitador) porque transformará uma única entrada em várias entradas, mas é a mais fácil de ler e mais conciso.
  • O problema com a resposta do cara do CAD é que ela é feia e requer que o modelo seja alterado, o que é uma má prática de design (veja o comentário de Marcell Toth sobre a resposta de Sasan ). Mas é a única resposta segura para os dados.
Mathieu VIALES
fonte
7
bravo, esta provavelmente deve ser a resposta aceita
Shirkan
1
Eu gostaria que isso funcionasse no .NET Framework e EF 6, é uma solução muito elegante.
CAD cara
Esta é uma solução incrível. Obrigado
Marlon
Você é capaz de fazer consultas nesse campo? Minhas tentativas falharam miseravelmente: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();não encontra nada.
Nicola Iarocci
3
Para responder à minha própria pergunta, citando os documentos : "O uso de conversões de valor pode afetar a capacidade do EF Core de traduzir expressões para SQL. Um aviso será registrado para esses casos. A remoção dessas limitações está sendo considerada para um lançamento futuro." - Ainda seria bom.
Nicola Iarocci
44

Eu sei que esta é uma pergunta antiga, e Pawel deu a resposta correta , eu só queria mostrar um exemplo de código de como fazer algum processamento de string e evitar uma classe extra para a lista de um 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
fonte
1
Por que não métodos estáticos em vez de usar propriedades públicas? (Ou estou mostrando meu viés de programação procedural?)
Duston
@randoms porque é necessário definir 2 listas? um como uma propriedade e outro como a lista real? Eu agradeceria se você também pudesse explicar como funciona a ligação aqui, porque essa solução não está funcionando bem para mim e não consigo descobrir a ligação aqui. Obrigado
LiranBo
2
há uma lista privada, que possui duas propriedades públicas associadas, Strings, que você usará em seu aplicativo para adicionar e remover strings, e StringsAsString, que é o valor que será salvo no banco de dados, como uma lista separada por vírgulas. Não tenho certeza do que você está perguntando, porém, a ligação é a lista privada _strings, que conecta as duas propriedades públicas.
randoms
1
Lembre-se de que esta resposta não sai ,(vírgula) em strings. Se uma string na lista contém uma ou mais ,(vírgula), a string é dividida em várias strings.
Jogge
2
Na string.Joinvírgula deve ser colocado entre aspas duplas (para uma string), não aspas simples (para um caractere). Consulte msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris
29

JSON.NET para o resgate.

Você o serializa em JSON para persistir no banco de dados e desserializa para reconstituir a coleção .NET. Parece ter um desempenho melhor do que eu esperava com o Entity Framework 6 e o ​​SQLite. Eu sei que você pediu, List<string>mas aqui está um exemplo de uma coleção ainda mais complexa que funciona muito bem.

Eu marquei a propriedade persistida com, [Obsolete]portanto, seria muito óbvio para mim que "esta não é a propriedade que você está procurando" no curso normal da codificação. A propriedade "real" é marcada com, [NotMapped]portanto, a estrutura Entity a ignora.

(tangente não relacionada): Você poderia fazer o mesmo com tipos mais complexos, mas precisa se perguntar se acabou de tornar a consulta das propriedades daquele objeto muito difícil para você? (sim, no meu 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);
    }
}
Cara CAD
fonte
Acho essa solução bastante feia, mas, na verdade, é a única sensata. Todas as opções que oferecem para entrar na lista usando qualquer caractere e, em seguida, dividi-lo novamente podem se tornar uma bagunça selvagem se o caractere de divisão for incluído nas strings. Json deve ser muito mais são.
Mathieu VIALES
1
Acabei dando uma resposta que é uma "mesclagem" desta e de outra para resolver cada problema de resposta (feiura / segurança de dados) usando os pontos fortes da outra.
Mathieu VIALES
13

Só para simplificar -

A estrutura da entidade não oferece suporte a primitivos. Você pode criar uma classe para envolvê-la ou adicionar outra propriedade para formatar a lista como uma string:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Adam Tal
fonte
1
Isso ocorre no caso de um item da lista não conter uma string. Caso contrário, você precisará escapar. Ou para serializar / desserializar a lista para situações mais complexas.
Adam Tal
3
Além disso, não se esqueça de usar [NotMapped] na propriedade ICollection
Ben Petersen
7

Claro que Pawel deu a resposta certa . Mas descobri neste post que desde o EF 6+ é possível salvar propriedades privadas. Então, eu preferiria esse código, porque você não consegue salvar as Strings de maneira errada.

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
fonte
6
E se a string contiver uma vírgula?
Chalky
4
Eu não recomendaria fazer dessa maneira. StringsAsStringssó será atualizado quando a Strings referência for alterada, e a única vez em seu exemplo que acontece é na atribuição. Adicionar ou remover itens de sua Stringslista após a atribuição não atualizará a StringsAsStringsvariável de apoio. A maneira adequada de implementar isso seria expor StringsAsStringscomo uma visualização da Stringslista, em vez do contrário. Junte os valores no getacessador da StringsAsStringspropriedade e divida-os no setacessador.
jduncanator
Para evitar a adição de propriedades privadas (que não são livres de efeitos colaterais), torne privado o configurador da propriedade serializada. jduncanator é claro que está certo: se você não pegar as manipulações da lista (usar um ObservableCollection?), as mudanças não serão percebidas pelo EF.
Leônidas
Como @jduncanator mencionou, esta solução não funciona quando uma modificação na Lista é feita (vinculação no MVVM, por exemplo)
Ihab Hajj
7

Ligeiramente ajustes @Mathieu Viales 's resposta , aqui está um trecho compatível .NET padrão usando o novo serializador System.Text.Json eliminando assim a dependência 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));

Observe que, embora o segundo argumento em ambos Serialize()e Deserialize()seja normalmente opcional, você receberá um erro:

Uma árvore de expressão não pode conter uma chamada ou invocação que use argumentos opcionais

Definir explicitamente como o padrão (nulo) para cada esclarece isso.

Xaniff
fonte
3

Você pode usar este ScalarCollectioncontêiner que confina uma matriz e fornece algumas opções de manipulação ( 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
fonte
8
parece um pouco mais de engenharia ?!
Falco Alexander
1
@FalcoAlexander Eu atualizei meu post ... Talvez um pouco prolixo, mas dá conta do recado. Certifique-se de substituir NET462pelo ambiente apropriado ou adicioná-lo a ele.
Shimmy Weitzhandler
1
1 pelo esforço de juntar tudo. A solução é um pouco exagerada para armazenar uma série de strings :)
GETah