【问题标题】:EF Core property value conversion not working in a derived entity configurationEF Core 属性值转换在派生实体配置中不起作用
【发布时间】:2021-02-04 17:57:42
【问题描述】:

我有一个通用的 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();

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

数据库上下文

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

            base.OnModelCreating(builder);
        }

【问题讨论】:

  • 我不确定你所说的其他场景是什么意思,但不是这个特定的场景。很明显Data 属性没有被忽略,它的类型是object,所以这个异常是有意义的。您能否指出任何其他工作场景,其中您有一个 object 属性映射到您的数据库表中的某个列?
  • 问题是我不想忽略它。对象类型的 Data 属性通过 HasJsonConversion() 更改为 db 模型中的“jsonb”列。如果我直接实现 IEntityTypeConfiguration 则工作正常,但如果我继承实现此接口的基类,则由于某种原因值转换器被忽略。 Type 属性也是如此,但只有错误有点不同(类似于“您需要为此实体定义主键。”)。
  • 您可以尝试调试派生配置以查看它是否实际运行?我怀疑ApplyConfigurationsFromAssembly 有问题,出于这个可能的原因,请尝试直接应用您的派生配置。
  • 我刚刚尝试了一些关于应用您所说的配置的方法,在手动添加派生配置的过程中,我从 BaseEntityConfiguration 的构造函数中删除了记录器。这以某种方式解决了问题,并且它也可以与 ApplyConfigurationsFromAssembly 一起使用。我只是不确定为什么此配置只能与空构造函数一起使用,而其他没有任何对象属性(或值转换)的配置与构造函数中传递的记录器一起使用。
  • 我认为ApplyConfigurationsFromAssembly 不支持IEntityTypeConfiguration 的DI,因为它使用无参数构造函数创建实例。它也可能支持 DI,但在这种情况下,您可以尝试手动将派生配置类型注册为单例,以查看它是否有效(只是猜测,这种情况很棘手,因为通常我们有一个用于配置类的无参数构造函数并且它只是有效)

标签: c# .net entity-framework asp.net-core entity-framework-core


【解决方案1】:

TL;DR

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


我认为IEntityTypeConfiguration 中注入的记录器不会与ApplyConfigurationsFromAssembly 一起工作。从该方法的the source code 看来,在使用反射搜索配置类时,它需要一个空的构造函数以便实例化它们。

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

如果您仍想在实体类型配置中使用 DI,可能值得查看 this issue,其中 @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);
        }
    }
}

【讨论】:

  • 谢谢,伙计!您和@KingKong 的回答非常有帮助,让我走上了正确的道路。我想整个问题是你不能用默认的实体配置注入一些东西。我最终创建了自己的实体配置实现,它有自己的接口和一个带有虚拟配置方法的基类。然后我注册了每个配置并将它们注入 DbContext。在 DbContext 的 OnModelCreating 中,我对它们进行了搜索并在每个上调用了 Configure()。这样就成功了,这样我就可以在配置中注入尽可能多的东西。
  • @VeselinTodorov 很高兴听到这个消息! ?
猜你喜欢
  • 1970-01-01
  • 2021-03-20
  • 2018-09-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-06-04
相关资源
最近更新 更多