Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Generic Insert or Update for Entity Framework

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)

like image 316
dagda1 Avatar asked Aug 24 '16 18:08

dagda1


People also ask

How do I update entity Entity Framework?

The steps to update an existing entity are quite simple. First retrieve an instance of the entity from the EntitySet<T> (in our case ObjectSet<Customer>), then edit the properties of the Entity and finally call SaveChanges() on the context.

Which function is used for insert operation in Entity Framework?

Insert Data Use the DbSet. Add method to add a new entity to a context (instance of DbContext ), which will insert a new record in the database when you call the SaveChanges() method.


2 Answers

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.

like image 120
Gert Arnold Avatar answered Sep 24 '22 08:09

Gert Arnold


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;
    }
}

Edit

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.

like image 27
Igor Avatar answered Sep 25 '22 08:09

Igor