A complete sample code project for this article is available here.

In a previous post, I had proposed an idea on how to control the lifetime of the DataContext in LINQ to SQL (L2S).  I basically created a provider class that would detect the presence of an HttpContext.  If the context was present, it would use the HttpContext and store the DataContext on a per-request scope.  Otherwise, it would store the DataContext in the CallContext, basically creating a per-thread scope for the DataContext. 

I actually felt a little dirty after having done this, but for some reason the idea of injecting the provider eluded me until I read Steve Sanderson’s blog entry on this.  Duh!  Basically: dependency Injection of the caching strategy instead of trying to detect the HttpContext and figure it out for the user.  Of course!  So I refactored in this way.  Then I came across a different issue.  A coworker of mine had a project with multiple clients that used the same L2S model.  They had both a web and a windows app riding on the same service layer. 

The problem was that he wanted to be able to flush the DataContext in his windows app so that he could ensure the identity map of L2S wasn’t causing the build up of stale data on the client.  However, he still wanted the lifetime control so that he could use ADO.Net transactions without escalating to DTC.  If you weren’t aware of this, every time the DataContext is newed up, it’s pulling a new connection from the pool.  With our datacontext-instance-per-repository approach, transactions across repositories were resulting in the transaction being elevated to Distributed Transaction Coordinator (DTC)….definitely something you want to avoid if possible.  So the thread-based lifetime needed a way to allow flushing of cached contexts.  The problem I had with constructor injecting the caching strategy into my context provider was that I didn’t have a clean way to register the caching provider so that I could flush it later.

To solve this issue, I stuck with a similar theme as I had previously.  A user registers their DataContext using static method of the DataContextProvider, but at the same time they also register a caching strategy.  This will typically be dependent on the type of application.  This now allows tracking of the caching provider and the ability to flush the cached DataContexts.  Now my windows app can perform a transaction across a few repositories and then get a fresh DataContext.  Here’s the approach:

DataContextProvider Class

This is the class that does all of the heavy lifting.  To start with, it has some static methods for registering the DataContext and caching strategies.  It has a few overloads, but they siphon into the following static method:

   1: /// <summary>
   2:  /// Registers the data context.
   3:  /// </summary>
   4:  /// <typeparam name="T">Type of the datacontext to register.</typeparam>
   5:  /// <typeparam name="CacheProviderType">The type of the cache provider.</typeparam>
   6:  /// <param name="contextKey">The context key to uniquely identify this context.</param>
   7:  /// <param name="connectionString">The connection string to use with the context.</param>
   8:  public static void RegisterDataContext<T, CacheProviderType>(string contextKey, string connectionString) 
   9:      where T : DataContext, new()
  10:      where CacheProviderType : IDataContextCache 
  11:  {
  12:      lock (syncLock)
  13:      {
  14:          var contextInfoKey = GetContextInfoKey<T>(contextKey);
  15:          contextRegistry[contextInfoKey] = new DataContextInfo
  16:            {
  17:                ConnectionString = connectionString,
  18:                ContextInfoKey = contextInfoKey,
  19:                Type = typeof (T),
  20:                CacheType = typeof (CacheProviderType)
  21:            };
  22:      }
  23:  }

This method registers a DataContext and it’s caching strategy (with optional connection string and key) to a static method for retrieval when the DataContext is needed.  It doesn’t store the DataContext class itself, but instead stores information about the DataContext using a DataContextInfo class.  The DataContext is registered using it’s type as the key.  If a contextKey is passed in, it’s added to the end of the key.  It’s really just there to differentiate if a user has multiples of the same DataContext registered.  

When a context is needed, it is requested using the GetDataContext method.  This is an instance method.  I’ll get to how it’s used in a second, but first an explanation of how this works. 

   1: /// <summary>
   2: /// Gets the data context.
   3: /// </summary>
   4: /// <typeparam name="T">Type of the data context to retrieve.</typeparam>
   5: /// <param name="contextKey">The context key to uniquely identify the context.</param>
   6: /// <returns>The data context.</returns>
   7: public T GetDataContext<T>(string contextKey) where T : DataContext, new()
   8: {
   9:     var contextInfoKey = GetContextInfoKey<T>(contextKey);
  10:     var contextInfo = GetRegisteredDataContextInfo(contextInfoKey);
  11:  
  12:     var dataContext = RetrieveDataContextFromCache<T>(contextInfo) ??
  13:                       CreateAndCacheDataContext<T>(contextInfo);
  14:  
  15:     return dataContext;
  16: }

The context info key is reconstructed and the DataContextInfo is pulled from the DataContext registry.  Using this key, the DataContextProvider will first attempt to retrieve the DataContext from cache.  If this is null, it will new up the DataContext and cache it for later use. 

   1: /// <summary>
   2: /// Retrieves the data context from cache.
   3: /// </summary>
   4: /// <typeparam name="T">Type of the data context.</typeparam>
   5: /// <param name="contextInfo">The context info for the requested context.</param>
   6: /// <returns>Data context if already cached.</returns>
   7: protected T RetrieveDataContextFromCache<T>(DataContextInfo contextInfo) where T : DataContext
   8: {
   9:     var contextCache = GetDataContextCache(contextInfo);
  10:     if (contextCache.Items.ContainsKey(contextInfo.ContextInfoKey))
  11:     {
  12:         return (T)contextCache.Items[contextInfo.ContextInfoKey];
  13:     }
  14:     return default(T);
  15: }
  16:  
  17: /// <summary>
  18: /// Creates and caches the data context.
  19: /// </summary>
  20: /// <typeparam name="T">Type of the data context.</typeparam>
  21: /// <param name="contextInfo">The context info for the requested context.</param>
  22: /// <returns>A new DataContext of the type requested.</returns>
  23: protected T CreateAndCacheDataContext<T>(DataContextInfo contextInfo) where T : DataContext
  24: {
  25:     if(contextInfo == null)
  26:         throw new ArgumentException("DataContext was not registered with the application.");
  27:  
  28:     if(contextInfo.Type != typeof(T))
  29:         throw new InvalidOperationException("Context is registered, but it's type does not match the type requested.");
  30:  
  31:     var dataContext = string.IsNullOrEmpty(contextInfo.ConnectionString)
  32:         ? (T)Activator.CreateInstance(typeof(T))
  33:         : (T)Activator.CreateInstance(typeof(T), new object[] { contextInfo.ConnectionString });
  34:  
  35:     var contextCache = GetDataContextCache(contextInfo);
  36:     if (contextCache != null)
  37:     {
  38:         contextCache.Items[contextInfo.ContextInfoKey] = dataContext;
  39:     }
  40:  
  41:     return dataContext;
  42: }
  43:  
  44: protected IDataContextCache GetDataContextCache(DataContextInfo contextInfo)
  45: {
  46:     if (dataContextCache == null)
  47:     {
  48:         dataContextCache = CreateDataContextCache(contextInfo);
  49:     }
  50:     return dataContextCache;
  51: }
  52:  
  53: private static IDataContextCache CreateDataContextCache(DataContextInfo contextInfo)
  54: {
  55:     return (IDataContextCache)Activator.CreateInstance(contextInfo.CacheType);
  56: }

So how does this work?  The GetDataContextCache method is called first.  If the cache has already been instantiated, this will be present in a local variable, otherwise CreateDataContextCache retrieves the caching strategy type from the DataContextInfo and instantiates it.  The caching strategy isn’t the cache itself, it just provides a standard interface for us to access whichever cache we are using to store the DataContext.  I’ll get to the implementation of this strategy in a little bit. 

Once the cache strategy is obtained, the ContextInfoKey is used to retrieve the DataContext from the cache.  If the DataContext doesn’t exist, it’s instantiated, optionally using the connection string provided when the DataContext was registered.  Now we’ve got our cached DataContext!  

Caching Strategies

So all the caching strategy has to do is implement the IDataContextCache interface which exposes an Items property getter and a Clear() function.  I put together three basic strategies out of the box:  HttpRequest lifetime, Thread lifetime, and “No Cache”.  The interface and cache provider code is shown below:

   1: public interface IDataContextCache
   2: {
   3:     /// <summary>
   4:     /// Gets the data context cache.
   5:     /// </summary>
   6:     IDictionary<string, DataContext> Items { get; }
   7:  
   8:     /// <summary>
   9:     /// Clears items (DataContexts) in the cache and disposes resources.
  10:     /// </summary>
  11:     void Clear();
  12: }
   1: /// <summary>
   2: /// Caches the context in http context storage.
   3: /// </summary>
   4: public class DataContextWebCache : DataContextCacheBase
   5: {
   6:  
   7:     protected override IDictionary<string, DataContext> GetDataContextCache()
   8:     {
   9:         var dataContextCache = (Dictionary<string, DataContext>)HttpContext.Current.Items[DataContextCacheKey];
  10:  
  11:         if (dataContextCache == null)
  12:         {
  13:             dataContextCache = new Dictionary<string, DataContext>();
  14:             HttpContext.Current.Items[DataContextCacheKey] = dataContextCache;
  15:         }
  16:         return dataContextCache;
  17:     }
  18: }
  19:  
  20: /// <summary>
  21: /// Caches the context in thread local storage.
  22: /// </summary>
  23: public class DataContextThreadCache : DataContextCacheBase
  24: {
  25:  
  26:     protected override IDictionary<string, DataContext> GetDataContextCache()
  27:     {
  28:         var dataContextCache = (Dictionary<string, DataContext>)CallContext.GetData(DataContextCacheKey);
  29:  
  30:         if (dataContextCache == null)
  31:         {
  32:             dataContextCache = new Dictionary<string, DataContext>();
  33:             CallContext.SetData(DataContextCacheKey, dataContextCache);
  34:         }
  35:         return dataContextCache;
  36:     }
  37:  
  38: }
  39:  
  40: /// <summary>
  41: /// Just return an empty dictionary because we are not supporting caching in this provider.
  42: /// </summary>
  43: public class DataContextNoCache : DataContextCacheBase
  44: {
  45:     /// <summary>
  46:     /// Creates the data context cache.
  47:     /// </summary>
  48:     protected override IDictionary<string, DataContext> GetDataContextCache()
  49:     {
  50:         return new Dictionary<string, DataContext>();
  51:     }
  52:  
  53:     /// <summary>
  54:     /// Gets the data context cache.
  55:     /// </summary>
  56:     public override IDictionary<string, DataContext> Items
  57:     {
  58:         get { return GetDataContextCache(); }
  59:     }
  60: }
  61:  
  62: public abstract class DataContextCacheBase : IDataContextCache
  63: {
  64:     protected const string DataContextCacheKey = "ApplicationDataContext";
  65:  
  66:     /// <summary>
  67:     /// Gets or sets the data context cache.
  68:     /// </summary>
  69:     /// <value>The data context cache.</value>
  70:     private IDictionary<string, DataContext> dataContextCache;
  71:  
  72:     /// <summary>
  73:     /// Creates the data context cache.
  74:     /// </summary>
  75:     protected abstract IDictionary<string, DataContext> GetDataContextCache();
  76:  
  77:     /// <summary>
  78:     /// Gets the data context cache.
  79:     /// </summary>
  80:     public virtual IDictionary<string, DataContext> Items
  81:     {
  82:         get
  83:         {
  84:             if (dataContextCache == null)
  85:             {
  86:                 dataContextCache = GetDataContextCache();
  87:             }
  88:             return dataContextCache;
  89:         }
  90:     }
  91:  
  92:     /// <summary>
  93:     /// Clears items (DataContexts) in the cache and disposes resources.
  94:     /// </summary>
  95:     public void Clear()
  96:     {
  97:         // Dispose managed resources.
  98:         if (dataContextCache != null)
  99:         {
 100:             foreach (var item in dataContextCache)
 101:             {
 102:                 if (item.Value != null)
 103:                 {
 104:                     item.Value.Dispose();
 105:                 }
 106:             }
 107:             dataContextCache.Clear();
 108:         }
 109:     }
 110:  
 111: }

Refreshing

You may remember that towards the beginning, I mentioned the need to clear the cached contexts to prevent data staleness.  I also mentioned previously that each cache strategy exposes a Clear method.  To clear all currently cached DataContexts, the DataContextProvider exposes a Refresh method.  This method simply iterates all the registered DataContexts and gets the associated caching strategy using the same CreateDataContextCache method that the GetDataContext method used.  Once the caching strategy is obtained, it’s Clear method is called to clear out all existing DataContexts.

   1: /// <summary>
   2: /// Refreshes the data contexts in the current application scope.
   3: /// </summary>
   4: public static void RefreshDataContexts()
   5: {
   6:     foreach (var item in contextRegistry.Values)
   7:     {
   8:         var dataContextCache = CreateDataContextCache(item);
   9:         dataContextCache.Clear();
  10:     }
  11: }

Using the DataContextProvider

To use the provider, I would set up my repositories to accept an IDataContextProvider using constructor injection.  The repository can then request the DataContext.

   1: public class MemberRepository : IMemberRepository
   2: {
   3:     private readonly AppDataContext dataContext;
   4:  
   5:     public MemberRepository(IDataContextProvider dataContextProvider)
   6:     {
   7:         dataContext = dataContextProvider.GetDataContext<AppDataContext>();
   8:     }
   9:  
  10:     protected AppDataContext DataContext
  11:     {
  12:         get { return dataContext; }
  13:     }
  14:  
  15:     public IEnumerable<Member> GetMembersByCountry(string country)
  16:     {
  17:         return dataContext.Members.Where(m => m.Country == country).ToList();
  18:     }
  19: }

I now set up my DataContextProvider, basically at the same level that I’d set up my IOC container for the given project.  Notice that when I register the DataContext, I also register the caching strategy.

   1: public class ObjectRegistrar
   2: {
   3:     private static readonly UnityContainer container = new UnityContainer();
   4:  
   5:     public void RegisterDataContext()
   6:     {
   7:         DataContextProvider.RegisterDataContext<AppDataContext, DataContextThreadCache>();
   8:     }
   9:  
  10:     public void RegisterTypes()
  11:     {
  12:         container
  13:             .RegisterType<IDataContextProvider, DataContextProvider>()
  14:             .RegisterType<IMemberRepository, MemberRepository>();
  15:     }
  16: }

That’s it!  Any comments are appreciated.

kick it on DotNetKicks.com

Technorati Tags: ,,,