Blog of Keith Craig, containing my thoughts, comments and questions. RSS Feed


LINQ to SQL and the "Request-scoped DataContext" Pattern--generalized to support both WinForms and ASP.NET

A few weeks ago I posted about an architectural pattern for using LINQ to SQL. In the meantime, partially based on some feedback, I've been thinking about how best to use this pattern in both a web app and a Windows Forms app. (Or, for that matter, anything that isn't an ASP.NET app--a WPF or a console app, for example.)

I also have a personal interest in this: I'm working on a project where a custom ASP.NET app shares a data layer and business layer are with a WinForm utility. The same business and data access logic is run from either ASP.NET or a WinForm. Since the original version I posted relied on HttpContext.Current.Items, it would only work in an ASP.NET app, and would fail when used in a WinForm or other non-web app.

Before I discuss the changes, I want to talk a bit more about the pattern and related architecture from last time.

 

Background

The idea for my last post was borrowed from the book Hibernate in Action. It increases the scope of the DataContext in LINQ to SQL to that of an ASP.NET request, by using a LinqUtil class that automatically manages the DataContext. This simplifies much of the handling of the DataContext, and seemed to be the smoothest way to integrate LINQ to SQL into a layered architecture. Here's a picture of how I think of the layer interactions in this model:

Relationship between the layers

As described in the previous post, the DataContext is implicitly created at the first request, and persists until the response is generated.

Since I wrote this, I've talked with some people about it, and some interesting points came up.

Some people were concerned that the presentation layer communicated directly with the data access layer. The main concern here was that if the DAL changed, then you'd need to modify both the presentation layer and the BLL. This is true, but I don't see this as a serious problem--if the DAL changes significantly, then it's going to be a lot of work to find and fix all the issues, and it's likely not that much more work to have the changes spread across two layers. And this type of change is relatively rare.

The main strength I see of this architecture is that it allows a single representation of the objects for all layers. The business objects are stateful and have associated logic, but the DAL can return and save them in that format.

A common architecture in .NET is to have the BLL reference the DAL, in which case the DAL cannot reference the BLL (it would be a circular reference). This means that either:

  • You need to have all communication with the DAL go through primitive variables rather than objects, because the DAL doesn't know about the objects. (So, for example, instead of passing the DAL a "person" object to save, you need to pass the first name, last name, telephone number, and any other "person" fields as separate strings or other primitive types.) Needless to say, this is a bit painful, and it becomes even more difficult and unpleasant with LINQ to SQL, as you may end up losing all the information about object graphs and collection properties and such.
  • You need to create another layer (sometimes called a "model" layer) that spans the whole application. These objects have no logic, but simply act as data transfer objects from the DAL to the BLL and to the presentation layer. This is okay, but it means more classes, layers, and complexity. In addition, it means that you cannot use many of the common object-oriented techniques and patterns, as these are not "objects" in the classical sense--they have data but no logic.

These seem to me to be more painful issues than having the presentation reference both the BLL and the DAL. Again, it's a relatively unlikely event to completely replace the DAL.

In addition, the question came up of merging the DAL and BLL for simplicity. If you did this, you would likely have a set of static "finder" methods that would be used to find data in the database and return it, alongside instance methods to do business logic on the objects. Both of these would reside in partial classes that are compiled together with the LINQ designer-generated partial classes. I think this is possible, but I'm not certain I like the idea of merging the layers like this for the following reasons:

  • It makes it more difficult to draw a clear separation between business logic and data access logic when both types of methods are mixed together in the same file.
  • It makes it necessary to expose the BiddingDataContext property of the LinqUtil class to the business logic (because it's in the same class as the data access logic), which makes the separation of these even more difficult to maintain. On the other hand, if they are in two different assemblies, the LinqUtil can be placed in the DAL assembly, and the BiddingDataContext can be given internal scope, meaning that only the DAL assembly can access it.
  • It would likely make substituting the DAL difficult for unit testing, because the methods are static and are mixed in with the business logic. (Otherwise, you could implement an interface for the DAL, and then swap the database DAL or the testing DAL using configuration.)

To me, the separation of data access logic and business logic is important because they are separate concerns--thinking of both at the same time tends to make it harder to understand the code. However, I'm not convinced on this point, and it might work out fine to merge the two.

 

Changes to LinqUtil class to support non-ASP.NET technologies

The main change I made to the LinqUtil class to support WinForms and other non-ASP.NET technologies was to add switching code to the InternalDataContext property:

/// <summary>
/// Private property to store the DataContext in the HttpContext.Current.Items or thread local storage
/// </summary>
private static BiddingDbDataContext InternalDataContext
{
    get
    {
        if (HttpContext.Current == null)
            return _context;
        else
            return (BiddingDbDataContext)HttpContext.Current.Items[DATACONTEXT_ITEMS_KEY];
    }
    set
    {
      if (HttpContext.Current == null)
            _context = value;
        else
            HttpContext.Current.Items[DATACONTEXT_ITEMS_KEY] = value;
    }
}

 

This code checks if the HttpContext.Current is null--if it is, then it's certainly not running in ASP.NET, and it uses a newly added ThreadStatic field _context to store the DataContext on the thread. Otherwise, it is running in ASP.NET, and it uses the HttpContext.Current.Items collection.

Here's the definition of the _context field:

[ThreadStatic]
private static BiddingDbDataContext _context;

 

The [ThreadStatic] attribute makes the field local to a thread, exactly what is desired in a WinForm or other non-ASP.NET application. However, because ASP.NET can switch threads during heavy utilization in a single web request, this isn't completely safe to use in ASP.NET, whereas the HttpContext.Current.Items is. (For more on the risks, see Scott Hanselman's post on the subject.)

Here's the complete LinqUtil class for reference:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
 
namespace DAL
{
    /// <summary>
    /// Simple class to implement request-scoped DataContext pattern for LINQ to SQL.
    /// 
    /// Note: users must change "BiddingDbDataContext" to their own strongly-typed DataContext.
    /// </summary>
    public static class LinqUtil
    {
        #region Privates
 
        /// <summary>
        /// Dictionary key for the DataContext in HttpContext.Current.Items
        /// </summary>
        private const string DATACONTEXT_ITEMS_KEY = "LinqUtilDataContextKey";
 
        [ThreadStatic]
        private static BiddingDbDataContext _context;
 
        /// <summary>
        /// Private property to store the DataContext in the HttpContext.Current.Items or thread local storage
        /// </summary>
        private static BiddingDbDataContext InternalDataContext
        {
            get
            {
                if (HttpContext.Current == null)
                    return _context;
                else
                    return (BiddingDbDataContext)HttpContext.Current.Items[DATACONTEXT_ITEMS_KEY];
            }
            set
            {
                if (HttpContext.Current == null)
                    _context = value;
                else
                    HttpContext.Current.Items[DATACONTEXT_ITEMS_KEY] = value;
            }
        }
 
        #endregion
 
        #region Public and Protected Properties and Methods
 
        /// <summary>
        /// Returns the current DataContext. If none configured yet, then creates a new one and returns it. Internal access
        /// so that only the DAL layer can access.
        /// </summary>
        /// <returns>A reference to a DataContext</returns>
        internal static BiddingDbDataContext Db
        {
            get
            {
                // If the context is missing, create a new one
                if (InternalDataContext == null)
                {
                    // Note: in a real app, this should get the connection string from secure storage and pass it to the context constructor.
                    // However, it doesn't work because you cannot pass in parameters to new(). It's messy, but you can use reflection to solve this.
                    string connectionString = @"Data Source=(local)\sqlexpress;Initial Catalog=BiddingDb;Integrated Security=True;Pooling=False";
 
                    InternalDataContext = new BiddingDbDataContext(connectionString);
                }
 
                return InternalDataContext;
            }
        }
 
        /// <summary>
        /// Saves all changes on the current DataContext. Public scope to allow calling from upper tiers of application.
        /// </summary>
        public static void SubmitChanges()
        {
            Db.SubmitChanges();
        }
 
        /// <summary>
        /// Cleanup the context (dispose the context and set it to null). Public scope allows calling from upper tiers.
        /// </summary>
        public static void CleanUp()
        {
            if (InternalDataContext != null)
            {
                InternalDataContext.Dispose();
                InternalDataContext = null;
            }
        }
 
        #endregion
    }
}
 
 
Posted by Keith Craig | 5 Comments | Trackback Url | Bookmark with:        
Tags:

Links to this Post

Comments

Tuesday, 7 Aug 2007 07:03 by decouple: custom DALWrapper class in PL would address evolution
Anyone who is concerned about decoupling PL (Presentation Layer) from DAL could do so within this pattern, simply by having a wrapper class in PL that is the only PL class allowed to reference DAL. The simplest design of such a wrapper is a proxy; that is, each property or method needed in any DAL class has a corresponding property or method in the wrapper class. The proxy thus documents which DAL features are accessed; and can also be instrumented during debugging, profiling. And in the future, any DAL changes can be addressed in the wrapper class. An alternate approach is to have the wrapper do more work: design the "ideal DAL interface" for your specific (application or subsystem -- here the PL)'s needs, have the wrapper implement that design, and call the actual DAL to do the work. The idea here is that any general-purpose interface (here, the DAL) is an over-generalized compromise [so to speak] between the needs of many different clients; so do the modeling work to identify exactly what you need, and centralize that work within a single class of your subsystem. -- ToolmakerSteve

Monday, 24 Sep 2007 05:14 by HttpContext Error
Hi, I am getting 'The name 'HttpContext' does not exist in the current context' error. LinqUtil class should be in DAL or BLL ? Thanks Veera

Monday, 24 Sep 2007 02:01 by RE: HttpContext Error
Hi Veera, Yes--thanks for letting me know! I was playing around with merging the BLL and DAL layers, and somehow the merged code leaked into the blog post for the utility class. I've updated the post above with the proper namespace. Sorry for the confusion! Cheers, Keith

Wednesday, 26 Sep 2007 04:55 by Very nice
Hi there, Stumbled in your page whilst trying to find an NTier App example using Linq. Is it possible to download your solution or have it emailed thanks a lot gb@devnet247.com

Wednesday, 26 Sep 2007 11:12 by RE: Very nice
Sorry, unfortunately I don't happen to have the code anymore--it was on a VPC, and I ended up refactoring it into something I didn't like as much. My earlier blog post (http://my/personal/keithc/Blog/Lists/Posts/Post.aspx?ID=11) has the important code--hopefully it should be fairly easy to fill in the other tidbits. Hope this helps! --Keith

Name:
URL:
Email:
Comments:

CAPTCHA Image Validation