Skip Ribbon Commands
Skip to main content
Navigate Up
Sign In

Blog

:

Quick Launch

Blog > Posts > LINQ to SQL and the "Request-scoped DataContext" Pattern
June 28
LINQ to SQL and the "Request-scoped DataContext" Pattern

In this post, I describe a LINQ to SQL architectural pattern for ASP.NET I call “request-scoped DataContext”. It’s based on the Hibernate pattern “thread-local session”.

LINQ to SQL is looking promising, and has the possibility to really increase productivity for enterprise application development. Most enterprise development is done using a layered architecture, usually with the following three layers:

· Presentation—displays data and manages user interaction

· Business logic—implements logic of the application

· Data access—encapsulates communication with the database to load and save data

These layers allow developers to think mainly about one set of concerns at a time, to centralize similar types of code, and to increase unit testability. Essentially, it’s easier to understand and maintain a large application written this way. However, most of the blog posts and examples I have seen about LINQ to SQL (I’ll just call it “LINQ” from here on) involve these three layers smashed together into a single code-behind file for a web page. It’s certainly easier to write, read, and understand a simple example when architected this way, but it leaves open the question of how best to use LINQ in an N-tier ASP.NET application.

When developing with LINQ, management of the DataContext is crucial. The DataContext needs to be created before objects are modified, and it needs to be around to call SubmitChanges() on after they have been modified. As it is a stateful object, you need to call SubmitChanges() on the same instance of the DataContext as you got the objects from (or attached them to).

Given this, I wrote a simple static utility class to assist in management of the DataContext. This example and pattern is based on some of the ideas in the excellent book Hibernate in Action by Christian Bauer and Gavin King. I also borrowed their example of a simple online auction site, as it’s a simple yet understandable example. Here’s the code for my LINQ utility class:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
using BLL;
 
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";
 
 
        /// <summary>
        /// Private property to store the DataContext in the HttpContext.Current.Items
        /// </summary>
        private static BiddingDbDataContext InternalDataContext
        {
            get
            {
                return (BiddingDbDataContext)HttpContext.Current.Items[DATACONTEXT_ITEMS_KEY];
            }
            set
            {
                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.
                    string connectionString = @"Data Source=(local)\sqlexpress;Initial Catalog=CaveatEmptor;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
    }
}

Note that “BiddingDbDataContext” needs to be replaced with your actual strongly-typed DataContext type.

Basically, this class automates the handling of the DataContext, by storing a single DataContext in the System.Web.HttpContext.Current.Items collection for use by the request (hence the name “request-scoped DataContext”). For more background on why I'm using the HttpContext.Current.Items collection and why my earlier plan of using a static variable marked as [ThreadStatic] isn’t completely safe, see this excellent post by Scott Hanselman. One slightly weird aspect of this is that the DAL assembly needs to reference System.Web. (Note that for a WinForm or WPF application, you could use a static variable tagged with [ThreadStatic] to store the DataContext. In this case, you would need to be sure to do all your work with a given DataContext on the same thread.)

The LinqUtil class offers three non-private static properties and methods:

· Db – returns a reference to the strongly-typed DataContext for the application. If there isn’t one yet, it creates a new one

· SubmitChanges() – saves all tracked object changes

· CleanUp() – disposes the DataContext and sets it to null on the thread

Note that the Db property is scoped as internal—this class is assumed to be part of the data access layer assembly, and it’s cleaner if the presentation and business layers cannot get direct access to the DataContext. The other two methods are public, and are meant to be used from the presentation layer.

Here’s a high-level diagram of the sequence of events when a web request comes in using this pattern (time flows from left to right here):

clip_image002

First the presentation layer calls the DAL to load the relevant objects. It then calls methods on the loaded objects to do logic (perhaps multiple times), and finally saves the objects to the database using the data layer. The nice thing about using the utility class is that everything in this request will be done using the same DataContext, without having to explicitly pass it around. Note that the presentation layer references both the DAL and the BLL, the DAL references the BLL, and the BLL references no other layers. (The fact that the BLL doesn’t reference any other layers makes it much easier to unit test it!) Here’s a screenshot of what the example solution looks like:

clip_image004

The presentation layer would do something like the following example code (the itemId and userId are hardcoded for this example; the code here is modeled after the example code for the thread-local session Hibernate pattern from Hibernate in Action):

        protected void btnBid_Click(object sender, EventArgs e)
        {
            decimal bidAmount = decimal.Parse(txtBidAmount.Text);
            int itemId = 1;
            int userId = 1;
 
            ItemDAO itemDAO = new ItemDAO();
            UserDAO userDAO = new UserDAO();
 
            decimal maxBidAmount = itemDAO.GetMaxBidAmount(itemId);
            Item item = itemDAO.GetItemById(itemId);
            Bid newBid = item.PlaceBid(userDAO.GetUserById(userId), bidAmount, maxBidAmount);
 
            LinqUtil.SubmitChanges();
        }
 

Note that the first call into the DAL (GetMaxBidAmount) implicitly creates a strongly-typed DataContext for this request and stores it in the HttpContext.Current.Items collection. Then all subsequent calls to the DAL reuse that same DataContext, which is then also used in the final SubmitChanges() call.

ItemDAO is a simple DAL class for loading Item-related data:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Data.Linq;
using System.Text;
using BLL;
 
namespace DAL
{
    public class ItemDAO
    {
        public Item GetItemById(int itemId)
        {
            return LinqUtil.Db.Items.Single(i => i.ItemId == itemId);
        }
 
        public decimal GetMaxBidAmount(int itemId)
        {
            decimal? maxBidAmount = LinqUtil.Db.Bids.Select(b => b.Amount).Max();
 
            return maxBidAmount.GetValueOrDefault(0M);
        }
 
    }
}

The GetItemById() ends up being a one-liner, since the LinqUtil class automatically manages the DataContext.

Item is a multi-part class in the BLL which was partially generated by the LINQ design surface; the other part contains custom business logic code:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
 
namespace BLL
{
    public partial class Item
    {
        public Bid PlaceBid(User bidder, decimal bidAmount, decimal maxBidAmount)
        {
            if (this.EndDate < DateTime.Today)
                throw new ApplicationException("Auction already ended.");
 
            if (bidAmount <= maxBidAmount)
                throw new ApplicationException("Bid not high enough");
 
            Bid newBid = new Bid();
            newBid.Amount = bidAmount;
            newBid.Item = this;
            newBid.DatePlaced = DateTime.Today;
            newBid.User = bidder;
 
            this.Bids.Add(newBid);
            return newBid;
        }
    }
}

One thing I really like about this pattern is that the BLL methods have no explicit database calls or references to the DataContext—they may lazy-load something if it’s not already loaded, but other than that, it’s free of database logic, and therefore much easier to unit test.

Finally, since it implements IDisposable, it’s probably a good idea to clean up the DataContext at the end of the request. I did this in the Application_EndRequest event handler in the global.asax.cs:

        protected void Application_EndRequest(object sender, EventArgs e)
        {
            LinqUtil.CleanUp();
        }

 

I hope the idea at the core of this pattern is useful; I wanted to share it because it seemed more elegant than the example LINQ to SQL code I’ve seen so far.

PS: I wrote this using Beta 1 of Orcas. The code details may change as Orcas evolves, but the idea should remain relatively unchanged.

Comments

Re: LINQ to SQL and the "Request-scoped DataContext" Pattern

Keith,

Thanks for this; I've been trying to find an example of n-tier LINQ for a while now.

It's reassuring to see you have come to the same conclusion about putting the DC in in the HTTPContext as I have.

One minor thing I would chnage is to make the DAO methods static so that you do not have to instantiate the DAO objects first (or have I missed something?).

I am also very tempted to put the DAO methods into the BLL class as well. I know this breaks your principle of keeping data access out of the BLL so that it is testible - but I can't help thinking it would make it a lot neater and you would still be able to test the actual logic methods without data access.

Have you made any improvements or even chnaged your mind about any of this since you wrote this?

Many thanks for sharing,
JasonBSteele

 on 7/2/2007 1:05 AM

Reply to Re: LINQ to SQL...

Jason,

Thanks for the feedback!

I don't see any issues with making the DAO methods static--I think it's a fine idea. Another option would be to put all the DAL methods into a single static class rather than separate classes for each BLL object. As long as you have a good naming scheme, this might be easier. (It's likely best for smaller projects.)

In terms of putting the data access methods into the BLL, the only issue that I can think of is that I made the Db property that returns the DataContext internal, so that only the DAL assembly could access it. Merging the BLL and DAL would mean that both the "BLL-like" and "DAL-like" methods could access it. Likely this would just mean that developers would need to be a bit more careful to make sure they don't access the property from the wrong method--I don't think this would be too problematic.

I agree that mixing the BLL and DAL methods in a single class doesn't affect testability as long as the BLL-like methods don't call into the database.

Other than the thoughts above inspired by your comments, I haven't thought of any other improvements. I'll write more if I do.
Keith CraigNo presence information on 7/2/2007 9:41 AM

ExecutionContext

Did you considered using ExecutionContext instead of HttpContext? This way the same utility class can be used in other scenarios, like WinForm.
 on 7/14/2007 1:12 AM

Re: ExecutionContext

Until you posted your reply, I had not known about the ExecutionContext! Thanks!

Yes, I think this is a good idea--the only concern I have is that I'm not sure if the ExecutionContext survives between web requests. If so, I think this could be a bit of a risk, as intentionally unsaved changes from a previous request could be unintentionally saved by a subsequent request, creating a very hard to reproduce bug.

If the ExecutionContext is persisted between web requests, then it is a simple modification to build a check into the LinqUtil class to see if you are running in ASP.NET or something else. If HttpContext.Current is null, it is not ASP.NET, and you should use the ExecutionContext; otherwise, use the HttpContext.Current.Items collection.
Keith CraigNo presence information on 7/17/2007 10:06 AM

distributed architecture

Would this would work in a distributed application (ie seperate servers for web server and application server where only the app server has access to the database)? If you wanted to modify a database row then save the changes would you be able to make the modifications in the presentation layer then call submit changes, or would you call a specific method in the bll with the parameters to change and it would do the work?
 on 7/18/2007 7:38 AM

Re: distributed architecture

Good question!

I myself usually try to keep the web and business logic running on the same server (it is simpler and performs better), but sometimes you must separate them for other reasons.

My understanding is that the DataContext is not serializable (see http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=482413&SiteID=1, for example). Given this, I think that you would need to do something different in a distributed application. I haven't thought much about that type of architecture, but specific methods in the BLL to change values is one option.

--Keith
Keith CraigNo presence information on 7/23/2007 12:57 PM

ObjectTrackingEnabled Option

Might be a good idea to also include an optional value to turn off ObjectTrackingEnabled to speed things up where you do not need to modify data like getting a list of products for display on a shopping cart.

Maybe something like this would work:
        Friend Shared ReadOnly Property Db(Optional ByVal TrackObjects As Boolean = True) As UltraWellnessDataClassesDataContext
            Get
                ' If the context is missing, create a new one
                If InternalDataContext Is Nothing Then
                    ' Note: in a real app, this should get the connection string from secure storage and pass it to the context constructor.
                    ' Dim connectionString As String = ConfigurationManager.ConnectionStrings("ultrawellnesscenterConnectionString").ConnectionString

                    InternalDataContext = New UltraWellnessDataClassesDataContext
                    InternalDataContext.ObjectTrackingEnabled = TrackObjects
                End If

                Return InternalDataContext
            End Get
        End Property

Then use this instead:

  Public Function GetCustomer(ByVal CustomerID As Guid) As Customer
            Return (From c In LinqUtil.Db(False).Customers Where c.CustomerID = CustomerID).Single
        End Function
 on 8/27/2007 6:47 AM

Connection caching etc?

Can someone make me feel a lot better here...? Is the DC as good at managing its connection pooling etc as for instance, MTS and COM+ were? When you destroy the DC at the end of the request cycle, will its connection be completely destroyed and then re-opened again on the next request (if necessary)?

I'm somewhat concerned about this, as a constantly opening and closing connection, per user, is not a good thing. But I do suppose we could just write our own caching mechanism and make sure that the DC uses that cached connection whenever it needs to do anything; what that means though is that I am assuming that if I assign the DC a shared connection that its not going to kill it when I recycle the DC at the end of the request.

Any feedback here would be great, thanks in advance. BTW Keith, FANTASTIC post m8, thanks for sharing.
 on 9/4/2007 1:17 AM

Connection caching etc?

Can someone make me feel a lot better here...? Is the DC as good at managing its connection pooling etc as for instance, MTS and COM+ were? When you destroy the DC at the end of the request cycle, will its connection be completely destroyed and then re-opened again on the next request (if necessary)?

I'm somewhat concerned about this, as a constantly opening and closing connection, per user, is not a good thing. But I do suppose we could just write our own caching mechanism and make sure that the DC uses that cached connection whenever it needs to do anything; what that means though is that I am assuming that if I assign the DC a shared connection that its not going to kill it when I recycle the DC at the end of the request.

Any feedback here would be great, thanks in advance. BTW Keith, FANTASTIC post m8, thanks for sharing.
 on 9/4/2007 1:18 AM

RE: Connection caching etc?

The .NET managed data access classes generally implement connection pooling themselves, unless you turn it off. For example, see http://msdn2.microsoft.com/en-us/library/8xx3tyca(vs.71).aspx, or http://www.ondotnet.com/pub/a/dotnet/2004/02/09/connpool.html.

I've mainly used SQL Server, but connection pooling has worked fine in .NET without using COM+ or MTS. I believe that COM+ and MTS connection pooling was only important in the days when ADO (not ADO.NET, but plain vanilla ADO) didn't support pooling.

Given this, I don't know of any issues related to pooling when using the DataContext.

Hope this helps--and thanks for the positive feedback!
Keith CraigNo presence information on 9/4/2007 9:08 AM
1 - 10Next

Add Comment

Items on this list require content approval. Your submission will not appear in public views until approved by someone with proper rights. More information on content approval.

Title


Body *


CommentUrl


Attachments