Entity Framework дублирует значения поиска при назначении вложенному элементу списка

asp.net-mvc c# entity-framework-6 orm

Вопрос

Это решение включает в себя три объекта: Client , Competency и WeaponType .

  • Client экземпляр может иметь нулевой или более- Competency экземпляров в пределах List<Competency> члена.
  • Экземпляр Competency может иметь один или несколько экземпляров WeaponType в List<WeaponType> . ( WeaponType - наш элемент поиска)

Перед обновлением DbContext клиенту назначается новый объект List. Это представляет собой полный обновленный список компетенций для клиента, где старые компетенции могут быть удалены и созданы новые.

Проблема заключается в том, что dbContext.SaveChanges () вызывает создание дубликатов записей WeaponType.

Вот код для моих сущностей:

public class Client : Person
    {
        public ICollection<CompetencyCertificate> CompetencyCertificates
        {
            get;
            set;
        }

    }

public class CompetencyCertificate
    {


        public Int64 Id { get; set; }

        [Required]
        public string CertificateNumber { get; set; }

        [Required]
        public List<WeaponType> CompetencyTypes { get; set; }    

    }



 public class WeaponType
    {
        public Int16 Id { get; set; }

        [Required]
        public string Name { get; set; }

    }

И при этом код для сохранения моей обновленной информации о клиенте и компетенции (что также отражает мои попытки преодолеть эту проблему:

 private void SaveClientProfile()
        {
            HttpRequestBase rb = this.Request;

            string sId = "";
            if (rb.Form["Id"] != null)
                sId = rb.Form["Id"];
            Int64 int64_id = 0;
            if (sId.Trim().Length > 0)
                int64_id = Int64.Parse(sId);
            Client client = loadOrCreateClient(int64_id);

            //Set the newly submitted form data for the client

            client.IDSocialSecurityPassNum = rb.Form["IDNumber"];
            client.EmailAddress = rb.Form["EmailAddress"];
            client.NickName = rb.Form["Name"];
            client.Surname = rb.Form["Surname"];

            //MAP AND TRANSLATE JSON COLLECTION OBJECTS TO ENTITY COLLECTIONS, UPDATE THE CONTEXT    

            Mapper.CreateMap<Client_Competency_ViewModel, CompetencyCertificate>();
            client.CompetencyCertificates = Mapper.Map<List<CompetencyCertificate>>(System.Web.Helpers.Json.Decode<System.Collections.Generic.List<Client_Competency_ViewModel>>(rb.Form["CompetencyCollection"]));    

            //PREVENT EF FROM DUPLICATING LOOKUP VALUES
            AttachLookup<WeaponType>(JCGunsDb.WeaponTypes.ToList<WeaponType>());


            //FNIALISE AND SAVE
            dbContext.UserId = User.Identity.GetUserName();
            dbContext.SaveChanges();
        }



private void AttachLookup<T>(ICollection<T> itemsToAttach) where T : class
        {
            foreach(T item in itemsToAttach)
            {
                JCGunsDb.Entry(item).State = EntityState.Unchanged;
            }
        }

Я могу подтвердить, что разбор JSON и отображение в приведенном выше коде работают как ожидалось - идентификаторы для существующих объектов находятся в такте, а для новых идентификаторов объекта установлено значение 0.

Что я делаю, это вызывает такое поведение? Как это исправить?


ОБНОВЛЕНИЕ: Как рекомендовал Герт, я попытался реализовать решение, использующее GraphDiff (что, похоже, соответствует моим требованиям). Тем не менее, я изо всех сил пытаюсь заставить его работать. Вот что я сделал (как поднял вопрос Гитуба):

У меня есть следующее:

Клиентский клиент >> Список КомпетенцияСертификаты КомпетентностьСертификат >> Список CompetencyTypes

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

Впоследствии я вызываю следующий код:

dbContext.UpdateGraph(client, map => map
                .OwnedCollection(cc => cc.CompetencyCertificates, with => with
                    .AssociatedCollection(kt => kt.CompetencyTypes))
                );

dbContext.SaveChanges();

Вот stacktrace для исключения, которое бросается на вызов UpdateGraph:

Член «CurrentValues» не может быть вызван для объекта типа «CompetencyCertificate», поскольку объект не существует в контексте. Чтобы добавить объект в контекст, вызовите метод добавления или прикрепления DbSet.

Описание: Необработанное исключение возникло во время выполнения текущего веб-запроса. Просмотрите трассировку стека для получения дополнительной информации об ошибке и ее возникновении в коде.

Сведения об исключении: System.InvalidOperationException: член «CurrentValues» не может быть вызван для объекта типа «CompetencyCertificate», поскольку объект не существует в контексте. Чтобы добавить объект в контекст, вызовите метод добавления или прикрепления DbSet.

Ошибка источника:

Строка 138: Строка 139: // UPDATE GRAPH DETACHED ENTITIES Строка 140: dbContext.UpdateGraph (client, map => map Строка 141: .OwnedCollection (cc => cc.CompetencyCertificates, with => с строкой 142: .AssociatedCollection (kt => kt.CompetencyTypes))

Исходный файл: [Не важно] Строка: 140

Трассировки стека:

[InvalidOperationException: член «CurrentValues» не может быть вызван для объекта типа «CompetencyCertificate», потому что сущность не существует в контексте. Чтобы добавить объект в контекст, вызовите метод добавления или прикрепления DbSet.]
System.Data.Entity.Internal.InternalEntityEntry.ValidateNotDetachedAndInitializeRelatedEnd (метод String) +102
System.Data.Entity.Internal.InternalEntityEntry.ValidateStateToGetValues ​​(метод String, EntityState invalidState) +55
System.Data.Entity.Internal.InternalEntityEntry.get_CurrentValues ​​() +53 System.Data.Entity.Infrastructure.DbEntityEntry.get_CurrentValues ​​() +44 RefactorThis.GraphDiff.DbContextExtensions.RecursiveGraphUpdate (контекст DbContext, объект dataStoreEntity, объект updateEntity, член UpdateMember) +942
RefactorThis.GraphDiff.DbContextExtensions.UpdateGraph (контекст DbContext, объект T, 1 mapping) +631
JCGunsOnline.Controllers.ClientController.SaveClientProfile() in c:\Users\Ben\Dropbox\Mighty IT\Active Projects\JCGunsOnline\JCGunsOnline\Views\Client\ClientController.cs:140 JCGunsOnline.Controllers.ClientController.SubmitStep1() in c:\Users\Ben\Dropbox\Mighty IT\Active Projects\JCGunsOnline\JCGunsOnline\Views\Client\ClientController.cs:60 lambda_method(Closure , ControllerBase , Object[] ) +101
System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +59
System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary
выражения 1 mapping) +631
JCGunsOnline.Controllers.ClientController.SaveClientProfile() in c:\Users\Ben\Dropbox\Mighty IT\Active Projects\JCGunsOnline\JCGunsOnline\Views\Client\ClientController.cs:140 JCGunsOnline.Controllers.ClientController.SubmitStep1() in c:\Users\Ben\Dropbox\Mighty IT\Active Projects\JCGunsOnline\JCGunsOnline\Views\Client\ClientController.cs:60 lambda_method(Closure , ControllerBase , Object[] ) +101
System.Web.Mvc.ActionMethodDispatcher.Execute(ControllerBase controller, Object[] parameters) +59
System.Web.Mvc.ReflectedActionDescriptor.Execute(ControllerContext controllerContext, IDictionary
2) +435
System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod (ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary 2 parameters) +60
System.Web.Mvc.Async.ActionInvocation.InvokeSynchronousActionMethod() +76 System.Web.Mvc.Async.AsyncControllerActionInvoker.<BeginInvokeSynchronousActionMethod>b__39(IAsyncResult asyncResult, ActionInvocation innerInvokeState) +36
System.Web.Mvc.Async.WrappedAsyncResult
2.CallEndDelegate (IAsyncResult asyncResult) +73
System.Web.Mvc.Async.WrappedAsyncResultBase 1.End() +136
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +102
System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethod(IAsyncResult asyncResult) +49
System.Web.Mvc.Async.AsyncInvocationWithFilters.<InvokeActionMethodFilterAsynchronouslyRecursive>b__3f() +117 System.Web.Mvc.Async.<>c__DisplayClass48.<InvokeActionMethodFilterAsynchronouslyRecursive>b__41() +323 System.Web.Mvc.Async.<>c__DisplayClass33.<BeginInvokeActionMethodWithFilters>b__32(IAsyncResult asyncResult) +44
System.Web.Mvc.Async.WrappedAsyncResult
1.CallEndDelegate (IAsyncResult asyncResult) +47
System.Web.Mvc.Async.WrappedAsyncResultBase 1.End() +136
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +102
System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeActionMethodWithFilters(IAsyncResult asyncResult) +50
System.Web.Mvc.Async.<>c__DisplayClass2b.<BeginInvokeAction>b__1c() +72 System.Web.Mvc.Async.<>c__DisplayClass21.<BeginInvokeAction>b__1e(IAsyncResult asyncResult) +185
System.Web.Mvc.Async.WrappedAsyncResult
1.CallEndDelegate (IAsyncResult asyncResult) +42
System.Web.Mvc.Async.WrappedAsyncResultBase 1.End() +133
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +56
System.Web.Mvc.Async.AsyncControllerActionInvoker.EndInvokeAction(IAsyncResult asyncResult) +40
System.Web.Mvc.Controller.<BeginExecuteCore>b__1d(IAsyncResult asyncResult, ExecuteCoreState innerState) +34
System.Web.Mvc.Async.WrappedAsyncVoid
1.CallEndDelegate (IAsyncResult asyncResult) +70
System.Web.Mvc.Async.WrappedAsyncResultBase 1.End() +139
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +59
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +40
System.Web.Mvc.Controller.EndExecuteCore(IAsyncResult asyncResult) +44 System.Web.Mvc.Controller.<BeginExecute>b__15(IAsyncResult asyncResult, Controller controller) +39
System.Web.Mvc.Async.WrappedAsyncVoid
1.CallEndDelegate (IAsyncResult asyncResult) +62
System.Web.Mvc.Async.WrappedAsyncResultBase 1.End() +139
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +59
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +40 System.Web.Mvc.Controller.EndExecute(IAsyncResult asyncResult) +39
System.Web.Mvc.Controller.System.Web.Mvc.Async.IAsyncController.EndExecute(IAsyncResult asyncResult) +39
System.Web.Mvc.MvcHandler.<BeginProcessRequest>b__5(IAsyncResult asyncResult, ProcessRequestState innerState) +39
System.Web.Mvc.Async.WrappedAsyncVoid
1.CallEndDelegate (IAsyncResult asyncResult) +70
System.Web.Mvc.Async.WrappedAsyncResultBase`1.End () +139
System.Web.Mvc.Async.AsyncResultWrapper.End (IAsyncResult asyncResult, тег объекта) +59
System.Web.Mvc.Async.AsyncResultWrapper.End (IAsyncResult asyncResult, тег объекта) +40
System.Web.Mvc.MvcHandler.EndProcessRequest (IAsyncResult asyncResult) +40 System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest (результат IAsyncResult) +38
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute () +9514928 System.Web.HttpApplication.ExecuteStep (шаг IExecutionStep, Boolean & completedSynchronously) +155

Принятый ответ

Поэтому мне удалось решить проблему, и поэтому я обобщаю ее здесь для дальнейшего использования.

Из кода, указанного в моем первоначальном вопросе, я де-сериализую от JSON до Enities, тем самым в основном создавая отключенный граф (поскольку граф не был загружен из базы данных, и поэтому никаких следов на сущности не происходило.

Entity Framework 6 (и ранее) не поддерживает работу с отключенными графиками. (См. Https://entityframework.codeplex.com/workitem/864 )

Как упоминал выше @Gert Arnold, есть подключаемый компонент GraphDiff, который его поддерживает. (Вы можете скачать его с https://github.com/refactorthis/GraphDiff ).

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

Наконец, имейте в виду, что GraphDiff еще не поддерживает работу с связанными графами / отслеживаемыми объектами, поэтому вы должны вызвать метод .AsNoTracking () при загрузке данных для вашего отключенного графика.


Популярные ответы

Проблема в строке

client.CompetencyCertificates = Mapper.Map<....

Все CompetencyCertificates в коллекции начинаются как неприкрепленные объекты, когда они десериализуются. Когда вы назначаете десериализованную коллекцию CompetencyCertificates , все объекты CompetencyCertificate меняются с Detached на Added .

Это изменение состояния - это тот, который заставляет все Detached объекты в графе объектов быть помечены как Added . Итак, на данный момент все WeaponType s Added и будут сохранены как новые объекты, если вы ничего не сделаете.

Если вы точно знаете, что все объекты WeaponType всегда будут существующими объектами, я думаю, что самым быстрым WeaponType все новые объекты CompetencyCertificate и пометить их WeaponType s как Unchanged .

Вероятно, это то, что вы пытаетесь сделать в AttachLookup , но мне кажется, что здесь задействован совершенно другой контекст, поэтому dbContext изменений dbContext никогда не участвует в этом.




Лицензировано согласно: CC-BY-SA with attribution
Не связан с Stack Overflow
Является ли этот КБ законным? Да, узнайте, почему
Лицензировано согласно: CC-BY-SA with attribution
Не связан с Stack Overflow
Является ли этот КБ законным? Да, узнайте, почему