Заменить класс сущности во время выполнения в Entity Framework Core

В нашей модели мы следуем принципам предметно-ориентированного проектирования, комбинируя данные и поведение в классах объектов сущностей и значений. Нам часто нужно настраивать поведение наших клиентов. Вот простой пример, когда клиент хочет изменить способ форматирования FullName:

public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    // Standard format is Last, First
    public virtual string FullName => $"{LastName}, {FirstName}";
}

public class PersonCustom : Person
{
    // Customer wants to see First Last instead
    public override string FullName => $"{FirstName} {LastName}";
}

DbSet настроен в DbContext, как и следовало ожидать:

public virtual DbSet<Person> People { get; set; }

Во время выполнения необходимо создать экземпляр класса PersonCustom вместо класса Person. Это не проблема, когда наш код отвечает за создание экземпляра объекта, например, при добавлении нового человека. Контейнер / фабрику IoC можно настроить для замены класса PersonCustom на класс Person. Однако при запросе данных EF отвечает за создание экземпляра объекта. При запросе context.People есть ли способ настроить или перехватить создание объекта, чтобы заменить PersonCustom на класс Person во время выполнения?

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

Редактировать: Еще немного информации о том, как это развертывается ... Класс Person будет частью стандартной сборки, которая предоставляется всем клиентам. PersonCustom перейдет в отдельную настраиваемую сборку, которая отправляется только клиенту, который хочет изменения, поэтому они получат стандартную сборку плюс настраиваемую сборку. Мы не будем создавать отдельную сборку всего проекта для адаптации.

Почему бы вместо этого не использовать DbSet<PersonCustom> People?

Jan Paolo Go 15.09.2018 20:52

@PaoloGo Я добавил в публикацию некоторую информацию о развертывании. Обратите внимание, что класс PersonCustom будет доступен только одному клиенту, которому нужна настройка. Вы предлагаете создать отдельный DbContext для этого клиента, чтобы указать DbSet<PersonCustom> People?

Joel Leach 15.09.2018 22:19

Наследование для этого не подходит. Наследование касается иерархических категорий «вещей», определяющих, что такое является. Просто наличие другого предпочтения формата имени по существу не меняет определения личности. Стратегия ИМО - правильный образец здесь.

Gert Arnold 16.09.2018 00:31
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
3
196
3
Перейти к ответу Данный вопрос помечен как решенный

Ответы 3

Ответ принят как подходящий

У меня была аналогичная задача и в моем проекте. Есть способы сделать это перехватчиками, но есть две проблемы:

  1. Объект не синхронизируется с прокси
  2. Это неуместно с архитектурной точки зрения.

Итак, я закончил преобразование объектов в требуемые типы с помощью AutoMapper. Есть и другие библиотеки, которые могут делать то же самое. Таким образом, в каждом домене вы легко сможете получить нужный объект.

Возможно, вы спросили не об этом, но я надеюсь, что это вам поможет.

Все предложения верны, но я думаю, что это лучшее решение для нашего сценария. Спасибо!

Joel Leach 18.09.2018 18:52

Я бы не стал пытаться использовать отдельный класс, если ваши данные в любом случае хранятся в базовом объекте. Что бы я сделал, так это использовать ViewModel или как вы хотите это назвать, и сопоставить (вручную или с помощью одного из множества AutoMapper) вашу сущность с этим. Как правило, я не рекомендую использовать вашу сущность для чего-либо, кроме взаимодействия с базой данных. Вся логика и другие манипуляции должны происходить в отдельном классе, даже если этот класс является копией всех свойств 1: 1 (гораздо легче справиться с изменениями позже, чем выполнять дополнительные миграции или рисковать другим нарушением). изменения).

Краткий пример, чтобы лучше объяснить мою мысль.

//entity
public class Person
{
    public string FirstName {get; set;}
    public string LastName {get; set;}
}

//base ViewModel
public class PersonModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    // Standard format is Last, First
    public virtual string FullName => $"{LastName}, {FirstName}";
}

//customer specific ViewModel
public class CustomerSpecificModel : PersonModel
{
    // Standard format is Last, First
    public virtual string FullName => $"{FirstName} {LastName}";
}


//Mapping
public class Mapping
{
    public void DoSomeWork()
    {
        var db = new People();
        var defaultPerson = db.People.Select(p => new PersonModel
        {
            FirstName = p.FirstName,
            LastName = p.LastName
        };

        var customerSpecificModel = db.People.Select(p => new CustomerSpecificModel
        {
            FirstName = p.FirstName,
            LastName = p.LastName
        }

        Console.WriteLine(defaultPerson.First().FullName); //would return LastName, FirstName
        Console.WriteLine(customerSpecificModel.First().FullName); //would return FirstName LastName
    }   
}

DDD способствует объединению данных и поведения в классах сущностей и отходу от Модель анемического домена. Я уже сопоставляю объекты с DTO для отправки по сети, поэтому я надеюсь избежать другого уровня сопоставления.

Joel Leach 15.09.2018 22:50

Исходя из всего, что вы предоставили, я считаю, что создание еще одного DbContext для другого клиента лучше всего подходит для вашего сценария.

В приведенном ниже коде показан тест, в котором я использовал PersonContext для добавления объекта Person. Затем используйте PersonCustomContext для чтения того же объекта, но он создан как PersonCustom.

Другой интересный момент - явная конфигурация ключа PersonCustom, указывающая на ключ PersonId, определенный в его базовом типе Person.

using Microsoft.Data.Sqlite;
using Microsoft.EntityFrameworkCore;
using System.Linq;
using Xunit;

public class Tests
{
    [Fact]
    public void PersonCustomContext_can_get_PersonCustom()
    {
        var connection = new SqliteConnection("datasource=:memory:");
        connection.Open();

        var options = new DbContextOptionsBuilder()
            .UseSqlite(connection)
            .Options;

        using (var ctx = new PersonContext(options))
            ctx.Database.EnsureCreated();

        using (var ctx = new PersonContext(options))
        {
            ctx.Add(new Person { FirstName = "John", LastName = "Doe" });
            ctx.SaveChanges();
        }

        using (var ctx = new PersonCustomContext(options))
        {
            Assert.Equal("John Doe", ctx.People.Single().FullName);
        }
    }
}
public class PersonContext : DbContext
{
    public PersonContext(DbContextOptions options) : base(options) { }
    public DbSet<Person> People { get; set; }
}
public class PersonCustomContext : DbContext
{
    public PersonCustomContext(DbContextOptions options) : base(options) { }
    public DbSet<PersonCustom> People { get; set; }
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<PersonCustom>(builder =>
        {
            builder.HasKey(p => p.PersonId);
        });
    }
}
public class Person
{
    public int PersonId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public virtual string FullName => $"{LastName}, {FirstName}";
}
public class PersonCustom : Person
{
    public override string FullName => $"{FirstName} {LastName}";
}

Предостережение: по какой-то причине тест не проходит с помощью провайдера в памяти. Так что вы можете подумать об этом, если вы или ваш клиент используете для тестирования in-memory. Это может быть ошибка в самом EF Core, поскольку он отлично работает с sqlite inmemory, как показано.

Другие вопросы по теме