Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / entity-framework

Attaching Detached POCO to EF DbContext - Simple and Fast

5.00/5 (5 votes)
11 Apr 2013CPOL4 min read 69.5K  
Simple generic extension method that will attach detached POCO to DbContext without reloading it form DB

Introduction

Recently, I was playing around with Entity Framework (EF) and evaluating it for some projects. I had a very hard time figuring out how to attach detached object graph to DBContext in a fast and reliable way. Here, I am sharing a simple AttachByIdValue() method implementation that can do this for you. If you are not interested in full explanation of the problem, jump straight to method implementation and start attaching your objects.

The Problem

Let’s say we are using EF in web app to implement page for managing Order and OrderLines. So we have parent-child relation (Order and OrderLines) and some referential data that is displayed but won’t be updated (Customer and Products).

Image 1

We would typically query the above object graph from database (DB) using EF and send it to client (browser). When client sends this object graph back to server, we would like to persist it and in order to do so, we must first attach it to DbContext.

The question is how to attach this detached graph without reloading it from DB and applying changes. Reloading form DB is performance hit and it is invasive. If I couldn’t do it without reloading, I would discard EF because this is very basic task that I expect my ORM to solve easily. Luckily, I found the solution after a lot of digging.

Add() or Attach()

There are two methods for attaching detached objects, Add() and Attach(), and they receive graph root object (Order). Add() method attaches all objects in graph and marks them as Added, while Attach() also attaches all objects in graph but marks them as Unchanged.

Since our object group will usually have new, modified and unchanged data, our only option is to use one of these two methods to attach the full graph and then traverse the graph and correct state of each entry.

So which method should we choose?

Well, actually Attach is not an option because attach can cause key conflicts due to duplicate key values for same object types. If we have Order with two new OrderLines, those OrderLines would probably have Id = 0. Attaching this Order with Attach method would break because Attach will mark these two OrderLines as Unchanged and EF insists that all existing entities should have unique primary keys. This is why we will be using Add method for attaching.

Resolving New and Modified Data by Id Value

The question is how will we know the state of each object in graph (New/Modified/Unchanged/Deleted)? Because detached objects are not tracked, the only reliable way would be to reload the object graph form DB, and as I stated before, I don’t want to do that because of the performance.

We can use simple convention. If Id > 0 object is modified, and if Id = 0 then object is new. This is pretty simple convention but with drawbacks:

  • We can’t detect unchanged objects so we will be saving to DB unchanged data.
    On the bright side, these object graphs should not be that big so this should not be a performance issue.
  • Deleting objects must be handled with custom logic.
    E.g., having something like Order.DeletedOrderLines collection.

In order to read Id value when attaching objects, all entities will implement IEntity interface.

C#
public interface IEntity
{ 
    long Id { get; }
}  

Ignoring Referent Data

Each object graph can contain referential (read-only) data. In our case, when we are saving Order, we might have Products and Customer objects in graph but we know that we don’t want to save them in DB. We know that we should save only Order and OrderLines. On the other hand, EF doesn’t know that. This is the way AttachByIdValue accepts array of Child types that should be attached for saving along with Order. All objects in graph that are not root nor are of Child Type will be attached to context, but will be marked as Unchanged so they won’t be saved to DB.

To save only Order (without OrderLines), we should call:

C#
myContext.AttachByIdValue(Order, null);
myContext.SaveChanges();  

So to save Order and OrderLines, we should call:

C#
myContext.AttachByIdValue(Order, new HashSet<Type>() { typeof(OrderLine) });
myContext.SaveChanges(); 

Of course, the above HashSet<Type> can be cached in static field to avoid calling typeof on every object attaching.

C#
private static readonly HashSet<Type> OrderChildTypes = new HashSet<Type>() { typeof(OrderLine) }; 
... 
myContext.AttachByIdValue(Order, OrderChildTypes);
myContext.SaveChanges();   

The Final Solution

C#
/// <summary>
/// Attaches entity graph to context using entity id to determinate if entity is new or modified.
/// If Id is zero, then entity is treated as NEW and otherwise, it is treated as modified.
/// If we want to save more than just root entity, then child types must be supplied.
/// If entity in graph is not root nor of child type it will be attached but not saved
/// (it will be treated as unchanged).
/// </summary>
/// <param name="context">The context.</param>
/// <param name="rootEntity">The root entity.</param>
/// <param name="childTypes">The child types that should be saved with root entity.</param>
public static void AttachByIdValue<TEntity>(this DbContext context, 
       TEntity rootEntity, HashSet<Type> childTypes)
    where TEntity : class, IEntity
{
    // mark root entity as added
    // this action adds whole graph and marks each entity in it as added
    context.Set<TEntity>().Add(rootEntity);
    // in case root entity has id value mark it as modified (otherwise it stays added)
    if (rootEntity.Id != 0)
    {
        context.Entry(rootEntity).State = EntityState.Modified;
    }
    // traverse all entities in context (hopefully they are all part of graph we just attached)
    foreach (var entry in context.ChangeTracker.Entries<IEntity>())
    {
        // we are only interested in graph we have just attached
        // and we know they are all marked as Added 
        // and we will ignore root entity because it is already resolved correctly
        if (entry.State == EntityState.Added && entry.Entity != rootEntity)
        {
            // if no child types are defined for saving then just mark all entities as unchanged)
            if (childTypes == null || childTypes.Count == 0)
            {
                entry.State = EntityState.Unchanged;
            }
            else
            {
                // request object type from context 
                // because we might have got reference to dynamic proxy
                // and we wouldn't want to handle Type of dynamic proxy
                Type entityType = ObjectContext.GetObjectType(entry.Entity.GetType());
                // if type is not child type than it should not be saved so mark it as unchanged
                if (!childTypes.Contains(entityType))
                {
                    entry.State = EntityState.Unchanged;
                }
                else if (entry.Entity.Id != 0)
                {
                    // if entity should be saved with root entity
                    // then if it has id mark it as modified 
                    // else leave it marked as added
                    entry.State = EntityState.Modified;
                }
            }
        }
    }
}  

One Gotcha

As I explained earlier, EF insists that all existing entities should have unique primary keys and this is why you cannot attach to DbContext two unchanged objects of the same type with the same Id. This shouldn’t be the case in general but I have found one edge case where it might occur. Let’s say we are loading Order, OrderLines and Products and we have two different Order Lines pointing to the same Product. Normally, EF will set reference to the same Product object to these OrderLines unless you are loading your data using AsNoTracking to get better performance in which case each OrderLine gets a reference to separate Product object that is equal by all values. I didn’t find documentation of this behavior anywhere, I discovered it by accident while I was struggling to attach objects to DBContext.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)