Из-за чего в Dictionary возникает исключение "Элемент с тем же ключом уже был добавлен"?

Суть такая. DataGrid заполняется данными из БД. Все работает хорошо. Исключение появляется когда повторно редактирую одну и ту же строку. Например: В таблице отделов изменяю название отдела - Enter, все хорошо, изменяю повторно - процесс падает с исключением. Трудность (так же и причина) в том, что исключение из внешнего кода как я понял EF библиотеки. Дебажить невозможно. Причину так и не смог понять, что ему не хватает. По выводу видно, что во время исполнения создается некий словарь, куда пишутся пары с редактируемыми из локального контекста объектами. И у меня они пишутся не совсем верно. Заранее спасибо.

DataGrid:

введите сюда описание изображения

ViewModel:

public IEnumerable<Subdivision> PrimordialSubdivisions
    {
        get => _primordialSubdivisions;
        set => SetProperty(ref _primordialSubdivisions, value);
    }
....
PrimordialSubdivisions = _appContext.Subdivisions.Local.ToBindingList();
....
subdivisionsGrid.ItemsSource = PrimordialSubdivisions;

Обработчик окончания редактирования после которого следует исключение:

        private void SubdivisionEditEnding(object sender, DataGridRowEditEndingEventArgs e)
    {
        Subdivision s = (Subdivision)e.Row.Item;
        //Получаем исходный объект из бд с которым будем сравнивать
        var nativeSubdivision = _appContext.GetNativeSubdivision(s);

        if (_changedSubdivisions.ContainsKey(nativeSubdivision))
        {
            //Если при редактировании объект вернули в исходное состояние
            if (_changedSubdivisions[nativeSubdivision].Equals(nativeSubdivision))
            {
                _changedSubdivisions.Remove(nativeSubdivision);
                e.Cancel = true;

                return;
            }

            return;
        }

        if (s.Equals(nativeSubdivision)) return;

        _changedSubdivisions[nativeSubdivision] = s;

        UndoChangesCommand.RaiseCanExecuteChanged();

    }

Models:

public class Worker : ObservableViewModel
{
    protected ILogWriter _innerWriter;

    private string _workerName;
    private string _workerLastName;
    private string _workerOtchestvo;
    private DateTime _workerBirthDate;
    private int _id;
    private Sex _workerSex;

    [Key]
    public int Id {
        get => _id;
        set => SetProperty(ref _id, value);
    }

    public string WorkerName
    {
        get => _workerName;
        set => SetProperty(ref _workerName, value);
    }

    public string WorkerLastName
    {
        get => _workerLastName;
        set => SetProperty(ref _workerLastName, value);
    }

    public string WorkerOtchestvo
    {
        get => _workerOtchestvo;
        set => SetProperty(ref _workerOtchestvo, value);
    }

    public DateTime WorkerBirthDate
    {
        get => _workerBirthDate;
        set => SetProperty(ref _workerBirthDate, value);
    }

    public Sex WorkerSex
    {
        get => _workerSex;
        set => SetProperty(ref _workerSex, value);
    }

    [Column("WorkerSubdivision")]
    public int? WorkerSubdivision { get; set; }

    [ForeignKey("WorkerSubdivision")]
    public virtual Subdivision Subdivision { get; set; }

    public Worker() { }

    public Worker(Subdivision subdivision, string name, string lastName, string otchestvo, DateTime birthDate, Sex workerSex)
    {
        Subdivision = GuardWithLog.GuardForNull(subdivision, "Subdivision");
        WorkerName = GuardWithLog.GuardForNull(name, "name");
        WorkerLastName = GuardWithLog.GuardForNull(lastName, "lastName");
        WorkerOtchestvo = GuardWithLog.GuardForNull(otchestvo, "otchestvo");
        WorkerBirthDate = GuardWithLog.GuardForNull(birthDate, "birthDate");
        WorkerSex = GuardWithLog.GuardForNull(workerSex, "worerSex");

        _innerWriter = AsyncLogWriter.CreateInstance();

        _innerWriter.WriteInLog(LogLevel.Information, typeof(Worker), $"Worker {WorkerName} {WorkerLastName} was created");
    }

    public override bool Equals(object obj)
    {
        var item = obj as Worker;
        if (item == null) return false;
        return Id.Equals(item.Id) && WorkerBirthDate.Equals(item.WorkerBirthDate)
               && WorkerLastName.Equals(item.WorkerLastName) && WorkerName.Equals(item.WorkerName) &&
               WorkerOtchestvo.Equals(item.WorkerOtchestvo)
               && WorkerSex.Equals(item.WorkerSex);
    }

    public override string ToString()
    {
        return $"{WorkerLastName} {WorkerName} {WorkerOtchestvo}";
    }
}

    public class Subdivision : ObservableViewModel, IEquatable<Subdivision>
{
    ILogWriter _innerWriter;
    
    private string _subdivName;
    private int _id;
    [Key]
    public int Id { get => _id; set => SetProperty(ref _id, value); }

    public string SubdivisionName
    {
        get => _subdivName; set => SetProperty(ref _subdivName, value);
    }

    [Column("HeadWorker")]
    public int? HeadWorker { get; set; }

    [ForeignKey("HeadWorker")]
    public virtual Worker Head { get; set; }

    public Subdivision() { }

    private Subdivision(string subdivisionName, Worker headWorker)
    {
        SubdivisionName = GuardWithLog.GuardForNull(subdivisionName, "name");
        Head = GuardWithLog.GuardForNull(headWorker, "headWorker");

        _innerWriter = AsyncLogWriter.CreateInstance();

        _innerWriter.WriteInLog(LogLevel.Information, typeof(Subdivision),
            $"Subdivision {subdivisionName} was created.");
    }

    public override string ToString()
    {
        return SubdivisionName;
    }


    //public static Subdivision CreateInstance(string subdivisionName, Worker headWorker)
    //{
    //    return new Subdivision(subdivisionName, headWorker);
    //}
    public bool Equals(Subdivision other)
    {
        if (ReferenceEquals(null, other)) return false;
        if (ReferenceEquals(this, other)) return true;
        return _subdivName == other._subdivName && _id == other._id && HeadWorker == other.HeadWorker && Equals(Head, other.Head);
    }

    public override bool Equals(object obj)
    {
        if (ReferenceEquals(null, obj)) return false;
        if (ReferenceEquals(this, obj)) return true;
        if (obj.GetType() != this.GetType()) return false;
        return Equals((Subdivision) obj);
    }

    public override int GetHashCode()
    {
        unchecked
        {
            var hashCode = (_subdivName != null ? _subdivName.GetHashCode() : 0);
            hashCode = (hashCode * 397) ^ _id;
            hashCode = (hashCode * 397) ^ HeadWorker.GetHashCode();
            hashCode = (hashCode * 397) ^ (Head != null ? Head.GetHashCode() : 0);
            return hashCode;
        }
    }
}

Output:

System.Transactions Critical: 0 : <TraceRecord xmlns="http://schemas.microsoft.com/2004/10/E2ETraceEvent/TraceRecord" Severity="Critical"><TraceIdentifier>http://msdn.microsoft.com/TraceCodes/System/ActivityTracing/2004/07/Reliability/Exception/Unhandled</TraceIdentifier><Description>Необработанное исключение</Description><AppDomain>FunnyWaterCarrier.exe</AppDomain><Exception><ExceptionType>System.ArgumentException, mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</ExceptionType><Message>Элемент с тем же ключом уже был добавлен.</Message><StackTrace>   в System.ThrowHelper.ThrowArgumentException(ExceptionResource resource)
   в System.Collections.Generic.Dictionary`2.Insert(TKey key, TValue value, Boolean add)
   в System.Collections.Generic.Dictionary`2..ctor(IDictionary`2 dictionary, IEqualityComparer`1 comparer)
   в System.Windows.Controls.Primitives.Selector.InternalSelectedItemsStorage..ctor(InternalSelectedItemsStorage collection, IEqualityComparer`1 equalityComparer)

Итоговый вариант метода после нахождение решения вопроса:

private void SubdivisionEditEnding(object sender, DataGridRowEditEndingEventArgs e)
    {
        Subdivision editableItem = (Subdivision)e.Row.Item;

        //Получаем исходный объект из бд с которым будем сравнивать
        var nativeSubdivision = _appContext.GetNativeSubdivision(editableItem);

        if (_changedSubdivisions.ContainsKey(nativeSubdivision.Id))
        {
            //Если при редактировании объект вернули в исходное состояние
            if (_changedSubdivisions[nativeSubdivision.Id].Equals(nativeSubdivision))
            {
                _changedSubdivisions.Remove(nativeSubdivision.Id);
                return;
            }
            return;
        }
        if (editableItem.Equals(nativeSubdivision)) return;
        _changedSubdivisions[nativeSubdivision.Id] = editableItem;
        UndoChangesCommand.RaiseCanExecuteChanged();
    }

Работает в совокупности с кодом из ответа.


Ответы (1 шт):

Автор решения: aepot

Ошибка связана с тем, что вы пытаете добавить в базу то, что там уже есть. Скорее всего проблема в формировании набора данных для записи. Словарь Dictionary думает, что вы пытаетесь в него воткнуть данные с ключом, который там уже есть.

Насколько я знаком с вашим вопросом, вы используете в качестве ключа ссылочный тип. Используйте значимый или немутабельный. Мутабельные типы нельзя использовать в качестве ключа.

Давайте уберем GetHashCode и поправим Equals и перегрузим операторы == и !=

public class Subdivision : ObservableViewModel, IEquatable<Subdivision>
{
    // ...

    public bool Equals(Subdivision? other)
    {
        return other is Subdivision division
               && (ReferenceEquals(this, division)
               || (_subdivName == division._subdivName
               && _id == division._id
               && HeadWorker == division.HeadWorker
               && Head == division.Head));
    }

    public static bool operator ==(Subdivision left, Subdivision right)
        => left.Equals(right);

    public static bool operator !=(Subdivision left, Subdivision right)
        => !left.Equals(right);
}

// здесь вы забыли добавить IEquatable<Worker>
public class Worker : ObservableViewModel, IEquatable<Worker>
{
    // ...

    public bool Equals(Worker? other)
    {
        return other is Worker worker
               && (ReferenceEquals(this, worker)
               || (Id == worker.Id
               && WorkerBirthDate == worker.WorkerBirthDate
               && WorkerLastName == worker.WorkerLastName
               && WorkerName == worker.WorkerName
               && WorkerOtchestvo == worker.WorkerOtchestvo
               && WorkerSex == worker.WorkerSex));
    }

    public static bool operator ==(Worker left, Worker right)
        => left.Equals(right);

    public static bool operator !=(Worker left, Worker right)
        => !left.Equals(right);
}

Всё, теперь у вас сравнение идет по содержимому, и если возникает исключение, значит вы пытаетесь добавить в словарь то, что уже там есть.

→ Ссылка