Kerangka Kerja Entitas - Kode Pertama - Tidak Dapat Menyimpan Daftar <String>

106

Saya menulis kelas seperti itu:

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

dan

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

Setelah menjalankan kode:

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

data saya disimpan tetapi hanya file Id. Saya tidak memiliki tabel atau hubungan apa pun yang berlaku untuk daftar String .

Apa yang saya lakukan salah? Saya juga mencoba membuat Strings virtual tetapi tidak mengubah apa pun.

Terima kasih untuk bantuannya.

Paul
sumber
3
Bagaimana Anda mengharapkan List <sting> disimpan ke dalam db? Itu tidak akan berhasil. Ubah menjadi string.
Wiktor Zychla
4
Jika Anda memiliki daftar, itu harus mengarah ke beberapa entitas. Untuk EF untuk menyimpan daftarnya, dibutuhkan tabel kedua. Di tabel kedua, itu akan meletakkan semuanya dari daftar Anda, dan menggunakan kunci asing untuk menunjuk kembali ke Testentitas Anda . Jadi buatlah entitas baru dengan Idproperty dan MyStringproperty, lalu buat daftarnya.
Daniel Gabriel
1
Benar ... Ini tidak dapat disimpan di db secara langsung tetapi saya berharap Entity Framework membuat entitas baru untuk melakukannya sendiri. Terima kasih atas komentar anda
Paul

Jawaban:

161

Kerangka Kerja Entitas tidak mendukung kumpulan tipe primitif. Anda dapat membuat entitas (yang akan disimpan ke tabel yang berbeda) atau melakukan beberapa pemrosesan string untuk menyimpan daftar Anda sebagai string dan mengisi daftar setelah entitas terwujud.

Pawel
sumber
bagaimana jika suatu entitas berisi Daftar entitas? bagaimana pemetaan akan disimpan?
A_Arnold
Tergantung - kemungkinan besar ke tabel terpisah.
Pawel
dapat mencoba membuat serial dan kemudian mengompres dan menyimpan teks yang diformat json, atau mengenkripsi dan menyimpannya jika diperlukan. dengan cara apa pun Anda tidak dapat memiliki kerangka kerja yang melakukan pemetaan tabel tipe kompleks untuk Anda.
Niklas
90

EF Core 2.1+:

Properti:

public string[] Strings { get; set; }

OnModelCreating:

modelBuilder.Entity<YourEntity>()
            .Property(e => e.Strings)
            .HasConversion(
                v => string.Join(',', v),
                v => v.Split(',', StringSplitOptions.RemoveEmptyEntries));
Sasan
sumber
5
Solusi hebat untuk EF Core. Meskipun tampaknya memiliki masalah sedikit pun konversi char ke string. Saya harus mengimplementasikannya seperti: .HasConversion (v => string.Join (";", v), v => v.Split (new char [] {';'}, StringSplitOptions.RemoveEmptyEntries));
Peter Koller
8
Ini adalah satu-satunya jawaban IMHO yang benar. Semua yang lain mengharuskan Anda untuk mengubah model Anda, dan itu melanggar prinsip bahwa model domain harus mengabaikan ketekunan. (Tidak masalah jika Anda menggunakan ketekunan dan model domain yang terpisah, tetapi hanya sedikit orang yang benar-benar melakukannya.)
Marcell Toth
2
Anda harus menerima permintaan edit saya karena Anda tidak dapat menggunakan char sebagai argumen pertama string.Join dan Anda harus memberikan char [] sebagai argumen pertama string.Split jika Anda juga ingin menyediakan StringSplitOptions.
Dominik
2
Di .NET Core Anda bisa. Saya menggunakan bagian kode ini di salah satu proyek saya.
Sasan
2
Tidak tersedia di .NET Standard
Sasan
54

Jawaban ini berdasarkan yang diberikan oleh @Sasan dan @CAD bloke .

Hanya bekerja dengan EF Core 2.1+ (tidak kompatibel .NET Standard) (Newtonsoft JsonConvert)

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

Dengan menggunakan konfigurasi fasih EF Core, kami membuat serial / deserialisasi List ke / dari JSON.

Mengapa kode ini adalah perpaduan sempurna dari semua yang dapat Anda perjuangkan:

  • Masalah dengan jawaban asli Sasn adalah bahwa itu akan menjadi kekacauan besar jika string dalam daftar berisi koma (atau karakter apapun yang dipilih sebagai pembatas) karena itu akan mengubah satu entri menjadi beberapa entri tetapi paling mudah dibaca dan paling ringkas.
  • Masalah dengan jawaban pria CAD adalah bahwa itu jelek dan membutuhkan model untuk diubah yang merupakan praktek desain yang buruk (lihat komentar Marcell Toth tentang jawaban Sasan ). Tapi itu satu-satunya jawaban yang aman untuk data.
Mathieu VIALES
sumber
7
Bravo, ini mungkin seharusnya jawaban yang diterima
Shirkan
1
Saya berharap ini berhasil di .NET Framework & EF 6, ini adalah solusi yang sangat elegan.
Cowok CAD
Ini adalah solusi yang luar biasa. Terima kasih
Marlon
Apakah Anda mampu melakukan kueri di bidang itu? Upaya saya telah gagal total: var result = await context.MyTable.Where(x => x.Strings.Contains("findme")).ToListAsync();tidak menemukan apa pun.
Nicola Iarocci
3
Untuk menjawab pertanyaan saya sendiri, mengutip dokumen : "Penggunaan konversi nilai dapat memengaruhi kemampuan EF Core untuk menerjemahkan ekspresi ke SQL. Peringatan akan dicatat untuk kasus seperti itu. Penghapusan batasan ini sedang dipertimbangkan untuk rilis mendatang." - Akan tetap menyenangkan.
Nicola Iarocci
44

Saya tahu ini adalah pertanyaan lama, dan Pawel telah memberikan jawaban yang benar , saya hanya ingin menunjukkan contoh kode cara melakukan pemrosesan string, dan menghindari kelas tambahan untuk daftar tipe 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(); }
    }
}
acak
sumber
1
Mengapa tidak menggunakan metode statis daripada menggunakan properti publik? (Atau apakah saya menunjukkan bias pemrograman prosedural saya?)
Duston
@randoms mengapa perlu mendefinisikan 2 daftar? satu sebagai properti dan satu sebagai daftar sebenarnya? Saya akan sangat menghargai jika Anda juga dapat menjelaskan cara kerja pengikatan di sini, karena solusi ini tidak berfungsi dengan baik untuk saya, dan saya tidak dapat menemukan pengikatan di sini. Terima kasih
LiranBo
2
ada satu daftar pribadi, yang memiliki dua properti publik terkait, Strings, yang akan Anda gunakan dalam aplikasi Anda untuk menambah dan menghapus string, dan StringsAsString yang merupakan nilai yang akan disimpan ke db, sebagai daftar yang dipisahkan koma. Saya tidak begitu yakin apa yang Anda tanyakan, bindingnya adalah private list _strings, yang menghubungkan dua properti publik menjadi satu.
acak
1
Harap diingat bahwa jawaban ini tidak lepas ,(koma) dalam string. Jika sebuah string dalam daftar berisi satu atau lebih ,(koma) string tersebut dipecah menjadi beberapa string.
Jogge
2
Dalam string.Joinkoma harus diapit oleh tanda kutip ganda (untuk string), bukan tanda kutip tunggal (untuk karakter). Lihat msdn.microsoft.com/en-us/library/57a79xd0(v=vs.110).aspx
Michael Brandon Morris
29

JSON.NET untuk menyelamatkan.

Anda membuat serial ke JSON untuk bertahan dalam Database dan Deserialisasi untuk menyusun kembali koleksi .NET. Ini tampaknya berkinerja lebih baik dari yang saya harapkan dengan Entity Framework 6 & SQLite. Saya tahu Anda memintanya, List<string>tetapi berikut adalah contoh koleksi yang bahkan lebih kompleks yang berfungsi dengan baik.

Saya menandai properti yang dipertahankan dengan [Obsolete]sehingga akan sangat jelas bagi saya bahwa "ini bukan properti yang Anda cari" dalam proses pengkodean normal. Properti "nyata" diberi tag[NotMapped] sehingga kerangka Entitas mengabaikannya.

(tangen tidak terkait): Anda dapat melakukan hal yang sama dengan tipe yang lebih kompleks tetapi Anda perlu bertanya pada diri sendiri apakah Anda hanya membuat kueri properti objek itu terlalu sulit untuk diri Anda sendiri? (ya, dalam kasus saya).

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);
    }
}
Cowok CAD
sumber
Saya menemukan solusi ini cukup jelek, tetapi sebenarnya itu satu-satunya solusi yang waras. Semua opsi yang menawarkan untuk bergabung dengan daftar menggunakan karakter apa pun dan kemudian membaginya kembali dapat berubah menjadi kekacauan liar jika karakter pemisah disertakan dalam string. Json seharusnya jauh lebih waras.
Mathieu VIALES
1
Saya akhirnya membuat jawaban yang merupakan "penggabungan" dari yang satu ini dan yang lainnya untuk memperbaiki setiap masalah jawaban (keburukan / keamanan data) menggunakan poin kuat yang lain.
Mathieu VIALES
13

Hanya untuk menyederhanakan -

Kerangka entitas tidak mendukung primitif. Anda bisa membuat kelas untuk membungkusnya atau menambahkan properti lain untuk memformat daftar sebagai string:

public ICollection<string> List { get; set; }
public string ListString
{
    get { return string.Join(",", List); }
    set { List = value.Split(',').ToList(); }
}
Adam Tal
sumber
1
Ini jika item daftar tidak dapat berisi string. Jika tidak, Anda harus menghindarinya. Atau untuk membuat serial / deserialisasi daftar untuk situasi yang lebih kompleks.
Adam Tal
3
Juga, jangan lupa untuk menggunakan [NotMapped] di properti ICollection
Ben Petersen
7

Tentu Pawel sudah memberikan jawaban yang benar . Tetapi saya menemukan dalam posting ini bahwa sejak EF 6+ dimungkinkan untuk menyimpan properti pribadi. Jadi saya lebih suka kode ini, karena Anda tidak dapat menyimpan String dengan cara yang salah.

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
sumber
6
Bagaimana jika string berisi koma?
Chalky
4
Saya tidak akan merekomendasikan melakukannya dengan cara ini. StringsAsStringshanya akan diperbarui ketika Strings referensi diubah, dan satu-satunya waktu dalam contoh Anda yang terjadi adalah saat penugasan. Menambahkan atau menghapus item dari Stringsdaftar Anda setelah tugas tidak akan memperbarui StringsAsStringsvariabel dukungan. Cara yang tepat untuk mengimplementasikan ini adalah dengan mengekspos StringsAsStringssebagai tampilan Stringsdaftar, bukan sebaliknya. Gabungkan nilai-nilai tersebut di dalam getpengakses StringsAsStringsproperti, dan pisahkan di setpengakses.
jduncanator
Untuk menghindari penambahan properti pribadi (yang tidak bebas efek samping) buat penyetel properti serial menjadi pribadi. jduncanator tentu saja benar: jika Anda tidak menangkap manipulasi daftar (gunakan ObservableCollection?), perubahan tidak akan diperhatikan oleh EF.
Leonidas
Seperti yang disebutkan @jduncanator, solusi ini tidak berfungsi ketika modifikasi Daftar dibuat (mengikat dalam MVVM misalnya)
Ihab Hajj
7

Sedikit tweaker @Mathieu Viales 's jawaban , di sini adalah potongan NET Standard kompatibel menggunakan serializer System.Text.Json baru sehingga menghilangkan ketergantungan pada 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));

Perhatikan bahwa sementara argumen kedua di keduanya Serialize()dan Deserialize()biasanya opsional, Anda akan mendapatkan kesalahan:

Pohon ekspresi tidak boleh berisi panggilan atau pemanggilan yang menggunakan argumen opsional

Secara eksplisit mengaturnya ke default (null) untuk setiap membersihkannya.

Xaniff
sumber
3

Anda dapat menggunakan ScalarCollectioncontainer ini yang membatasi array dan menyediakan beberapa opsi manipulasi ( Gist ):

Pemakaian:

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

Kode:

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
sumber
8
terlihat agak terlalu direkayasa ?!
Falco Alexander
1
@FalcoAlexander Saya telah memperbarui posting saya ... Mungkin sedikit bertele-tele tetapi berhasil. Pastikan Anda mengganti NET462dengan lingkungan yang sesuai atau menambahkannya ke dalamnya.
Shimmy Weitzhandler
1
1 untuk upaya menyatukan ini. Solusinya agak berlebihan untuk menyimpan array string :)
GETah