I have a controller which calls the ChangeActivity method, the source and destination is gained using another dbContext than what is used here. All code can be found here.

I also have a lot of unit tests which also test aginst an SQL Server database, and they do not encounter this problem.

Models

public class EntryModel
{
   public int Id { get; set; }
   public List<ActivityModel> Activities { get; set; }
   ...
}

public class ActivityModel
{
   public int Id { get; set; }
   ...
}

Problematic Code

public ActivityChangeModel ChangeActivity(ActivityModel source, ActivityModel destination, Guid userGuid)
{
    DeleteActivity(source);
    OverrideEntries(source, destination, userGuid);

    var change = AddChange(source, destination, userGuid);
    _db.SaveChanges();
    return change;
}

private void DeleteActivity(ActivityModel source)
{
    var model = _db.Activity.Single(x => x.Id == source.Id);
    model.Deleted = true;
}

private void OverrideEntries(ActivityModel source, ActivityModel destination, Guid userGuid)
{
    var entries = _db.Entry.Include(x => x.Activities).Where(x => x.Activities.Contains(source)).ToList();
    foreach (var entry in entries)
    {
        entry.Deleted = true;
    }

    foreach (var entry in entries)
    {
        var newEntry = new EntryModel(entry.StartTime, entry.EndTime, entry.RecordedTime, new List<ActivityModel>(), false, userGuid);
        _db.Entry.Add(newEntry);

        var activities = entry.Activities;
        activities.Remove(source);
        activities.Add(destination);
        newEntry.Activities = activities;
    }
}

private ActivityChangeModel AddChange(ActivityModel source, ActivityModel destination, Guid userGuid)
{
    var change = new ActivityChangeModel(source, destination, _timeService.Current, userGuid);
    _db.ActivityChange.Add(change); // Throws Error
    return change;
}

The error which is thrown is

System.InvalidOperationException: 'The instance of entity type 'ActivityModel' cannot be tracked because another instance with the key value '{Id: 5}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.'

I have tried

  • Using _db.ChangeTracker.Clear(); before I add the activity to activities
  • Looking at the changes with _db.ChangeTracker.DebugView.LongView just before var activities = entry.Activities; which says that the activity Id: 5 is being tracked, but it is unchanged

Any help is much appreciated

3

There are 3 best solutions below

0
Theodor349 On BEST ANSWER

The reason why I get the error is that the soruce and destination objects that ChangeActivity recieves is retrived using another dbContext. This means that when destination is retrived from the current dbContext we now have two equvilent destination, one which is tracked by the new dbContext and another which is not.

The fix is then to either override soruce and destination by _db.Activity.SingleOrDefault(x => x.Id == source.Id) or in my case change the dbContext to be scoped instead of a transiant service. Changing it to scoped, means that the two destination objects we had before, are now the same one and tracked by the dbContext. (Here is the changes I made)

5
User12345 On

To resolve this issue, you can detach the ActivityModel entity before attaching it again in the OverrideEntries method.You can use the DbContext.Entry() method to explicitly detach the entity from the context. Here is the sample implementation:

private void OverrideEntries(ActivityModel source, ActivityModel destination, Guid userGuid)
{
    //your code here

    foreach (var entry in entries)
    {
        // Detach the source activity from the context
        _db.Entry(source).State = EntityState.Detached;

        var newEntry = new EntryModel(entry.StartTime, entry.EndTime, entry.RecordedTime, new List<ActivityModel>(), false, userGuid);
        //your code here
    }
}
2
ChristP On

I Faced this issue time ago and i resolved used dbContextFactory instead dbContext.

here the approch

on Program.cs add context factory and remove reference to tb context

services.AddDbContextFactory<MyContext>(opt => opt.UseSqlServer(Configuration.GetConnectionString("MyDbConnection")));

Inside service class add the following code

public class MyServiceClass: IMyServiceClass
{
    private readonly IConfiguration _config;
    private readonly IDbContextFactory<MyContext> _contextFactory;


    public MyServiceClass(IConfiguration config, IDbContextFactory<MyContext> contextFactory)
    {
        _config = config;
        _contextFactory = contextFactory;
    }
...

Then i modify the methods

    private void DeleteActivity(ActivityModel source)
    {
        using (var ctx = _contextFactory.CreateDbContext())
        {
            var model = ctx.Activity.Single(x => x.Id == source.Id);
            model.Deleted = true;
            ctx.SaveChanges();
        }
    }

    private void OverrideEntries(ActivityModel source, ActivityModel destination, Guid userGuid)
    {

        using (var ctx = _contextFactory.CreateDbContext())
        {
            var entries = ctx.Entry.Include(x => x.Activities).Where(x => x.Activities.Contains(source)).ToList();
            foreach (var entry in entries)
            {
                entry.Deleted = true;
            }
            ctx.SaveChanges();
        }
        using (var ctx = _contextFactory.CreateDbContext())
        {
            foreach (var entry in entries)
            {
                var entries = ctx.Entry.Include(x => x.Activities).Where(x => x.Activities.Contains(source)).ToList();

                var newEntry = new EntryModel(entry.StartTime, entry.EndTime, entry.RecordedTime, new List<ActivityModel>(), false, userGuid);
                ctx.Entry.Add(newEntry);

                var activities = entry.Activities;
                activities.Remove(source);
                activities.Add(destination);
                newEntry.Activities = activities;
                ctx.SaveChanges(); // Throws Error
            }
        }
    }