Сбой SaveChanges для сущностей с самоссылкой!! Я что-то упустил?

Вопрос или проблема

У меня есть сущность

public class ExpenseCategory
{
    public int Id { get; set; }
    public string Label { get; set; } = string.Empty;

    public int? ParentId { get; set; }
    public ExpenseCategory? Parent { get; set; }

    public int? PreviousSiblingId { get; set; }
    public ExpenseCategory? PreviousSibling { get; set; }

    public int? NextSiblingId { get; set; }
    public ExpenseCategory? NextSibling { get; set; }


    public List<ExpenseCategory> Children { get; set; } = [];

    public List<Expense> Expenses { get; set; } = [];
}

В моем DbContext я имею

        modelBuilder.Entity<ExpenseCategory>().ToTable("ExpenseCategory")
          .HasOne(e => e.Parent)
          .WithMany(e => e.Children)
          .HasForeignKey(e => e.ParentId)
          .OnDelete(DeleteBehavior.Restrict); // Предотвращает каскадные удаления

        // Настройка сущности ExpenseCategory
        modelBuilder.Entity<ExpenseCategory>()
            .HasKey(ec => ec.Id);

        modelBuilder.Entity<ExpenseCategory>()
            .Property(ec => ec.Label)
            .IsRequired(); // Убедиться, что Label обязательный

        modelBuilder.Entity<ExpenseCategory>()
            .HasOne(e => e.PreviousSibling)
            .WithOne()
            .HasForeignKey<ExpenseCategory>(e => e.PreviousSiblingId)
            .OnDelete(DeleteBehavior.Restrict);

        modelBuilder.Entity<ExpenseCategory>()
            .HasOne(e => e.NextSibling)
            .WithOne()
            .HasForeignKey<ExpenseCategory>(e => e.NextSiblingId)
            .OnDelete(DeleteBehavior.Restrict);


В коде форм у меня есть

    private void AddRootButton_Click(object sender, EventArgs e)
    {
        var label = GetNewCategoryLabel();
        if (label.IsNullOrEmpty()) return;

        ExpenseCategory? prevCategory = null;

        TreeNode? prevNode = null;
        var rootNodes = TV.Nodes;
        if (rootNodes.Count > 0)
        {
            prevNode = rootNodes[rootNodes.Count - 1];
            if (prevNode != null)
            {
                prevCategory = (ExpenseCategory)prevNode.Tag;
            }
        }

        // Создать новую категорию
        var newCategory = new ExpenseCategory()
        {
            Label = label,
            Parent = null,
            PreviousSibling = prevCategory,
            NextSibling = null, // Изначально null
            Children = [],
            Expenses = []
        };

        // Добавить новую категорию в контекст
        var entry = _context.ExpenseCategories.Add(newCategory);

        // Установить ссылки на братьев и сестер условно
        if (prevCategory != null)
        {
            // Установить NextSibling предыдущей категории на новую
            prevCategory.NextSibling = entry.Entity;
            entry.Entity.PreviousSibling = prevCategory; // Установить взаимную ссылку
        }
        else
        {
            // Если нет предыдущей категории, мы можем явно установить null, чтобы избежать путаницы
            entry.Entity.PreviousSibling = null;
        }

        // Добавить новый узел в TreeView
        TreeNode newNode = new TreeNode(newCategory.Label)
        {
            Tag = newCategory
        };

        var index = TV.Nodes.Add(newNode);
        TV.SelectedNode = TV.Nodes[index];
        ActiveControl = TV;

        // Примечание: _context.SaveChanges() будет вызван позже
    }

Поскольку я хочу, чтобы _context сохранял изменения при закрытии формы, а не когда вводится новая запись
Я вызываю это

   private async void ExpenseCategoriesDialog_FormClosing(object sender, FormClosingEventArgs e)
    {

        await SaveChangesWithTrackingChecksAsync();
    }

   private async Task SaveChangesWithTrackingChecksAsync()
   {
       try
       {
           foreach (var entry in _context.ChangeTracker.Entries<ExpenseCategory>())
           {
               var category = entry.Entity;
               Debug.WriteLine($"Сущность: {category.Label}, Состояние: {entry.State}");

               // Проверить наличие PreviousSibling и NextSibling и состояние отслеживания
               if (category.PreviousSibling != null)
               {
                   var previousSiblingEntry = _context.ChangeTracker.Entries().FirstOrDefault(e => e.Entity == category.PreviousSibling);
                   if (previousSiblingEntry == null)
                   {
                       Debug.WriteLine($"    PreviousSibling (Label: {category.PreviousSibling.Label}, Id: {category.PreviousSibling.Id}) не отслеживается. Присоединяем его сейчас.");
                       _context.Attach(category.PreviousSibling);
                   }
                   else
                   {
                       Debug.WriteLine($"    PreviousSibling (Label: {category.PreviousSibling.Label}, Id: {category.PreviousSibling.Id}) отслеживается.");
                   }
               }
               else
               {
                   Debug.WriteLine("    PreviousSibling равен null.");
               }

               if (category.NextSibling != null)
               {
                   var nextSiblingEntry = _context.ChangeTracker.Entries().FirstOrDefault(e => e.Entity == category.NextSibling);
                   if (nextSiblingEntry == null)
                   {
                       Debug.WriteLine($"    NextSibling (Label: {category.NextSibling.Label}, Id: {category.NextSibling.Id}) не отслеживается. Присоединяем его сейчас.");
                       _context.Attach(category.NextSibling);
                   }
                   else
                   {
                       Debug.WriteLine($"    NextSibling (Label: {category.NextSibling.Label}, Id: {category.NextSibling.Id}) отслеживается.");
                   }
               }
               else
               {
                   Debug.WriteLine("    NextSibling равен null.");
               }

               // Дополнительная проверка для проверки целостности цепочки братьев и сестер
               if (category.NextSibling?.PreviousSibling != category)
               {
                   Debug.WriteLine($"Предупреждение: у {category.Label} NextSibling ({category.NextSibling?.Label}) не ссылается на {category.Label} как PreviousSibling.");
               }

               if (category.PreviousSibling?.NextSibling != category)
               {
                   Debug.WriteLine($"Предупреждение: у {category.Label} PreviousSibling ({category.PreviousSibling?.Label}) не ссылается на {category.Label} как NextSibling.");
               }
           }

           await _context.SaveChangesAsync();
       }
       catch (NullReferenceException ex)
       {
           Debug.WriteLine("Обнаружено исключение NullReferenceException: " + ex.Message);
           throw;
       }
   }

Когда я добавляю 1 и 2 в качестве входных записей, когда вызывается SaveChangesAsync, я получаю

System.NullReferenceException: 'Ссылка на объект не установлена на экземпляр объекта.'

в то время как в окне вывода я получаю

Сущность: 1, Состояние: Добавлено
    PreviousSibling равен null.
    NextSibling (Label: 2, Id: 0) отслеживается.
Предупреждение: PreviousSibling у 1 () не ссылается на 1 как NextSibling.
Сущность: 2, Состояние: Добавлено
    PreviousSibling (Label: 1, Id: 0) отслеживается.
    NextSibling равен null.
Предупреждение: NextSibling у 2 () не ссылается на 2 как PreviousSibling.
Вызвано исключение: 'System.NullReferenceException' в System.Private.CoreLib.dll
Ссылка на объект не установлена на экземпляр объекта.

Как вы можете видеть, хотя ссылки правильно назначены, я продолжаю получать это исключение и не могу понять, является ли это слабостью EFCore или я что-то пропускаю!!!
Также

Предупреждение: PreviousSibling у 1 () не ссылается на 1 как NextSibling.

и

Предупреждение: NextSibling у 2 () не ссылается на 2 как PreviousSibling.

не действительны, поскольку PreviousSibling у 1 равен null, поэтому нет NextSibling для PreviousSibling 1
а также NextSibling у 2 также равен null, поэтому нет PreviousSibling для NextSibling 2 тоже

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

Я показал вам выше, что я пробовал до сих пор
Только когда SaveChanges вызывается немедленно после добавления новой записи, все работает нормально
В противном случае, когда форма закрывается, я всегда получаю исключение: ссылка на объект не установлена на экземпляр объекта, и я не могу понять, какая ссылка на объект не установлена на экземпляр объекта

Ответ или решение

Ваша проблема, связанная с присвоением ссылочных сущностей и последующим вызовом SaveChangesAsync в Entity Framework Core, действительно может быть сложной. Позвольте детально разобрать предоставленные вами данные и попытаться найти корень ошибки.

Обзор сущности и контекста

Вы работаете с сущностью ExpenseCategory, которая имеет сложную иерархическую структуру. Основные свойства включают ссылки на родительскую категорию, предыдущую и следующую категории-соседи. Ваше определение модели кажется корректным, и связи с помощью Fluent API также настроены верно.

Ошибка при вызове SaveChangesAsync

Ваша ошибка NullReferenceException, которая возникает, когда вы вызываете метод SaveChangesAsync, указывает на то, что в процессе проверки состояния ваших сущностей что-то идет не так. Давайте рассмотрим некоторые возможные причины и решения:

  1. Проблемы с отслеживанием сущностей: Когда вы добавляете категорию в контекст, как вы заметили, и ссылки на PreviousSibling и NextSibling у вас настроены с использованием entry.Entity. Однако, необходимо убедиться, что все изменения для этих ссылок правильно отслеживаются Entity Framework.

  2. Проверка ссылок на null: В вашем коде вы используете условные проверки для PreviousSibling и NextSibling. Убедитесь, что вы проверяете не только наличие объекта, но и корректность его состояния (например, чтобы Id был ненулевым). При вызове Attach и проверке состояния соблюдайте осторожность, чтобы избежать установки NextSibling или PreviousSibling в null, если они не существуют.

  3. Ссылочные циклы и взаимные ссылки: Вам нужно быть осторожным с тем, как настраиваются взаимные ссылки между категориями. Если у вас есть циклы или неправильные ссылки, это может привести к проблемам при сохранении метода. Для отладки добавьте дополнительную проверку, чтобы гарантировать, что ссылки корректны перед вызовом SaveChangesAsync.

  4. Использование Debug вывода: Вы используете Debug.WriteLine для вывода состояния ваших объектов. Это полезный инструмент, однако, обратите внимание на сообщение о предупреждениях:

    Warning: 1's PreviousSibling () does not reference 1 as NextSibling.

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

Рекомендации по коду

  1. Измените логику установки ссылок:
    Пересмотрите логику в методе AddRootButton_Click. Убедитесь, что ссылки на PreviousSibling и NextSibling правильно инициализированы перед вызовом SaveChangesAsync.

  2. Оптимизируйте метод SaveChangesWithTrackingChecksAsync:
    Подумайте о более строгих проверках, чтобы предотвратить вызов SaveChangesAsync, когда состояние содержит ошибочные ссылки.

Пример улучшенного кода

private async Task SaveChangesWithTrackingChecksAsync()
{
    try
    {
        // Убедитесь, что все элементы отслеживаются здесь
        foreach (var entry in _context.ChangeTracker.Entries<ExpenseCategory>())
        {
            // Ваш код для проверки ссылок
            // ...
        }

        // Запустите SaveChanges после всех проверок
        await _context.SaveChangesAsync();
    }
    catch (Exception ex) when (ex is NullReferenceException)
    {
        Debug.WriteLine("NullReferenceException encountered: " + ex.Message);
        throw;
    }
    catch (Exception ex)
    {
        Debug.WriteLine("An unexpected error occurred: " + ex.Message);
        throw; // Бросьте исключение дальше для обработки
    }
}

Заключение

Проблемы с изменением статуса отслеживания и правильной инициализацией объектов могут привести к состояниям, вызывающим NullReferenceException в EF Core. Запуск SaveChanges после каждого добавления избавляет от подобных проблем, так как каждое изменение сразу же фиксируется. Постарайтесь создать более устойчивую логику обработки ссылок и отслеживания в вашем коде.

Оцените материал
Добавить комментарий

Капча загружается...