CallContext Behaves Inconsistently When Used With Awaited Tasks
I’ve been working a bit with Serilog and ASP.NET Core lately. In both cases, there are constructs that use CallContext to store data across an asynchronous flow. For Serilog, it’s the LogContext
class; for ASP.NET Core it’s the HttpContextAccessor
.
Running tests, I’ve noticed some inconsistent behavior depending on how I set up the test fakes. For example, when testing some middleware that modifies the Serilog LogContext
, I might set it up like this:
var mw = new SomeMiddleware(ctx => Task.FromResult(0));
Note the next RequestDelegate
I set up is just a Task.FromResult
call because I don’t really care what’s going on in there - the point is to see if the LogContext
is changed after the middleware executes.
Unfortunately, what I’ve found is that the static Task
methods, like Task.FromResult
and Task.Delay
, don’t behave consistently with respect to using CallContext
to store data across async calls.
To illustrate the point, I’ve put together a small set of unit tests here:
public class CallContextTest
{
[Fact]
public void SimpleCallWithoutAsync()
{
var value = new object();
SetCallContextData(value);
Assert.Same(value, GetCallContextData());
}
[Fact]
public async void AsyncMethodCallsTaskMethod()
{
var value = new object();
await NoOpTaskMethod(value);
Assert.Same(value, GetCallContextData());
}
[Fact]
public async void AsyncMethodCallsAsyncFromResultMethod()
{
var value = new object();
await NoOpAsyncMethodFromResult(value);
// THIS FAILS - the call context data
// will come back as null.
Assert.Same(value, GetCallContextData());
}
private static object GetCallContextData()
{
return CallContext.LogicalGetData("testdata");
}
private static void SetCallContextData(object value)
{
CallContext.LogicalSetData("testdata", value);
}
/*
* Note the difference between these two methods:
* One _awaits_ the Task.FromResult, one returns it directly.
* This could also be Task.Delay.
*/
private async Task NoOpAsyncMethodFromResult(object value)
{
// Using this one will cause the CallContext
// data to be lost.
SetCallContextData(value);
await Task.FromResult(0);
}
private Task NoOpTaskMethod(object value)
{
SetCallContextData(value);
return Task.FromResult(0);
}
}
As you can see, changing from return Task.FromResult(0)
in a non async
/await
method to await Task.FromResult(0)
in async
/await
suddenly breaks things. No amount of configuration I could find fixes it.
StackOverflow has related questions and there are forum posts on similar topics, but this is the first time this has really bitten me.
I gather this is why AsyncLocal<T>
exists, which means maybe I should look into that a bit deeper.