Entity Framework duplicates lookup values when assigned to nested list member

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

Question

Three parties are involved in this solution,Client , Competency and WeaponType .

  • A Client for example, can contain zero or moreCompetency examples within aList<Competency> member.
  • A Competency for example, can have one or moreWeaponType examples within aList<WeaponType> a member.WeaponType is a member of our lookup)

A fresh List object is given to the client prior to changing the DbContext. This is the client's complete updated list of competences, which may include previous competencies that have been eliminated and new ones that have been added.

That dbContext is the issue at hand. Duplicate WeaponType entries are created by SaveChanges().

The code for my entities is as follows:

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; }

    }

Here is the code for preserving my most recent customer and competency information, which also shows my efforts to solve this issue:

 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;
            }
        }

The Ids for existing entities are preserved, while new entity Ids are set to 0. I can validate the JSON parsing and mapping in the aforementioned code works as intended.

What am I doing to cause this behavior, exactly? How do I correct it?


UPDATE: I've tried to build a fix using GraphDiff, as Gert suggested (which seems to be an exact fit for my requirements). But I'm having trouble making it function. According to the Github problem raised, this is what I did:

I have these things:

Client: List CompetencyCertificates CompetencyCertificate: List CompetencyTypes Client: List CompetencyCertificates

I first load a client object from the database before giving the aforementioned List members new List values.

Following that, I dial the following code:

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

dbContext.SaveChanges();

The exception that is thrown when calling UpdateGraph has the following stacktrace:

Member 'CurrentValues' cannot be called for the entity of type 'CompetencyCertificate' because the entity does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet.

Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: System.InvalidOperationException: Member 'CurrentValues' cannot be called for the entity of type 'CompetencyCertificate' because the entity does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet.

Source Error:

Line 138: Line 139: //UPDATE GRAPH OF DETACHED ENTITIES Line 140: dbContext.UpdateGraph(client, map => map Line 141: .OwnedCollection(cc => cc.CompetencyCertificates, with => with Line 142: .AssociatedCollection(kt => kt.CompetencyTypes))

Source File: [Not Important] Line: 140

Stack Trace:

[InvalidOperationException: Member 'CurrentValues' cannot be called for the entity of type 'CompetencyCertificate' because the entity does not exist in the context. To add an entity to the context call the Add or Attach method of DbSet.]
System.Data.Entity.Internal.InternalEntityEntry.ValidateNotDetachedAndInitializeRelatedEnd(String method) +102
System.Data.Entity.Internal.InternalEntityEntry.ValidateStateToGetValues(String method, EntityState invalidState) +55
System.Data.Entity.Internal.InternalEntityEntry.get_CurrentValues() +53 System.Data.Entity.Infrastructure.DbEntityEntry.get_CurrentValues() +44 RefactorThis.GraphDiff.DbContextExtensions.RecursiveGraphUpdate(DbContext context, Object dataStoreEntity, Object updatingEntity, UpdateMember member) +942
RefactorThis.GraphDiff.DbContextExtensions.UpdateGraph(DbContext context, T entity, Expression1 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 parameters) +435
System.Web.Mvc.ControllerActionInvoker.InvokeActionMethod(ControllerContext controllerContext, ActionDescriptor actionDescriptor, IDictionary2 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.WrappedAsyncResultBase1.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.WrappedAsyncResultBase1.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.WrappedAsyncResultBase1.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.WrappedAsyncResultBase1.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.WrappedAsyncResultBase1.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, Object tag) +59
System.Web.Mvc.Async.AsyncResultWrapper.End(IAsyncResult asyncResult, Object tag) +40
System.Web.Mvc.MvcHandler.EndProcessRequest(IAsyncResult asyncResult) +40 System.Web.Mvc.MvcHandler.System.Web.IHttpAsyncHandler.EndProcessRequest(IAsyncResult result) +38
System.Web.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute() +9514928 System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously) +155

1
2
3/2/2014 5:14:35 PM

Accepted Answer

I was able to find a solution, therefore I'm summarizing it here for future use.

Because the graph was not loaded from the database and no tracking was done on the entities, I was essentially generating a disconnected graph from the code provided in my original query by de-serializing JSON to Enities.

Working with unconnected graphs is not supported by Entity Framework 6 (and earlier). (View zzzz-13 zzzz)

There is a plug-in component called GraphDiff that does support it, as @Gert Arnold previously indicated. It is available for download at https://github.com/refactorthis/GraphDiff.

Because the Nuget package was out-of-date when I tried it and thus ran into a number of flaws that had already been fixed in the most recent version, I highly advise against using it and instead building the code from source.

Last but not least, remember that GraphDiff does not yet support dealing with connected graphs or tracked entities; as a result, you must execute the.AsNoTracking() method when loading the data for your disconnected graph.

1
3/6/2014 10:41:13 PM

Popular Answer

The line has a problem.

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

All CompetencyCertificates when they are deserialized, the objects in the collection begin as detached objects. When a deserialized collection is assigned toCompetencyCertificates , allCompetencyCertificate things shift betweenDetached to Added .

This shift in condition results in allDetached identifying things in an object graphAdded and also In light of this, allWeaponType s areAdded and if you do nothing, they will be saved as new items.

If you are certain that allWeaponType Since existing items will always remain, I believe the easiest fix would be to loop through all new objects.CompetencyCertificate items and indicate theirWeaponType s asUnchanged .

I assume that this is what you're aiming for inAttachLookup however it seems to me that there is a very distinct context at play, thusdbContext 's change tracking system stays out of this.



Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow