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
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:
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:
To retrieve the object from the database and to delete it through the Delete() method (and call SaveChanges()), like this:
To use the BulkDelete feature like this:
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.