I’ve significantly altered the approach presented below in the following post.  However, the post below may still be helpful for further reference.

So I’ve been a user of NHibernate for a while on my main project at work.  I got a change to refactor some services related to the application, and decided to use LINQ to SQL as the ORM, mainly so I could do a little more LINQ and check out the features of LINQ to SQL in a little more detail.  One issue that seems to keep coming up among users is the lifetime of a DataContext.  This is much like a session in NHibernate, in which case I usually use the Session-Per-Request approach in which the session lifetime is controlled in an http module.  In this case however, I actually have a service which gets kicked off via a timer.  This is kind of my catch-all utility service for this application, and a lot of different little tasks are scheduled and kicked off at regular intervals by a central scheduler.  The tasks all follow the command pattern, so they each expose an Execute() method and a factory provides the correct ICommand to execute depending on parameters passed to the main program logic.  All of the commands are pretty short lived, so I decided to take a similar approach: Create a context per command execution.  In my case, I’m just dealing with one database, so this also means that transactions across multiple repositories used by a command should be easy to manage because all database interactions will occur on the same connection.  This doesn’t mean the the context will be created at the beginning of the command.  It will still be instantiated in a lazy fashion, it will just be available for the duration of the command unless it is specifically released.  In addition, a coworker needed similar functionality, but specified that they would like to specify a connection string rather than rely on the connection string in the config file.  In addition, he suggested that I introduce generics in some way to allow the creation of the correct context in reusable code.  I decided to call this class the DataContextProvider.  The idea is that all of my repositories will accept an IDataContextProvider to allow access to the correct context.

Registering a Data Context

The first concept I wanted was some way to register a data context.  I decided to create some static Register Data Context methods.  I suppose these don’t need to be static and I could have done fluent interface, but I don’t anticipate the registering of a bunch of contexts.  The code sample is demonstrated below:

   1: private static readonly object syncLock = new object();
   2: private static readonly IDictionary<string, DataContextInfo> contextRegistry = new Dictionary<string, DataContextInfo>();
   3:  
   4: /// <summary>
   5: /// Registers the data context.
   6: /// </summary>
   7: /// <typeparam name="T">Type of the datacontext to register.</typeparam>
   8: /// <param name="contextKey">The context key to uniquely identify this context.</param>
   9: /// <param name="connectionString">The connection string to use with the context.</param>
  10: public static void RegisterDataContext<T>(string contextKey, string connectionString) where T : DataContext, new()
  11: {
  12:     lock (syncLock)
  13:     {
  14:         contextRegistry[GetContextInfoKey<T>(contextKey)] = new DataContextInfo
  15:                                                                 {
  16:                                                                     ConnectionString = connectionString,
  17:                                                                     ContextKey = contextKey,
  18:                                                                     Type = typeof (T)
  19:                                                                 };
  20:     }
  21: }
  22:  
  23: /// <summary>
  24: /// Registers the data context.
  25: /// </summary>
  26: /// <typeparam name="T">Type of the datacontext to register.</typeparam>
  27: /// <param name="contextKey">The context key to uniquely identify this context.</param>
  28: public static void RegisterDataContext<T>(string contextKey) where T : DataContext, new()
  29: {
  30:     RegisterDataContext<T>(contextKey, null);
  31: }
  32:  
  33: /// <summary>
  34: /// Registers the data context.
  35: /// </summary>
  36: /// <typeparam name="T">Type of the datacontext to register.</typeparam>
  37: public static void RegisterDataContext<T>() where T : DataContext, new()
  38: {
  39:     RegisterDataContext<T>(null, null);
  40: }
  41:  
  42: /// <summary>
  43: /// Gets the context info key based on the contextKey and the DataContext type.
  44: /// Used when registering and accessing a Data Context.
  45: /// </summary>
  46: /// <typeparam name="T">DataContext type.</typeparam>
  47: /// <param name="contextKey">The context key.</param>
  48: /// <returns>Key to be used when retrieving the data context.</returns>
  49: protected static string GetContextInfoKey<T>(string contextKey)
  50: {
  51:     return typeof(T).Name + (contextKey ?? "");
  52: }
  53:  
  54:  
  55: /// <summary>
  56: /// Holds information on the data context
  57: /// </summary>
  58: private class DataContextInfo
  59: {
  60:     public string ContextKey { get; set; }
  61:     public string ConnectionString { get; set; }
  62:     public Type Type { get; set; }
  63: }

The RegisterDataContext method has a few overloads which all feed into the main version.  This takes a contextKey (optional) and a connectionString (optional).  The contextKey is only needed if there will be multiple versions of the same context registered.  The connectionString is necessary if the user wishes to use a specific connectionString, rather than the string provided in the config file.  All this method does is register the information regarding the data context.  It stores it by key composed of the type name and the contextKey into a static dictionary so that it may be accessed throughout the lifetime of the application.  The application would register the data context whenever it was convenient; typically on the startup of the application.  The lock statement is pretty basic, but again, I don’t expect a lot of contention registering contexts.

Retrieving a Data Context

When a data context is actually required, the GetDataContext methods are called.  There are two overloads available:  One which takes the type and another which accepts the type and contextKey. 

   1: /// <summary>
   2: /// Gets the data context.
   3: /// </summary>
   4: /// <typeparam name="T">Type of the data context to retrieve.</typeparam>
   5: /// <returns>The data context.</returns>
   6: public T GetDataContext<T>() where T : DataContext, new()
   7: {
   8:     return GetDataContext<T>(null);
   9: }
  10:  
  11: /// <summary>
  12: /// Gets the data context.
  13: /// </summary>
  14: /// <typeparam name="T">Type of the data context to retrieve.</typeparam>
  15: /// <param name="contextKey">The context key to uniquely identify the context.</param>
  16: /// <returns>The data context.</returns>
  17: public T GetDataContext<T>(string contextKey) where T : DataContext, new()
  18: {
  19:     var contextInfoKey = GetContextInfoKey<T>(contextKey);
  20:  
  21:     var dataContext = RetrieveDataContextFromCache<T>(contextInfoKey) ??
  22:                       CreateAndCacheDataContext<T>(contextInfoKey);
  23:  
  24:     return dataContext;
  25: }
  26:  
  27:  
  28: /// <summary>
  29: /// Application should access the datacontextcache in this manner, don't use the private variable
  30: /// which is for local optimization only.
  31: /// </summary>
  32: protected Dictionary<string, DataContext> DataContextCache
  33: {
  34:     get
  35:     {
  36:         if (dataContextCache == null)
  37:         {
  38:             if (IsWebApplication)
  39:             {
  40:                 dataContextCache = IsWebApplication
  41:                                    ? (Dictionary<string, DataContext>)HttpContext.Current.Items[dataContextCacheKey]
  42:                                    : (Dictionary<string, DataContext>)CallContext.GetData(dataContextCacheKey);
  43:             }
  44:             if (dataContextCache == null)
  45:             {
  46:                 dataContextCache = new Dictionary<string, DataContext>();
  47:                 if (IsWebApplication)
  48:                     HttpContext.Current.Items[dataContextCacheKey] = dataContextCache;
  49:                 else
  50:                     CallContext.SetData(dataContextCacheKey, dataContextCache);
  51:             }
  52:  
  53:         }
  54:         return dataContextCache;
  55:     }
  56: }
  57:  
  58:  
  59: /// <summary>
  60: /// Retrieves the data context from cache.
  61: /// </summary>
  62: /// <typeparam name="T">Type of the data context.</typeparam>
  63: /// <param name="contextInfoKey">The context info key.</param>
  64: /// <returns>Data context if already cached.</returns>
  65: protected T RetrieveDataContextFromCache<T>(string contextInfoKey)
  66: {
  67:     object contextObject = DataContextCache[contextInfoKey];
  68:  
  69:     if (contextObject != null)
  70:     {
  71:         return (T)contextObject;
  72:     }
  73:     return default(T);
  74: }
  75:  
  76: /// <summary>
  77: /// Creates and caches the data context.
  78: /// </summary>
  79: /// <typeparam name="T">Type of the data context.</typeparam>
  80: /// <param name="contextInfoKey">The context info key.</param>
  81: /// <returns>A new DataContext of the type requested.</returns>
  82: protected T CreateAndCacheDataContext<T>(string contextInfoKey) where T : DataContext
  83: {
  84:     var contextInfo = contextRegistry[contextInfoKey];
  85:  
  86:     if(contextInfo == null)
  87:         throw new ArgumentException("DataContext was not registered with the application.");
  88:  
  89:     if(contextInfo.Type != typeof(T))
  90:         throw new InvalidOperationException("Context is registered, but it's type does not match the type requested.");
  91:  
  92:     var dataContext = string.IsNullOrEmpty(contextInfo.ConnectionString)
  93:         ? (T)Activator.CreateInstance(typeof(T))
  94:         : (T)Activator.CreateInstance(typeof(T), new object[] { contextInfo.ConnectionString });
  95:  
  96:     DataContextCache[contextInfoKey] = dataContext;
  97:  
  98:     return dataContext;
  99: }
 100:  
 101:  
 102: private static bool IsWebApplication
 103: {
 104:     get { return HttpContext.Current != null; }
 105: }
 106:  
 107: /// <summary>
 108: /// Gets the context info key based on the contextKey and the DataContext type.
 109: /// Used when registering and accessing a Data Context.
 110: /// </summary>
 111: /// <typeparam name="T">DataContext type.</typeparam>
 112: /// <param name="contextKey">The context key.</param>
 113: /// <returns>Key to be used when retrieving the data context.</returns>
 114: protected static string GetContextInfoKey<T>(string contextKey)
 115: {
 116:     return typeof(T).Name + (contextKey ?? "");
 117: }

This code essentially looks up the context info from the registered entries previously entered.  If it finds the context entry and then attempts to access the data context. The data context is accessed using RetrieveDataContextFromCache().  If the data context can’t be found in the local cache, a new data context is created and put in the local cache using CreateAndCacheDataContext().  If it’s a web application, it uses the HttpContext.Current to cache the data context, otherwise it caches it in the thread’s CallContext.  This approach basically limits the scope of this context to the current request for web applications or the current thread for non-web apps.

Repository Example

The repository then just accepts an IDataContextProvider (only exposes the GetDataContext methods) in the constructor.  This provides the context to the repository for query operations.  Could use an IOC container to facilitate this.

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4:  
   5: namespace Test.Repositories
   6: {
   7:     public class TestRepository
   8:     {
   9:         private readonly AppDataContext dataContext;
  10:  
  11:         public ChargeImportRepository(IDataContextProvider dataContextProvider)
  12:         {
  13:             dataContext = dataContextProvider.GetDataContext<AppDataContext>();
  14:         }
  15:  
  16:  
  17:         public List<Thang> GetImportNotificationCards()
  18:         {
  19:             return (from thang in dataContext.Thangs
  20:                     where thang.Active && thang.User.Active
  21:                     select thang).Distinct().ToList();
  22:         }
  23:  
  24:  
  25:     }
  26: }

That’s it in a nutshell.  If anybody sees major issue with this, please comment and let me know what you think.

 

Technorati Tags: ,,,