Putting Things in Context

Posted on February 15, 2017 in dotnet , umbraco

An elegant pattern in .NET application, inspired by aspect-oriented programming, consists in storing some objects "in context" in order to increase modularity by separating concerns.

The immediate example is, in the ASP.NET world, HttpContext. In any request, one can refer to HttpContext.Current and retrieve the "ambient" http context. It is "just there" and does not need to be passed around in every method call.

The TransactionScope works in a similar way. Look at the following code:

using (var scope = new TransactionScope())
{
  using (var conn = new SqlConnection(connectionString))
  {
    // do things ...
  }

  scope.Complete();
}

It creates an "ambient" transaction scope, which is nested under an already existing ambient scope (if any), and then the new SqlConnection enlists within the scope and participates in the transaction, even though no explicit reference to the scope is used in its constructor.

Our own ambient context

Implementing our own ambient context in the ASP.NET world is quite easy, precisely because HttpContext already implements the required magic. Say we want to have an ambient OurContext object:

public class OurContext
{
  private const string ContextKey = "OurContext";

  public static OurContext Current
  {
    get { return (OurContext) HttpContext.Current.Items(ContextKey); }
    set { HttpContext.Current.Items[ContextKey] = value; }
  }
}

This is enough to let the application reference OurContext.Current wherever needed and maintain an ambient instance of the OurContext for the duration of a request. This works quite well... until you write a WebAPI controller similar to:

[HttpPost]
public async Task<HttpResponseMessage> DoSomething()
{
  // register a new ambient context
  OurContext.Current = new OurContext();

  // let a component do something
  var result = await SomeComponent.DoSomethingAsync();

  // retrieve the ambient context
  var ourContext = OurContext.Current;

  // etc
}

And retrieving the ambient context throws, because HttpContext.Current is... null. What happened? Well, it turns out that SomeComponent was meant to be used both in ASP.NET and Windows applications—and Windows applications have this unique UI thread which should not be blocked and so (to make it very short), deep into the component, every asynchronous call looks like:

var result = await GetResult().ConfigureAwait(false);

Skipping details (but see this StackOverflow answer by Stephen Cleary) what happens is that the async method resumes on a thread that is not controlled by ASP.NET SynchronizationContext, and the HttpContext magic does not execute, and the thread does not have an HttpContext anymore.

And besides... what if we want to write a component that can run in a standalone application where there is no HttpContext?

Enters Call Context

Every CLR thread has an excution context data structure associated with it. It includes things such as security settings, host settings, the current HttpContext... and one can register objects into this context.

This context is actually composed of an illogical call context (though it's not clear what is illogical about it) and a logical call context. The illogical call context does not flow from thread to thread, however the logical call context does. The HttpContext for example is maintained in the illogical call context (since it is not thread-safe) and ASP.NET has some special extra code to migrate it from thread to thread, should the request migrates too.

The details can be pretty ugly. Read Understanding the SerializationContext, Call Contexts vs. ASP.NET or ThreadStatic, CallContext and HttpContext of you are into that type of things1.

All we need to understand is this: the logical call context flows from thread to thread. And so, the idea is to store our context object in the logical call context:

public class OurContext
{
  private const string ContextKey = "OurContext";

  public static OurContext Current
  {
    get { return (OurContext) CallContext.LogicalGetData(ContextKey); }
    set 
    {
      if (value == null)
        CallContext.FreeNamedDataSlot(ContextKey);
      else
        CallContext.LogicalSetData(ContextKey);
    }
  }
}

One thing to notice, though: it can flow in more places that you would think. For example:

CallContext.LogicalSetData("key", value);
ThreadPool.QueueUserWorkItem(state => 
{
  // here, v == value !
  var v = CallContext.LogicalGetData("key");
});

This is because, by default, the CLR automatically causes the initating thread's execution context to flow to any helper threads, although the ExecutionContext class in System.Threading can be used to change that behavior.

So, in multi-thread situations, you might want to prevent our context from flowing, either by manually removing it from the logical call context before creating the new thread, or by preventing the logical call context from flowing:

CallContext.LogicalSetData("key", value);
ExecutionContext.SuppressFlow();
ThreadPool.QueueUserWorkItem(state => 
{
  // here, v == null !
  var v = CallContext.LogicalGetData("key");
});
ExecutionContext.RestoreFlow();

All in all, we have a solution that works quite well, including in async situations.

And then it all fails.

The Serialization Problem

At some point, while an ambient context resides in the logical call context, code running e.g. an ASP.NET application tries to read a value from a configuration files... and throws a weird exception about the context being not serializable.

First, notice CallContext's namespace: System.Runtime.Remoting.Messaging. Note the remoting and messaging there: CallContext was initialy conceived to be used in remoting scenarios, where the call context is supposed to flow across app domains. And how do objects flow across app domains? In two ways: anything that inherits from MashallByRefObject is passed by reference, anything else is serialized.

Which means that, theoretically, one should only store objects in the logical call context that either inherit from MarshallByRefObject, or are serializable. But, practically, our application is a simple ASP.NET application that does not do anything fancy such as remoting. So, it feels safe to assume that whatever goes in the call context would in fact never be part of a cross app domain operation. Right? Wrong.

As explained in, for example, this MSDN article, or in our article about IIdentity serialization, each app pool has a "default" app domain which is created when the w3wp.exe process starts, and is responsible for, in turn, starting and managing the app domains for the various sites hosted by the app pool. And there are some operations an ASP.NET applications can do, such as reading config files or shutting down, that do trigger cross app domain exchanges between the ASP.NET application app domain and the default app domain.

I mention ASP.NET here because it is an easy example, but there are other, more subtle, cases of cross app domain exchanges, such as... when ReSharper runs unit tests, it creates various app domains and not all of them have access to our DLLs. In some cases, if a test case ends with a non-serializable object in call context, ReSharper and/or Visual Studio can crash, reporting the dreaded serialization exception.

Lightweight Context

Shall we make our context object serializable or inherit from MarshallByRefObject? Well... that would not work either. It would solve half of the problem: our object would be serialized and sent over to the "other" app domain. Alas, that app domain does not usually have access to our own DLLs and therefore cannot deserialize our object.

Unless, of course, we register our DLLs in the GAC, but that is probably not what we want to do. Time to step back: one should probably not put anything "heavy" and "custom" into call context (unless, of course, actually doing remoting), but only standard CLR objects such as strings, objects or Guids.

And one should not do anything based on the reference of these objects, but only their value. If any cross app domain operation takes place, the object is serialized and deserialized, meaning that the call context ends up containing a different object with the same value.

Putting it all together, here is an example of an ambient, call context based context:

public class OurContext
{
  private const string ContextKey = "Context.OurContext";

  private static Dictionary<Guid, OurContext> _objects
    = new Dictionary<Guid, OurContext>();

  private Guid _id = Guid.NewGuid;

  public static OurContext Current
  {
    get
    {
      var guidObject = CallContext.LogicalGetData(ContextKey);
      if (guidObject == null) return null;
      var guid = (Guid) guidObject;
      OurContext context;
      if (!_objects.TryGetValue(guid, out context))
        throw new Exception("panic");
      return context;
    }
    set
    {
      var guidObject = CallContext.LogicalGetData(ContextKey);
      if (guidObject != null)
      {
        var guid = (Guid) guidObject;
        _objects.Remove(guid);
      }
      if (value == null)
      {
        CallContext.FreeNamedDataSlot(ContextKey);
      }
      else
      {
        _objects[value._id] = value;
        CallContext.LogicalSetData(ContextKey, value._id);
      }
    }
  }
}

Pretty, isn't it?

One final word: one might worry about the potential leak of OurContext objects in the _objects dictionary. What if an OurContext instance is not properly removed from the call context? Well... it should be properly removed. Contrary to HttpContext, which is destroyed at the end of the request, anything you leave in the call context will keep flowing around in a rather ugly way. You probably want to implement a mechanism (such as using blocks) to ensure that whatever goes into call context, eventually goes out.

Let's say this is left as an exercise for the reader, as this post is becoming quite long.


  1. and also this, Stephen Cleary again, and this, this and this... 

There used to be Disqus-powered comments here. They got very little engagement, and I am not a big fan of Disqus. So, comments are gone. If you want to discuss this article, your best bet is to ping me on Mastodon.