Возникают исключения при попытке управления данными через Entity Framework

Есть класс контекста данных ApplicationContext.cs:

internal class ApplicationContext : DbContext
{
    public DbSet<Contact>? Contacts { get; set; }
    
    public DbSet<PhoneType>? PhoneTypes { get; set; }
    
    public DbSet<ContactInfo>? ContactsInfo { get; set; }
    
    public ApplicationContext() : base("Data Source=XXX;Initial Catalog=ContactsNotebookDB;Integrated Security=True;TrustServerCertificate=True")
    {
        Database.CreateIfNotExists();
    }
}

Детализация записей:

Contact.cs:

internal class Contact : IDatabase
{
    public required int ID { get; set; }
    
    public required string PhoneNumber { get; set; }
    
    public int? PhoneTypeID { get; set; }
    
    public required int ContactInfoID { get; set; }
}

ContactInfo.cs:

[Table("ContactsInfo")] // обязательное явное указание названия таблицы
(по умолчанию EF читает как "ContactInfoes")
internal class ContactInfo : IDatabase
{
    public required int ID { get; set; }
    
    public required string Name { get; set; }
    
    public required string Surname { get; set; }
    
    public string? Patronymic { get; set; }
    
    public required char Sex { get; set; }
}

Когда я пытаюсь сохранить данные в коде одного из View, то возникает исключение:

using ApplicationContext ApplicationContext = new();

int ContactsInfoMaxID = ApplicationContext.ContactsInfo.Count();
int ContactsMaxID = ApplicationContext.Contacts.Count();

ContactInfo ContactInfo = new()
{
    ID = ContactsInfoMaxID + 1,
    Name = NameTextBox.Text,
    Surname = SurnameTextBox.Text,
    Patronymic = Patronymic,
    Sex = SexComboBox.Text[0]
};

Contact Contact = new()
{
    ID = ContactsMaxID + 1,
    PhoneNumber = PhoneNumberTextBox.Text,
    PhoneTypeID = PhoneTypeComboBox.SelectedIndex,
    ContactInfoID = ContactInfo.ID,
};

ApplicationContext.ContactsInfo.Add(ContactInfo);
ApplicationContext.Contacts.Add(Contact);

ApplicationContext.SaveChanges(); // тут возникает исключение при сохранении

Исключение при попытке добавления записей

Строка ApplicationContext.SaveChanges(); вызывает следующее исключение:

System.Data.Entity.Infrastructure.DbUpdateException: "An error occurred while updating the entries. See the inner exception for details."

И 2 внутренних исключения:

UpdateException: An error occurred while updating the entries. See the inner exception for details.

SqlException: Не удалось вставить значение NULL в столбец "ID", таблицы "ContactsNotebookDB.dbo.Contacts"; в столбце запрещены значения NULL. Ошибка в INSERT.
Выполнение данной инструкции было прервано.

Обновление проблемы:

При попытке удаление записи из БД также возникает исключение.

Фрагмент кода ViewModel с методом, который пытается удалить данные:

/// <summary>
/// Команда удаления контакта
/// </summary>
/// <param name="parameter"></param>
private void DeleteContact(object parameter)
{
    MessageBoxResult DeleteContactMessageBoxResult = MessageBox.Show($"Внимание, вы действительно хотите удалить следующий контакт:\n\n{PhoneNumber}\n{Surname} {Name}{(Patronymic == "—" ? null : " " + Patronymic)}?", "Удаление контакта", MessageBoxButton.YesNo, MessageBoxImage.Warning);
    
    if (DeleteContactMessageBoxResult == MessageBoxResult.Yes)
    {
        using ApplicationContext ApplicationContext = new();
        
        bool IsContactInUse = ApplicationContext.Contacts.Any(Contact => Contact.ContactInfoID == ID);
        
        if (!IsContactInUse)
        {
            ApplicationContext.ContactsInfo?.Remove(ContactInfo);
        }
        
        else
        {
            Console.WriteLine("Текущий используется в Contact и не может быть удален.");
        }
        
        ApplicationContext.Contacts?.Remove(Contact);
        ApplicationContext.SaveChanges();
    }
}

Исключение при попытке удаления записи

Строка ApplicationContext.Contacts?.Remove(Contact); вызывает исключение:

System.InvalidOperationException: "The object cannot be deleted because it was not found in the ObjectStateManager."

Дополнительная информация:

  1. Версия Entity Framework — 6.5.1;
  2. Я уверен в правильности записей, вносимых в БД, потому что во время проверки через отладку записи вносятся в ApplicationContext: Добавление записи в ContactsInfo

Добавление записи в Contacts

Уже странно, что ContactsInfo возвращает только 1 новую запись, в то время как Contacts возвращает все записи + новую запись (она, кстати, почему-то помещается наверх таблицы).

Даже если я явно задам Null-данные, запись по прежнему не будет вноситься в БД, отображая то же исключение.

P. S. Я увидел 0 в добавляемой записи Contacts (атрибут PhoneTypeID), исправил его на 1, но на ошибке это не отразилось.

  1. Выражение ApplicationContext.ContactsInfo.Sql в отладке возвращает значение:

    SELECT [Extent1].[ID] AS [ID], [Extent1].[Name] AS [Name], [Extent1].[Surname] AS [Surname], [Extent1].[Patronymic] AS [Patronymic] FROM [dbo].[ContactsInfo] AS [Extent1]

То есть тут почему-то отсутствует столбец Sex из таблицы БД.

  1. Я пересоздавал БД, создавал дубликат БД без записей, без связей, у которого не было первичных ключей и обязательных полей, но в таком случае вылетает другое исключение:

    System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: "Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions."

И внутреннее:

OptimisticConcurrencyException: Store update, insert, or delete statement affected an unexpected number of rows (0). Entities may have been modified or deleted since entities were loaded. See http://go.microsoft.com/fwlink/?LinkId=472540 for information on understanding and handling optimistic concurrency exceptions.
  1. Струткура таблиц в БД MS SQL:

ContactsInfo:

Проект таблицы ContactsInfo

Contacts:

Проект таблицы Contacts

  1. Если попробовать добавить запись не в Contacts, а в ContactsInfo и попытаться сохранить изменения, то возникает то же исключение, но с другим внутренним исключением:

    SqlException: Не удалось вставить значение NULL в столбец "Sex", таблицы "ContactsNotebookDB.dbo.ContactsInfo"; в столбце запрещены значения NULL. Ошибка в INSERT. Выполнение данной инструкции было прервано.

  2. А, ну из очевидного, конечно, ApplicationContext получает данные из БД, ведь он возвращает верные значения при обращении к нему (кол-во таблиц и их записи), поэтому проблема не в подключении.

Обновлено:

  1. Непонятно почему строка ApplicationContext.Contacts?.Remove(Contact); вызывает исключение, ведь данные из БД по контактам загрузились в ApplicationContext (хотя проверка в отладке работает 50/50: либо отображает загруженные Contacts, либо пишет, что их кол-во — 0).

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

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

Предложение @Alexander Petrov в комментариях:

Короче, убирайте строки int ContactsInfoMaxID = ApplicationContext.ContactsInfo.Count(); int ContactsMaxID = ApplicationContext.Contacts.Count();. Убирайте ID = ContactsInfoMaxID + 1, и ID = ContactsMaxID + 1, (оставьте айдишники неинициализированными, БД сама их задаст).

было прямо в яблочко! Вот только я воспринимал это как совет, но никак не думал, что это будет единственным способом решения данной проблемы. Кстати, применяя его в первый раз почему-то мне не удавалось решить проблему.

Как ни странно, начну с починки возможности удаления записи из БД, ведь она происходит легче.

Для этого нужно просто прикрепить удаляемую запись с помощью метода Attach() перед удалением с помощью Remove().

Например:

ApplicationContext.Contacts.Attach(Contact); // прикрепляем удаляемый объект к контексту
ApplicationContext.Contacts.Remove(Contact);
await ApplicationContext.SaveChangesAsync();

Но Attach() нужен лишь в том случае, если:

  • объект был получен из другого источника (например, из кэша, с фронтенда или другого контекста);
  • объект был создан вручную и необходимо удалить или обновить его в базе данных.

То есть если объект был получен из базы данных через текущий контекст (через методы вроде Find, FirstOrDefault, LINQ-запросы и т. д.), то в таком случае объект уже отслеживается и использование метода Attach() не требуется.

И всё. Этого достаточно для того, чтобы записи удалялись. И никаких "танцев с бубном" по типу изменения первичного ключа на автоинкрементный. Я не говорю, что автоинкрементный ключ — плохо. Я лишь говорю то, что Remove() хотя бы предоставляет свободу выбора.

Сразу говорю, ответ взят отсюда: https://stackoverflow.com/questions/7791149/the-object-cannot-be-deleted-because-it-was-not-found-in-the-objectstatemanager.

Чего не скажешь о добавлении записи в БД с помощью метода Add()...

Для того, чтобы можно было вносить новые записи в БД нужно... нет, не изменить код, а изменить архитектуру в БД.

А именно указать ключу ID то, что он автоинкрементный. И всё!

То есть в проекте таблицы в MS SQL изменить параметр поля ID следующим образом:

Параметр автоинкрементности для поля ID

Итоговая версия кода:

ContactInfo ContactInfo = new()
{
    Name = NameTextBox.Text,
    Surname = SurnameTextBox.Text,
    Patronymic = Patronymic,
    Sex = SexComboBox.Text/*[0]*/
};

Contact Contact = new()
{
    PhoneNumber = PhoneNumberTextBox.Text,
    PhoneTypeID = PhoneTypeID,
    ContactInfoID = ContactInfo.ID
};

using ApplicationContext ApplicationContext = new();

if (ContactViewModel != null) // если редактируем запись
{
    Contact? DBContact = ApplicationContext.Contacts?.FirstOrDefault(Contact => Contact.ID == ContactViewModel.ID);
    ContactInfo? DBContactInfo = ApplicationContext.ContactsInfo?.FirstOrDefault(Contact => Contact.ID == ContactViewModel.ContactInfo.ID);
    
    if (DBContactInfo != null)
    {
        DBContactInfo.Name = ContactInfo.Name;
        DBContactInfo.Surname = ContactInfo.Surname;
        DBContactInfo.Patronymic = ContactInfo.Patronymic;
        DBContactInfo.Sex = ContactInfo.Sex;
    }
    
    if (DBContact != null)
    {
        DBContact.PhoneNumber = Contact.PhoneNumber;
        DBContact.PhoneTypeID = Contact.PhoneTypeID;
    }
}

else // если создаём новую запись
{
    ApplicationContext.ContactsInfo?.Add(ContactInfo);
    await ApplicationContext.SaveChangesAsync();
    
    Contact.ContactInfoID = ContactInfo.ID;
    ApplicationContext.Contacts?.Add(Contact);
}

await ApplicationContext.SaveChangesAsync();

Обновлено: Важно! Обратите внимание на то, что если у вас есть подчинённая сущность (у меня в данном случае свойство ContactInfoID у Contact, зависящее от ID у ContactInfo), то перед добавлением новых записей в БД нужно сохранять сущности поочерёдно:

ApplicationContext.ContactsInfo?.Add(ContactInfo);
await ApplicationContext.SaveChangesAsync();

Contact.ContactInfoID = ContactInfo.ID;
ApplicationContext.Contacts?.Add(Contact);
await ApplicationContext.SaveChangesAsync();

Потому-что в момент инициализации ID у ContactInfo равно 0 — ему присвоется ID только после добавления в БД! Особенно явно это видно если в БД выставлены ограничения по внешним ключам:

ContactInfo.Id = 0 при инициализации

Исключение на ограничение внешних ключей

→ Ссылка