Преобразование значения основного свойства EF не работает в конфигурации производной сущности

#c# #.net #entity-framework #asp.net-core #entity-framework-core

#c# #.net #entity-framework #asp.net-core #entity-framework-core

Вопрос:

У меня есть общий класс BaseEntityConfiguration EF, который используется для настройки базовых свойств (например, первичного ключа, свойств, используемых для мягкого удаления, фильтров запросов и т. Д.) И производной конфигурации для объекта, в котором хранится System .Тип и свойство 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;
        }
    }
 

DbContext

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

            base.OnModelCreating(builder);
        }
 

Комментарии:

1. Я не уверен, что вы подразумеваете под другими сценариями, работающими, но не с этим конкретным. Очевидно, что Data свойство не игнорируется, и его тип таков object , что исключение имеет смысл. Не могли бы вы указать какие-либо другие рабочие сценарии, в которых у вас есть свойство object , сопоставленное с некоторым столбцом в вашей таблице БД?

2. Дело в том, что я не хочу это игнорировать. Свойство данных, которое имеет тип object, изменяется с помощью HasJsonConversion () на столбец ‘jsonb’ в модели db. Все работает нормально, если я напрямую реализую IEntityTypeConfiguration<MessageLog>, но если я наследую базовый класс, который реализует этот интерфейс, то по какой-то причине преобразователи значений игнорируются. То же самое относится и к свойству Type, но только ошибка немного отличается (что-то вроде «Вам нужен первичный ключ, определенный для этой сущности».).

3. Можете ли вы попробовать отладить производную конфигурацию, чтобы проверить, действительно ли она запущена? Я сомневаюсь, что что-то не так с ApplyConfigurationsFromAssembly , по этой возможной причине попробуйте вместо этого применить производную конфигурацию напрямую.

4. Я просто попробовал пару вещей, касающихся применения конфигурации, как вы сказали, и в процессе ручного добавления производной конфигурации я удалил регистратор из конструктора BaseEntityConfiguration. Это каким-то образом устранило проблему, и она также работает с ApplyConfigurationsFromAssembly. Я просто не уверен, почему эта конфигурация будет работать только с пустым конструктором, но другие, у которых нет никаких свойств объекта (или преобразований значений), работают с регистратором, переданным в конструкторе.

5. Я думаю ApplyConfigurationsFromAssembly , что он не поддерживает DI for IEntityTypeConfiguration , потому что он создает экземпляр с использованием конструктора без параметров. Он также может поддерживать DI, но в этом случае вы можете попробовать зарегистрировать свой производный тип конфигурации как одноэлементный вручную, чтобы проверить, работает ли он (просто предположение, этот сценарий сложный, потому что обычно у нас есть конструктор без параметров для класса configuration, и он просто работает)

Ответ №1:

TL; DR

Попробуйте добавить пустой конструктор в свои IEntityTypeConfiguration реализации. В противном случае, если вы все еще хотите использовать DI в своих конфигурациях типов сущностей, возможно, стоит рассмотреть эту проблему.


Я не думаю, что внедренный регистратор в вашем IEntityTypeConfiguration будет работать вместе с. ApplyConfigurationsFromAssembly Из исходного кода этого метода кажется, что при использовании отражения для поиска классов конфигурации требуется пустой конструктор, чтобы он мог создавать их экземпляры.

Исходный код ядра EF для ApplyConfigurationsFromAssembly

Поскольку в вашем IEntityTypeConfiguration s отсутствует пустой конструктор по умолчанию, он, 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
                        amp;amp; !t.IsGenericTypeDefinition
                        amp;amp; 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);
        }
    }
}
 

Комментарии:

1. Спасибо, чувак! Ваши ответы и ответы @KingKong были чрезвычайно полезны и привели меня на правильный путь. Я думаю, вся проблема заключалась в том, что вы не можете действительно внедрить что-то с конфигурацией сущности по умолчанию. В итоге я создал собственную реализацию конфигурации сущности с собственным интерфейсом и базовым классом с виртуальным методом Configure. Затем я зарегистрировал каждую конфигурацию и ввел их в DbContext. В OnModelCreating в DbContext я предварительно извлек их и вызвал Configure() для каждого. Это сделало свое дело, и таким образом я могу вводить в конфигурацию столько вещей, сколько захочу.

2. @VeselinTodorov приятно слышать! 😀