Part 2 – The DataContext
To get us started on the right foot, let’s take a quick look at what is needed to write a custom DataContext class. What’s really remarkable about the sample code below is that it is complete. Simply deriving from the System.Data.Linq.DataContext base class and providing a connection string gives us all of the functionality we need to work effectively with the data entities and the data source. Obviously, the connection string should be abstracted into a configuration file and read in. The code below was written this way to illustrate how easy it is to create a custom context for your project. In our case, we’ll use the IDE and SQLMetal to generate our DataContext class which adds properties to expose each table dragged onto the designer surface. The important thing to remember is that the GetTable<TEntity>() method is exposed as a public member which I will use in my samples to illustrate its IQueryable<T> properties.
public partial class NorthwindDataContext: System.Data.Linq.DataContext
{
public MyDataContext()
: base(“Data Source=.;Initial Catalog=Northwind;Integrated Security=True”)
{
}
}
LINQ to SQL’s DataContext is both a blessing and a curse all at once. I say this because it gives us a lot of out-of-the-box functionality with object state tracking and identity mapping, but it creates a problem when we want to separate the data layer from the business logic layer. Imagine a typical business case where the properties of a business object are updated and then persisted to the data store. Using an Active Record implementation, the instance of the object is created with a static factory method. After modifying the object properties, a call to the Save() method is all that is needed to persist the state to the data store.
Customer cust = Customer.GetByCustomerId(“ALFKI”);
cust.City = “Frankfurt”;
cust.Save();
Using LINQ to SQL’s DataContext, the pattern changes quite a bit. Instead of a call to the static factory method on the class, as with the previous example, the entry point to retrieve an object instance changes to the DataContext. Since there is no Save() method defined for the class, a call to SubmitChanges() on the DataContext is needed to invoke persistence logic.
using (NorthwindDataContext ctx = new NorthwindDataContext())
{
Customer cust = ctx.GetTable<Customer>().Single(c => c.CustomerID == “ALFKI”);
cust.City = “Frankfurt”;
ctx.SubmitChanges();
}
Obviously, the Active Record implementation is concise and far easier to read when compared with the DataContext sample. But that’s barely scratching the surface of potential issues. Most troubling to me is that the DataContext object is now referenced in the business layer creating a strong coupling of the data and business layers. How do we elegantly manage the DataContext without making it a first-class citizen in the business layer? There are many different ways to approach this problem. One way would be to wrap the context in a context manager utility class that exposes a method to load a customer entity by key. Let’s call this class the NorthwindBusinessContext. With the NorthwindBusinessContext class we could manage the lifecycle of a given context by implementing the IDisposable interface. We can also ensure the integrity of context references to table entities that the DataContext exposes as shown with the GetCustomerById() method. Lastly, the PersistState() method exposes the persistence method of the DataContext object which is, of course, necessary to invoke data store persistence logic.
public partial class NorthwindBusinessContext : IDisposable
{
NorthwindDataContext DataContext = null;
public NorthwindBusinessContext()
{
DataContext = new NorthwindDataContext();
}
public Customer GetCustomerById(string customerId)
{
return DataContext.GetTable<Customer>().Single(c => c.CustomerID == customerId);
}
public void PersistState()
{
DataContext.SubmitChanges();
}
#region IDisposable Members
public void Dispose()
{
DataContext.Dispose();
}
#endregion
}
There has been some talk about how to scope the DataContext to a specific thread/web request/execution context to help ensure the integrity of the given DataContext. I think there is a lot of merit in researching how to scope a given DataContext, however, I will not be exploring those efforts in this post. We’ll leave that topic for another post, perhaps in the near future.
So why go to all this trouble of writing a BusinessContext? Why not just use the DataContext outright and skip this step? The answer: decoupling. By encapsulating the DataContext in a single class, we can shield ourselves from any changes that are made to the DataContext implementation. No one can argue that a single point of failure is a lot easier to refactor than a pervasive failure. Things are looking good so far. We have a BusinessContext class that composes the DataContext class and encapsulates DataContext specific implementation by implementing business specific methods for loading and persisting data entities. Luckily, all this code is not to bad to write and can easily be incorporated to a custom code generator allowing us to write only the customizations needed by the domain layer.
Now all we have left is to deal with those data entities. We want to deal with business entities, not data entities. How do we best create a business entity that best allows for reuse of the data entities? In my next post we’ll explore options on how to tackle this task in a fun an arguably elegant way.