Entity Framework - сначала код - невозможно сохранить список <String>

105

Я написал такой класс:

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

и

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

После запуска кода:

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

мои данные сохраняются, но только файлы Id. У меня нет таблиц или отношений, применимых к списку строк .

Что я делаю не так? Я также пробовал делать струны, virtual но это ничего не меняло.

Спасибо за помощь.

Павел
источник
3
Как вы ожидаете, что List <sting> будет храниться в базе данных? Это не сработает. Измените его на строку.
Wiktor Zychla
4
Если у вас есть список, он должен указывать на какую-то сущность. Чтобы EF сохранил список, ему нужна вторая таблица. Во второй таблице он поместит все из вашего списка и использует внешний ключ, чтобы указать на вашу Testсущность. Итак, создайте новую сущность со Idсвойством и MyStringсвойством, а затем составьте их список.
Daniel Gabriel
1
Правильно ... Его нельзя сохранить в базе данных напрямую, но я надеялся, что Entity Framework создаст новый объект, чтобы сделать это самостоятельно. Спасибо за ваши Коментарии.
Пол

Ответы:

160

Entity Framework не поддерживает коллекции примитивных типов. Вы можете создать объект (который будет сохранен в другой таблице) или выполнить некоторую строковую обработку, чтобы сохранить список как строку и заполнить список после того, как объект будет материализован.

Павел
источник
что, если сущность содержит список сущностей? как будет сохранено отображение?
A_Arnold 06
Зависит - скорее всего в отдельную таблицу.
Павел
может попытаться сериализовать, а затем сжать и сохранить текст в формате json или зашифровать и сохранить его, если это необходимо. в любом случае вы не можете заставить фреймворк выполнять сопоставление таблиц сложных типов за вас.
Niklas
94

EF Core 2.1+:

Свойство:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Сасан
источник
5
Отличное решение для EF Core. Хотя, похоже, есть проблема с преобразованием char в строку. Мне пришлось реализовать это так: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Питер Коллер
8
ИМХО, это единственный действительно правильный ответ. Все остальные требуют, чтобы вы изменили вашу модель, а это нарушает принцип, согласно которому модели предметной области должны игнорировать постоянство. (Это нормально, если вы используете отдельные модели персистентности и предметной области, но мало кто на самом деле это делает.)
Марсел Тот
2
Вы должны принять мой запрос на редактирование, потому что вы не можете использовать char в качестве первого аргумента string.Join, и вы должны предоставить char [] в качестве первого аргумента string.Split, если вы также хотите предоставить StringSplitOptions.
Доминик
2
В .NET Core это возможно. Я использую именно этот фрагмент кода в одном из своих проектов.
Сасан
2
Недоступно в .NET Standard
Сасан,
55

Этот ответ основан на ответах @Sasan и @CAD bloke .

Работает только с EF Core 2.1+ (несовместимо с .NET Standard) (Newtonsoft JsonConvert)

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

Используя свободную конфигурацию EF Core, мы сериализуем / десериализуем ListJSON в / из.

Почему этот код - идеальное сочетание всего, к чему вы могли стремиться:

  • Проблема с исходным ответом Sasn заключается в том, что он превратится в большой беспорядок, если строки в списке будут содержать запятые (или любой символ, выбранный в качестве разделителя), потому что он превратит одну запись в несколько записей, но ее легче всего читать и максимально лаконично.
  • Проблема с ответом парня из САПР заключается в том, что он уродлив и требует изменения модели, что является плохой практикой проектирования (см. Комментарий Марселла Тота к ответу Сасана ). Но это единственный ответ, который безопасен для данных.
Mathieu VIALES
источник
7
браво, это должно вероятно быть общепринятый ответ
Shirkan
1
Я бы хотел, чтобы это работало в .NET Framework и EF 6, это действительно элегантное решение.
CAD парень
Это потрясающее решение. Спасибо
Марлон
Можете ли вы делать запросы в этом поле? Мои попытки с треском провалились: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();ничего не находит.
Никола Яроччи
3
Чтобы ответить на свой вопрос, процитирую документы : «Использование преобразования значений может повлиять на способность EF Core преобразовывать выражения в SQL. В таких случаях будет регистрироваться предупреждение. В будущих выпусках рассматривается снятие этих ограничений». - Все равно было бы хорошо.
Никола Яроччи
43

Я знаю, что это старый вопрос, и Павел дал правильный ответ , я просто хотел показать пример кода, как выполнить некоторую обработку строк, и избежать дополнительного класса для списка примитивного типа.

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
Почему не использовать статические методы вместо использования общедоступных свойств? (Или я проявляю предвзятость к процедурному программированию?)
Дастон
@randoms зачем нужно определять 2 списка? один как собственность и один как фактический список? Я был бы признателен, если бы вы также объяснили, как здесь работает привязка, потому что это решение не работает для меня, и я не могу понять привязку здесь. Спасибо
LiranBo
2
есть один частный список, который имеет два связанных общедоступных свойства: Strings, которые вы будете использовать в своем приложении для добавления и удаления строк, и StringsAsString, который представляет собой значение, которое будет сохранено в базе данных в виде списка, разделенного запятыми. Я не совсем уверен, что вы спрашиваете, привязка - это частный список _strings, который соединяет два общедоступных свойства вместе.
randoms
1
Имейте в виду, что этот ответ не выводится через ,запятую в строках. Если строка в списке содержит одну или несколько ,(запятых), строка разбивается на несколько строк.
Jogge 05
2
В string.Joinзапятой должны быть заключены в двойные кавычки (для строки), а не одиночные кавычки (для полукокса). См. Msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Майкл Брэндон Моррис
29

JSON.NET приходит на помощь.

Вы сериализуете его в JSON, чтобы он сохранялся в базе данных, и десериализуете его, чтобы восстановить коллекцию .NET. Похоже, это работает лучше, чем я ожидал, с Entity Framework 6 и SQLite. Я знаю, что вы просили, List<string>но вот пример еще более сложной коллекции, которая отлично работает.

Я пометил постоянное свойство тегом, [Obsolete]чтобы мне было очень очевидно, что «это не то свойство, которое вы ищете» в обычном процессе кодирования. «Настоящее» свойство помечено, [NotMapped]поэтому платформа Entity игнорирует его.

(несвязанный касательный): вы можете сделать то же самое с более сложными типами, но вам нужно спросить себя, не слишком ли усложнили для себя запросы свойств этого объекта? (да, в моем случае).

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 парень
источник
Я считаю это решение довольно уродливым, но на самом деле оно единственно разумное. Все варианты, предлагающие присоединиться к списку с использованием любого символа, а затем разделить его обратно, могут превратиться в дикий беспорядок, если символ разделения включен в строки. Json должен быть более разумным.
Mathieu VIALES
1
Я закончил тем, что дал ответ, который представляет собой «слияние» этого и другого, чтобы исправить каждую проблему с ответом (уродство / безопасность данных), используя сильные стороны другого.
Mathieu VIALES
13

Чтобы упростить -

Платформа Entity Framework не поддерживает примитивы. Вы либо создаете класс, чтобы обернуть его, либо добавить другое свойство для форматирования списка в виде строки:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Адам Тал
источник
1
Это на случай, если элемент списка не может содержать строку. В противном случае вам придется сбежать. Или сериализовать / десериализовать список для более сложных ситуаций.
Адам Тал
3
Также не забудьте использовать [NotMapped] в свойстве ICollection
Бен Петерсен
7

Конечно, Павел дал правильный ответ . Но в этом посте я обнаружил, что, начиная с EF 6+, можно сохранять частные свойства. Поэтому я бы предпочел этот код, потому что вы не можете сохранить строки неправильно.

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
Что делать, если строка содержит запятую?
Меловой
4
Я бы не рекомендовал делать это таким образом. StringsAsStringsбудет обновляться только при изменении Strings ссылки , и в вашем примере это происходит только при назначении. Добавление или удаление элементов из вашего Stringsсписка после назначения не приводит к обновлению StringsAsStringsподдерживающей переменной. Правильный способ реализовать это - открыть StringsAsStringsв виде Stringsсписка, а не наоборот. Соедините значения вместе в getметоде доступа StringsAsStringsсвойства и разделите их в setметоде доступа.
jduncanator
Чтобы избежать добавления частных свойств (что не является свободным от побочных эффектов), сделайте установщик сериализованного свойства закрытым. jduncanator, конечно, прав: если вы не поймаете манипуляции со списком (используйте ObservableCollection?), EF не заметит изменений.
Леонидас
Как упоминал @jduncanator, это решение не работает при внесении изменений в список (например, привязка в MVVM)
Ихаб Хадж,
7

Слегка щипая @Mathieu Viales «S ответ , вот совместимый сниппет .NET Standard с использованием нового System.Text.Json сериалайзер , таким образом устраняя зависимость от 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));

Обратите внимание, что хотя второй аргумент в обоих Serialize()и Deserialize()обычно является необязательным, вы получите сообщение об ошибке:

Дерево выражения не может содержать вызов или вызов, использующий необязательные аргументы.

Явная установка этого значения по умолчанию (null) для каждого очищает это.

Ксанифф
источник
3

Вы можете использовать этот ScalarCollectionконтейнер, который ограничивает массив и предоставляет некоторые параметры манипуляции ( Gist ):

Использование:

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

Код:

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
выглядит немного надуманным ?!
Falco Alexander
1
@FalcoAlexander Я обновил свой пост ... Может быть, немного многословен, но работает. Убедитесь, что вы заменили NET462соответствующую среду или добавили ее.
Шимми Вайцхандлер 08
1
+1 за попытку собрать это вместе. Решение немного избыточно для хранения массива строк :)
GETah 02