C# 如何使用EF Core将JSON存储在实体字段中?
我正在使用.NETCore(目标是.netstandard1.4)和EntityFrameworkCore(两者都是新的)创建一个可重用的库。我有一个实体类,看起来像:C# 如何使用EF Core将JSON存储在实体字段中?,c#,json.net,entity-framework-core,C#,Json.net,Entity Framework Core,我正在使用.NETCore(目标是.netstandard1.4)和EntityFrameworkCore(两者都是新的)创建一个可重用的库。我有一个实体类,看起来像: public class Campaign { [Key] public Guid Id { get; set; } [Required] [MaxLength(50)] public string Name { get; set; } public JObject Exten
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
我有一个DbContext类,它定义了DbSet:
public DbSet<Campaign> Campaigns { get; set; }
公共数据库集活动{get;set;}
(我也将存储库模式用于DI,但我认为这与此无关。)
我的单元测试给出了以下错误:
System.InvalidOperationException:无法确定关系
由类型为的导航属性“JToken.Parent”表示
“JContainer”。手动配置关系或忽略
此属性来自模型
有没有办法表明这不是一个关系,而是应该存储为一个大字符串?您能试试这样的方法吗
[NotMapped]
private JObject extraData;
[NotMapped]
public JObject ExtraData
{
get { return extraData; }
set { extraData = value; }
}
[Column("ExtraData")]
public string ExtraDataStr
{
get
{
return this.extraData.ToString();
}
set
{
this.extraData = JsonConvert.DeserializeObject<JObject>(value);
}
}
[未映射]
私有作业对象外部数据;
[未映射]
公共工作项目外部数据
{
获取{return extraData;}
设置{extraData=value;}
}
[列(“额外数据”)]
公共字符串extradastr
{
得到
{
返回此.extraData.ToString();
}
设置
{
this.extraData=JsonConvert.DeserializeObject(值);
}
}
以下是迁移输出:
ExtraData = table.Column<string>(nullable: true),
ExtraData=table.Column(可空:true),
@Michael的回答让我走上了正轨,但我的实施方式有点不同。最后,我将该值作为字符串存储在私有属性中,并将其用作“备份字段”。然后,ExtendedData属性在set上将JObject转换为字符串,在get上将JObject转换为字符串:
public class Campaign
{
// https://docs.microsoft.com/en-us/ef/core/modeling/backing-field
private string _extendedData;
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
[NotMapped]
public JObject ExtendedData
{
get
{
return JsonConvert.DeserializeObject<JObject>(string.IsNullOrEmpty(_extendedData) ? "{}" : _extendedData);
}
set
{
_extendedData = value.ToString();
}
}
}
更新:Darren关于使用EF Core值转换的回答(EF Core 2.1的新版本-在回答此问题时不存在)似乎是目前最好的方法。将以不同的方式回答此问题 理想情况下,域模型应该不知道数据是如何存储的。添加备份字段和额外的
[NotMapped]
属性实际上是将域模型耦合到基础结构
记住——你的领域才是王者,而不是数据库。数据库只是用来存储您的域的一部分
相反,您可以在EntityTypeBuilder
对象上使用EF Core的HasConversion()
方法在您的类型和JSON之间进行转换
考虑到这两种域模型:
公共类人物
{
公共int Id{get;set;}
[必需]
[MaxLength(50)]
公共字符串名{get;set;}
[必需]
[MaxLength(50)]
公共字符串LastName{get;set;}
[必需]
公共日期时间出生日期{get;set;}
公共IList地址{get;set;}
}
公共课堂演讲
{
公共字符串类型{get;set;}
公共字符串公司{get;set;}
公共字符串编号{get;set;}
公共字符串Street{get;set;}
公共字符串City{get;set;}
}
我只添加了域感兴趣的属性,而没有DB感兴趣的细节;即没有[键]
My DbContext为人员
提供了以下IEntityTypeConfiguration
:
公共类PersonConfiguration:IEntityTypeConfiguration
{
公共void配置(EntityTypeBuilder)
{
//此转换器将执行Json与所需类型之间的转换
属性(e=>e.Addresses).HasConversion(
v=>JsonConvert.SerializeObject(v,新的JsonSerializerSettings{NullValueHandling=NullValueHandling.Ignore}),
v=>JsonConvert.DeserializeObject(v,新的JsonSerializerSettings{NullValueHandling=NullValueHandling.Ignore});
}
}
使用此方法,您可以将域与基础架构完全解耦。不需要所有支持字段和额外属性。对于使用EF 2.1的用户,有一个很好的小NuGet包,使其非常简单
using Innofactor.EfCoreJsonValueConverter;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
public class Campaign
{
[Key]
public Guid Id { get; set; }
[Required]
[MaxLength(50)]
public string Name { get; set; }
public JObject ExtendedData { get; set; }
}
public class CampaignConfiguration : IEntityTypeConfiguration<Campaign>
{
public void Configure(EntityTypeBuilder<Campaign> builder)
{
builder
.Property(application => application.ExtendedData)
.HasJsonValueConversion();
}
}
使用Innofactor.EfCoreJsonValueConverter;
使用Microsoft.EntityFrameworkCore;
使用Microsoft.EntityFrameworkCore.Metadata.Builders;
公课运动
{
[关键]
公共Guid Id{get;set;}
[必需]
[MaxLength(50)]
公共字符串名称{get;set;}
public JObject ExtendedData{get;set;}
}
公共类活动配置:IEntityTypeConfiguration
{
公共void配置(EntityTypeBuilder)
{
建设者
.Property(application=>application.ExtendedData)
.HasJsonValueConversion();
}
}
注意此方法:EF Core仅当字段指定给时才将实体标记为已修改。因此,如果使用person.Addresses.Add,实体将不会标记为已更新;您需要调用属性设置程序person.Addresses=UpdateAddresses 这让我采取了一种不同的方法,所以这个事实是显而易见的:使用Getter和Setter方法,而不是属性
public void SetExtendedData(JObject extendedData) {
ExtendedData = JsonConvert.SerializeObject(extendedData);
_deserializedExtendedData = extendedData;
}
//just to prevent deserializing more than once unnecessarily
private JObject _deserializedExtendedData;
public JObject GetExtendedData() {
if (_extendedData != null) return _deserializedExtendedData;
_deserializedExtendedData = string.IsNullOrEmpty(ExtendedData) ? null : JsonConvert.DeserializeObject<JObject>(ExtendedData);
return _deserializedExtendedData;
}
但更清楚的是,这并不像你想象的那样™.
如果您首先使用数据库并使用某种类型的类自动生成器来生成EF,那么这些类通常会声明为
partial
,因此,您可以将这些内容添加到一个单独的文件中,下次从数据库更新类时不会被吹走。正确实现更改跟踪功能的关键是实现ValueComparer和ValueConverter。以下是实现该功能的扩展:
public static class ValueConversionExtensions
{
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<T>(v) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonConvert.SerializeObject(l) == JsonConvert.SerializeObject(r),
v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(),
v => JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(v))
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("jsonb");
return propertyBuilder;
}
}
public静态类ValueConversionExtensions
{
公共静态PropertyBuilder具有JSONversion(此PropertyBuilder PropertyBuilder),其中T:class,new()
{
ValueConverter converter=新的ValueConverter
(
v=>JsonConvert.SerializeObject(v),
v=>JsonConvert.DeserializeObject(v)??新的T()
);
ValueComparer comparer=新的ValueComparer
(
(l,r)=>JsonConve
campaign.GetExtendedData().Add(something);
public static class ValueConversionExtensions
{
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<T>(v) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonConvert.SerializeObject(l) == JsonConvert.SerializeObject(r),
v => v == null ? 0 : JsonConvert.SerializeObject(v).GetHashCode(),
v => JsonConvert.DeserializeObject<T>(JsonConvert.SerializeObject(v))
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("jsonb");
return propertyBuilder;
}
}
public class Person
{
public int Id { get; set; }
[Required]
[MaxLength(50)]
public string FirstName { get; set; }
[Required]
[MaxLength(50)]
public string LastName { get; set; }
[Required]
public DateTime DateOfBirth { get; set; }
public List<Address> Addresses { get; set; }
}
public class Address
{
public string Type { get; set; }
public string Company { get; set; }
public string Number { get; set; }
public string Street { get; set; }
public string City { get; set; }
}
public class PersonsConfiguration : IEntityTypeConfiguration<Person>
{
public void Configure(EntityTypeBuilder<Person> builder)
{
// This Converter will perform the conversion to and from Json to the desired type
builder.Property(e => e.Addresses).HasJsonConversion<IList<Address>>();
}
}
public class FacilityModel
{
public string Name { get; set; }
public JObject Values { get; set; }
}
[Table("facility", Schema = "public")]
public class Facility
{
public string Name { get; set; }
public Dictionary<string, string> Values { get; set; } = new Dictionary<string, string>();
}
this.CreateMap<Facility, FacilityModel>().ReverseMap();
base.OnModelCreating(builder);
builder.Entity<Facility>()
.Property(b => b.Values)
.HasColumnType("jsonb")
.HasConversion(
v => JsonConvert.SerializeObject(v),
v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));
public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder) where T : class, new()
{
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
WriteIndented = true,
AllowTrailingCommas = true,
PropertyNameCaseInsensitive = true
};
ValueConverter<T, string> converter = new ValueConverter<T, string>
(
v => JsonSerializer.Serialize(v, options),
v => JsonSerializer.Deserialize<T>(v, options) ?? new T()
);
ValueComparer<T> comparer = new ValueComparer<T>
(
(l, r) => JsonSerializer.Serialize(l, options) == JsonSerializer.Serialize(r, options),
v => v == null ? 0 : JsonSerializer.Serialize(v, options).GetHashCode(),
v => JsonSerializer.Deserialize<T>(JsonSerializer.Serialize(v, options), options)
);
propertyBuilder.HasConversion(converter);
propertyBuilder.Metadata.SetValueConverter(converter);
propertyBuilder.Metadata.SetValueComparer(comparer);
propertyBuilder.HasColumnType("LONGTEXT");
return propertyBuilder;
}