Вопрос или проблема
У меня есть сущность
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
, указывает на то, что в процессе проверки состояния ваших сущностей что-то идет не так. Давайте рассмотрим некоторые возможные причины и решения:
-
Проблемы с отслеживанием сущностей: Когда вы добавляете категорию в контекст, как вы заметили, и ссылки на
PreviousSibling
иNextSibling
у вас настроены с использованиемentry.Entity
. Однако, необходимо убедиться, что все изменения для этих ссылок правильно отслеживаются Entity Framework. -
Проверка ссылок на null: В вашем коде вы используете условные проверки для
PreviousSibling
иNextSibling
. Убедитесь, что вы проверяете не только наличие объекта, но и корректность его состояния (например, чтобы Id был ненулевым). При вызовеAttach
и проверке состояния соблюдайте осторожность, чтобы избежать установкиNextSibling
илиPreviousSibling
вnull
, если они не существуют. -
Ссылочные циклы и взаимные ссылки: Вам нужно быть осторожным с тем, как настраиваются взаимные ссылки между категориями. Если у вас есть циклы или неправильные ссылки, это может привести к проблемам при сохранении метода. Для отладки добавьте дополнительную проверку, чтобы гарантировать, что ссылки корректны перед вызовом
SaveChangesAsync
. -
Использование Debug вывода: Вы используете
Debug.WriteLine
для вывода состояния ваших объектов. Это полезный инструмент, однако, обратите внимание на сообщение о предупреждениях:Warning: 1's PreviousSibling () does not reference 1 as NextSibling.
Это указывает на нарушения в логике ваших ссылок. Постарайтесь добавить дополнительные проверки и убедитесь, что состояние каждого элемента логично.
Рекомендации по коду
-
Измените логику установки ссылок:
Пересмотрите логику в методеAddRootButton_Click
. Убедитесь, что ссылки наPreviousSibling
иNextSibling
правильно инициализированы перед вызовомSaveChangesAsync
. -
Оптимизируйте метод 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
после каждого добавления избавляет от подобных проблем, так как каждое изменение сразу же фиксируется. Постарайтесь создать более устойчивую логику обработки ссылок и отслеживания в вашем коде.