#ajax #asp.net-core #thread-safety #dbcontext #lifetime
Вопрос:
Контекст:
Из javascript я делаю вызов ajax контроллеру для обновления сущности «Владелец». Контроллер использует службу для обработки запроса и соответствующего обновления сущности. В методе обновления службы я также обновляю связанные данные «Владельца», а в некоторых случаях я также добавляю новую сущность «Владелец».
Проблема:
Здесь(в службе) метод обновления по какой-то причине вызывается несколько раз. Если посмотреть на список потоков, он показывает несколько активных рабочих потоков. Из — за этой проблемы я в конечном итоге добавляю несколько дублированных строк и обновляю противоречивые данные (некоторые поля отсутствуют-например, брюки и куртка). Я хочу, чтобы он запустился только один раз.
Если я выполню только операцию РЕДАКТИРОВАНИЯ, ничего плохого не произойдет.
Если я выполню операцию ДОБАВЛЕНИЯ, а затем операцию РЕДАКТИРОВАНИЯ, редактирование будет выполнено 2 раза, добавив еще 2 строки в базу данных.
Если я выполню 2 или более(назовем это число N) операций ДОБАВЛЕНИЯ, а затем и операции РЕДАКТИРОВАНИЯ, редактирование будет выполняться N x 2 раза, создавая N x 2 дублированных строки.
Я свел код к минимуму, насколько мог. Это немного длинновато, но я не хотел исключать ни одного случая, который действительно мог бы быть виновником.
Если предоставленного свернутого кода недостаточно, я также могу предоставить доступ к хранилищу для волшебников кода среди нас. Пожалуйста, помогите мне. Я действительно не знаю ничего другого, что я мог бы попытаться заставить это работать.
Используемый стек: .net core 2.2, последняя версия jquery, mysql, pomelo, ядро ef
Вещи, которые я уже пробовал:
- асинхронность / ожидание
- регистрация служб и репозиториев при запуске в области действия
- регистрация служб и репозиториев при запуске как переходных
- регистрация DbContext при запуске как переходный
- ThreadPool.максимальный лимит(1,1)
- js onClick / on («щелчок») / один («щелчок»)
- all of the above combined
Frontend:
<script>
$(document).one("click", "#buttonId", function () {
$(this).attr("disabled", "disabled");
var result = validator.validate(validator);
if (result.formIsValid) {
$.ajax({
type: "POST",
url: "/controllerName/Edit",
data: $('#editForm').serialize(),
dataType: "json",
success: function (response) {
CloseModalById('editModal');
ShowHeaderAlert(response, "success", 5000);
$('#ownerListState').change();
},
error: function (error) {
CloseModalById('editModal');
swalErrorTimer(error.responseText, 7000);
}
});
} else {
updateUi(result.validationResults, "form-group", "error_span");
}
});
</script>
Controller:
public OwnerController(IOwnerService ownerService,IMapper mapper)
{
_ownerService = ownerService;
_mapper = mapper;
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult EditModal(OwnerViewModel ownerVM)
{
try
{
var dto = _mapper.Map<OwnerDto> ownerVM);
var result = _ownerService.UpdateOwner(dto);
if (result.Successful)
{
return Ok(JsonConvert.SerializeObject("Successfully updated!"));
}
return BadRequest("Something went wrong!");
}
catch (Exception e)
{
Log.Error(e, "Something went wrong!");
return BadRequest("Something went wrong!");
}
}
Service:
public OwnerService(IOwnerRepository ownerRepository, ICatRepository catRepository, ICatService catService, IMapper mapper)
{
_ownerRepository = ownerRepository;
_catRepository = catRepository;
_catService = catService;
_mapper = mapper;
}
public Result<OwnerDto> UpdateOwner(OwnerDto ownerDto)
{
try
{
var oldOwner = ownerDto.Copy();
// case: changed status in other than AtWork => empty the position for this set
if (ownerDto.LocationId != null amp;amp; ownerDto.Status != "AtWork")
{
_catService.UpdateCatPosition(ownerDto.LocationId.Value, ownerDto.Biscuits, PositionEnum.InTheHouse);
}
// case: was AtWork, will be in raft => position changed
if (ownerDto.LocationId != null amp;amp; ownerDto.Status == "AtWork" amp;amp; ownerDto.LocationId != null amp;amp; ownerDto.Biscuits == ownerDto.OldNrOfBiscuits)
{
_catService.UpdateCatPosition(ownerDto.LocationId.Value, ownerDto.Biscuits, PositionEnum.InTheHouse);
_catService.UpdateCatPosition(ownerDto.LocationId.Value, ownerDto.Biscuits, PositionEnum.InTheGarden);
}
// case: no change for position but change for nr of biscuits =>
if (ownerDto.LocationId != null amp;amp; ownerDto.Status == "AtWork" amp;amp; ownerDto.Biscuits != ownerDto.OldNrOfBiscuits amp;amp; ownerDto.LocationId == null)
{
ownerDto.LocationId = ownerDto.LocationId;
// case we have added some new anv to this set => we have to also update the nr inside position
if (ownerDto.Biscuits > ownerDto.OldNrOfBiscuits)
{
var newNrBiscuits = ownerDto.Biscuits - ownerDto.OldNrOfBiscuits;
var catPosition = PositionEnum.InTheGarden;
_catService.UpdateCatPosition(ownerDto.LocationId.Value, newNrBiscuits, catPosition);
}
// case we have removed some biscuits from this owner => we have to also update the nr for the cat
if (ownerDto.Biscuits < ownerDto.OldNrOfBiscuits)
{
var newNrBiscuits = ownerDto.OldNrOfBiscuits - ownerDto.Biscuits;
var catPosition = PositionEnum.InTheHouse;
_catService.UpdateCatPosition(ownerDto.LocationId.Value, newNrBiscuits, catPosition);
}
}
// case: was AtWork, will be AtWork, Position changed, Biscuits changed
if (ownerDto.LocationId != null amp;amp; ownerDto.Status == "AtWork" amp;amp; ownerDto.LocationId != null amp;amp; ownerDto.LocationId != ownerDto.LocationId amp;amp; ownerDto.Biscuits != ownerDto.OldNrOfBiscuits)
{
var newNrBiscuits = ownerDto.OldNrOfBiscuits -(ownerDto.OldNrOfBiscuits - ownerDto.Biscuits);
var catPosition = PositionEnum.InTheGarden;
_catService.UpdateCatPosition(ownerDto.LocationId.Value, newNrBiscuits, catPosition);
}
// case: was not AtWork but will be AtWork
if (ownerDto.LocationId != null amp;amp; ownerDto.LocationId == null)
{
_catService.UpdateCatPosition(ownerDto.LocationId.Value, ownerDto.Biscuits, PositionEnum.InTheGarden);
}
if (ownerDto.LocationId != null)
{
var catPosition = catRepository.GetPositionById(ownerDto.LocationId.Value);
if (!catPosition.Successful)
{
Log.Error("Something went wrong!");
throw new Exception("Something went wrong!");
}
ownerDto.CatPosition = _mapper.Map<CatDto>(catPosition);
}
//------------ Pants
var pants = _ownerRepository.GetPants(ownerDto.Pants);
if (!pants.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
if (Pants == null amp;amp; !string.IsNullOrEmpty(ownerDto.Pants))
{
pants = _ownerRepository.AddPants(new DbModelPants() { Label = ownerDto.Pants.ToUpper() });
}
if (!pants.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
ownerDto.pantsId = pants.Id;
///////////////////// Jacket
var jacket = _ownerRepository.GetjacketByLabel(ownerDto.jacket);
if (!jacket.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
if (jacket == null amp;amp; !string.IsNullOrEmpty(ownerDto.Jacket))
{
jacket = _ownerRepository.Addjacket(new DbModelJacket() { Label = ownerDto.jacket.ToUpper() });
}
if (!jacket.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
ownerDto.JacketId = jacket.Id;
// ------------------------------------------------------------------------------------------
var modelForDatabase = _mapper.Map<OwnerDbModel>(ownerDto);
modelForDatabase.LastModified = DateTime.Now;
var value = _ownerRepository.UpdateOwner(modelForDatabase);
if (!value.Successful)
{
return Result<OwnerDto>.ResultError(value.Error);
}
var returnModel = _mapper.Map<OwnerDto>(value);
// if we move the biscuits we need to create a new owner with the remaining biscuits values that were not moved to the new location
//--------- This adds a new Owner to the db with oldData
if (oldOwner.OldNrOfBiscuits > oldOwner.Biscuits amp;amp; oldOwner.LocationId != oldOwner.LocationId amp;amp; oldOwner.LocationId != null amp;amp; oldOwner.LocationId != null)
{
oldOwner.Biscuits = oldOwner.OldNrOfBiscuits - oldOwner.Biscuits;
oldOwner.LocationId = oldOwner.LocationId;
oldOwner.Id = 0;
var addedOldSet = AddOwner(oldOwner, PositionEnum.AtThePetStore);
if (!addedOldSet.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
}
return Result<OwnerDto>.ResultOk(returnModel);
}
catch (Exception er)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
}
public Result<OwnerDto> AddOwner(OwnerDto owner, PositionEnum catPosition = PositionEnum.InTheGarden)
{
try
{
if (owner.LocationId != null amp;amp; owner.Status == "AtWork")
{
var catPosition = _catService.GetPositionById(owner.LocationId.Value);
if (!catPosition.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
_catService.UpdatePosition(owner.LocationId.Value, owner.NrBucati, catPosition);
catPosition = _catService.GetPositionById(owner.LocationId.Value);
owner.CatPosition = catPosition;
}
//------------ Pants
// if we don't find the pants with provided name in the table Pants, we add a new set of Pants and provide the Owner with that set. Same for the Jacket.
var pants = _ownerRepository.GetPants(ownerDto.Pants);
if (!pants.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
if (Pants == null amp;amp; !string.IsNullOrEmpty(ownerDto.Pants))
{
pants = _ownerRepository.AddPants(new DbModelPants() { Label = ownerDto.Pants.ToUpper() });
}
if (!pants.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
ownerDto.PantsId = pants.Id;
//----------- Jacket
var jacket = _ownerRepository.GetjacketByLabel(ownerDto.Jacket);
if (!jacket.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
if (jacket == null amp;amp; !string.IsNullOrEmpty(ownerDto.Jacket))
{
jacket = _ownerRepository.Addjacket(new DbModelJacket() { Label = ownerDto.jacket.ToUpper() });
}
if (!jacket.Successful)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
ownerDto.JacketId = jacket.Id;
var modelForDatabase = _mapper.Map<OwnerDbModel>(owner);
modelForDatabase.LastModified = DateTime.Now;
modelForDatabase.Name = modelForDatabase.Name.ToUpper();
// send model to database
var addedOwner = _ownerRepository.AddOwnerToDatabase(modelForDatabase);
var returnModel = _mapper.Map<OwnerDto>(addedOwner);
return Result<OwnerDto>.ResultOk(returnModel);
}
catch (Exception er)
{
Log.Error("log stuff here");
throw new Exception("Something went wrong!");
}
}
Методы обновления и добавления во всех репозиториях аналогичны. Пример:
public Entity UpdateEntity(Entity entityToUpdate)
{
_db.EntityDbSet.Update(entityToUpdate);
_db.SaveChanges();
return entityToUpdate;
}
public Entity AddEntity(Entity entityToAdd)
{
_db.EntityDbSet.Add(entityToAdd);
_db.SaveChanges();
return entityToAdd;
}
Комментарии:
1. Потоки пула потоков не имеют никакого отношения к вашей проблеме; просто игнорируйте их. 1) Звоните только
SaveChanges
один раз на HTTP-запрос. 2) Почитайте немного о «оптимистичном параллелизме».2. Спасибо @StephenCleary, я немного разберусь в этом.
Ответ №1:
Проблема была в HTML-файле. Частичное представление, содержащее модал, также загрузило файл js, и из-за этого оно загружало его несколько раз. Вот откуда поступили многочисленные звонки. Чтобы это сработало, я загрузил файл js в индекс. Такое простое решение сумасшедшей проблемы с сумасшедшими результатами…