The Problem:

In some of earlier tips we talked about using Attach to load things into the ObjectContext in the unchanged state without wearing the cost of doing a query.

Attach is the weapon of choice if performance is your goal.

Unfortunately our APIs aren't tailored for the 99% case, which is where you only have just one entity set per type. The Entity Framework supports Multiple Entity Sets per Type or MEST, and the API reflects this, demanding that you provide the name of the EntitySet when you do an attach.

i.e. something like:

ctx.Attach("Orders", order);

Now if you are anything like me you probably hate having to specify strings in your code. They are error prone and kind of pollute your code, and it is essentially a "quiz".

Solution in .NET 4.0:

We've fixed this in .NET 4.0 by returning ObjectSet<T> instead of ObjectQuery<T> from our strongly typed properties for each of the EntitySets. ObjectSet<T> has methods for Add, Delete and Attach hanging directly off it, so you can write this:

ctx.Order.Attach(order);

Without a string in sight!

This solution is ideal, you attach to the required set, and whether you have MEST or not, it just works.

Solution for .NET 3.5 SP1:

What about .NET 3.5 SP1 though?

In my opinion we should have provided an generic version of Attach, lets say something like this:

void AttachToDefaultSet<T>(T Entity);

This method would see how many EntitySets exist for T, and if there is just one it would attach the Entity to that set for you. If however there is more than one it would throw an Exception.

While this method doesn't exist with the power of Extension Methods, it is pretty easy to write.

Here is what we need to do:

  1. Work out the EntityType for <T>
  2. Work out the list of possible EntitySet types that this EntityType could belong to. The EntityType could be a derived type (like Car) that actually belongs to a parent type set (like Vehicles).
  3. Loop over the EntityContainers, and each of the EntityContainer's EntitySets looking for a match.
  4. If we find one we attach, if not we throw.

So lets do this:

But first a warning, this is demo quality code, I'm a Program Manager not a Developer so use at your own risk :)

First we add an extension method to MetadataWorkspace to get the Conceptual Model (C-Space) EntityType for a CLR type (O-Space):

public static EntityType GetCSpaceEntityType<T>(
       this MetadataWorkspace workspace
)
{
    if (workspace == null
        throw new ArgumentNullException("workspace");
   
// Make sure the assembly for "T" is loaded
    workspace.LoadFromAssembly(typeof(T).Assembly);
    // Try to get the ospace type and if that is found
    // look for the cspace type too.
    EntityType ospaceEntityType = null;
    StructuralType cspaceEntityType = null;
    if (workspace.TryGetItem<EntityType>(
        typeof(T).FullName, 
        DataSpace.OSpace,
        out ospaceEntityType))
    {
        if (workspace.TryGetEdmSpaceType(
            ospaceEntityType,
            out cspaceEntityType))
        {
            return cspaceEntityType as EntityType;
        }
    }
    return null;
}

Because you could call this code before the metadata for <T> is loaded, one of the first lines makes sure the Assembly for <T> is loaded. This is pretty close to a no-op if the Assembly has already been loaded.

Next we add a method to get the an enumeration of all types we need to match, i.e. the Hierarchy of parent types including the current type:

public static IEnumerable<EntityType> GetHierarchy(
    this EntityType entityType)
{
    if (entityType == null)
        throw new ArgumentNullException("entityType");
    while (entityType != null)
    {
        yield return entityType;
        entityType = entityType.BaseType as EntityType;
    }
}

Now we need to do something that takes an EntityType and looks for possible EntitySets, both for that EntityType and parent types:

public static IEnumerable<EntitySet> GetEntitySets(
    this MetadataWorkspace workspace, 
    EntityType type)
{
    if (workspace == null)
        throw new ArgumentNullException("workspace");
    if (type == null)
        throw new ArgumentNullException("type");

    foreach (EntityType current in type.GetHierarchy())
    {
        foreach (EntityContainer container in 
            workspace.GetItems<EntityContainer>(DataSpace.CSpace))
        {
            foreach (EntitySet set in 
                container.BaseEntitySets.OfType<EntitySet>()
                .Where(e => e.ElementType == current))
            {
                yield return set;
            }
        }
    }
}

And now finally we are ready for the AttachToDefaultSet method:

public static void AttachToDefaultSet<T>(
    this ObjectContext ctx, 
    T entity)
{
    if (ctx== null) throw new ArgumentNullException("ctx");
    if (entity == null) throw new ArgumentNullException("entity");

 
    MetadataWorkspace wkspace = ctx.MetadataWorkspace;

    EntitySet set = wkspace
       .GetEntitySets(wkspace.GetCSpaceEntityType<T>())
       .Single();

 

    ctx.AttachTo(set.Name, entity);
}

This uses the standard .Single() method, which will throw an exception if there isn't exactly one possible set for the EntityType.

With this in place we can now write code like this:

Product p = new Product { ID = 1, Name = "Chocolate Fish" } ctx.AttachToDefaultSet(p);

 

Unless of course you use MEST... but you probably don't!

Extra Credit

While this code should perform okay, it definitely hasn't optimized in any way. 

It probably makes sense to cache the names of the possible sets for a CLR type, so that everytime you an Attach, you don't do the same checks, but I'll leave that as an exercise for you!

Index of Tips

Yes. There is an index of the rest of the tips in this series