EF Core 属性值转换在派生实体配置中不起作用

Ves*_*rov 5 .net c# entity-framework entity-framework-core asp.net-core

我有一个通用 EF BaseEntityConfiguration 类,用于设置基本属性(如主键、用于软删除的属性、查询过滤器等)以及存储 System.Type 和 JSON 属性的实体的派生配置。如果我不使用泛型类而仅实现 IEntityTypeConfiguration,则值转换将起作用并且不会出现错误。但是,如果我从基类继承,则会遇到有关在不进行任何转换的情况下保存类型和对象的 EF Core 问题。从基类继承并且不需要转换的其他配置可以正常工作。

错误:

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

        }
    }
Run Code Online (Sandbox Code Playgroud)
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);
            }
        }
    }
Run Code Online (Sandbox Code Playgroud)
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; }
    }
Run Code Online (Sandbox Code Playgroud)
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;
        }
    }
Run Code Online (Sandbox Code Playgroud)

数据库上下文

 protected override void OnModelCreating(ModelBuilder builder)
        {
            builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly());

            base.OnModelCreating(builder);
        }
Run Code Online (Sandbox Code Playgroud)

dgl*_*ano 5

长话短说

尝试在您的实现中添加一个空的构造函数IEntityTypeConfiguration。否则,如果您仍然想在实体类型配置中使用 DI,则可能值得查看此问题


我不认为你的注入记录器IEntityTypeConfiguration会与ApplyConfigurationsFromAssembly. 从该方法的源代码来看,似乎在使用反射搜索配置类时,它需要一个空构造函数,以便可以实例化它们。

ApplyConfigurationsFromAssembly 的 EF 核心源代码

由于您的IEntityTypeConfigurations 缺少默认的空构造函数,因此ApplyConfigurationsFromAssembly可能不会选择它们。

如果您仍然想在实体类型配置中使用 DI,则可能值得查看此问题,其中@ajcvickers给出了如何执行此操作的详细说明。

这是 Github 问题答案代码的副本/意大利面:

public abstract class EntityTypeConfigurationDependency
{
    public abstract void Configure(ModelBuilder modelBuilder);
}

public abstract class EntityTypeConfigurationDependency<TEntity>
    : EntityTypeConfigurationDependency, IEntityTypeConfiguration<TEntity> 
    where TEntity : class
{
    public abstract void Configure(EntityTypeBuilder<TEntity> builder);

    public override void Configure(ModelBuilder modelBuilder) 
        => Configure(modelBuilder.Entity<TEntity>());
}

public class Blog
{
    public int Pk { get; set; }
    public ICollection<Post> Posts { get; set; }
}

public class BlogConfiguration : EntityTypeConfigurationDependency<Blog>
{
    public override void Configure(EntityTypeBuilder<Blog> builder)
    {
        builder.HasKey(e => e.Pk);
    }
}

public class Post
{
    public int Pk { get; set; }
    public Blog Blog { get; set; }
}

public class PostConfiguration : EntityTypeConfigurationDependency<Post>
{
    public override void Configure(EntityTypeBuilder<Post> builder)
    {
        builder.HasKey(e => e.Pk);
    }
}

public class Program
{
    private static ILoggerFactory ContextLoggerFactory
        => LoggerFactory.Create(b => b.AddConsole().SetMinimumLevel(LogLevel.Information));

    public static void Main()
    {
        var services = new ServiceCollection()
            .AddDbContext<SomeDbContext>(
                b => b.UseSqlServer(Your.ConnectionString)
                    .EnableSensitiveDataLogging()
                    .UseLoggerFactory(ContextLoggerFactory));
        
        foreach (var type in typeof(SomeDbContext).Assembly.DefinedTypes
            .Where(t => !t.IsAbstract
                        && !t.IsGenericTypeDefinition
                        && typeof(EntityTypeConfigurationDependency).IsAssignableFrom(t)))
        {
            services.AddSingleton(typeof(EntityTypeConfigurationDependency), type);
        }

        var serviceProvider = services.BuildServiceProvider();
        
        using (var scope = serviceProvider.CreateScope())
        {
            var context = scope.ServiceProvider.GetService<SomeDbContext>();

            context.Database.EnsureDeleted();
            context.Database.EnsureCreated();
        }
    }
}

public class SomeDbContext : DbContext
{
    private readonly IEnumerable<EntityTypeConfigurationDependency> _configurations;

    public SomeDbContext(
        DbContextOptions<SomeDbContext> options,
        IEnumerable<EntityTypeConfigurationDependency> configurations)
        : base(options)
    {
        _configurations = configurations;
    }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        foreach (var entityTypeConfiguration in _configurations)
        {
            entityTypeConfiguration.Configure(modelBuilder);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)