Entity Framework - Code First - Impossibile archiviare l'elenco <String>


106

Ho scritto una lezione del genere:

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

Dopo il codice di esecuzione:

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

i miei dati vengono salvati ma solo il file Id. Non ho tabelle né relazioni applicabili all'elenco delle stringhe .

Che cosa sto facendo di sbagliato? Ho provato anche a fare degli archi virtual ma non è cambiato nulla.

Grazie per l'aiuto.


3
Come ti aspetti che List <sting> venga memorizzato nel db? Non funzionerà. Cambialo in stringa.
Wiktor Zychla

4
Se hai un elenco, deve puntare a un'entità. Affinché EF memorizzi l'elenco, è necessaria una seconda tabella. Nella seconda tabella inserirà tutto dal tuo elenco e utilizzerà una chiave esterna per puntare nuovamente alla tua Testentità. Quindi crea una nuova entità con Idproprietà e MyStringproprietà, quindi fai un elenco di ciò.
Daniel Gabriel

1
Giusto ... Non può essere archiviato direttamente nel db, ma speravo che Entity Framework creasse una nuova entità per farlo da solo. Grazie per i vostri commenti.
Paul

Risposte:


161

Entity Framework non supporta le raccolte di tipi primitivi. È possibile creare un'entità (che verrà salvata in una tabella diversa) o eseguire un'elaborazione di stringhe per salvare l'elenco come stringa e popolare l'elenco dopo che l'entità è stata materializzata.


cosa succede se un'entità contiene un elenco di entità? come verrà salvata la mappatura?
A_Arnold

Dipende, molto probabilmente da una tabella separata.
Pawel

può provare a serializzare e quindi comprimere e salvare il testo formattato json, oppure crittografarlo e salvarlo se necessario. in entrambi i casi non puoi fare in modo che il framework esegua la complessa mappatura della tabella dei tipi per te.
Niklas

90

EF Core 2.1 e versioni successive:

Proprietà:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));

5
Ottima soluzione per EF Core. Anche se sembra avere un problema con la conversione da caratteri a stringa. Ho dovuto implementarlo in questo modo: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller

8
Questa è l'unica risposta veramente corretta IMHO. Tutti gli altri richiedono che tu modifichi il tuo modello e questo viola il principio che i modelli di dominio dovrebbero ignorare la persistenza. (Va bene se utilizzi persistenza e modelli di dominio separati, ma poche persone lo fanno effettivamente.)
Marcell Toth,

2
Dovresti accettare la mia richiesta di modifica perché non puoi usare char come primo argomento di string.Join e devi fornire un char [] come primo argomento di string.Split se vuoi fornire anche StringSplitOptions.
Dominik

2
In .NET Core puoi. Sto usando questo pezzo di codice esatto in uno dei miei progetti.
Sasan

2
Non disponibile in .NET Standard
Sasan

54

Questa risposta si basa su quelle fornite da @Sasan e @CAD bloke .

Funziona solo con EF Core 2.1+ (non compatibile 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 configurazione fluente di EF Core serializziamo / deserializziamo il Listto / from JSON.

Perché questo codice è il mix perfetto di tutto ciò per cui potresti lottare:

  • Il problema con la risposta originale di Sasn è che si trasformerà in un grande pasticcio se le stringhe nell'elenco contengono virgole (o qualsiasi carattere scelto come delimitatore) perché trasformerà una singola voce in più voci ma è la più facile da leggere e molto conciso.
  • Il problema con la risposta del tizio CAD è che è brutta e richiede la modifica del modello, il che è una cattiva pratica di progettazione (vedere il commento di Marcell Toth sulla risposta di Sasan ). Ma è l'unica risposta sicura per i dati.

7
bravo, questa dovrebbe probabilmente essere la risposta accettata
Shirkan

1
Vorrei che funzionasse in .NET Framework e EF 6, è una soluzione davvero elegante.
CAD bloke

Questa è una soluzione straordinaria. Grazie
Marlon

Sei in grado di interrogare su quel campo? I miei tentativi sono falliti miseramente: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();non trova nulla.
Nicola Iarocci

3
Per rispondere alla mia domanda, citando i documenti : "L'utilizzo di conversioni di valori può influire sulla capacità di EF Core di tradurre espressioni in SQL. In questi casi verrà registrato un avviso. La rimozione di queste limitazioni è in fase di valutazione per una versione futura". - Sarebbe comunque carino.
Nicola Iarocci

44

So che questa è una vecchia domanda e Pawel ha dato la risposta corretta , volevo solo mostrare un esempio di codice su come eseguire un'elaborazione di stringhe ed evitare una classe extra per l'elenco di 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(); }
    }
}

1
Perché non metodi statici invece di utilizzare proprietà pubbliche? (O sto mostrando il mio pregiudizio di programmazione procedurale?)
Duston

@randoms perché è necessario definire 2 elenchi? uno come proprietà e uno come elenco effettivo? Ti sarei grato se mi spiegassi anche come funziona l'associazione qui, perché questa soluzione non funziona bene per me e non riesco a capire l'associazione qui. Grazie
LiranBo

2
c'è un elenco privato, a cui sono associate due proprietà pubbliche, Strings, che utilizzerai nella tua applicazione per aggiungere e rimuovere stringhe, e StringsAsString che è il valore che verrà salvato nel db, come un elenco separato da virgole. Non sono proprio sicuro di quello che stai chiedendo, tuttavia, l'associazione è la lista privata _strings, che collega le due proprietà pubbliche insieme.
casuali

1
Tieni presente che questa risposta non sfugge ,(virgola) nelle stringhe. Se una stringa nell'elenco contiene una o più ,(virgola), la stringa viene suddivisa in più stringhe.
Jogge

2
Nella string.Joinvirgola dovrebbe essere racchiuso tra virgolette doppie (per una stringa), non virgolette singole (per un carattere). Vedi msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris

29

JSON.NET in soccorso.

Puoi serializzarlo in JSON per persistere nel database e deserializzarlo per ricostituire la raccolta .NET. Questo sembra funzionare meglio di quanto mi aspettassi con Entity Framework 6 e SQLite. So che l'hai chiesto, List<string>ma ecco un esempio di una collezione ancora più complessa che funziona perfettamente.

Ho etichettato la proprietà persistente con [Obsolete]così sarebbe molto ovvio per me che "questa non è la proprietà che stai cercando" nel normale corso della codifica. La proprietà "reale" è contrassegnata con [NotMapped]così Entity framework la ignora.

(tangente non correlata): potresti fare lo stesso con tipi più complessi, ma devi chiederti se hai appena reso troppo difficile per te stesso interrogare le proprietà di quell'oggetto? (sì, nel mio 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);
    }
}

Trovo questa soluzione piuttosto brutta, ma in realtà è l'unica sana. Tutte le opzioni che offrono di unirsi alla lista usando qualsiasi carattere e poi dividerlo di nuovo potrebbero trasformarsi in un pasticcio selvaggio se il carattere di divisione è incluso nelle stringhe. Json dovrebbe essere molto più sano di mente.
Mathieu VIALES

1
Ho finito per fare una risposta che è una "fusione" di questa e un'altra per risolvere ogni problema di risposta (bruttezza / sicurezza dei dati) utilizzando i punti di forza dell'altro.
Mathieu VIALES

13

Solo per semplificare -

Entity Framework non supporta le primitive. Puoi creare una classe per racchiuderla o aggiungere un'altra proprietà per formattare l'elenco come una stringa:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}

1
Questo nel caso in cui una voce di elenco non possa contenere una stringa. Altrimenti, dovrai evadere. O per serializzare / deserializzare l'elenco per situazioni più complesse.
Adam Tal

3
Inoltre, non dimenticare di utilizzare [NotMapped] sulla proprietà ICollection
Ben Petersen

7

Ovviamente Pawel ha dato la risposta giusta . Ma l'ho trovato in questo post che da EF 6+ è possibile salvare proprietà private. Quindi preferirei questo codice, perché non sei in grado di salvare le stringhe in modo sbagliato.

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

6
Cosa succede se la stringa contiene una virgola?
Chalky

4
Non consiglierei di farlo in questo modo. StringsAsStringsverrà aggiornato solo quando il Strings riferimento viene modificato e l'unica volta nel tuo esempio che accade è durante l'assegnazione. L'aggiunta o la rimozione di elementi Stringsdall'elenco dopo l'assegnazione non aggiornerà la StringsAsStringsvariabile di supporto. Il modo corretto per implementare questo sarebbe esporre StringsAsStringscome una visualizzazione Stringsdell'elenco, invece del contrario. Unisci i valori insieme nella funzione di getaccesso della StringsAsStringsproprietà e suddividili nella funzione di setaccesso.
jduncanator

Per evitare di aggiungere proprietà private (che non sono prive di effetti collaterali) rendere privato il setter della proprietà serializzata. jduncanator ha ovviamente ragione: se non prendi le manipolazioni della lista (usi una ObservableCollection?), le modifiche non verranno notate da EF.
Leonida

Come menzionato da @jduncanator, questa soluzione non funziona quando viene apportata una modifica all'elenco (ad esempio, associazione in MVVM)
Ihab Hajj

7

Un po 'tweaking @Mathieu Viales 's risposta , ecco un frammento di .NET compatibile standard utilizzando il nuovo serializzatore System.Text.Json eliminando così la dipendenza 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));

Nota che mentre il secondo argomento in entrambi Serialize()e Deserialize()è in genere facoltativo, riceverai un errore:

Un albero delle espressioni non può contenere una chiamata o una chiamata che utilizza argomenti opzionali

L'impostazione esplicita di quello al valore predefinito (null) per ciascuno lo cancella.


3

È possibile utilizzare questo ScalarCollectioncontenitore che delimita un array e fornisce alcune opzioni di manipolazione ( Gist ):

Utilizzo:

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

Codice:

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

8
sembra un po 'troppo ingegnerizzato ?!
Falco Alexander

1
@FalcoAlexander ho aggiornato il mio post ... Forse un po 'prolisso ma fa il lavoro. Assicurati di sostituire NET462con l'ambiente appropriato o di aggiungerlo ad esso.
Shimmy Weitzhandler

1
+1 per lo sforzo di metterlo insieme. La soluzione è un po 'eccessiva per la memorizzazione di un array di stringhe :)
GETah
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.