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).
Comments
Anonymous
July 23, 2007
Hello Mark, Great post. I agree entirely. I have the hardest time convincing people that Dependency Injection comes in more than one implementation form. To my question, though. In a web context, is it safe to use the CallContext in this way. I use Spring.NET a lot and notice that they always prefer the HttpContext instead of TLS due to asynchronicity problems related to the way that ASP.NET uses the ThreadPool for some processing. When a thread is picked from the pool, the CallContext is cleared for security reasons. What would you recommend when using ASP.NET? Thanks, W. Kevin HazzardAnonymous
July 23, 2007
I'm not sure I understand... why not simply use a static member variable (rather than CallContext.LogicalSetData)?Anonymous
July 23, 2007
Yay for global variables... (or not?)Anonymous
July 28, 2007
In my former post on Ambient Contexts , I described how you can use CallContext or Thread Local StorageAnonymous
July 28, 2007
Hi W. Kevin Hazzard Thank you for your question and my apologies for the delay in answering: My experience with ASP.NET is a bit fragmented, and it's been a while (years, as a matter of fact) since I last wrote any serious ASP.NET code. To make matters worse, your question isn't entirely simple to answer, so I ended up writing an entirely new blog post, just for you ;) Take a look at http://blogs.msdn.com/ploeh/archive/2007/07/28/CallContextsVsAspNet.aspx and see if that answers your question.Anonymous
July 28, 2007
Hi Paul Thank you for your question. If you come from a SmartClient/rich client background (which I don't know if you do), I can understand why you would want to use a simple static member. In that case, there's typically only a single user and a single call context per AppDomain, so a static member will work fine. In a server process, however, there may be multiple requests served simultaneously by the same process, so you need some way to distinguish one from the other. Any static member will apply to all call contexts, so that doesn't capture the case where we want to deal with a context which is unique to each request. As you can read from my latest post, dealing with the call/request context is complex in some server scenarios (ASP.NET), but may be simpler in others (WCF services, for instance). I hope this answers your question.Anonymous
July 28, 2007
In my former post on Ambient Contexts , I described how you can use CallContext or Thread Local StorageAnonymous
July 29, 2007
That did answer my question, thanks!Anonymous
August 20, 2007
Besides logging, one of the most common types of ambient context is the user. Who is the user? Was theAnonymous
September 27, 2007
The comment has been removedAnonymous
September 27, 2007
Hi Jiho Han Thank you for asking. In this case, it doesn't make any difference. I just picked LogicalGetData and LogicalSetData because they most closely resemble how Thread.CurrentPrincipal works. This little forum discussion highlights the difference between LogicalGetData and GetData: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=747611&SiteID=1