One of the most typical examples of a context object is a logger. There are two very different approaches that are available in .NET; System.Diagnostics.Trace class and the Page.Trace object (System.Web.TraceContext class). The Trace class provides static methods that you can use anywhere in your code. The TraceContext on the other hand, is only available in an ASP.NET application. But TraceContext ensures all logged messages are kept without being effected from other logging operations that are simultaneously done in other threads. Unfortunately, Trace class doesn’t care about the current threads context. It just logs whatever is passed to it, and whenever it’s passed to it. So you usually see your messages in an interwoven pattern in the log file rather than a contiguous one. In other words, the TraceContext knows about the current page context and behaves in such a way to support page related logging, while the Trace class knows only about the global app context. So how do we get TraceContext behavior like ASP.NET without having to use a global Trace object? Unfortunately, there’s no such thing in .NET.
Good news is there’s such a thing in SOCF. You just create a logging context as follows, and all the code that is called within the enclosing using() block is able to use this context. And this is done without effecting or being effected by other threads. More importantly, this is not just available in ASP.NET page, it is available wherever you need it.
using (var log = new LoggingContext()) { try { MyMethod(); } finally { log.Dump(); } }
When you want to log something to the current logging context specific to the current thread, you just use it as if it is a static class:
LoggingContext.Add("Processing order");
Or just access it as if it’s a singleton:
LoggingContext.Current;
This method just writes a message to the enclosing LoggingContext object. Then you can write it to Trace, or Debug output.
Now let’s look into what’s actually going on behind the scenes.
When an instance of LoggingContext is created it puts a reference of itself into the HttpContext or CallContext. These two context objects are available in .NET class library and provide a shared storage attached to a thread. HttpContext actually uses CallContext behind the scenes, but also makes sure that the context is migrated when the ASP.NET switches thread that is running the current request. In other words, CallContext is thread specific, whereas HttpContext is request specific.
CallContext.SetData(key, data);
A string key specific to the collaboration context is generated and the collaboration context object is set to the CallContext.
The .NET call context makes sure that the context is available to all code in the execution code path. The CallContext is used as if it’s a singleton, but it’s really like a virtual singleton that knows about your current thread and won’t cause concurrency issues in a multithreaded environment. When you access the logging context for adding a message, it’ll first check if there’s a context available, and if so it’ll get it and add the message.
public static void Add(string message) { if (Current != null) Current.AddMessage(message); }
The Current property eventually uses the previously used key to get the previously cached collaboration context object from CallContext and uses it:
CallContext.GetData(key);
The collaboration context objects all derive from a base NamedCollaborationContext.
It is possible to nest such collaboration context objects like the LoggingContext. The nesting provides three important features: Overriding, limited scope and lifetime, reuse of parent(s).
Overriding is the simplest. When a new using() block is encountered while executing, the new object will just put itself to the current context just like the previous one, and become the new current context. So all code within the new using() block will see the new context object.
Also, the new context object will keep a reference back to the parent (or super) context object so it can refer back to it when it needs data or functionality. Here’s a pseudo code that shows how it does this:
this.SuperConext = CallContext.GetData(key); CallContext.SetData(key, this);
The collaboration context object also raises some events when such context switches occur so you can do more stuff if needed.
Finally, when the using() block is exited, it’ll automatically call Dispose() method of the collaboration context object. And Dispose() method just makes sure that the super context is activated back again. Here’s a pseudo code that shows this:
this.SuperConext = CallContext.GetData(key); CallContext.SetData(key, this.SuperConext);
Because of the construction and disposal behaviors, the collaboration context objects should be treated specially. Preferably they should be used only in a using() block as shown. But there’s nothing to prevent them from being just created directly. If you have to do that, you must also make sure the object is disposed of at a proper point of time when they aren’t needed.
If you don’t use using blocks, you may end up calling dispose methods in the wrong order. In that case, you’ll get an exception.
You can nest such context blocks inside the same method, or even in different methods. There’s no difference because it relies on the CallContext which is always available for the current thread. Here’s an exaggerated example for demonstration purposes:
using (new LoggingContext()) { LoggingContext.Add("Message 1"); using (new LoggingContext()) { LoggingContext.Add("Message 2"); using (new LoggingContext()) { LoggingContext.Add("Message 3"); MyMethod(); LoggingContext.Add("Message 4"); } LoggingContext.Add("Message 5"); } LoggingContext.Add("Message 6"); } private void MyMethod() { using (new LoggingContext()) { LoggingContext.Add("Message 8"); } }
It turns out, using those context objects, we can do pretty interesting things. For instance, we can merge the current context data or behavior with the super context or contexts. Multiple nested blocks of IdentityMap contexts can provide data from the local context, or super contexts if allowed. Or a notification context can notify only local context handlers, or super context or contexts. We can use it to provide an object creation context to do inversion of control.
In the next post, I’ll explain other types of collaboration objects that I've implemented so far.