При выполнении обновления вашего кода с помощью mapper.Map правильно, однако вам необходимо удалить следующие строки:
context.Masters.Add(master);
context.Entry(master).State = EntityState.Modified;
Ваш контекст загружен и отслеживает экземпляр Master, поэтому все, что вам нужно сделать обновить свойства (что делает Mapper.Map
), затем вызвать SaveChanges
в контексте, а EF позаботится об остальном.
Add
используется для добавления нового экземпляра сущности в DbContext. Установка состояния в Modified
необходима только при подключении экземпляра к DbContext. В вашем случае сущность уже связана.
Обычно эта проблема возникает, когда разработчики используют вызов по умолчанию mapper.Map:
// Loads the entity which the Context will track, but then mapper.Map() returns a new instance in the reference. The context is still tracking the first reference.
var master = context.Masters.Single(x => x.MasterId = masterDTO.MasterId);
master = mapper.Map<Master>(masterDTO);
Этот метод создает новую сущность Mapper со свойствами, которые не связан с контекстом, поэтому они будут использовать Add
, Update
или Attach
+ .State = EntitySate.Modified
, чтобы попытаться поместить его в контекст, что приведет к ошибке, когда контекст уже отслеживает соответствующий объект.
Обновление: чтобы включить отслеживание изменений через связанные свойства, необходимо пометить свойства навигации как virtual
, чтобы включить прокси.
public class Master
{
public int id {get;set;}
public string masterInfo {get;set;}
public virtual ICollection<Detail> details { get;set; } = new Collection<Detail>();
}
public class Detail
{
public int id {get;set;}
public int masterId {get;set;}
public virtual Master master {get;set;}
public string detailInfo {get;set;}
}
Обновление 2: Сбой при обновлении сценария ios.
Похоже, что путаница основана на смешении понятий из двух основных способов обновления сущностей в EF. Вот краткий анализ двух подходов:
Подход 1: С отслеживанием / прокси. По умолчанию EF DbContexts будет отслеживать объекты, которые они загружают, используя прокси-оболочки. Это позволяет загружать связанные объекты с отложенной загрузкой, но, что более важно, позволяет EF обнаруживать, когда отдельные столбцы изменяются для использования в инструкциях UPDATE. Чтобы использовать этот подход, свойства навигации должны быть помечены как virtual
, контекст БД должен быть настроен для автоматического обнаружения изменений. (включено по умолчанию) и запросы должны не использовать AsNoTracking
. Использование этого подхода является самым простым способом загрузки данных, внесения обновлений и сохранения изменений. Для связанных сущностей, которые вы хотите обновить, используйте Include
для их загрузки.
var parent = context.Parents.Include(x => x.Children).Single(x => x.ParentId == parentId);
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
context.SaveChanges();
Преимущества этого подхода в том, что он прост. Нет необходимости устанавливать измененное состояние, присоединять к контексту или беспокоиться о дубликатах записей. Недостатком этого подхода является попытка обновить большие объемы данных. Чем больше строк отслеживает DbContext, тем дольше считывания и обновления будут выполняться. Кроме того, что-то простое, например, случайное добавление AsNoTracking()
к запросу или отключение виртуального свойства навигации, приведет к ухудшению поведения.
Подход 2: Без отслеживания. Иногда код, использующий EF, захочет работать с отсоединенными сущностями. Это может быть связано с тем, что объекты сериализуются назад и вперед к клиенту / потребителю, или имеют дело с большим количеством объектов, или просто предпочтительным (хотя и сложным) решением проекта, разработанным командой разработчиков. В этом случае DbContext не должен отслеживать экземпляры, и эти экземпляры должны находиться в отдельном состоянии. Так что простым примером этого будет что-то вроде:
var parent = context.Parents.AsNoTracking().Include(x => x.Children.AsNoTracking()).Single(x => x.ParentId == parentId);
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
Теперь в этом случае мы не можем просто вызвать context.SaveChanges()
. Не будет ошибки, но ничего не будет сохранено, поскольку контекст не отслеживает эти объекты или не обнаруживает изменения.
Мы должны явно связать их обратно с DbContext и установить их измененное состояние:
context.Attach(parent); // This will attach the parent, and the children, but in an Unmodified state.
context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
context.Entity(child).State = EntityState.Modified;
}
context.SaveChanges();
// In some cases we will want to detach the parent and children again here.
При таком подходе вам нужно быть более осмотрительным при повторной ассоциации сущностей с DbContext. Проблема может возникнуть, когда рассматриваемая сущность была десериализована или контекст довольно долгоживущий, где она, возможно, уже отслеживала сущность. В этих случаях вызов Attach()
может завершиться ошибкой, поэтому, чтобы быть в безопасности, вы должны убедиться, что объект еще не отслежен контекстом. Если сущность была передана в метод, в котором вы хотите выполнить обновление, вы также должны проверить, что сущность не отслеживается другим DbContext.
Например, с помощью метода, подобного приведенному ниже:
public void UpdateParentDetails(Parent parent)
{
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
_context.Attach(parent);
_context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
context.Entity(child).State = EntityState.Modified;
}
_context.SaveChanges();
}
Код, подобный этому, может быть подвержен проблемам и неправильному использованию. Был ли переданный родительский объект уже связан с контекстом, с тем же _context или другим экземпляром контекста? _Context отслеживает другую ссылку на этого родителя? Дети были загружены? Кто-нибудь из детей отслеживается? Что мы должны делать в любом из этих случаев?
Как минимум, мы должны утверждать, что переданный родительский элемент не был нулевым, не был связан с DbContext, и проверить, что мы еще не отслеживаем parent:
public void UpdateParentDetails(Parent parent)
{
if (parent == null)
throw new ArgumentNullException("parent");
if (parent.State != EntityState.Detached)
throw new ArgumentException("Parent was associated to a DbContext");
var existingParent = _context.Parents.Local.Single(x => x.ParentId == parentId);
if (existingParent != null)
{
existingParent.PhoneNumber= "0456-7689";
foreach(var child in existingParent.Children)
{
child.IsAttending = true;
}
}
else
{
parent.PhoneNumber= "0456-7689";
foreach(var child in parent.Children)
{
child.IsAttending = true;
}
_context.Attach(parent);
_context.Entity(parent).State = EntityState.Modified;
foreach(var child in parent.Children)
{
context.Entity(child).State = EntityState.Modified;
}
}
_context.SaveChanges();
}
Как вы можете видеть, это начинает немного усложняться, чтобы попытаться убедиться, что предположения о состоянии объекта и о том, отслеживает ли DbContext экземпляр. Вот почему я обычно не советую командам разработчиков пытаться работать с отдельными объектами. Код / намерение начинается достаточно просто, но почти всегда начинает сталкиваться с проблемами, которые приводят к большему количеству кода, большей сложности и большему количеству ошибок. По этой причине я рекомендую, чтобы сущности никогда не передавались за пределы области DbContext, в которой они были прочитаны. Использование DTO или ViewModels является наиболее предпочтительным подходом к этому, а затем использует подход № 1 выше для загрузки, обновления и сохранения объекта. Ключ, чтобы избежать смешивания элементов из подхода № 2.