I have a webapi and I want to make my logic inside this controller thread safe.

I want user can only update payroll when the last one updated and two update at the same time should not be happend.

As you can see in the code, I added a column in Payroll entity with the name of IsLock as boolean and try to handle multiple request for update in this way but it is not thread-safe.

How can I make it thread-safe ?

 [HttpPut("{year}/{month}")]
    public async Task<NoContentResult> Approve([FromRoute] int year, [FromRoute] int month)
    {
         var payroll = _dataContext.Payrolls
            .SingleOrDefaultAsync(p =>
            p.Month == month && p.Year == year);

        if (payroll.IsLock)
        {
            throw new ValidationException(
                $"The payroll {payroll.Id} is locked.");
        }

        try
        {
            payroll.IsLock = true;
            _dataContext.Payrolls.Update(payroll);
            await _dataContext.SaveChangesAsync(cancellationToken);

            payroll.Status = PayrollStatus.Approved;
            _dataContext.Payrolls.Update(payroll);
            await _dataContext.SaveChangesAsync(cancellationToken);

            payroll.IsLock = false;
            _dataContext.Payrolls.Update(payroll);
            await _dataContext.SaveChangesAsync(cancellationToken);

             return NoContent();
        }
        catch (Exception)
        {
            payroll.IsLock = false;
            _dataContext.Payrolls.Update(payroll);
            await _dataContext.SaveChangesAsync(cancellationToken);
            throw;
        }
    }

You are looking for Concurrency Tokens. Each row in the payroll table would have one. When a user loaded the edit interface for a payroll, the concurrency token would be sent to the client. The client would include the concurrency token in the request to update the payroll. The update would only succeed of the concurrency token had not changed - meaning that the data had not changed since the user fetched it to start the edit.

Entity Framework uses the concurrency tokens internally, as well, so it won't save changes from a stale entity (where the data has changed since it was loaded).

The current IsLocked solution has some flaws. If two API requests are received at the same time, both may read the payroll data and see that it isn't locked. Both requests would then lock the row, make competing changes, and release the lock without ever realizing there were simultaneous edits.