How to store JSON in an entity field with EF Core?

前端 未结 10 434
死守一世寂寞
死守一世寂寞 2020-11-29 17:13

I am creating a reusable library using .NET Core (targeting .NETStandard 1.4) and I am using Entity Framework Core (and new to both). I have an entity class that looks like

相关标签:
10条回答
  • 2020-11-29 17:57

    @Michael's answer got me on track but I implemented it a little differently. I ended up storing the value as a string in a private property and using it as a "Backing Field". The ExtendedData property then converted JObject to a string on set and vice versa on 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();
            }
        }
    }
    

    To set _extendedData as a backing field, I added this to my context:

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

    Update: Darren's answer to use EF Core Value Conversions (new to EF Core 2.1 - which didn't exist at the time of this answer) seems to be the best way to go at this point.

    0 讨论(0)
  • 2020-11-29 17:58

    Its simple

    public class Person
    {
        [Required]
        [MaxLength(50)]
        public string FirstName { get; set; }
    
        [Required]
        [MaxLength(50)]
        public string LastName { get; set; }
    
        [Required]
        public DateTime DateOfBirth { get; set; }
    
        public string AddressesJson { get; set; }   //save this json properity in table.
    
        [NotMapped]
        public List<Address> Addresses 
        {
           get => !string.IsNullOrEmpty(AddressesJson) ? JsonConvert.DeserializeObject<List<Address>>(AddressesJson) : null; 
        }    
    }
    
    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; }
    }
    
    0 讨论(0)
  • 2020-11-29 18:03

    The key to making the the Change Tracker function correctly is to implement a ValueComparer as well as a ValueConverter. Below is an extension to implement such:

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

    Example of how this works.

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

    This will make the ChangeTracker function correctly.

    0 讨论(0)
  • 2020-11-29 18:03

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

    Create an attribute to handle the properties of the entities.

    
    public class HasJsonConversionAttribute : System.Attribute
        {
    
        }
    

    Create extention class to find Josn properties

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

    Entity example

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

    Download Source

    0 讨论(0)
  • 2020-11-29 18:11

    For developers, who work with EF Core 3.1 and meet such error ("The entity type 'XXX' requires a primary key to be defined. If you intended to use a keyless entity type call 'HasNoKey()'.") the solution is just to move .HasConversion() method with it's lambda from: public class OrderConfiguration : IEntityTypeConfiguration to: protected override void OnModelCreating(ModelBuilder modelBuilder) //in YourModelContext : DbContext class.

    0 讨论(0)
  • 2020-11-29 18:12

    For those using EF 2.1 there is a nice little NuGet package EfCoreJsonValueConverter that makes it pretty simple.

    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();
        }
    }
    
    0 讨论(0)
提交回复
热议问题