none
Calling the ChangeTracker.Entries method does more than checking the state of the entries RRS feed

  • Question

  • Hi,
     
    I am using Entity Framework 4.1, Database first and the DbContext API. In my WPF application I am binding directly to my Entity Framework entities.
    There are two entities: Employee and Organisation having a one-to-many relationship (Employee has a navigation property to a single organisation).
     
    All my entitity classes implement a custom IEntity interface. This interface has one property being the primary key in the database.
    public interface IEntity
    {
      int ID { get; set; }
    }
    

    To change an existing employee my UI contains a ComboBox to select the organisation the employee belongs to. For performance reasons the Organisation entities are retrieved using the AsNoTracking method. The employee is being tracked by the DbContext.
     
    Note that the the employee's Organisation property is bound to the SelectedValue of a ComboBox. So when the user selects an organisation from the ComboBox the employee's Organisation property is set to the selected organisation.
    Before saving the employee the selected organisation has to be added to the context having state Unchanged.
    I will try to explain my problem with the following code:
    private Entities dbContext;
    
    public Window1()
    {
      InitializeComponent();
      Test();
    }
    
    // This method is called very often, each time the UI changes to determine the state of the UI controls.
    // (the Save button is disabled when there are no changes)
    private bool HasChanges()
    {
      return this.dbContext.ChangeTracker.Entries().Any(
        e => e.State == EntityState.Added || e.State == EntityState.Modified || e.State == EntityState.Deleted);
    }
    
    private void Test()
    {
      this.dbContext = Entities.CreateDbContext();
    
      ObjectStateManager objectStateManager = (this.dbContext as IObjectContextAdapter).ObjectContext.ObjectStateManager;
      objectStateManager.ObjectStateManagerChanged += new CollectionChangeEventHandler(OnObjectStateManagerChanged);
    
      // Retrieve all organisations. These entities will not be cached in the DbContext.
      this.dbContext.Configuration.ProxyCreationEnabled = false;
      List<Organisation> organisations = this.dbContext.Organisations.AsNoTracking().ToList();
      this.dbContext.Configuration.ProxyCreationEnabled = true;
    
      // Retrieve the entity to change. This entity will be cached in the DbContext.
      Employee employee = this.dbContext.Employees.First();
    
      // Update the navigation property of employee, referencing organisation
      Organisation organisation = organisations.First();
      employee.Organisation = organisation;
    
      // Check all DBContext entries. 
      // As a result of this call the organisation is automatically attached to the DbContext and 
      // the state of the entity is set to Added. Why?
      // At this time the OnObjectStateManagerChanged event handler is called. In this handler we will try 
      // to change the state of the entity to Unchanged.
      bool hasChanges = HasChanges();
    
      dbContext.SaveChanges();
    }
    
    private void OnObjectStateManagerChanged(object sender, CollectionChangeEventArgs e)
    {
      if (e.Action == CollectionChangeAction.Add)
      {
        EntityBase entity = e.Element as EntityBase;
    
        ObjectStateManager objectStateManager = (this.dbContext as IObjectContextAdapter).ObjectContext.ObjectStateManager;
        ObjectStateEntry objectStateEntry = objectStateManager.GetObjectStateEntry(entity);
    
        if (objectStateEntry.State == EntityState.Added)
        {
          // Check if the entity has an primary key. In that case the entity already exists in the database.
          // Try to change the state of entry to Unchanged. Otherwise the entity will be inserted in the database 
          // a second time resulting in duplicate entities.
          int ID = ((IEntity)entity).ID;
          if (ID > 0)
          {
            // This call raises an exception!
            objectStateManager.ChangeObjectState(entity, EntityState.Unchanged);
          }
        }
      }
    }
    
    The call to the HasChanges method results in adding the selected organisation to the context. The state is set to Added.
    In the OnObjectStateManagerChanged event handler I try to change this state to Unchanged, but this results in an exception.
    Apparently calling the ChangeTracker.Entries method does more than checking the state of the entries. It would be nice if the entity was added to the context having state Unchanged.
    Thanks,

    Michel Miranda
    Sunday, July 17, 2011 1:12 PM

Answers

  • Hi,

    There isn't any easy solutions here. If you don't want to changetrack you have to merge the organisation objects with your employee.organisation object.

    What you are experiencing is similar to what Diego B. Vega wrote in the link below about self-tracking entities (Since STE are doing the same that you want to do).

    http://blogs.msdn.com/b/diego/archive/2010/10/06/self-tracking-entities-applychanges-and-duplicate-entities.aspx

    First, solution 1 is the most obvious way to solve your problem. Instead of binding to Employee.Organisation, bind to the FK instead Employee.OrganisationId (In WPF, bind Employee.OrganisationId to SelectedValue instead of Employee.Organisation to SelectedItem and set SelectedValuePath to Organisation.Id).

    Also take a look at solution 3 - Perform identity resolution on client. This is what you need to do to merge the entities.

    So, in your example above you have to:

    1. Get the organisations list with AsNoTracking
    2. Get the employee object with the Organisation object
    3. Merge employee.Organistation object into your organisations list by using solution 3 in Diego's post

    Now you can skip your actions around ChangeObjectState and things should work.

    Of course STE and your DbContext isn't EXACTLY the same, so you have to adjust it to fit to your code. But the idea is to merge organisation objects that are already tracked in the context with the one in your list.

    A another thing.. This function CreateDbContext, does this return a new context to you, or is it returning some sort of member of Entities object? If you are using the same DbContext across multiple calls, you have to do it a bit different. Instead of getting the employees, you have to get all organisation entities that already exists in the ObjectStateManager and merge these into your organisations list.

    Other than doing this manual work you don't actually have any other solutions to this problem...


    --Rune
    Monday, July 18, 2011 12:13 PM
  • Hi,

    No, not in any easy way.

    But you actually doesn't need adding the organisation entity to the context in an unchanged state if you instead of attaching the organisation to the current Employee set's Employee.OrganisationId to the primary key instead. This will save you a lot of hazzle. It's a bit complicated to just describe this here, but you can get an example here, the file is named DemoFKIdUse.zip.

    The code contains inline explanations which should tell you what I do.

    Other than this, I don't have any easy solution code on this matter.


    --Rune
    Thursday, July 21, 2011 9:26 AM

All replies

  • Hi,

    It would help us to know what your exception message says, but I suspect that your problem is that the context already contains another Organisation object with the same entity key. This will cause your call to ChangeObjectState to notify you about this.

    There isn't really any good solution to this other than you should first check the ChangeTracker.Entities for an exisiting organisation object and if this is found use this instead of the one you're trying to set.

    I suggest that you should track your organisation objects too to avoid this problem.


    --Rune
    Monday, July 18, 2011 8:30 AM
  • Hi Rune Gulbrandsen,

    Thank you for your response. In my case tracking the Organisations is not an option. I need the context to be as small as possible. All my lookup data is retrieved as detached data. It would be nice if there is a generic solution to automatically detect when a navigation property changes and then add the detached related entity to the context having state Unchanged.

    Here is the exception:

       at System.Data.Objects.DataClasses.RelatedEnd.CheckReferentialConstraintProperties(EntityEntry ownerEntry)
       at System.Data.Objects.DataClasses.RelationshipManager.CheckReferentialConstraintProperties(EntityEntry ownerEntry)
       at System.Data.Objects.EntityEntry.AcceptChanges()
       at System.Data.Objects.EntityEntry.ChangeObjectState(EntityState requestedState)
       at System.Data.Objects.ObjectStateManager.ChangeObjectState(Object entity, EntityState entityState)
       at UserInterface.TestPage.Window1.OnObjectStateManagerChanged(Object sender, CollectionChangeEventArgs e) in D:\PM\PM-FlameKlantkaart\UserInterface\TestPage\Window1.xaml.cs:line 81
       at System.ComponentModel.CollectionChangeEventHandler.Invoke(Object sender, CollectionChangeEventArgs e)
       at System.Data.Objects.ObjectStateManager.OnObjectStateManagerChanged(CollectionChangeAction action, Object entity)
       at System.Data.Objects.ObjectStateManager.AddEntry(IEntityWrapper wrappedObject, EntityKey passedKey, EntitySet entitySet, String argumentName, Boolean isAdded)
       at System.Data.Objects.ObjectContext.AddSingleObject(EntitySet entitySet, IEntityWrapper wrappedEntity, String argumentName)
       at System.Data.Objects.DataClasses.RelatedEnd.AddEntityToObjectStateManager(IEntityWrapper wrappedEntity, Boolean doAttach)
       at System.Data.Objects.DataClasses.RelatedEnd.AddGraphToObjectStateManager(IEntityWrapper wrappedEntity, Boolean relationshipAlreadyExists, Boolean addRelationshipAsUnchanged, Boolean doAttach)
       at System.Data.Objects.DataClasses.RelatedEnd.Add(IEntityWrapper wrappedTarget, Boolean applyConstraints, Boolean addRelationshipAsUnchanged, Boolean relationshipAlreadyExists, Boolean allowModifyingOtherEndOfRelationship, Boolean forceForeignKeyChanges)
       at System.Data.Objects.ObjectStateManager.PerformAdd(IEntityWrapper wrappedOwner, RelatedEnd relatedEnd, IEntityWrapper entityToAdd, Boolean isForeignKeyChange)
       at System.Data.Objects.ObjectStateManager.PerformAdd(IList`1 entries)
       at System.Data.Objects.ObjectStateManager.DetectChanges()
       at System.Data.Entity.Internal.InternalContext.DetectChanges(Boolean force)
       at System.Data.Entity.Internal.InternalContext.GetStateEntries(Func`2 predicate)
       at System.Data.Entity.Internal.InternalContext.GetStateEntries()
       at System.Data.Entity.Infrastructure.DbChangeTracker.Entries()
       at UserInterface.TestPage.Window1.HasChanges() in D:\PM\PM-FlameKlantkaart\UserInterface\TestPage\Window1.xaml.cs:line 55
       at UserInterface.TestPage.Window1.Test() in D:\PM\PM-FlameKlantkaart\UserInterface\TestPage\Window1.xaml.cs:line 48
       at UserInterface.TestPage.Window1..ctor() in D:\PM\PM-FlameKlantkaart\UserInterface\TestPage\Window1.xaml.cs:line 23

    I would really appreciate it when someone is able to tell me what is going on in my sample code.

    Thanks,

    Michel Miranda

    Monday, July 18, 2011 10:16 AM
  • Hi,

    There isn't any easy solutions here. If you don't want to changetrack you have to merge the organisation objects with your employee.organisation object.

    What you are experiencing is similar to what Diego B. Vega wrote in the link below about self-tracking entities (Since STE are doing the same that you want to do).

    http://blogs.msdn.com/b/diego/archive/2010/10/06/self-tracking-entities-applychanges-and-duplicate-entities.aspx

    First, solution 1 is the most obvious way to solve your problem. Instead of binding to Employee.Organisation, bind to the FK instead Employee.OrganisationId (In WPF, bind Employee.OrganisationId to SelectedValue instead of Employee.Organisation to SelectedItem and set SelectedValuePath to Organisation.Id).

    Also take a look at solution 3 - Perform identity resolution on client. This is what you need to do to merge the entities.

    So, in your example above you have to:

    1. Get the organisations list with AsNoTracking
    2. Get the employee object with the Organisation object
    3. Merge employee.Organistation object into your organisations list by using solution 3 in Diego's post

    Now you can skip your actions around ChangeObjectState and things should work.

    Of course STE and your DbContext isn't EXACTLY the same, so you have to adjust it to fit to your code. But the idea is to merge organisation objects that are already tracked in the context with the one in your list.

    A another thing.. This function CreateDbContext, does this return a new context to you, or is it returning some sort of member of Entities object? If you are using the same DbContext across multiple calls, you have to do it a bit different. Instead of getting the employees, you have to get all organisation entities that already exists in the ObjectStateManager and merge these into your organisations list.

    Other than doing this manual work you don't actually have any other solutions to this problem...


    --Rune
    Monday, July 18, 2011 12:13 PM
  • Hi Rune Gulbrandsen,

    Thank you for your response. Not all my questions are answered, but your advise is very helpful to me. I really appreciate it.

    I need a generic solution to automatically detect when a navigation property changes. In my sample when the user selects an organisation from the ComboBox I want to add the detached organisation to the context having state Unchanged. Is there a way to achieve this in a general way without hard coding it for each entity?

    Thanks,

    Michel Miranda

    Wednesday, July 20, 2011 6:18 AM
  • Hi,

    No, not in any easy way.

    But you actually doesn't need adding the organisation entity to the context in an unchanged state if you instead of attaching the organisation to the current Employee set's Employee.OrganisationId to the primary key instead. This will save you a lot of hazzle. It's a bit complicated to just describe this here, but you can get an example here, the file is named DemoFKIdUse.zip.

    The code contains inline explanations which should tell you what I do.

    Other than this, I don't have any easy solution code on this matter.


    --Rune
    Thursday, July 21, 2011 9:26 AM
  • Hi Rune Gulbrandsen,

    I appreciate your help.

    Thanks for your time,

    Michel Miranda

    Saturday, July 23, 2011 6:08 PM
  • I had the same error and solution 1 you suggested worked!

    First, solution 1 is the most obvious way to solve your problem. Instead of binding to Employee.Organisation, bind to the FK instead Employee.OrganisationId (In WPF, bind Employee.OrganisationId to SelectedValue instead of Employee.Organisation to SelectedItem and set SelectedValuePath to Organisation.Id)

    So in WPF bind to the FKeys and not the navigation properties....

     

    Thanks

    Thursday, November 10, 2011 10:35 PM