Удаление экземпляра из DbContext выдает ошибку дублирующегося идентификатора - PullRequest
0 голосов
/ 10 февраля 2020

У меня есть тест, который заполняет базу данных, затем вызывает метод, который загружает засеянный объект и удаляет его. Однако, когда метод переходит к вызову dbContext.Remove(...), я получаю сообщение об ошибке:

System.InvalidOperationException: экземпляр типа сущности FiveWhysAnalysis не может быть отслежен, поскольку другой экземпляр с тем же ключом значение для {'Id'} уже отслеживается. При подключении существующих объектов убедитесь, что подключен только один экземпляр объекта с данным значением ключа.

Мой код выглядит так ...

Seed.cs

public Seed(MyContext dbContext) {
    this.dbContext = dbContext;
}

public Task Seed() {
    this.DataStory = new DataStory(...)
    this.FiveWhyAnalysis = new FiveWhyAnalysis(this.DataStory.Id, ...) // Doesn't touch Id property
    this.dbContext.FiveWhyAnalyses.Add(fivewhy);
    return this.dbContext.SaveChangesAsync();
}

DeleteFiveWhyMutator.cs

public DeleteFiveWhyMutator(MyContext dbContext, int dataStoryId) {
    this.dbContext = dbContext;
    this.dataStoryId = dataStoryId;
}

public async Task Load(MyContext dbContext) {
    DataStory dataStory = await dbContext.DataStories.FirstAsync(ds => ds.Id == this.dataStoryId);
    dataStory.FiveWhysAnalysis = await dbContext.FiveWhysAnalyses.SingleOrDefaultAsync(fw => fw.DataStoryId == dataStory.Id);

    // NOTE: I have also tried using an include by doing:
    // DataStory dataStory = await dbContext.DataStories.Include(ds => ds.FiveWhyAnalysis).FirstAsync(ds => ds.Id == this.dataStoryId);
    // rather than the above implementation of this method.
    return dataStory;
}

public async Task<DataStory> Run(MyContext dbContext, DataStory dataStory) {
    dbContext.FiveWhysAnalyses.Remove(dataStory.FiveWhysAnalysis); // Error here
    await dbContext.SaveChangesAsync();
    return dataStory;
}

DeleteFiveWhyMutatorTest.cs

MyContext dbContext = ... // Injected using Microsoft DI
Seed seed = new Seed(dbContext);
await seed.Seed();
var mutator = new DeleteFiveWhyMutator(dbContext, seed.DataStory.Id);
DataStory dataStory = await mutator.Load(dbContext);
await mutator.Run(dbContext);

1 Ответ

1 голос
/ 11 февраля 2020

Немного об этом примере сценария кажется неправильным. Например, это:

DataStory dataStory = await dbContext.DataStories.FirstAsync(ds => ds.Id == this.dataStoryId);
dataStory.FiveWhysAnalysis = await dbContext.FiveWhysAnalyses.SingleOrDefaultAsync(fw => fw.DataStoryId == dataStory.Id);

Если в DataStory уже есть свойство навигации для FiveWhysAnalysis, и вы хотите выбрать соответствующее: , Это правильный путь. Загрузка связанных сущностей в виде дополнительных запросов и перезапись свойства навигации не нужны и могут привести к ошибкам.

Далее будет возможное чрезмерное использование асинхронных c операций. Хотя они отлично подходят для разгрузки долго выполняющихся запросов, чтобы освободить веб-сервер для запуска других запросов в ожидании ответа, их обычно не следует использовать по умолчанию. Вызов нескольких вызовов DbContext / w asyn c может привести к тому, что эти вызовы пересекают потоки, а DbContext не является поточно-ориентированным. Например, если вы обновите свой пример до:

public async Task Load(MyContext dbContext) 
{
    Console.WriteLine("Thread @1: " + Thread.CurrentThread.ManagedThreadId);
    DataStory dataStory = await dbContext.DataStories.FirstAsync(ds => ds.Id == this.dataStoryId);
    Console.WriteLine("Thread: @2" + Thread.CurrentThread.ManagedThreadId);
    dataStory.FiveWhysAnalysis = await dbContext.FiveWhysAnalyses.SingleOrDefaultAsync(fw => fw.DataStoryId == dataStory.Id);
    Console.WriteLine("Thread: @3" + Thread.CurrentThread.ManagedThreadId);

   //...
}

Вы увидите что-то вроде: Поток @ 1: 13 Поток @ 2: 14 Поток @ 3: 15

В зависимости от как приложение сконфигурировано для запуска, асинхронные c операции возобновляют выполнение в другом потоке, чем они были вызваны. Обычно это нормально, если все операции ожидаются. AFAIK, DbContext не будет отбрасывать соответствия из запросов, возобновляющихся в альтернативном потоке, если это один поток за раз. Однако, если вы забыли или забыли дождаться выполнения операции, вы можете застрять с многопоточным доступом. Операции Asyn c также добавляют накладные расходы, поэтому они действительно подходят для экономного использования только для тех запросов, для выполнения которых вы ожидаете, что потребуется больше времени. Их использование по умолчанию сделает ваш код в целом незначительно медленнее.

Далее вы упомянули об использовании Dependency Injection, хотя все ваши методы сконструированы так, чтобы принимать DbContext, что противоречит цели внедрения зависимостей. Я также хотел бы взглянуть на то, как ваш DbContext ограничен на протяжении всей жизни, чтобы убедиться, что он не является переходным, а ограничен для каждого запроса или явной области. DbContext должен быть необходим только для конструкторов ваших предложений, а DI должен гарантировать, что все созданные классы получают один и тот же экземпляр DbContext. (Не передавая DbContexts в методах)

Ваши типы возврата также не имеют смысла. Вы вернули Task, но не используете его, и у вас есть методы publi c для «загрузки» сущности, когда цель мутатора - контролировать, как манипулируют экземплярами объекта. Ничто в этой реализации шаблона не мешает кому-то просто загружать сущность и иметь с ней дело. У этого кода пахнет сложностью без какой-либо четкой цели.

Ни один из этих моментов сам по себе не объясняет поведение, которое вы видите, но в комбинации они могут скрывать неверное предположение, приводящее к тому, что множество ссылок связаны с DbContext, который уже отслеживает соответствующий экземпляр. Я бы порекомендовал начать с реализации сначала самой простой вещи, проверки поведения, а затем постепенного перефакторинга для желаемых шаблонов, которые вы пытаетесь выполнить sh. Во-первых, удалите все операции asyn c и исправьте DI, чтобы ссылки DbContext инициализировались только на конструкторах. Извлеките сущность / загрузку, затем вызовите мутатор.

public DeleteFiveWhyMutator(MyContext dbContext, int dataStoryId) {
    this.dbContext = dbContext;
    this.dataStoryId = dataStoryId;
}

private FiveWhyAnalysis GetAnalysis() {
    var fwAnalysis = dbContext.DataStories
       .Where(ds => ds.Id = dataStoryId)
       .Select(ds => ds.FiveWhyAnalysis)
       .SingleOrDefault();
    return fwAnalysis;
}

public void Run() {
    var fwAnalysis = GetAnalysis();
    if (fwAnalysis == null)
        return;

    dbContext.FiveWhysAnalyses.Remove(fwAnalysis);
    dbContext.SaveChanges();
}

Затем в тесте:

using (var dbContext = new MyContext()) // Real code will DI the context.
{
    Seed seed = new Seed(dbContext); 
    seed.Seed(); // remove async here as well to test.

    // Assert we have a FiveWhy...
    Assert.IsNotNull(seed.DataStory.FiveWhyAnalysis, "FiveWhyAnalysis was not seeded.");
    var mutator = new DeleteFiveWhyMutator(dbContext, seed.DataStory.Id);
    mutator.Run();

    // Assert the FiveWhy was removed...
    Assert.IsNull(seed.DataStory.FiveWhyAnalysis, "FiveWhyAnalysis was not removed.");
}

Если это работает, развернитесь оттуда. В целом, я бы посоветовал соблюдать осторожность с такой реализацией шаблона, так как в ней могут быть несколько мутаторов или других классов, совместно использующих ссылку DbContext, все из которых в разное время вызывают SaveChanges(), что может привести к частичным изменениям, совершаемым на разных этапах более крупной операции. Расширение мутатора может включать метод Run для существующих ссылок DataStory:

public void Run(DataStory dataStory) {
    if (dataStory == null)
        throw new ArgumentNullException("dataStory");

    if (dataStory.FiveWhyAnalysis == null)
        return;

    dbContext.FiveWhysAnalyses.Remove(dataStory.FiveWhyAnalysis);
    dbContext.SaveChanges();
}

Эту альтернативу можно вызывать в тех случаях, когда вы уже загрузили историю данных и хотели использовать мутатор для управления удалением FiveWhyAnalysis. Чтобы этот шаблон имел большую ценность по сравнению с непосредственным изменением состояния объекта, он должен предоставить дополнительную ценность, такую ​​как аудит переноса, отслеживание изменений или другое общее поведение.

...