Список <Задачa> - запись базы данных UPSERT с использованием С# Entity Framework

У меня есть объект Employee, я пытаюсь обновить запись (т.е. Update/Remove) с помощью нескольких задач (Parallel Execution) с использованием одного контекста Entity Entity. Но я получаю следующее исключение.

Сообщение = "Ссылка на объект не установлена ​​в экземпляр объекта."

Рассмотрим следующие DTO

public class Employee
{
    public int EmployeeId { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public List<ContactPhone> ContactPhoneNumbers { get; set; }
    public List<ContactEmail> ContactEmailAddress { get; set; }
}

public class ContactPhone
{
    public int ContactId { get; set; }
    public string Type { get; set; }
    public string Number { get; set; }
}

public class ContactEmail
{
    public int ContactId { get; set; }
    public string Type { get; set; }
    public string Number { get; set; }
}

Таблица сотрудников:

EmployeeId  FirstName   LastName
_________________________________
1           Bala        Manigandan

Таблица ContactPhone:

ContactId   EmployeeId  Type    Number
__________________________________________
1           1           Fax     9123456789
2           1           Mobile  9123456789

Таблица ContactPhone:

ContactId   EmployeeId  Type    EmailAddress
______________________________________________
1           1           Private [email protected]
2           1           Public  [email protected]

Входящий API-объект

DTO.Employee emp = new DTO.Employee()
{
    EmployeeId = 1,
    FirstName = "Bala",
    LastName = "Manigandan",
    ContactPhoneNumbers = new List<DTO.ContactPhone>
        {
            new DTO.ContactPhone()
            {
                Type = "Mobile",
                Number = "9000012345"
            }
        },
    ContactEmailAddress = new List<DTO.ContactEmail>()
        {
            new DTO.ContactEmail()
            {
                Type = "Private",
                EmailAddress = "[email protected]"
            },
            new DTO.ContactEmail()
            {
                Type = "Public",
                EmailAddress = "[email protected]"
            }
        }
};

Я получаю запрос API для обновления мобильного номера и удаления номера факса для указанного сотрудника.

Рассмотрим методы задачи:

public void ProcessEmployee(DTO.Employee employee)
{
    if(employee != null)
    {
        DevDBEntities dbContext = new DevDBEntities();

        DbContextTransaction dbTransaction = dbContext.Database.BeginTransaction();

        List<Task> taskList = new List<Task>();
        List<bool> transactionStatus = new List<bool>();

        try
        {
            Employee emp = dbContext.Employees.FirstOrDefault(m => m.EmployeeId == employee.EmployeeId);

            if (emp != null)
            {
                Task task1 = Task.Factory.StartNew(() =>
                {
                    bool flag = UpdateContactPhone(emp.EmployeeId, employee.ContactPhoneNumbers.FirstOrDefault().Type, employee.ContactPhoneNumbers.FirstOrDefault().Number, dbContext).Result;
                    transactionStatus.Add(flag);
                });

                taskList.Add(task1);

                Task task2 = Task.Factory.StartNew(() =>
                {
                    bool flag = RemoveContactPhone(emp.EmployeeId, "Fax", dbContext).Result;
                    transactionStatus.Add(flag);
                });

                taskList.Add(task2);
            }

            if(taskList.Any())
            {
                Task.WaitAll(taskList.ToArray());
            }
        }
        catch
        {
            dbTransaction.Rollback();
        }
        finally
        {
            if(transactionStatus.Any(m => !m))
            {
                dbTransaction.Rollback();
            }
            else
            {
                dbTransaction.Commit();
            }

            dbTransaction.Dispose();
            dbContext.Dispose();
        }
    }
}

public async Task<bool> UpdateContactPhone(int empId, string type, string newPhone, DevDBEntities dbContext)
{
    bool flag = false;

    try
    {
        var empPhone = dbContext.ContactPhones.FirstOrDefault(m => (m.EmployeeId == empId) && (m.Type == type));
        if (empPhone != null)
        {
            empPhone.Number = newPhone;
            await dbContext.SaveChangesAsync();
            flag = true;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return flag;
}

public async Task<bool> RemoveContactPhone(int empId, string type, DevDBEntities dbContext)
{
    bool flag = false;

    try
    {
        var empPhone = dbContext.ContactPhones.FirstOrDefault(m => (m.EmployeeId == empId) && (m.Type == type));
        if (empPhone != null)
        {
            dbContext.ContactPhones.Remove(empPhone);
            await dbContext.SaveChangesAsync();
            flag = true;
        }
    }
    catch (Exception ex)
    {
        throw ex;
    }

    return flag;
}

Я получаю следующее исключение:

Сообщение = "Ссылка на объект не установлена ​​в экземпляр объекта."

Здесь я прикрепляю скриншот к вашей ссылке

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

Мое требование состоит в том, чтобы выполнять все процессы базы данных UPSERT при параллельном выполнении, любезно помогите мне, как добиться этого без каких-либо исключений, используя Task

Ответ 1

1st) Прекратите использование контекста в разных потоках.
DbContext не является потокобезопасным, это само по себе может вызвать множество странных проблем, даже сумасшедшее исключение NullReference

Теперь вы уверены, что ваш параллельный код быстрее, чем не параллельная реализация?
Я очень сомневаюсь в этом.

Из того, что я вижу, вы даже не меняете свой объект Employee, поэтому я не понимаю, почему вы должны его загрузить (дважды)

Я думаю, что все, что вам нужно, - это 1) Загрузите телефон, который необходимо обновить, и установите новый номер
2) Удалите неиспользуемый мобильный
   НЕ нужно загружать эту запись. Просто используйте конструктор по умолчанию и установите Id.
EF может справиться с остальными (конечно, вам нужно прикрепить вновь созданный объект)

3) Сохраните изменения
(Используйте метод 1,2,3 в 1, используя тот же контекст)

Если по какой-то причине вы решите пойти с несколькими задачами

  • Создайте новый контекст в каждой задаче
  • Оберните свой код в TransactionScope

Обновление
Я просто заметил это:

catch (Exception ex) { throw ex;    }

Это плохо (вы потеряете stacktrace)
Либо удалите try/catch, либо используйте

catch (Exception ex) { throw ; }

Обновление 2
Некоторые примеры кода (я предполагаю, что ваш ввод содержит идентификаторы объектов, которые вы хотите обновить/удалить)

 var toUpdate= ctx.ContactPhones.Find(YourIdToUpdate);
 toUpdate.Number = newPhone;

 var toDelete= new ContactPhone{ Id = 1 };
 ctx.ContactPhones.Attach(toDelete);
 ctx.ContactPhones.Remove(toDelete);
 ctx.SaveChanges();

Если вы идете с параллельным подходом

using(TransactionScope tran = new TransactionScope()) {
    //Create and Wait both Tasks(Each task should create it own context)
    tran.Complete();
}

Ответ 2

Возможные места, где может возникать эта ошибка, - employee.ContactPhoneNumbers.FirstOrDefault().Type, employee.ContactPhoneNumbers.FirstOrDefault()

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

Итак, чтобы исправить эту проблему: 1. Отметьте навигационные свойства как virtual, чтобы ленивая загрузка

public virtual List<ContactPhone> ContactPhoneNumbers { get; set; }
public virtual List<ContactEmail> ContactEmailAddress { get; set; }
  1. Или загружать объекты с помощью .Include
  2. Или Явная загрузка объектов

dbContext.Entry(emp).Collection(s => s.ContactPhoneNumbers).Load(); dbContext.Entry(emp).Collection(s => s.ContactEmailAddress ).Load();