Дублированные объекты в базе данных из контроллера API

У меня есть модель EF Core, определенная как:

namespace TestApp.DataAccess.Models {
    public class Candidate
    {
        public int CandidateId { get; set; }
        public Guid UniqueKey { get; set; }
        public string Name { get; set; }
        public virtual List<Job> Jobs { get; set; }
    }

    public class Job
    {
        public int JobId { get; set; }
        public Guid UniqueKey { get; set; }

        public int CandidateId { get; set; }
        public virtual Candidate Candidate { get; set; }

        public string Title { get; set; }
    }
}

Где CandidateId и JobId — первичные ключи.

Обе сущности также имеют свойство UniqueKey, которое является Guid. Это генерируется нашим клиентом и публикуется в нашем API в теле запроса. Мы никогда не должны получить более одного Job с одним и тем же свойством UniqueKey.

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

Вместо этого в нашем контроллере мы проверяем, существует ли уже UniqueKey. Если это не так, мы создаем новый Job. Если это так, то мы обновляем существующую запись:

foreach (var jobModel in model.Jobs) {
    //Check if the job already exists for the entity
    var jobEntity = candidate.Jobs.FirstOrDefault(x => x.UniqueKey == jobModel.UniqueKey);

    //If not, then create it
    if (jobEntity == null) {
        jobEntity = new Job { UniqueKey = jobModel.UniqueKey };
        candidate.Jobs.Add(jobEntity);
    }

    jobEntity.Title = jobModel.Title;
    //...
}

В последнее время я начал видеть повторяющиеся Jobs:

JobId         CandidateId         Title         UniqueKey
201           100                 Teacher       4177b6da-7a4c-4032-b13d-8e3e2d2aeaca
202           100                 Teacher       4177b6da-7a4c-4032-b13d-8e3e2d2aeaca

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

Что могло привести к созданию этих двух записей? Может ли это быть как-то связано с дублированием запросов, одновременно попадающих в нашу конечную точку API?

Это полный пример кода:

namespace TestApp.DataAccess.Models {
    public class Candidate
    {
        public int CandidateId { get; set; }
        public Guid UniqueKey { get; set; }
        public string Name { get; set; }
        public virtual List<Job> Jobs { get; set; }
    }

    public class Job
    {
        public int JobId { get; set; }
        public Guid UniqueKey { get; set; }

        public int CandidateId { get; set; }
        public virtual Candidate Candidate { get; set; }

        public string Title { get; set; }
    }
}

namespace TestApp.Api.Controllers {
    [Route("api/[controller]")]
    public class CandidateController : BaseController {
        public ServerApplicationContext _context { get; set; }

        public CandidateController(ServerApplicationContext context) {
            _context = context;
        }

        public async Task<Candidate> findEntityOrDefault(Guid key) {

            if(entity == null) {
                return null;
            }


        }
        [HttpPost]
        public async Task<IActionResult> Post([FromBody]CandidateViewModel model) {

            //Load the existing entity
            var candidate = await _context.Candidates.FirstOrDefaultAsync(x => x.UniqueKey == key);

            //If we don't find a candidate then create one
            if(entity == null) {
                //...
                //candidate = new Candidate { ... }
                //...

            } else {
                //Else load in child properties for the existing candidate

                candidate.Jobs = await _context.Jobs.Where(x => x.CandidateId == entity.CandidateId).ToListAsync();

                return entity;
            }

            //Add jobs from the model to our entity
            foreach (var jobModel in model.Jobs) {
                //Check if the job already exists for the entity
                var jobEntity = candidate.Jobs.FirstOrDefault(x => x.UniqueKey == jobModel.UniqueKey);

                //If not, then create it
                if (jobEntity == null) {
                    jobEntity = new Job { UniqueKey = jobModel.UniqueKey };
                    candidate.Jobs.Add(jobEntity);
                }

                jobEntity.Title = jobModel.Title;
                //...
            }


            //Add or update the candidate to the DB
            if (candidate.CandidateId == 0)
                _context.Add(candidate);
            else
                _context.Update(candidate);

            //Commit changes
            await _context.SaveChangesAsync();

            var viewModel = Mapper.Map<CandidateViewModel>(candidate);
            return new ObjectResult(viewModel);
        }        
    }
}

person Joseph    schedule 17.06.2019    source источник
comment
Похоже, как вы и предполагали - может быть, несколько запросов одновременно попадают на контроллер. Дело в том, что если вы поставите уникальное ограничение, по крайней мере, контроллер выдаст исключение, и вы сможете увидеть условия исключения.   -  person Charleh    schedule 17.06.2019
comment
@Чарле Спасибо. Как он обрабатывает несколько запросов, попадающих в контроллер? Запросы асинхронны? И поэтому EF не является потокобезопасным для запросов?   -  person Joseph    schedule 17.06.2019
comment
@joseph Да, EF не является потокобезопасным. Это несколько ссылок, которые действительно полезны в вашем случае. stackoverflow.com/questions/19754368/, stackoverflow.com/questions/6126616/is-dbcontext-thread-safe< /а>   -  person Aparna Gadgil    schedule 18.06.2019