Generic Insert or Update for Entity Framework

c# entity-framework

Question

Say I have an insert method:

public T Add<T>(T t)
{
   context.Set<T>().Add(t);
   context.SaveChanges();
   return t;
}

And a generic update:

public T Update<T>(T updated,int key)
{
    if (updated == null)
        return null;

    T existing = _context.Set<T>().Find(key);

    if (existing != null)
    {
        context.Entry(existing).CurrentValues.SetValues(updated);
        context.SaveChanges();
    }

    return existing;
}

I'd like to combine them into one SaveOrUpdate that accepts any entity method:

How would I best achieve this and is there a more efficient way that avoids a round trip to the database than using context.Set<T>().Find(key)

1
5
8/24/2016 8:41:06 PM

Popular Answer

I have a method that acts a little bit differently:

  • It doesn't set an existing entity as Modified, but as Attached.
  • It doesn't execute SaveChanges().

I'll explain why, but first the source:

public static class DbContextExtensions
{
    public static void AddOrAttach<T>(this DbContext context, T entity)
        where T : class
    {
#region leave conditions
        if (entity == null) return;

        var entry = context.Entry(entity);
        var leaveStates = new[]
        {
            EntityState.Deleted,
            EntityState.Modified,
            EntityState.Unchanged
        };
        if (leaveStates.Contains(entry.State)) return;
#endregion

        var entityKey = context.GetEntityKey(entity);
        if (entityKey == null)
        {
            entry.State = EntityState.Unchanged;
            entityKey = context.GetEntityKey(entity);
        }
        if (entityKey.EntityKeyValues == null 
            || entityKey.EntityKeyValues.Select(ekv => (int)ekv.Value).All(v => v <= 0))
        {
            entry.State = EntityState.Added;
        }
    }

    public static EntityKey GetEntityKey<T>(this DbContext context, T entity)
        where T : class
    {
        var oc = ((IObjectContextAdapter)context).ObjectContext;
        ObjectStateEntry ose;
        if (null != entity && oc.ObjectStateManager
                                .TryGetObjectStateEntry(entity, out ose))
        {
            return ose.EntityKey;
        }
        return null;
    }
}

As you see, in the AddOrAttach method there are a number of states that I leave unaltered.

Then there is some logic to determine whether the entity should be added or attached. The essence is that every entity that's tracked by the context has an EntityKey object. If it hasn't, I attach it first so it gets one.

Then, there are scenarios in which an entity does have an EntityKey, but without key values. If so, it will be Added. Also when it's got key values, but they're all 0 or smaller, it will be Added. (Note that I assume that you use int key fields, possibly as composite primary keys).

Why no SaveChanges?

Your methods store entities one-by-one. However, it's far more common to save multiple objects (object graphs) by one SaveChanges call, i.e. in one transaction. If you'd want to do that by your methods, you'd have to wrap all calls in a TransactionScope (or start and commit a transaction otherwise). It's far more convenient to build or modify entities you work with in one logical unit of work and then do one SaveChanges call. That's why I only set entity state by this method.

Why Attach?

People made similar methods that do an "upsert" (add or update). The drawback is that it marks a whole entity as modified, not just its modified properties. I prefer to attach an entity and then continue the code with whatever happens to it, which may modify one or some of its properties.

Evidently, you are well aware of the benefit of setting properties as modified, because you use

context.Entry(existing).CurrentValues.SetValues(updated);

This is indeed the recommended way to copy values into an existing entity. Whenever I use it, I do it outside (and following) my AddOrAttach method. But...

is there a more efficient way that avoids a round trip to the database

CurrentValues.SetValues only works if the current values are the database values. So you can't do without the original entity to use this method. So, in disconnected scenarios (say, web applications), if you want to use this method, you can't avoid a database roundtrip. An alternative is to set the entity state to Modified (with the drawbacks mentioned above). See my answer here for some more discussion on this.

3
5/23/2017 12:00:43 PM


Related Questions





Related

Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow