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

时间:2017-06-29 15:49:46

标签: json.net entity-framework-core

我正在使用.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:无法确定关系   以导航属性为代表的JToken.Parent&#39;类型   &#39; JContainer&#39 ;.手动配置关系,或忽略   这个属性来自模特..

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

10 个答案:

答案 0 :(得分:54)

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

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

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

相反,您可以在HasConversion()对象上使用EF Core的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; }
}

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

我的DbContext对于IEntityTypeConfiguration具有以下Person

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

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

答案 1 :(得分:15)

@ Michael的回答让我走上了正轨,但我的实施方式略有不同。我最终将值作为字符串存储在私有属性中,并将其用作&#34; Backing Field&#34;。然后,ExtendedData属性将JObject转换为set上的字符串,反之亦然:get:

id
message
sender
chat_id

要将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

更新:Darren回答使用EF Core Value Conversions(EF Core 2.1的新功能 - 在此答案时尚未存在)似乎是目前最好的方法。

答案 2 :(得分:4)

你能尝试这样的事吗?

    [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),

答案 3 :(得分:2)

正确执行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正常运行。

答案 4 :(得分:2)

// DbContext

// This is our model

class Task {
  var name = ""
  var checked = false
  var date = Date()
  var category: String
  var number: Int
  var itemID: String?

  public init() {
    self.category = ""
    self.number = 0
  }
}

// Extension for init from firebase response

extension Task {
  convenience init(with firebase: [String: Any]) {
    self.init()
    self.name = (firebase["name"] as? String) ?? ""
  }
}

// We create service for document
// We use this service like an API

final class DocumentService {

  static let shared = DocumentService()
  private let database: FirebaseDatabase
  private var tasks: [[Task]] = []

  public init(database: FirebaseDatabase = FirebaseDatabase()) {
    self.database = database
  }

  func load(in section: Int, completion: @escaping (([Task]) -> Void)) {
    database.loadData(section: section) { [unowned self] tasks in
      self.tasks[section] = tasks.map(Task.init)
      completion(self.tasks[section])
    }
  }

  func check(at indexPath: IndexPath, isChecked: Bool) {
    tasks[indexPath.section][indexPath.row].checked = isChecked
  }

}

// We create firebase database class we can add some features in here

final class FirebaseDatabase {

  func loadData(section: Int, completion: @escaping (([[String: Any]]) -> Void)) {
    // TODO: firebase load data
    let response: [[String: Any]] = [
      ["name": "Stackoverflow"]
    ]
    completion(response)
  }
}

final class TestController: UIViewController {

  private let service = DocumentService.shared

  override func viewDidLoad() {
    super.viewDidLoad()
    service.load(in: 0) { tasks in
      // TODO
    }
  }

}

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

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


创建扩展类以查找Josn属性


public class HasJsonConversionAttribute : System.Attribute
    {

    }

实体示例

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

Download Source

答案 5 :(得分:1)

对于使用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();
    }
}

答案 6 :(得分: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();

DBContext

base.OnModelCreating(builder); 
        builder.Entity<Facility>()
        .Property(b => b.Values)
        .HasColumnType("jsonb")
        .HasConversion(
        v => JsonConvert.SerializeObject(v),
        v => JsonConvert.DeserializeObject<Dictionary<string, string>>(v));

答案 7 :(得分:0)

comment by @Métoule

  

请谨慎使用此方法:仅当字段分配给时,EF Core才会将实体标记为已修改。因此,如果您使用person.Addresses.Add,则该实体不会被标记为已更新;您需要致电属性设置器人员。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,因此您可以将此内容添加到一个单独的文件中,而不会下次您从数据库更新课程时,就不会感到惊讶。

答案 8 :(得分:0)

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

答案 9 :(得分:0)

我根据 Robert Raboud's 的贡献提出了一个解决方案。我所做的改变是我的实现使用了依赖 System.Text.Json 包而不是 Newtonsofts 库的 HasJsonConversion 方法:

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

另请注意,由于我使用的是 MySQL 设置,因此此实现要求列为 LONGTEXT。