如何使用EF Core将JSON存储在实体字段中?


74

我正在使用.NET Core(针对.NETStandard 1.4)创建可重用的库,并且正在使用Entity Framework Core(这两个都是新的)。我有一个看起来像这样的实体类:

public class Campaign
{
    [Key]
    public Guid Id { get; set; }

    [Required]
    [MaxLength(50)]
    public string Name { get; set; }

    public JObject ExtendedData { get; set; }
}

并且我有一个定义DbSet的DbContext类:

public DbSet<Campaign> Campaigns { get; set; }

(我还在DI上使用Repository模式,但我认为这不相关。)

我的单元测试给我这个错误:

System.InvalidOperationException:无法确定类型为“ JContainer”的导航属性“ JToken.Parent”表示的关系。手动配置关系,或从模型中忽略此属性。

有没有办法表明这不是关系但是应该存储为大字符串?


我想你应该改变的类型ExtendedDatastring,然后存储字符串化JSON
迈克尔

1
@Michael我曾考虑过这一点,但我想确保它始终是有效的JSON。
Alex

@Alex-如果这是检查其是否为有效JSON的唯一问题,为简单起见,您可以将解析添加到属性的set-method(即尝试反序列化)-并抛出InvalidDataException或JsonSerializationException无效。
马特

Answers:


149

要以不同的方式回答这一问题。

理想情况下,领域模型应该不知道如何存储数据。添加支持字段和额外的[NotMapped]属性实际上是在将域模型耦合到基础架构。

记住-您的域名是王,而不是数据库。该数据库仅用于存储域的一部分。

相反,您可以HasConversion()EntityTypeBuilder对象类型和JSON之间进行转换。

给定这两个领域模型:

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

我仅添加了域感兴趣的属性-并没有添加数据库感兴趣的详细信息;IE浏览器没有[Key]

我的DbContext有以下IEntityTypeConfigurationPerson

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).HasConversion(
            v => JsonConvert.SerializeObject(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }),
            v => JsonConvert.DeserializeObject<IList<Address>>(v, new JsonSerializerSettings { NullValueHandling = NullValueHandling.Ignore }));
    }
}

使用这种方法,您可以将域与基础架构完全脱钩。不需要所有的支持字段和额外的属性。


46
请谨慎使用此方法:仅当将字段分配给EF Core时,EF Core才会将其标记为已修改。因此,如果使用person.Addresses.Add,则不会将该实体标记为已更新;您需要调用属性设置器person.Addresses = updatedAddresses
Métoule

1
@DarrenWainwright我同意模型应该对它的存储方式一无所知,但是不幸的是,如果仍然走IEntityTypeConfiguration路线,因为没有等效项,我们将被困在[NotMapped]中。除非使用EntityTypeBuilder在任何地方都看不到它。
基尔霍夫

6
此方法对我有用,但您也需要应用此配置!modelBuilder.ApplyConfiguration(new PersonsConfiguration());
CodeThief

1
@Métoule-我的解决方案解决了您需要ValueComparer的问题。请参阅下面的答案。
罗伯特·拉布

3
在ef core 3.1上尝试此操作,得到以下错误。关于如何解决的任何想法?“实体类型'Address'需要定义一个主键。如果您打算使用无密钥实体类型,请调用'HasNoKey()'。”
Nairooz NIlafdeen

34

@Michael的回答使我步入正轨,但我在实现方面有所不同。我最终将值作为字符串存储在私有属性中,并将其用作“背景字段”。然后,ExtendedData属性在set上将JObject转换为字符串,在get上将其转换为字符串:

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

为了设置_extendedData为背景字段,我将其添加到上下文中:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Campaign>()
        .Property<string>("ExtendedDataStr")
        .HasField("_extendedData");
}

更新:达伦(Darren)使用EF核心价值转换的答案(是EF Core 2.1的新功能-在此答案发布之时尚不存在)似乎是此时的最佳选择。


这也可能是一个选择。这两种方法都在EF核心文档中。后场看起来更干净,但以后可能不太容易理解:)
Michael

2
两者都是正确的方法。唯一的区别是,由于EF内核支持阴影属性,因此后备场方法将利用阴影属性,并避免在域模型中具有额外的属性。:)
Smit

如何与jQuery一起使用?
Kavin404 '19

3
@ Kavin404-您不要将其与JQuery一起使用。EF Core是.NET数据访问技术,而JQuery是前端(浏览器)JavaScript框架。
亚历克斯

25

正确执行Change Tracker功能的关键是实现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 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>>();
    }
}

这将使ChangeTracker正常运行。


不错的解决方案!尝试了一下,就可以了。不过,其中一个漏掉了代码;该转换器具有类型约束,class因此您不能在上使用它IList<Address>。它必须是像的具体类型IList<Address>。这里要记住的重要一点是,您只能使用手写SQL查询JSON数据,从而导致带有CTE等的相当复杂的SQL。
Marnix van Valen

2
不错,我修复了代码。这实际上是我正在写的新文章和新的nuget包的一部分,以解决此问题。我正在研究一种通用的解决方案,用于添加元数据而无需不断修改实体。我使用Dictionary <string,object>通过名称值对存储元数据。是的,此解决方案无法轻松查询元数据。
罗伯特·拉布

@RobertRaboud:那篇文章或nuget包有什么新闻吗?会非常感激
马克斯R.

@RobertRaboud解决方案超级好!作为所有人的资源,文章会很棒–但是我没有被出售,也不认为一般的JSON blob是行之有效的方法。你仍然想保持小领域,并记住,而EF Core不具有JSON映射时,SQL Server本身并(你可以写你自己的) -所以经得起时间的考验,最好是有重量更轻JSON领域不一个大的<string,object>字典。当然,这是个例外,但是对于5-10%的情况,可能会更多,而不是+ 90%
Marchy

4
对于SQL Server,我必须更改jsonbnvarchar(max)。所推荐的微软
勒纳

6

对于使用EF 2.1的用户,有一个不错的NuGet软件包EfCoreJsonValueConverter,它非常简单。

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

5

你可以尝试这样的事情吗?

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

这是迁移输出:

ExtraData = table.Column<string>(nullable: true),

我认为这是正确的方向-看起来我可以使用“背景字段”来完成此任务(docs.microsoft.com/zh-cn/ef/core/modeling/backing-field)。
Alex

我们是否需要将extraData注释为后备字段?(根据docs.microsoft.com/en-us/ef/core/modeling/…)但是,该[BackingField(...)]属性不是此处描述的常规属性的一部分:docs.microsoft.com/en-us/ef/core/建模/…
马特

该解决方案是为较早的EF Core设计的。现在,您可以将extraData注释为后备字段(但您需要稍微更改逻辑),甚至可以使用阴影属性(docs.microsoft.com/en-us/ef/core/modeling/shadow-properties)。或者,如果仍然有效,则可以按原样使用。
迈克尔

2

// DbContext

  protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            var entityTypes = modelBuilder.Model.GetEntityTypes();
            foreach (var entityType in entityTypes)
            {
                foreach (var property in entityType.ClrType.GetProperties().Where(x => x != null && x.GetCustomAttribute<HasJsonConversionAttribute>() != null))
                {
                    modelBuilder.Entity(entityType.ClrType)
                        .Property(property.PropertyType, property.Name)
                        .HasJsonConversion();
                }
            }

            base.OnModelCreating(modelBuilder);
        }


创建一个属性来处理实体的属性。


public class HasJsonConversionAttribute : System.Attribute
    {

    }

创建扩展类以查找Josn属性

    public static class ValueConversionExtensions
    {
        public static PropertyBuilder HasJsonConversion(this PropertyBuilder propertyBuilder)
        {
            ParameterExpression parameter1 = Expression.Parameter(propertyBuilder.Metadata.ClrType, "v");

            MethodInfo methodInfo1 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("SerializeObject", types: new Type[] { typeof(object) });
            MethodCallExpression expression1 = Expression.Call(methodInfo1 ?? throw new Exception("Method not found"), parameter1);

            ParameterExpression parameter2 = Expression.Parameter(typeof(string), "v");
            MethodInfo methodInfo2 = typeof(Newtonsoft.Json.JsonConvert).GetMethod("DeserializeObject", 1, BindingFlags.Static | BindingFlags.Public, Type.DefaultBinder, CallingConventions.Any, types: new Type[] { typeof(string) }, null)?.MakeGenericMethod(propertyBuilder.Metadata.ClrType) ?? throw new Exception("Method not found");
            MethodCallExpression expression2 = Expression.Call(methodInfo2, parameter2);

            var converter = Activator.CreateInstance(typeof(ValueConverter<,>).MakeGenericType(typeof(List<AttributeValue>), typeof(string)), new object[]
                {
                    Expression.Lambda( expression1,parameter1),
                    Expression.Lambda( expression2,parameter2),
                    (ConverterMappingHints) null
                });

            propertyBuilder.HasConversion(converter as ValueConverter);

            return propertyBuilder;
        }
    }

实体示例

 public class Attribute
    {
        [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
        public int Id { get; set; }
        public string Name { get; set; }

        [HasJsonConversion]
        public List<AttributeValue> Values { get; set; }
    }

    public class AttributeValue
    {
        public string Value { get; set; }
        public IList<AttributeValueTranslation> Translations { get; set; }
    }

    public class AttributeValueTranslation
    {
        public string Translation { get; set; }

        public string CultureName { get; set; }
    }

下载源


1

这是我用过的东西

模型

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

0

@Métoule评论

请谨慎使用此方法:仅当字段分配给时,EF Core才会将实体标记为已修改。因此,如果您使用person.Addresses.Add,则该实体不会被标记为已更新;您需要致电属性设置者person.Addresses = UpdatedAddresses。

使我采用了另一种方法,以便使这一事实显而易见:使用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;
}

从理论上讲,您可以这样做:

campaign.GetExtendedData().Add(something);

但是,更明显的是,它并没有按照您的想法做™。

如果您使用数据库优先并且为EF使用某种类自动生成器,则这些类通常会声明为partial,因此您可以将这些内容添加到一个单独的文件中,下次您不会被吹走。从数据库更新您的类。


0

对于使用EF Core 3.1并遇到此类错误的开发人员(“实体类型'XXX'需要定义主键。如果您打算使用无键实体类型调用'HasNoKey()”。)只是将带有其lambda的.HasConversion()方法从:公共类OrderConfiguration:IEntityTypeConfiguration移至:保护重写void OnModelCreating(ModelBuilder modelBuilder)//在YourModelContext:DbContext类中。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.