Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AsyncLocal with ASP.NET Core Controller/ServiceProviderScope

It seems like the execution context is not kept until Dispose is called on elements resolved in the controller scope. This is probably due to the fact that asp.net core has to jump between native and managed code and resets the execution context at each jump. Seems like the correct context is not restored any more before the scope is disposed.

The following demonstrates the issue - simply put this in the default asp.net core sample project and register TestRepo as a transient dependency.

When calling GET api/values/ we set the value for the current task to 5 in a static AsyncLocal at the start of the call. That value flows as expected through awaits without any problem. But when the controller and its dependencies are disposed after the call the AsyncLocal context is already reset.

[Route("api/[controller]")]
public class ValuesController : Controller
{
    private readonly TestRepo _testRepo;

    public ValuesController(TestRepo testRepo) => _testRepo = testRepo;

    [HttpGet()]
    public async Task<IActionResult> Get()
    {
        _testRepo.SetValue(5);
        await Task.Delay(100);
        var val = _testRepo.GetValue(); // val here has correctly 5.
        return Ok();
    }
}

public class TestRepo : IDisposable
{
    private static readonly AsyncLocal<int?> _asyncLocal = new AsyncLocal<int?>();

    public int? GetValue() => _asyncLocal.Value;

    public void SetValue(int x) => _asyncLocal.Value = x;

    public void Foo() => SetValue(5);

    public void Dispose()
    {
        if (GetValue() == null)
        {
            throw new InvalidOperationException(); //GetValue() should be 5 here :(
        }
    }
}

Is this intentional? And if yes is there any workaround around this problem?

like image 459
Voo Avatar asked Jun 21 '18 11:06

Voo


1 Answers

The behavior you are seeing is an unfortunate quirk in the way that ASP.NET Core works. It's unclear to me why Microsoft choose this behavior, but it seems copied from the way Web API worked, which has the exact behavior. Disposing is obviously done at the end of the request, but for some reason the asynchronous context is already cleared before that point, making it impossible to run the complete request in a single asynchronous context.

You've basically got two options:

  1. Instead of using ambient state to share state, flow state through the object graph instead of using ambient state. In other words, make TestRepo Scoped, and store value in a private field.
  2. Move the operation that uses that value to an earlier stage in the request. For instance, you can define some middleware that wraps a request and invokes that operation at the end. At that stage, the asynchronous context will still exist.

Some DI containers actually apply this second technique. Simple Injector, for instance, uses scoping that is based on ambient state, using AsyncLocal<T> under the covers. When integrated in ASP.NET Core, it will wrap the request in a piece of middleware that applies this scope. This means that any Scoped component, resolved from Simple Injector, will be disposed before the ASP.NET Core pipeline disposes its services, and this happens while the asynchronous context is still available.

like image 89
Steven Avatar answered Oct 23 '22 15:10

Steven