Say I have an insert method:
public T Add(T t)
{
context.Set().Add(t);
context.SaveChanges();
return t;
}
And a
I have a method that acts a little bit differently:
Modified
, but as Attached
.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).
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.
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.
You could use an interface and do something like this. I used an explicit implementation so the rest of your code does not have to deal with it.
// I am not 100% sold on my chosen name for the interface, if you like this idea change it to something more suitable
public interface IIsPersisted {
bool IsPersistedEntity{get;}
int Key {get;}
}
public class SomeEntityModel : IIsPersisted{
public int SomeEntityModelId {get;set;}
/*some other properties*/
bool IIsPersisted.IsPersistedEntity{get { return this.SomeEntityModelId > 0;}}
int IIsPersisted.Key {get{return this.SomeEntityModelId;}}
}
public T UpdateOrCreate<T>(T updated) where T : class, IIsPersisted
{
if (updated == null)
return null;
if(updated.IsPersistedEntity)
{
T existing = _context.Set<T>().Find(updated.Key);
if (existing != null)
{
context.Entry(existing).CurrentValues.SetValues(updated);
context.SaveChanges();
}
return existing;
}
else
{
context.Set<T>().Add(updated);
context.SaveChanges();
return updated;
}
}
I just now saw this:
is there a more efficient way that avoids a round trip to the database than using
context.Set<T>().Find(key)
If you want the whole entity updated from a detached state then the easiest thing to do is this.
context.Entry(updated).State = EntityState.Modified;
context.SaveChanges();
This will mark the whole entity as dirty and save everything back to the database.