LogicalCallContext flows across await in Console App but not VS UnitTest
Asked Answered
S

1

7

I am using the Logical CallContext to flow information back across a series of awaits. Interestingly, in my test console app, everything works fine. However, when running my unit test in the context of a VS UnitTest, the call context does not seems to flow across the awaits.

Inside of the method: SendRequestAsyncImpl the call context is being set, and when I query the logical call context from a breakpoint at the moment the method returns, the call context is set just fine.

However, after the await returns in the below line:

Message response = await SendRequestAsyncImpl(m, true).ConfigureAwait(false);

The logical call context is empty. I thought maybe the problem would be fixed by setting ConfigureAwait(true) rather than false. But that does not fix the issue.

It doesn't matter what I try to flow, even setting a simple value type inside of SendRequestAsyncImpl like:

System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("flag", true);

is not retrievable after the await.

Why is this working from my console app? But not from my unit test? What's different? (I've seen some other stack overflow questions that refer to AppDomain issues. But I can't even marshal a bool across the await. The ability to marshal the data doesn't appear to be the issue here.)

Stealer answered 11/2, 2016 at 20:56 Comment(6)
What framework version is your unit test project targeting?Hannahannah
Good question. It's targeting .NET 4.5Stealer
If you replace your SUT with await Task.Delay(100);, does it flow then?Hannahannah
I can call LogicalSetData before the await and, after the await, the value is still set. It's only if LogicalSetData is called inside of the awaited method that it doesn't flow. Is that the question you're asking?Stealer
I'm looking at your write-up (blog.stephencleary.com/2013/04/…) on this topic. I notice that inside of the VSTest execution engine, Task.WhenAny(...) is called. You mentioned in one of the comments, that Task.WhenAll(...) results in losing the CallContext, because it's not clear how to merge the CallContext of multiple different tasks when they all return. That makes sense. Could the presence of Task.WhenAny(...) in VSTest be causing this behavior?Stealer
Logical call context doesn't flow out of any method. It will flow in to methods and across await points, but it won't flow out.Hannahannah
S
7

So after reading Stephen Clearly's comments, both in this question and on the article: http://blog.stephencleary.com/2013/04/implicit-async-context-asynclocal.html the answer has become clear.

While in synchronous code the logical CallContext does flow back out of a method. In asynchronous methods the CallContext does not flow back out. This makes sense, since how would .NET know how I want the CallContext to be merged after something like Task.WhenAll(...). Executing the below code illustrates:

static void Main(string[] args)
{
    SynchronousCall();
    Task.WaitAll(Test(), Test2());
    var s = CallContext.LogicalGetData("SynchronousCall");
    var test = CallContext.LogicalGetData("Test");
    var test2 = CallContext.LogicalGetData("Test2");

    Console.WriteLine("s val: {0}", (s == null) ? "{null}" : s);
    Console.WriteLine("test val: {0}", (test == null) ? "{null}" : test);
    Console.WriteLine("test2 val: {0}", (test2 == null) ? "{null}" : test2);
}

private static void SynchronousCall()
{
    CallContext.LogicalSetData("SynchronousCall", true);
}

private static async Task<bool> Test()
{
    CallContext.LogicalSetData("Test", true);
    var b = await Task.Run<bool>(() => 
    {
        return true; 
    });
    return b;
}

private static async Task<bool> Test2()
{
    CallContext.LogicalSetData("Test2", true);
    var b = await Task.Run<bool>(() =>
    {
        return true;
    });
    return b;
}

Prints the following:

s val: True
test val: {null}
test2 val: {null}

So, as you can see, the synchronous method allows CallContext to flow out, but the asynchronous method does not.

I modified my method to inject a thread-safe collection into the CallContext prior to the awaitable method. I inject information into that collection, rather than directly into the CallContext. Since the upstream and downstream all obtain a reference to the same collection, this allows me to flow context up out of my awaited method by retrieving it from the collection after the awaited method returns.

Hopefully this helps someone else in the future.

Stealer answered 12/2, 2016 at 18:51 Comment(1)
Thanks I used CallContext.LogicalSetData("MyDic", new Dictionary<string, string>()) and Dictionary<string, string> myDic = (Dictionary<string, string>)CallContext.LogicalGetData("MyDic");Afeard

© 2022 - 2024 — McMap. All rights reserved.