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):
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:
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.