71

Using EF5 with a generic Repository Pattern and ninject for dependency injenction and running into an issue when trying to update an entity to the database utilizing stored procs with my edmx.

my update in DbContextRepository.cs is:

public override void Update(T entity)
{
    if (entity == null)
        throw new ArgumentException("Cannot add a null entity.");

    var entry = _context.Entry<T>(entity);

    if (entry.State == EntityState.Detached)
    {
        _context.Set<T>().Attach(entity);
        entry.State = EntityState.Modified;
    }
}

From my AddressService.cs which goes back to my repository I have:

 public int Save(vw_address address)
{
    if (address.address_pk == 0)
    {
        _repo.Insert(address);
    }
    else
    {
        _repo.Update(address);
    }

    _repo.SaveChanges();

    return address.address_pk;
}

When it hits the Attach and EntityState.Modified it pukes with the error:

An object with the same key already exists in the ObjectStateManager. The ObjectStateManager cannot track multiple objects with the same key.

I have looked through many of the suggestions in stack and on the Internet and not coming up with anything that resolves it. Any work arounds would be appreciated.

Thanks!

Chris Schiffhauer
  • 17,102
  • 15
  • 79
  • 88
Juan
  • 1,144
  • 1
  • 11
  • 16

10 Answers10

125

Edit: Original answer used Find instead of Local.SingleOrDefault. It worked in combination with @Juan's Save method but it could cause unnecessary queries to database and else part was probably never executed (executing the else part would cause exception because Find already queried the database and hadn't found the entity so it could not be updated). Thanks to @BenSwayne for finding the issue.

You must check if an entity with the same key is already tracked by the context and modify that entity instead of attaching the current one:

public override void Update(T entity) where T : IEntity {
    if (entity == null) {
        throw new ArgumentException("Cannot add a null entity.");
    }

    var entry = _context.Entry<T>(entity);

    if (entry.State == EntityState.Detached) {
        var set = _context.Set<T>();
        T attachedEntity = set.Local.SingleOrDefault(e => e.Id == entity.Id);  // You need to have access to key

        if (attachedEntity != null) {
            var attachedEntry = _context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        } else {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}  

As you can see the main issue is that SingleOrDefault method needs to know the key to find the entity. You can create simple interface exposing the key (IEntity in my example) and implement it in all your entities you want to process this way.

Nicholas Petersen
  • 9,104
  • 7
  • 59
  • 69
Ladislav Mrnka
  • 360,892
  • 59
  • 660
  • 670
  • Thanks. So i created an interface IEntity with int Id { get; set; } then tried to do public override void Update(T entity) where T : IEntity but its not liking the where T : IEntity. This is in a repository class ie public class DbContextRepository : BaseRepository where T : class if that makes a difference. Thanks! – Juan Sep 25 '12 at 17:17
  • 1
    In such case put the constraint directly on class definition – Ladislav Mrnka Sep 25 '12 at 17:20
  • hmm.. still not having much luck. I wonder if its because I am using an edmx model. But I am unable to put the constraint directly on the class as it implements BaseRepository and IRepository. Plus in the edmx the entities are coming from views and the primary keys are something like address_pk. – Juan Sep 25 '12 at 20:26
  • I had [similar problem](http://stackoverflow.com/questions/13364665/updating-database-record-using-entity-framework-lazy-loading-and-virtual-proper) (still not solved). The problems are *virtual* reference-type variables properties which don't update. – patryk.beza Nov 13 '12 at 19:48
  • 2
    @LadislavMrnka: When you use set.Find() if it is not already in the object state manager it will be loaded from the db, right? So in the above code `attachedEntity` will always be not null and you will never attach the passed in entity? (ie: you will never reach the `else {` statement) Perhaps I am misunderstanding the documentation for `DbSet<>.Find()`. Should we not be using DbSet<>.Local? – BenSwayne Oct 23 '13 at 19:10
  • @BenSwayne: You are right. The code above doesn't reach else part if the entity exists in the database and if it doesn't it will fail because in such case else part must set the state to Added or simply add entity to the set. Using Local will fix it - I will modify the answer. – Ladislav Mrnka Oct 24 '13 at 08:53
  • @LadislavMrnka: when the attachedEntry is null, should that call the _context.Set().Attach(entity); to attach the entity first before setting the state? or it is what the comment about? – Stay Foolish Jan 03 '14 at 12:24
  • It's worth knowing that the copying over of values between entities will not copy over fields marked as [NotMapped]. This breaks what I want to do. – Alan Macdonald Feb 05 '14 at 19:05
  • What if you can't guess what is the entity Primary key name? here `e => e.Id == entity.Id` – mhesabi Mar 31 '14 at 09:39
  • @mhesabi You may need to add the predicate as second param `public virtual void Update(T entity,Func predicate)where T : IEntity { //---hidden T attachedEntity = set.Local.SingleOrDefault(predicate); // So You don't need to have access to key if (attachedEntity != null) {} } }` – Bellash Aug 07 '14 at 17:24
  • @LadislavMrnka note that testing Set().Local will not find entities that were removed via Set().Remove() - although they will still be attached – Leon van der Walt Apr 02 '15 at 08:57
8

I didn't want to pollute my auto generated EF classes by adding interfaces, or attributes. so this is really a little bit from some of the above answers (so credit goes to Ladislav Mrnka). This provided a simple solution for me.

I added a func to the update method that found the integer key of the entity.

public void Update(TEntity entity, Func<TEntity, int> getKey)
{
    if (entity == null) {
        throw new ArgumentException("Cannot add a null entity.");
    }

    var entry = _context.Entry<T>(entity);

    if (entry.State == EntityState.Detached) {
        var set = _context.Set<T>();
        T attachedEntity = set.Find.(getKey(entity)); 

        if (attachedEntity != null) {
            var attachedEntry = _context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        } else {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}  

Then when you call your code, you can use..

repository.Update(entity, key => key.myId);
David Sherret
  • 101,669
  • 28
  • 188
  • 178
Geoff Wells
  • 361
  • 3
  • 10
  • Should you be not using `set.Local.Find` instead of `set.Find`? I believe your code will always hit the database, thus never making the `attachedEntity` variable null. https://msdn.microsoft.com/en-us/library/jj592872(v=vs.113).aspx – Reuel Ribeiro May 08 '17 at 20:40
7

You can actually retreive the Id through reflection, see example below:

        var entry = _dbContext.Entry<T>(entity);

        // Retreive the Id through reflection
        var pkey = _dbset.Create().GetType().GetProperty("Id").GetValue(entity);

        if (entry.State == EntityState.Detached)
        {
            var set = _dbContext.Set<T>();
            T attachedEntity = set.Find(pkey);  // access the key
            if (attachedEntity != null)
            {
                var attachedEntry = _dbContext.Entry(attachedEntity);
                attachedEntry.CurrentValues.SetValues(entity);
            }
            else
            {
                entry.State = EntityState.Modified; // attach the entity
            }
        }
Toffee
  • 4,586
  • 2
  • 15
  • 6
2

@serj-sagan you should do it in this way:

**Note that YourDb should be a class derived from DbContext.

public abstract class YourRepoBase<T> where T : class
{
    private YourDb _dbContext;
    private readonly DbSet<T> _dbset;

    public virtual void Update(T entity)
    {
        var entry = _dbContext.Entry<T>(entity);

        // Retreive the Id through reflection
        var pkey = _dbset.Create().GetType().GetProperty("Id").GetValue(entity);

        if (entry.State == EntityState.Detached)
        {
           var set = _dbContext.Set<T>();
           T attachedEntity = set.Find(pkey);  // access the key
           if (attachedEntity != null)
           {
               var attachedEntry = _dbContext.Entry(attachedEntity);
               attachedEntry.CurrentValues.SetValues(entity);
           }
           else
           {
              entry.State = EntityState.Modified; // attach the entity
           }
       }
    }

}

Community
  • 1
  • 1
Toffee
  • 4,586
  • 2
  • 15
  • 6
2

If you set your context to AsNoTracking() this will stop aspmvc tracking the changes to the entity in memory (which is what you want anyway on the web).

_dbContext.Products.AsNoTracking().Find(id);  

I would recommend you read more about this at http://www.asp.net/mvc/tutorials/getting-started-with-ef-using-mvc/advanced-entity-framework-scenarios-for-an-mvc-web-application

Yashvit
  • 2,337
  • 3
  • 25
  • 32
1

Another solution (based on @Sergey's answer) could be:

private void Update<T>(T entity, Func<T, bool> predicate) where T : class
{
    var entry = Context.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        var set = Context.Set<T>();
        T attachedEntity = set.Local.SingleOrDefault(predicate); 
        if (attachedEntity != null)
        {
            var attachedEntry = Context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        }
        else
        {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}

And then you would call it like this:

Update(EntitytoUpdate, key => key.Id == id)
Andrés S.
  • 61
  • 4
0

Without reflection and if you don't want to use interfaces, you can use functional delegates to find an entity in the database. Here is the updated sample from above.

private void Update<T>(T entity, Func<ObservableCollection<T>, T> locatorMap) where T : class
{
    var entry = Context.Entry(entity);
    if (entry.State == EntityState.Detached)
    {
        var set = Context.Set<T>();
        T attachedEntity = locatorMap(set.Local); 

        if (attachedEntity != null)
        {
            var attachedEntry = Context.Entry(attachedEntity);
            attachedEntry.CurrentValues.SetValues(entity);
        }
        else
        {
            entry.State = EntityState.Modified; // This should attach entity
        }
    }
}

You would call it like this:

Update(EntitytoUpdate, p => p.SingleOrDefault(a => a.Id == id))
Sergey
  • 3,214
  • 5
  • 34
  • 47
0

Detaching the entity found (see attachedEntity in Ladislav's solution) and re-attaching the modified one worked for me just fine.

The reasoning behind this is simple: if something's immutable then replace it (as a whole, entity) from where it belongs with the desired one.

Here's an example of how to do this:

var set = this.Set<T>();
if (this.Entry(entity).State == EntityState.Detached)
{
    var attached = set.Find(id);
    if (attached != null) { this.Entry(attached).State = EntityState.Detached; }
    this.Attach(entity);
}

set.Update(entity);

Of course, one may easily figure out that this snippet is part of a generic method, hence the use of T, which is a template parameter, and Set<T>().

Alexander Christov
  • 9,625
  • 7
  • 43
  • 58
-2

That answer above may be EF 4.1+. For those on 4.0, try this simple method...not really tested but did attach and save my changes.

    public void UpdateRiskInsight(RiskInsight item)
    {
        if (item == null)
        {
            throw new ArgumentException("Cannot add a null entity.");
        }

        if (item.RiskInsightID == Guid.Empty)
        {
            _db.RiskInsights.AddObject(item);
        }
        else
        {
            item.EntityKey = new System.Data.EntityKey("GRC9Entities.RiskInsights", "RiskInsightID", item.RiskInsightID);
            var entry = _db.GetObjectByKey(item.EntityKey) as RiskInsight;
            if (entry != null)
            {
                _db.ApplyCurrentValues<RiskInsight>("GRC9Entities.RiskInsights", item);
            }

        }

        _db.SaveChanges();

    }
-4

You may have forgotten to instaçia the object fBLL = new FornecedorBLL(); in algun place