Ambient Context

These days, I'm becoming increasingly enamored with the idea of implementing cross-cutting concerns using Thread Local Storage (TLS) or the current call context. For the most typical aspects of software, such as security and logging, the .NET framework already takes this approach:

  • Security can be handled by reading or writing to Thread.CurrentPrincipal
  • Logging can be handled by using the methods of the Trace class. You configure the Trace class by adding or removing TraceListerners.

In both cases, you set up the context at the application's entry point (either imperatively or, in the case of logging, in the application's configuration), which makes it implicitly available for all other code in the call stack. Even if you are writing code in the data access layer (which is usually quite deep in the call stack), you can still access and use this ambient context. The nice thing about this approach is that it's just there, accessible by a static property or method on a well-defined class (i.e. Thread or Trace). This means that even though you might need to be able to address your cross-cutting concern in any layer in your application, you don't need to pass it around as a parameter value in every single method you implement.

Even though it's not passed to you explicitly as a parameter, you are allowed to assume that it's there; it's the responsibility of the hosting application to set up the ambient context in the correct way.

Since both IPrincipal and TraceListener are abstractions (one is an interface and the other an abstract class), these implementations offer full substitutability, so you can easily create test doubles of them when unit testing.

Obviously, you can just use Thread.CurrentPrincipal and the Trace class for security and logging, but if you have your own aspect to implement, you can use a similar approach. As always, I prefer to illustrate how to do this with an example. In order to avoid the usual suspects of security and logging, consider the following scenario:

We are building an application where you need to be able to control which features are available to the user at any given time. This is not a question of role-based security, but rather a decision which may be made based on licensing etc. One scenario may be a trial edition with certain features disabled (but where you can upgrade the installation to a full version without reinstalling). A more advanced scenario may be a SmartClient application (perhaps in a SaaS scenario) where the user can pay to upgrade the application's feature level for a limited time.

To keep things simple, we'll assume that we only need three levels of feature access:

 public enum FeatureLevel
 {
     Basic = 0,
     Silver,
     Gold
 }

Since we must allow developers to create an implementation where a level of access is only available for a limited time, the API should allow a great deal of flexibility. With the pseudo-requirements set forth so far, our goal could be to enable our fellow developers to write code akin to this:

 FeatureContext.Current.Demand(FeatureLevel.Silver);

The Demand method simply asserts that the requested feature level is currently enabled, and otherwise throws an exception. The FeatureContext class in itself is an abstract class:

 [Serializable]
 public abstract class FeatureContext
 {
     private const string contextSlotName_ = "FeatureContext";
  
     protected FeatureContext()
     {
     }
  
     public static FeatureContext Current
     {
         get
         {
             FeatureContext ctx = CallContext.LogicalGetData(
                 FeatureContext.contextSlotName_) as FeatureContext;
             if (ctx == null)
             {
                 ctx = new BasicContext();
                 CallContext.LogicalSetData(
                     FeatureContext.contextSlotName_, ctx);
             }
             return ctx;
         }
         set
         {
             CallContext.LogicalSetData(
                 FeatureContext.contextSlotName_, value);
         }
     }
  
     public abstract void Demand(FeatureLevel requestedLevel);
 }

There are two noteworthy details in this code: The first is the abstract Demand method that needs to be implemented by a derived class (I'll get back to that).

The second is the static Current property. Since it allows any client to read and write an instance of the abstract FeatureContext, substitutability is ensured. It also ensures that you can implement the Demand method in as simple or complex fashion as you need, and put this implementation on the ambient context. To keep things simple, I've not implemented the property in a thread-safe manner, which (obviously) you should do in a real production implementation.

In this implementation, I store the the FeatureContext in the CallContext; I could also have used TLS by using Thread.SetData, but there's a difference: Using TLS, the context only exists on the current thread; if you spawn a new thread, you must manually copy the context to the new thread if you want it to be available there as well. This happens automatically when you use the CallContext, but to store and retrieve an object via the CallContext, the class must be serializable, which is why FeatureContext is decorated with the Serializable attribute. Any derived class must also be serializable.

If no current context is not already set, it defaults to BasicContext:

 [Serializable]
 public class BasicContext : FeatureContext
 {
     public BasicContext()
         : base()
     {
     }
  
     public override void Demand(FeatureLevel requestedLevel)
     {
         if (requestedLevel > FeatureLevel.Basic)
         {
             throw new FeatureUseDeniedException();
         }
     }
 }

This implementation only allows basic functionality. If the requested feature level is higher than Basic, a FeatureUseDeniedException is thrown. Notice the Serializable attribute, which allows an instance of the class to be stored in the CallContext.

You can implement SilverContext and GoldContext classes in similar ways, so when you want to enable Gold functionality indefinitely, you'd do something like this:

 FeatureContext.Current = new GoldContext();

This will enable Gold funtionality until you explicitly change the FeatureContext to something else. If you want to enable a scenario where there's a time limit, you could create an expiring implementation of FeatureContext:

 [Serializable]
 public class ExpiringGoldContext : FeatureContext
 {
     private DateTime timeLimit_;
  
     public ExpiringGoldContext(TimeSpan allottedTime)
         : base()
     {
         this.timeLimit_ = DateTime.Now + allottedTime;
     }
  
     public override void Demand(FeatureLevel requestedLevel)
     {
         if (DateTime.Now > this.timeLimit_)
         {
             new BasicContext().Demand(requestedLevel);
             return;
         }
         new GoldContext().Demand(requestedLevel);
     }
 }

When you set the current FeatureContext to ExpiringGoldContext, it will allow Gold features to be used until the time limit expires. Even when the time limit expires, the current instance remains, but its behavior changes to only allow Basic functionality. Obviously, you could optimize the implementation of ExpiringGoldContext by defining static readonly instances of BasicContext and GoldContext, but to keep things simple for the example, I just create new instances of these classes when I want to delegate implementation to them.

Whenever your need to ensure that a member allows execution only if a certain functionality level is granted, you can demand the level as a guard clause in your code:

 public void DoSilverStuff()
 {
     FeatureContext.Current.Demand(FeatureLevel.Silver);
  
     // Implementation here
 }

To come full circle to aspect-oriented development, such a use of ambient context is an obvious candidate for the Enterprise Library Policy Injection Application Block, which would then allow declarative use of the ambient context by decorating your members with attributes (similar to PrincipalPermissionAttribute), but I'll leave that as an exercise for the interested reader (or, perhaps, a later post).