Previously: Architecting LINQ To SQL part 8
LINQ To SQL with N-Tier: Why is there pain?
People tend to experience pain using LINQ in N-Tier scenarios because they are trying to pass entities between layers. You can find some examples of this complaint, not to pick on anyone but just to avoid repeating it, here and here.
First up is that you will have trouble serializing EntitySet and EntityRef. As you will recall from Part 5, we need to use these collection types to support lazy loading. The problem is that they will not serialize with the XmlSerializer or because they contain circular references. They will however serialize using WCF’s DataContractSerializer.
Second, remember that we said that you should always access entities within the scope of a DataContext in Part 7. The DataContext provides the unit of work and change tracking, so objects outside the scope of their context must be attached if we are to tell LINQ to SQL that they are persistent and not transient. You need to provide LINQ with information as to the original state of the persistent entity, so as to avoid spurious concurrency errors .When we access a lazy loaded collection the proxy for that collection uses the DataContext it was loaded with to retrieve the persistent child items. If the DataContext has been disposed you will get an exception.
DataContext itself is not serializable and contains non-remotable resources like a Db connection. This means you cannot gain change tracking and lazy loading by simply serializing your DataContext too. This tends to throw people who come from a DataSet background who have become used to serializing the context with the data. Indeed this ability to work in a disconnected fashion is trumpeted as a feature of the DataSet. As a result, this set of users tries to use LINQ to SQL in the same way and then becomes frustrated when they can’t.
It is important to recognize that ORMs represent a different architectural style to DataSets. Persistence Ignorance means that we want our domain model to be unaware of persistence related concerns, such as the relational model, Db connections, SQL statements, and object change tracking. As a result, these types of architecture don’t support this model of interaction. Those of us who believe in persistence ignorance think that this is a good thing, because clean separation of concerns makes our software more amenable to incremental re-architecture which keeps our software fit and healthy as it changes.
Prefer Messaging
So if LINQ to SQL follows this school of development what is the answer to working in a multi-tier environment? Simply it is to prefer messaging when communicating across tiers.
The usual alternatives to messaging are shared database integration and remote procedure calls.
The former is not usually raised as a mechanism within LINQ to SQL discussion but having been the recipient of legacy systems that use shared database integration I would advise strongly against it. Problems occur when multiple systems update one Db. To support multiple applications you need to express the Db schema in a generic way. Each application has to transform this schema when it loads its entities to match its own domain model. These model differences, systems tend to comprehend the entities in different ways, lead to subtle inconsistencies. Whenever you change how we interact with the Db for one application, we have to test all applications that share that Db, increasing the cost of making changes. It is not just schema changes that are at issue here, how we update the Db, the values we write, are just as important a concern. Just because both of my systems have a Customer does not mean that the semantics of dealing with a Customer are the same to both of them. For the order fulfilment and complaints departments a customer may be very different. Domain models are application specific and not shareable.
Remote procedure calls are problematic because they exchange type. Exchanging type locks us into a relationship between client and server that becomes burdensome because when we change the server we have to change all the clients, or if we need a change for one client, we have to change the server and all other clients. This increases the total cost of ownership of the solution.
When we talk about messaging as a solution there are a number of important things to remember:
We should share schema not type. We want to send message not transfer an instance of a type. The sender may compose the message from one of more types and the receiver may consume the message into a type. But it’s a message not an entity.
Do not reference your domain model across the tiers. If you find yourself creating references to an assembly from one application in another, in an effort to send and receive entities, you have lost your way.
Exchange state, not behaviour. What we care about is passing a message that has some state, not the behaviours associated with an entity.
Assume you do not own the ‘other’ tier. Remember that when we talked about distribution one reason was to expose our services to a range of clients. If we make assumptions about the client, we limit our consumers.
Servers decide what to do with a message, clients decide what to do with replies, if there are any. With no transfer of behaviour each side decides what to do with the message, if anything.
Remember how we said the first law of distribution was don’t? Well one thing to think about is that if you balk at the idea of messages because ‘in the real world we need to get this code done in a reasonable timescale’ then your issue might be that you should not be opting for a multi-tier scenario anyway, because if you need n-tiers then you tend to be able to pay the price of messaging.
Insulate the Domain
When you use messages you want to insulate your domain from the message that goes over the wire. This is because you want to be able to change your domain but continue to support existing clients who use the current message format. Remember that you may not own your clients, so you cannot assume that they will change just because you change. So you need to separate your domain from the message.
In most cases you will tend to replace your RPC need to serialize the entity with a Document Message. To populate the document message it is tempting to serialize your entities directly over the wire as the message body. Instead, populate your messages from your entity.
The following trivial service represents this pattern, showing how we use ‘replay’ to load the entity affected by the message, update it, and then submit the changes:
public class RegionService : IRegionService
{
public RegionDTO GetRegion(int regionId)
{
NorthwindDataContext session = new NorthwindDataContext();
var query =
from r in session.Regions
where r.RegionID == regionId
select new RegionMessage { Id = r.RegionID, Description = r.RegionDescription };
return query.Single();
}
public void SendRegion(RegionMessage regionMessage)
{
NorthwindDataContext session = new NorthwindDataContext();
Region region =
(from r in session.Regions
where r.RegionID == regionMessage.Id
select r).Single();
region.RegionDescription = regionMessage.Description;
session.SubmitChanges();
}
}