I have a generic EF BaseEntityConfiguration class that is used for setting up base properties (like the primary key, properties used for soft deletion, query filters, etc.) and a derived configuration for an entity that stores System.Type and a JSON property. If I don't use the generic class and just implement the IEntityTypeConfiguration then the value conversion works and there are no errors. However if I inherit from the base class, I get EF Core issues about saving Type and object without any conversion. Other configurations that inherit from the base class and don't need conversions work fine.
The error:
Error: The property 'MessageLog.Data' could not be mapped because it is of type 'object', which is not a supported primitive type or a valid entity type. Either explicitly map this property, or ignore it using the '[NotMapped]' attribute or by using 'EntityTypeBuilder.Ignore' in 'OnModelCreating'.
public class MessageLogConfiguration
//: IEntityTypeConfiguration<MessageLog>
: BaseEntityConfiguration<MessageLog, int>
{
public MessageLogConfiguration(ILogger<MessageLogConfiguration> logger)
: base(logger)
{ }
public override void Configure(EntityTypeBuilder<MessageLog> builder)
{
base.Configure(builder);
//builder
// .HasKey(x => x.Id);
builder
.Property(m => m.MessageId)
.IsRequired();
builder
.Property(m => m.Data)
.HasJsonConversion()
.IsRequired();
builder
.Property(m => m.Type)
.IsRequired()
.HasConversion(
t => t.AssemblyQualifiedName,
t => Type.GetType(t!)!);
builder.HasIndex(m => m.MessageId).IsUnique();
}
}
public abstract class BaseEntityConfiguration<TEntity, TId> : IEntityTypeConfiguration<TEntity>
where TEntity : Entity<TId>
where TId : struct
{
protected BaseEntityConfiguration(ILogger<BaseEntityConfiguration<TEntity, TId>> logger)
{
this.Logger = logger;
}
protected ILogger<BaseEntityConfiguration<TEntity, TId>> Logger { get; }
public virtual void Configure(EntityTypeBuilder<TEntity> builder)
{
builder
.HasKey(x => x.Id);
if (typeof(IAuditableEntity).IsAssignableFrom(builder.Metadata.ClrType))
{
Logger.LogTrace($" > Configure properties for {nameof(IAuditableEntity)}'");
builder.Property(nameof(IAuditableEntity.CreatedOn)).IsRequired().ValueGeneratedOnAdd();
builder.Property(nameof(IAuditableEntity.CreatedBy)).IsRequired().HasMaxLength(255);
builder.Property(nameof(IAuditableEntity.ModifiedOn)).IsRequired(false);
builder.Property(nameof(IAuditableEntity.ModifiedBy)).IsRequired(false).HasMaxLength(255);
}
if (typeof(ISoftDeletableEntity).IsAssignableFrom(builder.Metadata.ClrType))
{
Logger.LogTrace($" > Configure properties for {nameof(ISoftDeletableEntity)}'");
builder.Property(nameof(ISoftDeletableEntity.DeletedAt)).IsRequired(false);
builder.Property(nameof(ISoftDeletableEntity.DeletedBy)).IsRequired(false);
builder.HasQueryFilter(m => EF.Property<int?>(m, nameof(ISoftDeletableEntity.DeletedBy)) == null);
}
}
}
public class MessageLog : AuditableEntity<int>
{
public MessageLog(string messageId, object data, MessageLogType messageLogType)
{
this.MessageId = messageId;
this.Type = data.GetType();
this.Data = data;
this.MessageLogType = messageLogType;
}
private MessageLog(string messageId)
{
this.MessageId = messageId;
this.Type = default!;
this.Data = default!;
this.MessageLogType = default!;
}
public string MessageId { get; private set; }
public Type Type { get; private set; }
public MessageLogType MessageLogType { get; private set; }
public object Data { get; private set; }
}
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;
}
}
DbContext
protected override void OnModelCreating(ModelBuilder builder)
{
builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());
base.OnModelCreating(builder);
}
