0

I'm using Entity Framework 6 (using generated models and DbContext) for my project. The architecture I'm using in my program is that the data access layer is abstracted from the application. This means my entities will mostly stay in detached state.

It seems that reattaching any previously connected entities seems impossible (or I'm not doing it properly).

I've tested a case in isolation and was able to narrow the problem down. Here are the steps.

  1. Get the entity from the context and dispose the context. This detaches the entity.

    public async Task<ArchiveEntry> GetEntry(string api, string id)
    {
        // Omitted some code for simplicity
        ArchiveEntry entry;
    
        using (var db = new TwinTailDb())
        {
            entry = await db.ArchiveEntries.Where(a => a.Id == id).SingleOrDefaultAsync();
        }
    
        return entry;
    }
    
  2. Save the entity regardless if it was changed or not.

    public async Task Save()
    {
        // Omitted some code for simplicity
        using (var db = new TwinTailDb())
        {
            db.ArchiveEntries.AddOrUpdate(a => a.Id, this);
            await db.SaveChangesAsync();
        }
    }
    
  3. Finally, reattach the detached entity.

    public async Task RevertChanges()
    {
        using (var db = new TwinTailDb())
        {
            if (db.Entry(this).State == EntityState.Detached && Id != 0)
            {
                db.ArchiveEntries.Attach(this);
                //await db.Entry(this).ReloadAsync(); // Commented since this is irrelevant
            }
        }
    }
    
  4. Then I run this function to use the code above.

    public async Task Test()
    {
        ArchiveEntry entry = await ArchiveService.GetEntry(null, "7");
        await entry.Save();
        await entry.RevertChanges();
    }
    

Then it throws this error:

Attaching an entity of type 'TwinTail.Entities.ArchiveEntry' failed because another entity of the same type already has the same primary key value. This can happen when using the 'Attach' method or setting the state of an entity to 'Unchanged' or 'Modified' if any entities in the graph have conflicting key values. This may be because some entities are new and have not yet received database-generated key values. In this case use the 'Add' method or the 'Added' entity state to track the graph and then set the state of non-new entities to 'Unchanged' or 'Modified' as appropriate.

Here's an important point. If I skip step 2, it doesn't throw an exception.

My speculation is that the entity was modified and saved in a different context. If step 2 was skipped, the entity remains unchanged so reattaching it doesn't pose a problem (just guessing). However, this entity is already in a detached state so this should be irrelevant.

Another point, the ChangeTracker doesn't contain anything during these steps. Also, if I perform any context operation on the detached entity, it throws an exception saying that it should be attached first. I've also noticed that the internal _entitywrapper still has a reference to the old context.

So, finally, here's the question. How do I properly reattach an entity and why does this exception happen.

I've asked something similar in a different question (How to revert changes on detached entities) but felt that I need to post a new one since this is more general.

Community
  • 1
  • 1
Sylpheed
  • 267
  • 1
  • 12
  • If you want to load an entity from context A, then dispose context A, then attach/save it using context B, then dispose context B, then reattach it to context C in order to reload it from the database, you are doing it wrong. – danludwig Mar 14 '16 at 13:12
  • Do you need the entities to be proxied? If you don't, you can disable proxying and the entities will stop carrying information about their original `DbContext` around with them. – Steve Ruble Mar 14 '16 at 13:30
  • @danludwig Oh wait. Is the "using" statement not enough? I thought it automatically and safely disposes the context right after the using block. If yes, then I'm doing everything wrong lol. For every database operation, I create a new context and dispose it (with using) before the function ends. I think I remember reading an article that this should be the case. But please enlighten me. – Sylpheed Mar 14 '16 at 16:35
  • @SteveRuble Okay I'll be trying that. – Sylpheed Mar 14 '16 at 16:35

2 Answers2

2

The architecture I'm using in my program is that the data access layer is abstracted from the application.

It looks like you are implementing these methods on the ArchiveEntry class itself. That is not an abstracted data access layer, and passing entities around to many short-lived contexts like this will get you into trouble.

Instead of giving the entity classes their own methods for managing persistence concerns, you should put that code into a class separate from the entity, and make sure that once an entity gets attached to a context, you keep using that same context until it gets disposed. Once the context is disposed, if you want to do another operation on the entity, you should retrieve it from a new context before trying to do any (attached) things with it.

danludwig
  • 46,965
  • 25
  • 159
  • 237
  • Agreed. Models should be separated from data access. I would suggest using something like the repository pattern. E.g. create a repository class for each model which will handle all database operations. – James Gardner Mar 14 '16 at 13:51
  • EF already implements a generic repository pattern out of the box. I would recommend a CQRS style messaging pattern, where write operations are abstracted as commands, with separate command handler method bodies. This would make it much easier to have a scoped `DbContext` instance that could be shared across commands for a particular execution context (like a single HTTP web request for example). – danludwig Mar 14 '16 at 13:55
  • Oh yeah sorry about that. Those partial classes are still pending for refactoring (prototyping =p). I do have IServices which works similarly to repository pattern. For now, I'm focusing on making everything work first since this is the first time I used both EF and WPF. I'm actually a game developer =p – Sylpheed Mar 14 '16 at 16:29
  • I should comment here instead. Didn't notice you were the one who commented on the question directly. About context lifetime, I see articles/posts (mostly) like this: http://stackoverflow.com/questions/10777630/questions-about-entity-framework-context-lifetime It says that context should be disposed immediately. Can you tell me these "troubles"? – Sylpheed Mar 14 '16 at 16:44
  • 1
    Well you are experiencing at least one of the troubles now -- shuffling entities between contexts breaks things. The answer you posted to is self-proclaimed as being controversial, and I have different opinions than its author. None of those things are real problems IMO. You should be aware of the consequences of lazy loading, and make sure your IoC container disposes things properly. – danludwig Mar 14 '16 at 17:43
  • Point taken. I'll read this article tomorrow during my break: http://www.asp.net/mvc/overview/older-versions/getting-started-with-ef-5-using-mvc-4/implementing-the-repository-and-unit-of-work-patterns-in-an-asp-net-mvc-application I think what you're saying is along this line (please confirm). For now, I have to sleep (gotta work tomorrow). I'll get back to you asap. – Sylpheed Mar 14 '16 at 17:58
  • Also, for read-only operations (where you don't have to make any changes to entities like add, update, or delete), there is the `.AsNoTracking()` extension method which will tell EF not to track on a context. Feel free to use that and attach the entity to only 1 context subseqently. As soon as you try to attach an entity instance to a second context though, you will be in trouble. – danludwig Mar 14 '16 at 17:58
  • @danludwig Okay I've read the article and this one: http://mehdi.me/ambient-dbcontext-in-ef6/ I get what you're saying so I'll just decide to disable proxy or refactor my code in a way that it uses a single context. How do you handle the overhead in memory since I've read that the context will bloat because of the caching? I'll mark this answered. – Sylpheed Mar 16 '16 at 12:20
  • AFAIK the context only caches when you are using the `.Find` method to load entities by primary key. If you are seeing memory bloat due to caching, then I would suspect you are doing something else wrong. Remember, you don't want a single context for the entire life of an application -- that will also get you into (different kinds of) trouble. Instead, you want one context instance *per execution context*. Generally, the scope of an execution context should be relatively short-lived, and the context should be disposed at the end, which should then clean up memory used by the context. – danludwig Mar 16 '16 at 12:38
  • Yeah, I was used in game development where systems/services should almost be alive and shared in the whole application (eg. spawners). I've decided to refactor all my services to lessen its scope/lifetime based on transactions. Currently they're all in the application's scope =p. Glad that I'm using IoC so it'll be a lot easier to refactor. – Sylpheed Mar 16 '16 at 17:19
0

I was using AsNoTracking() so, the exception was odd to me. The code below fixed the issue in my code. I think I could get ride of the code in the try block and only use the catch block, but I never tested the concept.

Entity Base class with PrimaryKey

public class EntityBase<K>
{
    [NotMapped]
    public virtual K PrimaryKey {get;}
}

Update function

public static void Update<T,K>(DbContext context, T t) where T:EntityBase<K>
{
    DbSet<T> set = context.Set<T>();
    if(set==null) return;
    if(context.Entry<T>(t).State == EntityState.Detached)
    {
         try
         {
             T attached = set.Attached(t);
             context.Entry<T>(attached).State = EntityState.Modified;
         }catch(Exception ex)
         {
             T found = set.Find(t.PrimaryKey);
             context.Entry(found).CurrentValues.SetValues(t);
         }
     }
     context.SaveChanges();
}

Extension Function

public static void Update<T,K>(this DbContext context, T t) where T:EntityBase<K> => Update<T,K>(context, t);
  • Could you please elaborate your answer? – Raphael Jan 09 '20 at 21:26
  • It happens, please review after posting. – Raphael Jan 09 '20 at 21:34
  • *The code below fixed the issue in my code.* Yeah, but we don't know the issue. To us this is just a random piece of (probably) working code that has no bearing on the question. – Gert Arnold Jan 09 '20 at 22:11
  • My code was meant as a workaround. The issued is caused due to the entry being compared to a context that was closed (using (var db = new TwinTailDb())). So it was not the current entry for the current context. – Shayne Pierce Apr 19 '21 at 13:59