Использование Automapper, отображение DTO обратно в Entity Framework, включая ссылочные объекты

У меня есть сущности домена POCO, которые сохраняются с помощью Entity Framework 5. Они получены из DbContext с использованием шаблона хранилища и доступны приложению RESTful MVC WebApi через шаблон UoW. Объекты POCO являются прокси и загружаются с отложенным доступом.

Я конвертирую свои сущности в DTO перед тем, как отправить их клиенту. Я использую Automapper для этого, и, похоже, он работает нормально, когда Automapper отображает POCO прокси в DTO, сохраняя свойства навигации без изменений. Я использую следующее сопоставление для этого:

    Mapper.CreateMap<Client, ClientDto>();

Пример объекта Domain/DTO:

[Serializable]
public class Client : IEntity
{
    public int Id { get; set; }

    [Required, MaxLength(100)]
    public virtual string Name { get; set; }

    public virtual ICollection<ClientLocation> ClientLocations { get; set; }

    public virtual ICollection<ComplianceRequirement> DefaultComplianceRequirements { get; set; }

    public virtual ICollection<Note> Notes { get; set; }
}

public class ClientDto : DtoBase
{
    public int Id { get; set; }

    [Required, MaxLength(100)]
    public string Name { get; set; }

    public ICollection<ClientLocation> ClientLocations { get; set; }

    public ICollection<ComplianceRequirementDto> DefaultComplianceRequirements { get; set; }

    public ICollection<Note> Notes { get; set; }
}

Теперь я пытаюсь обновить свой контекст, используя DTO, отправленные обратно с провода. У меня возникают конкретные проблемы с правильной работой навигационных свойств/связанных объектов. Отображение для этого я использую:

    Mapper.CreateMap<ClientDto, Client>()
        .ConstructUsing((Func<ClientDto, Client>)(c => clientUow.Get(c.Id)));

Выше clientUow.Get() ссылается на DbContext.Set.Find(), так что я получаю отслеживаемый прокси-объект POCO от EF (который содержит все связанные сущности также в качестве прокси).

В моем методе контроллера я делаю следующее:

    var client = Mapper.Map<ClientDto, Client>(clientDto);
    uow.Update(client);

Клиент успешно сопоставлен как прокси-объект POCO, однако его связанные сущности/навигационные свойства заменяются новым (непрокси) сущностью POCO со значениями свойств, скопированными из DTO.

Выше uow.Update() в основном относится к функции, которая выполняет постоянную логику, которая у меня есть как:

    _context.Entry<T>(entity).State = System.Data.EntityState.Modified;
    _context.SaveChanges();

Вышеупомянутое не сохраняется, даже сохраняется сущность, не говоря уже о связанных. Я пробовал варианты отображений и различные способы сохранения, используя отсоединение/состояния, но всегда получаю исключения "объект с таким же ключом уже существует в ObjectStateManager".

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

Есть ли учебный способ работы с Automapper с EF с помощью доменных объектов /DTO, имеющих навигационные свойства, которые также необходимо обновлять одновременно?

ОБНОВИТЬ:

    var originalEntity = _entities.Find(entity.Id);
    _context.Entry<T>(originalEntity).State = System.Data.EntityState.Detached;
    _context.Entry<T>(entity).State = System.Data.EntityState.Modified;

Вышеприведенная логика постоянства обновляет "корневой" прокси-объект EF в контексте, однако любые связанные объекты не обновляются. Я предполагаю, что это связано с тем, что они не сопоставляются с прокси-объектами EF, а являются простыми объектами домена. Помощь будет наиболее ценится!

ОБНОВЛЕНИЕ: Кажется, что то, что я пытаюсь достичь, на самом деле невозможно с использованием текущей версии EF (5), и что это основное ограничение EF, а не Automapper:

Ссылка на сайт

Ссылка на сайт

Я полагаю, это снова сделано вручную. Надеюсь, что это помогает кому-то еще, кому интересно то же самое.

Ответ 1

Вы уже определили проблему:

Вышеупомянутая логика сохранения обновляет "прокси-сервер" корневого EF в контекст, однако любые связанные объекты не обновляются

Вы устанавливаете измененное состояние только в корневом каталоге node. Вы должны написать код для повторения всех объектов и изменения состояния.

Ответ 2

Я реализовал шаблон для обработки этого состояния модели иерархии с помощью EF.

Каждый класс модели сущности реализует интерфейс, как показано ниже, также как и классы модели представления:

public interface IObjectWithState
{
    ObjectState ObjectState { get; set; }
}

Ниже перечислены перечисления ObjectState:

public enum ObjectState
{
    Unchanged  = 0,
    Added = 1,
    Modified = 2,
    Deleted = 3
}

Например, при сохранении глубокой иерархии объектов, использующих EF, я сопоставляю объекты модели представления с их эквивалентными объектами сущности, включая ObjectState.

Затем я прикрепляю корневой объект-объект к контексту (и, следовательно, ко всем дочерним объектам):

dbContext.MyCustomEntities.Attach(rootEntityObj);

Затем у меня есть метод расширения в DbContext, который перебирает все элементы в трекере изменения контекста и обновляет каждое состояние объекта (как вы это делали выше).

    public static int ApplyStateChanges(this DbContext context)
    {
        int count = 0;
        foreach (var entry in context.ChangeTracker.Entries<IObjectWithState>())
        {
            IObjectWithState stateInfo = entry.Entity;
            entry.State = ConvertState(stateInfo.ObjectState);
            if (stateInfo.ObjectState != ObjectState.Unchanged)
                count++;
        }
        return count;
    }

Затем мы можем просто сохранить изменения как обычно:

dbContext.SaveChanges();

Таким образом, вся иерархия дочерних объектов будет соответствующим образом обновлена ​​в базе данных.

Ответ 3

Что вы хотите сделать, сначала получить Entity из базы данных:

var centity = _context.Client.First(a=>a.Id = id)

Затем вы сопоставляете это и обновляете (это то, что вы искали, оно будет отображать только то, что он находит в inputDTO, и оставить остальные свойства в покое)

Mapper.Map<UpdateClientInput,  Client>(inputDto, centity);
_context.update();