2

I was trying to accomplish the conversion dynamically across all of the models I have in my project:

DbContext.cs

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var entityTypes = modelBuilder.Model.GetEntityTypes().ToList();
    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);
}

Then created a data annotation attribute to mark my fields in my model as "Json"

public class HasJsonConversionAttribute : Attribute {}

This is my extension method used to convert the Json to object and back to Json

public static class SqlExtensions
{
    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(propertyBuilder.Metadata.ClrType, typeof(string)), new object[]
        {
            Expression.Lambda( expression1,parameter1),
            Expression.Lambda( expression2,parameter2),
            (ConverterMappingHints) null
        });

        propertyBuilder.HasConversion(converter as ValueConverter);
        return propertyBuilder;
    }
}

For simplicity I'm using this User model:

public class User : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder)
    {
        // Apply some settings defined in custom annotations in the model properties
        //builder.ApplyCustomAnnotationsAndConfigs(this);
    }
    
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string Username { get; set; }

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

and This is the class that I want to convert to and from JSON:

public class LocalizedName
{
    public string Value { get; set; }
    public string Code { get; set; }
}

Now here's the problem I'm facing, it keeps on detecting LocalizedName object as another model that doesn't have a Key and throws an error telling me to add a Key/PK even though this is not flagged as a model.

Now if I execute this from the User -> Configure() it will work BUT it'll show me other issues like the model loses its own relationships and associations with other models and now it throws me other set of errors which I didn't even have in the first place.

I've also noticed that EF removes LocalizedName from the property list and shows it under Navigation properties list. And lastly, I've checked the OnModelCreating -> modelBuilder.Model.GetEntityTypes() and noticed that EF is treating it as a new Model which is kinda weird.

Is there a way I can tell EF to mark this as a string/json field instead of EF auto-assuming it's a model?

Any help would be appreciated.

Desolator
  • 22,411
  • 20
  • 73
  • 96

3 Answers3

2

So I ended up using this NuGet package from https://github.com/Innofactor/EfCoreJsonValueConverter

Then in my DbContext.cs:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Ignore<LocalizedName>(); //<--- Ignore this model from being added by convention
    modelBuilder.AddJsonFields(); //<--- Auto add all json fields in all models
}

Then in my User model:

public class User : IEntityTypeConfiguration<User>
{
    public void Configure(EntityTypeBuilder<User> builder) {}
    
    [Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
    public int Id { get; set; }
    public string Username { get; set; }

    [JsonField] //<------ Add this attribute
    public List<LocalizedName> Values { get; set; }
}

Now it works as expected.

Desolator
  • 22,411
  • 20
  • 73
  • 96
0

You can ignore your entity in OnModelCreating like this:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
   modelBuilder.Ignore<LocalizedName>();
   //rest of your code
}

And for ignore all model that has HasJsonConversion Attribute you can do this (I didn't test this):

       var entityTypes = modelBuilder.Model.GetEntityTypes().ToList();
        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();

                modelBuilder.Ignore(property.PropertyType);
            }
        }
sa-es-ir
  • 3,722
  • 2
  • 13
  • 31
  • If I ignore the entity it'll not be mapped to the database field. I already did that. Also it loses the associations – Desolator Jan 16 '21 at 07:11
  • @SolidSnake You want create json string from property so no need to be an entity as a table or maybe should use different model(not entity) that has json attribute and can ignore it – sa-es-ir Jan 16 '21 at 07:36
  • That means I have to create another field with string type to work and map this LocalizedName field to it. This is kinda adds more logic to the code and defeats the purpose of using HasConversion – Desolator Jan 16 '21 at 21:01
  • @SolidSnake No I said finally you have a column in table with nvarchar(string) type becuase it is a json. For your model you can use a non-entity model(models that have not a table) for those properties that have json conversion attribute and tell ef to ignore these models. – sa-es-ir Jan 17 '21 at 12:18
0
 public static class JsonConversionExtensions
{

    public static PropertyBuilder<T> HasJsonConversion<T>(this PropertyBuilder<T> propertyBuilder, string columnType = null, string columnName = "", JsonSerializerSettings settings = null)
    {
        var converter = new ValueConverter<T, string>(
            v => JsonConvert.SerializeObject(v, settings),
            v => JsonConvert.DeserializeObject<T>(v, settings));

        var comparer = new ValueComparer<T?>(
              (l, r) => JsonConvert.SerializeObject(l, settings) == JsonConvert.SerializeObject(r, settings),
              v => v == null ? 0 : JsonConvert.SerializeObject(v, settings).GetHashCode(),
              v => JsonConvert.DeserializeObject<T?>(JsonConvert.SerializeObject(v, settings), settings));

        propertyBuilder.HasConversion(converter);
        if (columnType != null) propertyBuilder.HasColumnType(columnType);

        if (columnName == "")
            propertyBuilder.HasColumnName($"Json_{propertyBuilder.Metadata.Name}");
        else if (columnName != null)
            propertyBuilder.HasColumnName(columnName);

        propertyBuilder.Metadata.SetValueConverter(converter);
        propertyBuilder.Metadata.SetValueComparer(comparer);

        return propertyBuilder;
    }

    public static PropertyBuilder<TEnum?> HasByteEnumConversion<TEnum>(this PropertyBuilder<TEnum?> propertyBuilder) where TEnum : struct
    {
        propertyBuilder
                .HasConversion(x => Convert.ToByte(x), x => Enum.Parse<TEnum>(x.ToString()));

        return propertyBuilder;
    }

    public static PropertyBuilder<TEnum> HasByteEnumConversion<TEnum>(this PropertyBuilder<TEnum> propertyBuilder) where TEnum : struct //Enum
    {
        propertyBuilder
                .IsRequired()
                .HasValueGenerator((p, e) => throw new NullReferenceException($@"The column ""{ p.Name }"" in ""{e.DisplayName()}"" does not allow nulls."))
                .HasConversion(x => Convert.ToByte(x), x => Enum.Parse<TEnum>(x.ToString()));

        return propertyBuilder;
    }
}