17

I'm trying to understand how AsyncLocal should work in .Net 4.6. I'm putting some data into AsyncLocal...but when the ThreadContext changes it is getting set to null. The whole reason I'm using AsyncLocal is to try to preserve/cache this value across threads as I await async operations. Any idea why this would be specifically called and set to a null as the context changes? Documentation on AsyncLocal is very sparse...perhaps I've got it all wrong.

public class RequestContextProvider : IRequestContextProvider
{
    private static readonly AsyncLocal<IRequestContext> _requestContext = new AsyncLocal<IRequestContext>(ValueChangedHandler);

    private static void ValueChangedHandler(AsyncLocalValueChangedArgs<IRequestContext> asyncLocalValueChangedArgs)
    {
        **//This is just here to observe changes...when I await a call that 
        //causes control to pass to a different thread..this is called 
        //with a current value of null.**
        var previousValue = asyncLocalValueChangedArgs.PreviousValue;
        var currentValue = asyncLocalValueChangedArgs.CurrentValue;
        var contextChanged = asyncLocalValueChangedArgs.ThreadContextChanged;
    }

    public void SetContext(IRequestContext requestContext)
    {
        _requestContext.Value = requestContext;
    }

    public IRequestContext GetContext()
    {
        return _requestContext.Value;
    }

}

Here is the example of how it is being called. This is an asynchronous event subscriber that is being called into using an EventStore connection (GetEventStore.com). If the two awaited tasks in here don't do anything (If there are no ids to look up) I have no issues because presumably the task runs synchronously. But if I do have work to do on these tasks I lose my context.

 private async Task PublishProduct(Guid productId, Guid productReferenceId, IEnumerable<Guid> disclosureIds,
        IEnumerable<Guid> addOnIds)
    {
        var disclosureReferenceIdsTask = _disclosureReferenceIdService.GetReferenceIdsAsync(disclosureIds);
        var addOnReferenceIdsTask = _addOnReferenceIdService.GetReferenceIdsAsync(addOnIds);
        await Task.WhenAll(disclosureReferenceIdsTask, addOnReferenceIdsTask);

        IEnumerable<Guid> disclosuresResult = await disclosureReferenceIdsTask;
        IEnumerable<Guid> addOnsResult = await addOnReferenceIdsTask;

        await _eventPublisher.PublishAsync(new ProductPublished(productId, productReferenceId,
            disclosuresResult.ToList(), addOnsResult.ToList()));
    }

And here is my hacky solution which appears to work:

    private static void ValueChangedHandler(AsyncLocalValueChangedArgs<IRequestContext> asyncLocalValueChangedArgs)
    {
        var previousValue = asyncLocalValueChangedArgs.PreviousValue;
        var currentValue = asyncLocalValueChangedArgs.CurrentValue;
        var contextChanged = asyncLocalValueChangedArgs.ThreadContextChanged;
        if (contextChanged && currentValue == null && previousValue != null)
        {
            _requestContext.Value = previousValue;
        }
    }

FYI, this is a 4.6 runtime project running under DNX RC1 as a console app.

swannee
  • 3,346
  • 2
  • 24
  • 40
  • Please show your test code that used `RequestContextProvider` that gave you the results you are seeing. – Scott Chamberlain Mar 04 '16 at 17:21
  • @ScottChamberlain - Without trying to get too in depth, I added the code that calls this. Just FYI the calls are async all the way through. – swannee Mar 04 '16 at 17:37
  • I think it is because you are running a console app and don't have a `SynchronizationContext`, does the problem go away if you do this from a thread that would have a context (The UI thread from Winforms, WPF, ASP.net, etc.)? – Scott Chamberlain Mar 04 '16 at 17:56
  • @ScottChamberlain - Scott, we do publish from ASP.Net Web APIs and I haven't run into this there but our scenarios there are also much simpler. Definitely possible that your assertion is correct...but there are no caveats mentioned in the documentation. I would think that would be a big issue to leave out...but again you may be correct. – swannee Mar 04 '16 at 18:06
  • 1
    Should it not store this in the ExecutionContext as opposed to the SynchronizationContext? – swannee Mar 04 '16 at 18:12
  • You may find Stephen Toub's article useful: http://blogs.msdn.com/b/pfxteam/archive/2012/01/20/10259049.aspx – David Peden Mar 04 '16 at 19:12
  • @swannee: Wondering if the problem is due to the `static` keyword. Can you reduce this to just a 4.6 console app using `AsyncLocal`? – Stephen Cleary Mar 05 '16 at 20:59
  • 1
    @StephenCleary - Sure I'll give that a shot and see what happens. I think that I've got the IRequestContext provider scoped as a singleton in DI anyway so pretty sure I can just remove "static" – swannee Mar 07 '16 at 14:26
  • 1
    @StephenCleary - Sorry for the slow response here...but changing from static didn't have any impact...in addition, I also see instances where the set applies a new value and a get later on retrieves the old value. I'll try reducing my complexity...but I'm thinking this might impact the outcome – swannee Apr 27 '16 at 15:10
  • 3
    @swannee: Reduce the complexity one step at a time. If the problem suddenly disappears, then you have a high degree of confidence which step causes the problem. – Stephen Cleary Apr 28 '16 at 02:02
  • @StephenCleary - Thanks Stephen, I think I discovered my issue...I was adding to AsyncLocal from aspnet 5 middleware and accessing it later from a controller. As I understand it, these are separate logical contexts...but that doesn't explain why sometimes I do see the data come across... Any ideas? Am I off base here...I read this on a different SO question concerning LogicalCallContext. – swannee Apr 28 '16 at 13:39
  • 4
    @StephenCleary - So OK AysncLocal worked fine across Middleware and Controller. It looks like what I'm dealing with is actually some weird scoping behavior by Autofac when using PerLifetimeScope. In certain situations it's changing context in a way that makes it unable to access the current AsyncLocal values...not sure what it's doing under the hood, but I believe I've solved it by using a different scoping approach. – swannee Apr 29 '16 at 13:11
  • 3
    @swannee Ever figured out the exact reasons? I'm having the same trouble with the default asp.net core `ServiceProvider` when using a `ServiceProviderFactory` to create a scope in a background task. Sadly really hard to reproduce in a smaller sample.. – Voo Jun 21 '18 at 07:46

1 Answers1

2

What you need to do is: ExecutionContext.SuppressFlow();
That will stop raising event valueChangedHandler when your thread context is lost, as result you will not get NULL values, also it will stop raising event when new ThreadContext is created and data is copied to this.

private static void ValueChanged(AsyncLocalValueChangedArgs<string> obj)
{
    Console.WriteLine(obj.CurrentValue);
}

public static void Main(string[] args)
{
    ExecutionContext.SuppressFlow();
    AsyncLocalContext.Value = "Main";
    Task.Run(() =>
        {
            AsyncLocalContext.Value = "Test1";
        }).Wait();

    Console.WriteLine("Main: " + AsyncLocalContext.Value);
}

Output is:

Main
Test1
Main: Main

If we comment ExecutionContext.SuppressFlow(); then will get this:

Main
Main        -- copied data to Task
Test1
            -- here is NULL when context has lost
Main: Main
Alexey Klipilin
  • 1,866
  • 13
  • 29