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:

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
}
}