Экземпляр типа сущности не может быть отслежен, потому что другой экземпляр этого типа с тем же ключом уже отслеживается

У меня есть служебный объект Update

public bool Update(object original, object modified)
{
    var originalClient = (Client)original;
    var modifiedClient = (Client)modified;
    _context.Clients.Update(originalClient); //<-- throws the error
    _context.SaveChanges();
    //Variance checking and logging of changes between the modified and original
}

Здесь я вызываю этот метод из:

public IActionResult Update(DetailViewModel vm)
{
    var originalClient = (Client)_service.GetAsNoTracking(vm.ClientId);
    var modifiedClient = (Client)_service.Fetch(vm.ClientId.ToString());
    // Changing the modifiedClient here
    _service.Update(originalClient, modifiedClient);
}

Вот метод GetAsNotTracking:

public Client GetAsNoTracking(long id)
{
    return GetClientQueryableObject(id).AsNoTracking().FirstOrDefault();
}

Fetch метод:

public object Fetch(string id)
{
   long fetchId;
   long.TryParse(id, out fetchId);
   return GetClientQueryableObject(fetchId).FirstOrDefault();
}

GetClientQueryableObject:

private Microsoft.Data.Entity.Query.IIncludableQueryable<Client, ActivityType> GetClientQueryableObject(long searchId)
{
    return _context.Clients
        .Where(x => x.Id == searchId)
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.BusinessUnit)
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.Probability)
        .Include(x => x.Industry)
        .Include(x => x.Activities)
        .ThenInclude(x => x.User)
        .Include(x => x.Activities)
        .ThenInclude(x => x.ActivityType);
 }

Любые идеи?

Я просмотрел следующие статьи/дискуссии. Безрезультатно: ASP.NET GitHub Issue 3839

UPDATE:

Вот изменения в GetAsNoTracking:

public Client GetAsNoTracking(long id)
{
    return GetClientQueryableObjectAsNoTracking(id).FirstOrDefault();
}

GetClientQueryableObjectAsNoTracking:

private IQueryable<Client> GetClientQueryableObjectAsNoTracking(long searchId)
{
    return _context.Clients
        .Where(x => x.Id == searchId)
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.BusinessUnit)
        .AsNoTracking()
        .Include(x => x.Opportunities)
        .ThenInclude(x => x.Probability)
        .AsNoTracking()
        .Include(x => x.Industry)
        .AsNoTracking()
        .Include(x => x.Activities)
        .ThenInclude(x => x.User)
        .AsNoTracking()
        .Include(x => x.Activities)
        .ThenInclude(x => x.ActivityType)
        .AsNoTracking();
}

Ответ 1

Без переопределения системы треков EF вы также можете отсоединить "локальную" запись и прикрепить свою обновленную запись до сохранения:

// 
var local = _context.Set<YourEntity>()
    .Local
    .FirstOrDefault(entry => entry.Id.Equals(entryId));

// check if local is not null 
if (!local.IsNull()) // I'm using a extension method
{
    // detach
    _context.Entry(local).State = EntityState.Detached;
}
// set Modified flag in your entry
_context.Entry(entryToUpdate).State = EntityState.Modified;

// save 
_context.SaveChanges();

UPDATE: Чтобы избежать избыточности кода, вы можете использовать метод расширения:

public static void DetachLocal<T>(this DbContext context, T t, string entryId) 
    where T : class, IIdentifier 
{
    var local = context.Set<T>()
        .Local
        .FirstOrDefault(entry => entry.Id.Equals(entryId));
    if (!local.IsNull())
    {
        context.Entry(local).State = EntityState.Detached;
    }
    context.Entry(t).State = EntityState.Modified;
}

Мой IIdentifier интерфейс имеет только свойство строки Id.

Независимо от вашего объекта, вы можете использовать этот метод в своем контексте:

_context.DetachLocal(tmodel, id);
_context.SaveChanges();

Ответ 2

Звучит так, как будто вы просто хотите отслеживать изменения, внесенные в модель, а не сохранять в памяти неизведанную модель. Могу ли я предложить альтернативный подход, который полностью устранит проблему?

EF будет автоматически отслеживать изменения для вас. Как использовать встроенную логику?

Ovverride SaveChanges() в DbContext.

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries<Client>())
        {
            if (entry.State == EntityState.Modified)
            {
                // Get the changed values.
                var modifiedProps = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).GetModifiedProperties();
                var currentValues = ObjectStateManager.GetObjectStateEntry(entry.EntityKey).CurrentValues;
                foreach (var propName in modifiedProps)
                {
                    var newValue = currentValues[propName];
                    //log changes
                }
            }
        }

        return base.SaveChanges();
    }

Хорошие примеры можно найти здесь:

Entity Framework 6: изменения аудита/дорожки

Внедрение журнала аудита/истории изменений с помощью MVC и инфраструктуры Entity Framework

EDIT: Client можно легко изменить на интерфейс. Скажем ITrackableEntity. Таким образом, вы можете централизовать логику и автоматически регистрировать все изменения для всех объектов, реализующих определенный интерфейс. Сам интерфейс не имеет каких-либо конкретных свойств.

    public override int SaveChanges()
    {
        foreach (var entry in ChangeTracker.Entries<ITrackableClient>())
        {
            if (entry.State == EntityState.Modified)
            {
                // Same code as example above.
            }
        }

        return base.SaveChanges();
    }

Также обратите внимание на eranga отличное предложение подписаться вместо фактического переопределения SaveChanges().

Ответ 3

В моем случае столбец идентификатора таблицы не был установлен как столбец идентификаторов.

Ответ 4

Если ваши данные менялись один раз, вы заметите, что не отслеживаете таблицу. Например, какой-то идентификатор обновления таблицы ([ключ]) использует tigger. Если вы отслеживаете, вы получите тот же идентификатор и получите проблему.

Ответ 5

Arhhh это получил меня, и я потратил много времени на устранение неполадок. Проблема заключалась в том, что мои тесты выполнялись в Parellel (по умолчанию с XUnit).

Для последовательного выполнения теста я украсил каждый класс этим атрибутом:

[Collection("Sequential")]

Вот как я работал: выполнять модульные тесты последовательно (а не параллельно)


Я копирую свой контекст EF In Memory с GenFu:

private void CreateTestData(TheContext dbContext)
{
    GenFu.GenFu.Configure<Employee>()
       .Fill(q => q.EmployeeId, 3);
    var employee = GenFu.GenFu.ListOf<Employee>(1);

    var id = 1;
    GenFu.GenFu.Configure<Team>()
        .Fill(p => p.TeamId, () => id++).Fill(q => q.CreatedById, 3).Fill(q => q.ModifiedById, 3);
    var Teams = GenFu.GenFu.ListOf<Team>(20);
    dbContext.Team.AddRange(Teams);

    dbContext.SaveChanges();
}

При создании тестовых данных, из того, что я могу вычесть, они были живы в двух областях (один раз в тестах сотрудников во время выполнения командных тестов):

public void Team_Index_should_return_valid_model()
{
    using (var context = new TheContext(CreateNewContextOptions()))
    {
        //Arrange
        CreateTestData(context);
        var controller = new TeamController(context);

        //Act
        var actionResult = controller.Index();

        //Assert
        Assert.NotNull(actionResult);
        Assert.True(actionResult.Result is ViewResult);
        var model = ModelFromActionResult<List<Team>>((ActionResult)actionResult.Result);
        Assert.Equal(20, model.Count);
    }
}

Обертывание обоих тестовых классов этим атрибутом последовательной коллекции очистило очевидный конфликт.

[Collection("Sequential")]

Дополнительные ссылки:

https://github.com/aspnet/EntityFrameworkCore/issues/7340
EF Core 2.1 В памяти БД не обновляются записи
http://www.jerriepelser.com/blog/unit-testing-aspnet5-entityframework7-inmemory-database/
http://gunnarpeipman.com/2017/04/aspnet-core-ef-inmemory/
https://github.com/aspnet/EntityFrameworkCore/issues/12459
Предотвращение проблем с отслеживанием при использовании EF Core SqlLite в модульных тестах

Ответ 6

public async Task<Product> GetValue(int id)
    {
        Product Products = await _context.Products.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id);
        return Products;
    }

AsNoTracking()