2

I am writing some tests to excersize the repository layer of a library built on Telerik OpenAccess ORM and am running into some problems with managing the Context.

I am creating a new RegionEntity object and adding it to the database. I use the using statement so that the context cleans up after itself. I additionally create a Detached copy of the added RegionEntity so that it can be re-attached to a context later on.

    private RegionEntity AddTestRegionToTable()
    {
        String regionName = Guid.NewGuid().ToString();
        RegionEntity newRegion = new RegionEntity () { /*...property assignment goes here ...*/ };
        RegionEntity ret = null;

        using (DbContext ctx = new DbContext())
        {
            ctx.Add(newRegion);
            ctx.SaveChanges();
            ret = ctx.CreateDetachedCopy<RegionEntity>(newRegion);
        }

        return ret;
    }

So far ... no problem. In my TestMethod below I call the above method and receive a Detached RegionEntity. (I have pulled out my assert statements as they are inconsequential to the issue). I then pass the entity to the Respository method I want to test.

    [TestMethod]
    public void RemoveRegion_Success()
    {
        //
        // Assemble
        RegionEntity origEntity    = AddTestRegionToTable();

        //
        // Act
        deletedEntity = RegionRepository.RemoveEntity<RegionEntity>(origEntity);

        //
        // Assert
    /* asserts go here */

    }

For the sake of completeness, below I have included ALL the remaining code, exactly as it appears in my application. The repository methods are Generic (again ... should not be relevant to the issue). The first method is the one that is called by the test method, passing in the region as the entityToRemove parameter. This method, in turn calls the DBUtils method, GetContext(), that will either retrieve the DbContext from the entity, or ... if one is not able to be derived... create a new context to be used. In our example a new context is being created.

    public class RegionRepository 
    {
        public static T RemoveEntity<T>(T entityToRemove) where T : class
        {
            T ret = null;

            using (DbContext ctx = DbUtils.GetContext<T>(entityToRemove))
            {
                ret = RemoveEntity<T>(ctx, entityToRemove);
                ctx.SaveChanges();
            }

            return ret;
        }

        public static T RemoveEntity<T>(DbContext ctx, T entityToRemove) where T : class
        {
            //
            // first chcek to see if the listingToUpdate is attached to the context 
            ObjectState state = OpenAccessContext.PersistenceState.GetState(entityToRemove);
            //
            //If the object is detached then attach it
            if (state.HasFlag(ObjectState.Detached))
            {
                ctx.AttachCopy<T>(entityToRemove);
            }
            //
            // confirm that the DETACHED flag is no longer present.
            ObjectState state2 = OpenAccessContext.PersistenceState.GetState(entityToRemove);

            if (state2.HasFlag(ObjectState.Detached))
            {
                throw new Exception("Unable to attach entity to context");
            }

            ctx.Delete(entityToRemove);
            return entityToRemove;
        }
}


public class DBUtils 
{
        public static DbContext GetContext<T>(T entity)
        {
            DbContext ret = OpenAccessContextBase.GetContext(entity) as DbContext;

            if(ret == null) 
            {
                ret = new DbContext();
            } 

            return ret;

        }

    }

Anyway, the method then passes this context and the entity as parameters to an overload. This method takes the DbContext as an additional parameter (allows a single context to be used in multi-step workflows). So the context that is used should still be the one we extracted from the entity or created in our GetContext() method. I then check to see if the entity is attached to the context or not. In this scenario I AM getting a flag of "Detached" as one of the state flags (others are MaskLoaded | MaskManaged | MaskNoMask) so the process then attaches the entity to the context and upon the second check I confirm that the Detached flag is no longer present.

As it turns out the entity is NOT being attached ... and the exception is being thrown.

I have read the Telerik documentation on Detaching and attaching objects to a context ... Attaching and Detaching Objects

Gary O. Stenstrom
  • 2,284
  • 9
  • 38
  • 59

1 Answers1

1

By design ObjectState is flags enum that contains both the basic values that form the persistent states of Data Access and the persistent states themselves.

In this enum, Detached is a value that participates in the three detached persistent states: DetachedClean, DetachedDirty, and DetachedNew. You can find more information about the values and the states in this article.

When you detach an object from the context, its state is DetachedClean. If at this point you change any of the properties, the state of the object will become DetachedDirty. If you attach the object back, it will remain in the state before the attachment. Simply put, the action of attaching the object does not change its state.

In other words, checking for Detached is the reason why you get the "Unable to attach entity to context" exception. This value will always be available in the state of your object.

As I am reading the code forward, on this line:

ctx.Delete(entityToRemove);

You will get an exception anyway, because Data Access does not allow you to delete objects that are retrieved through another instances of the context. The exception is:

InvalidOperationException: Object references between two different object scopes are not allowed.

I hope this helps.

-= EDIT =-

When you attach a certain object to an instance of the context and call the SaveChanges() method, Data Access will automatically decide whether to insert a new row in the database or to update an existing row. In this connection, the insert and update scenarios are handled by the Attach / Detach API.

Regarding the delete scenario, you have two options:

  1. To retrieve the object from the database and to delete it through the Delete() method (and call SaveChanges()), like this:

       var myObj = ctx.RegionEntities.First(r => r.Id == entityToRemove.Id);
       ctx.Delete(myObj);
       ctx.SaveChanges();
    
  2. To use the BulkDelete feature like this:

       var myObj = ctx.RegionEntities.Where(r => r.Id == entityToRemove.Id);
       int deletedObjects = myObj.DeleteAll();
    

Something you need to consider with the first option is whether to call SaveChanges() after you attach the object. It is a good idea to do so if there are changes you would like to persist before deleting the object. Additionally, when you use the Delete() method of the context you need to commit the change through the SaveChanges() method before you dispose the current instance of the context. If you do not do this, the transaction will be rolled back, meaning that the object will not be deleted. Details about the transaction handling are available here.

The second option, Bulk Delete, executes the delete operations in a separate transaction upon the call to the DeleteAll() method. Therefore, any other uncommitted changes are not affected. Nevertheless, you need to consider a call to SaveChanges() after attaching the object, especially if the attached object and the deleted one are one and the same object.

  • Thank you for your response. I am not going to lie ... this seems a little convoluted to me. If I MUST use the original context to delete or update an existing record, how then would I go about updating/deleting an entity (record) to which I no longer have a reference to the originating context? I thought that the whole point in CreateDetachedCopy was to mitigate that dependency; to make it basically a free entity that I could attach to any context that I might have available. – Gary O. Stenstrom Jun 25 '15 at 04:22
  • I am editing my original reply in order to address your questions. – Doroteya Agayna Jun 26 '15 at 11:13
  • Thank you, this is genuinely excellent information. The question I have though is what is the point in detaching an entity if I cannot re-attach it to ANOTHER instance of the context later down the road? In my OP I created a region using instance "A" of the context. My expectation was that I could detach it, make some changes to the entity instance, then save those changes using instance "B" of the context. Since it was detached all I should have to do is attach it to another instance of the context. I am sure that I am just not "getting it". What am I missing? – Gary O. Stenstrom Jul 01 '15 at 13:09