Search code examples
c#tasksynchronizationcontext.net-4.7.2

Is it possible to test if a method is running with a captured SynchronizationContext when started by a Task?


I might be missing the answer somewhere, or it's something trivial, but I haven't found it.

Here's effectively what I'm trying to accomplish in code:

public static async Task CapturesContext()
{
    await Task.Run(() => DoWhatever());
}

public static async Task DoesNotCaptureContext()
{
    await Task.Run(() => DoWhatever()).ConfigureAwait(false);
}

public static void DoWhatever()
{
    //Any way to test here that it was run with/without a captured SynchronizationContext?
}

The above is a very over-simplified example but expresses what I'm looking to accomplish.

The goal is to weed out improper usages of ConfigureAwait in a very large code base.

Given any method(s) that are run using a Task is it possible to check, via code like an Assert or unit test, whether the given method is being run using a captured SynchronizationContext?

If not, is there some alternate way I can accomplish my goal?


Solution

  • The goal is to weed out improper usages of ConfigureAwait in a very large code base.

    Some teams choose to use a code analysis tool for this. There are several available. The most common approach I've seen is to require a ConfigureAwait for every await, and explicitly specify either true or false. This ensures that each await has been reviewed and the flow of context is explicit. Other teams apply project-specific rules of "always use ConfigureAwait(false)" and just depend on code review for projects that can't follow that rule.

    The problem with your example code is that it's not possible for DoWhatever to know whether it was indirectly invoked, because of the Task.Run. If you rewrite those methods, this becomes clear:

    public static async Task CapturesContext()
    {
      var task = Task.Run(() => DoWhatever());
      await task;
    }
    
    public static async Task DoesNotCaptureContext()
    {
      var task = Task.Run(() => DoWhatever());
      var configuredTask = task.ConfigureAwait(false);
      await configuredTask;
    }
    

    The first lines of the rewritten methods should make it clear that DoWhatever has no idea whether CapturesContext or DoesNotCaptureContext will capture the context or not. Note the "will" (future tense) - it is entirely possible that DoWhatever runs and finishes executing before ConfigureAwait(false) is even called.

    Now, you can check from within a task whether it is running on a context right now. But in this case, for both example methods, DoWhatever will not see a context due to the Task.Run. So that doesn't help you detect the fact that CapturesContext does capture the context; DoWhatever doesn't see the context so it can't detect it.

    The custom SynchronizationContext is a good solution for unit tests, but it would be awkward to use at runtime, since you do have some methods that need the context. For this reason, most teams choose to depend on code review and/or code analysis tooling.