次の方法で共有


Call Contexts vs. ASP.NET

In my former post on Ambient Contexts, I described how you can use CallContext or Thread Local Storage (TLS) to store a context that is specific to the current context of a call. As it turns out, ASP.NET uses a threading model that disables this approach, since you are not guaranteed that a single request will be entirely executed within a single thread. That's not particularly nice of the ASP.NET platform (to put it politely), but that's the hand we've been dealt. As described by Rockford Lhotka, your first approach should be to use HttpContext.Items when your code runs within a web application, and the CallContext or TLS when outside of ASP.NET.

However, even if you create a wrapper around your storage mechanism, you are still going to carry around a reference to System.Web, which may seem... weird... if you are writing a WPF application or Windows Service or whatever. Besides, this approach is not extensible, since you can't subsequently decide to use yet another context (perhaps you'd want to use WCF's OperationContext). If you've been working with software design for some years, it shouldn't be surprising that the answer is to add yet another level of indirection.

The basic idea is that instead of simply checking whether HttpContext.Current is null or not, you define an abstract context container that can store your custom context in whatever call context is available.

To illustrate this approach, I'll extend my former example to store the context in an abstract container defined by the FeatureContextContainer class:

 public abstract class FeatureContextContainer
 {
     protected FeatureContextContainer()
     {
     }
  
     public abstract FeatureContext Current { get; set; }
 }

Since I don't want to carry around any (potentially) uneccessary references to System.Web or other implementation-specific context implementations, I want to be able to define concrete FeatureContextContainers in my application configuration file (remember that you should also consider including an imperative configuration option). Such a configuration might look something like this:

 <configuration>
   <configSections>
     <section name="featureContext"
              type="Ploeh.Samples.FeatureControl.FeatureContextConfigurationSection, Ploeh.Samples.FeatureControl" />
   </configSections>
   <featureContext containerType="Ploeh.Samples.AspNetContext.HttpContextContainer, Ploeh.Samples.AspNetContext" />
 </configuration>

In this case, the HttpContextContainer class would implement FeatureContextContainer by reading and writing the context to and from HttpContext.Items. The nice thing about this approach is that while the AspNetContext assembly has a dependency on System.Web, the rest of your application (including FeatureContext itself) doesn't.

The FeatureContext class itself must be modified to use whatever context container is provided to it:

 [Serializable]
 public abstract class FeatureContext
 {
     private static FeatureContextContainer container_ =
         FeatureContextConfigurationSection.GetConfigurationSection().GetContextContainer();
  
     protected FeatureContext()
     {
     }
  
     public static FeatureContext Current
     {
         get { return FeatureContext.container_.Current; }
         set { FeatureContext.container_.Current = value; }
     }
  
     public abstract void Demand(FeatureLevel requestedLevel);
 }

Instead of directly reading and writing the context to and from the CallContext as before, FeatureContext now delegates these operations to the configured FeatureContextContainer.

While it may seem that this approach now requires an application configuration file, this is actually not the case, since I implement FeatureContextConfigurationSection.GetConfigurationSection in this way:

 public static FeatureContextConfigurationSection GetConfigurationSection()
 {
     FeatureContextConfigurationSection config = 
         ConfigurationManager.GetSection(
         FeatureContextConfigurationSection.ConfigurationName)
         as FeatureContextConfigurationSection;
     if (config != null)
     {
         return config;
     }
     return new DefaultFeatureContextConfigurationSection();
 }

Notice that if the configuration section isn't found in the application configuration file (or if no such file exists at all), I return a DefaultFeatureContextConfigurationSection instead. This internal class simply overrides the virtual GetContextContainer method:

 internal class DefaultFeatureContextConfigurationSection :
     FeatureContextConfigurationSection
 {
     internal DefaultFeatureContextConfigurationSection()
         : base()
     {
     }
  
     public override FeatureContextContainer GetContextContainer()
     {
         return new CallContextContainer();
     }
 }

Instead of using a configured type name to create an instance of that type, this implementation simply returns an instance of the CallContextContainer class:

 internal class CallContextContainer : FeatureContextContainer
 {
     private const string contextSlotName_ = "FeatureContext";
  
     internal CallContextContainer()
         : base()
     {
     }
  
     public override FeatureContext Current
     {
         get
         {
             FeatureContext ctx = CallContext.LogicalGetData(
                 CallContextContainer.contextSlotName_) as FeatureContext;
             if (ctx == null)
             {
                 ctx = new BasicContext();
                 CallContext.LogicalSetData(
                     CallContextContainer.contextSlotName_, ctx);
             }
             return ctx;
         }
         set
         {
             CallContext.LogicalSetData(
                 CallContextContainer.contextSlotName_, value);
         }
     }
 }

If you recall the code from my former post, this is simply the original implementation of FeatureContext moved to a FeatureContextContainer implementation. By this, we have now come full circle: The default behavior of FeatureContextContainer is the same as described in my former post, and I have introduced no breaking changes, but now the implementation is extensible, which allows you to reuse this ambient context in an ASP.NET application.

In a unit testing scenario, CallContext works just fine, so you don't need to replace the default implementation in that case. This means that even if you would want to use FeatureContext in an ASP.NET application, you'd still write all your unit tests against the default implementation, and then, when you configure the entire application, you set it up with an HttpContextContainer. The HttpContextContainer class, on the other hand, is one of those software infrastructure types that you can't unit test, so if you really want to test it, you will need an integration test. On the other hand, the implementation is so simple that this will probably not be necessary.

Comments

  • Anonymous
    August 20, 2007
    Besides logging, one of the most common types of ambient context is the user. Who is the user? Was the