Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

AsyncLocal Value updated to null on ThreadContextChanged

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.

like image 430
swannee Avatar asked Mar 04 '16 17:03

swannee


Video Answer


1 Answers

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
like image 51
Alexey Klipilin Avatar answered Sep 21 '22 11:09

Alexey Klipilin