Search code examples
aspnetboilerplateunit-of-work

ABP UnitOfWork is not locking database record for IsolationLevel RepeatableRead


It's better if this issue is explained with an example. I have a database table Person with an int column named [Num]. It has only a record with the initial value of Num == 0.

In my PersonAppService.cs, there are the following 2 methods

public void TestIncrementA()
{
    using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions { IsolationLevel = IsolationLevel.RepeatableRead })
    {
        var person = _personRepository.Get(1);
        person.Num += 1;

        Thread.Sleep(3000);

        uow.Complete();
    }
}

public void TestIncrementB()
{
    using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions { IsolationLevel = IsolationLevel.RepeatableRead })
    {
        var person = _personRepository.Get(1);
        person.Num += 1;

        uow.Complete();
    }
}

The 2 methods are essentially the same which increment the value of the column Num by one except that the first method delays the thread.

Now in the console of a web browser, I run the following commands in quick succession.

abp.services.app.person.testIncrementA();
abp.services.app.person.testIncrementB();

I would expect the value of Num in my database to be 2 now since it's been incremented twice. However it's only 1.

It's clear the RepeatableRead UoW is not locking the row properly. I have also tried using the attribute [UnitOfWork(IsolationLevel.RepeatableRead)] to no avail.

But, if I were to set the following in the PreInitialize of a module, it works.

Configuration.UnitOfWork.IsolationLevel = IsolationLevel.RepeatableRead;

This will unfortunately force RepeatableRead app-wide. Is there something that I'm overlooking?


Solution

  • To set a different isolation level from the ambient unit of work, begin another with RequiresNew:

    using (var uow = _unitOfWorkManager.Begin(new UnitOfWorkOptions
    {
        Scope = TransactionScopeOption.RequiresNew, // Add this
        IsolationLevel = IsolationLevel.RepeatableRead
    })
    {
        ...
    }
    

    Explanation

    From https://aspnetboilerplate.com/Pages/Documents/Unit-Of-Work:

    If a unit of work method calls another unit of work method, both use the same connection & transaction. The first entered method manages the connection & transaction and then the others reuse it.

    The default IsolationLevel for a unit of work is ReadUncommitted if it is not configured. ...

    Conventional Unit Of Work Methods

    Some methods are unit of work methods by default:

    • ...
    • All Application Service methods.
    • ...