实体框架-代码优先-不能存储列表 < 字符串 >

我写过这样的课程:

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,但它没有改变任何东西。

谢谢你的帮助。

150315 次浏览

实体框架不支持基元类型的集合。您可以创建一个实体(它将被保存到另一个表中) ,也可以进行一些字符串处理,将列表保存为字符串,并在实体具体化后填充列表。

我知道这是一个老问题,而且 帕维尔给出了正确的答案,我只是想展示一个代码示例,说明如何进行一些字符串处理,并避免为基元类型的列表添加额外的类。

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

简而言之

实体框架不支持原语。您要么创建一个类来包装它,要么添加另一个属性来将列表格式化为字符串:

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

NET 来拯救。

你把它序列化为 JSON 以便在数据库中保存,然后在 Deserialize 重新建立。NET 集合。这似乎比我在实体框架6和 SQLite 中预期的要好。我知道您要求使用 List<string>,但这里有一个更复杂的集合的例子,它可以正常工作。

我用 [Obsolete]标记了持久化属性,所以在正常的编码过程中,“这不是您正在寻找的属性”对我来说是非常明显的。“ real”属性用 [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);
}
}

当然是 帕维尔给出了正确的答案。 但是我发现在这个 邮寄,因为 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"
};
}
}

您可以使用这个限制数组并提供一些操作选项(要点)的 ScalarCollection容器:

用法:

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

EF Core 2.1 + :

物业:

public string[] Strings { get; set; }

模型创建:

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

更新(2021-02-14)

PostgreSQL 有一个数组数据类型,Npgsql EF Core 提供程序支持这一点。因此,它会自动将 C # 数组和列表映射到 PostgreSQL 数组数据类型,不需要额外的配置。您还可以对数组进行操作,操作将被转换为 SQL。

更多有关 这一页的资料。

这个答案是基于 @ Sasan@ CAD 家伙提供的答案。

如果你想在.NET 标准2中使用这个,或者不想使用 Newtonsoft,请参阅下面的 Xaniff 的回答

只适用于 EF Core 2.1 + (与.NET 标准不兼容)(Newtonsoft JsonConvert)

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

使用 EF Core 流式配置,我们将 List序列化/反序列化到/从 JSON。

为什么这段代码是您可以追求的所有东西的完美组合:

  • Sasn 最初的答案的问题在于,如果列表中的字符串包含逗号(或任何选择作为分隔符的字符) ,它就会变得一团糟,因为它会把一个条目变成多个条目,但它是最容易阅读和最简洁的。
  • CAD 家伙的回答的问题是,它是丑陋的,并要求模型被改变,这是一个糟糕的设计实践(见马赛尔托特的评论 Sasan 的回答)。但这是数据安全的唯一答案。

稍微调整一下 @ Mathieu Viales回答。使用新系统的 NET 标准兼容代码段。短信。从而消除了对 Newtonsoft 的依赖。杰森。

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)可以解决这个问题。

我想补充的是,在使用 Npgsql (PostgreSQL 的数据提供程序)时,实际上支持基本类型的数组和列表:

Https://www.npgsql.org/efcore/mapping/array.html

链接到文档

文档中的例子:

public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }


public ICollection<string> Tags { get; set; }
}

使用 System. Text. Json:

modelBuilder.Entity<Post>()
.Property(e => e.Tags)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
new ValueComparer<ICollection<string>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => (ICollection<string>)c.ToList()));

@ Pawel-m 的解决方案起作用了,我设法让它适用于复杂的数据类型,而不仅仅是 string

public class AddressModel
{
public string City { get; set; }
public string State { get; set; }
}


public class Person
{
public int Id { get; set; }
public ICollection<AddressModel> Addresses { get; set; } = new List<AddressModel>();
}

序列化/反序列化表达式需要发生在 modelBuilder层范围内,这太糟糕了。我更愿意把它放在模型定义中,比如 OnModelBinding()(如果有这样一个选项的话)。我把它放在 DbContext->OnModelCreating()的启动级作用域中。

modelBuilder.Entity<AddressModel>().Property(p => p.Addresses )
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions) null),
v => JsonSerializer.Deserialize<List<AddressModel>>(v, (JsonSerializerOptions) null),
new ValueComparer<ICollection<AddressModel>>(
(c1, c2) => c1.SequenceEqual(c2),
c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
c => c.ToList()));