Eric's Blog

Day to day experience in .NET
Welcome to Blogs @ IRM Sign in | Join | Help
 Search

Disclaimer

The content of this site is my own personal opinion and does not in any way represent my employer, it's subsideries or affiliates. These postings are provided "AS IS" with no warranties, and confer no rights.

This Blog

Go Distributed With LINQ to SQL

The scenario is a classic tiered solution with for example a client making a server-side call by using WCF. So waht's the problem with this common scenario if you choose to use LINQ to SQL? The problem is that the change tracking infrastructure reside on the server and therefor will not detect any changes made to the objects on the client side. This is not a problem unique for LINQ to SQL. LINQ to SQL has an Attach (and AttachAll) method to support the distributed/disconnected scenario. The Attach method exists in three overloaded versions:

  • Attach(TEntity) that attaches the original object.
  • Attach(TEntity, bool) which can be used to attach an object and by sending it true as a second parameter will be seen as modified when calling DataContext.SubmitChanges(). This requires the object to use a version member or have no concurrency checking at all.
  • Attach(TEntity, TEntity) where you are supposed to have the modified object as first parameter and the original object as second parameter. This method will handle the object correctly, and by that I mean that it will have Modified state if new and original object differs, and otherwise it will be in unmodified state.

From this three only the last one is really useful in my opinion. I don't like the second one because I don't want unmodified objects to be updated in the database for many reasons (it's a waste of resources, all change logging of any kind will not work correctly and son on). In most demonstration a "simple" object like Customer is used with no related objects and Attach(customer, true) is most common. In the real world I seldom have a single object that I want to attach, even more seldom in distributed scenarios. So for the rest of this post let's handle a scenario with an Order and a collection of OrderRow objects. It is important to notice that all overloaded versions of Attach only attaches the object sent in the parameter and no related objects. This means if you attach the Order object, none of the OrderRow objects will be attached automatically, but must also be handled manually.

Now when we all have agreed the only the third overload of Attach is useful, how can we get both a new and an original object? We basically have two choices; get both object from the client or just get the modified object from the client and have the original on the server. Getting both from client means sedning twice as much data over the wire and handling it all on the server requires ou to have either a good cache strategy or making an extra call to the database (and using a version member for concurrency). I will not choose which strategy to use. There are more than one examples/blogs where some kind of ClientDataContext is created to get change tracking on the client for example:

I belive that a much easier way to have the original left on the client is to serialize the objects just after getting them using XmlSerialiser or DataContractSerializer. Then before calling the server again deserialize the objects and send them as original values together with the modified objects. In my opinion this is a lot easier than using a client-side DataContext, requireing you to attach objects on the client side to and so on.

If you choose to go for the other option and keep the original as an internal implementation for the server logic there are one important thing to have in mind. An entity can only be associated with one DataContext. This means that if you choose to get the "original" object from the database again (by using LINQ to SQL) you will have to clone that object graph (I choose to do it with DataContractSerializer), and the same is true if you used some kind of caching strategy.

So now we have managed to get in a state where we have the original object graph and the modified object graph. When I write modified object graph I actually mean that we could have a completly new order (and order rows), a modified order with modified, added and deleted orderrows. Our job now is to detect for each and every object if it is new, modified or deleted and handle that correctly. I have choosen to solve this by creating an extension method called Marge, that extends the DataContext. Lets start out with an example of the usage:

if (originalOrder == null)
{
    IOrderRepository repository = new OrderRepository(unitOfWork);
    repository.AddOrder(order);
}
else
{
    originalOrder = (Order)originalOrder.Clone();
    dc.Orders.Attach(order, originalOrder);

    dc.Merge(order.OrderRows, originalOrder.OrderRows);
}

unitOfWork.Complete();

 

The unitOfWork variable only wrapps the DataContext and potentially also the transaction object. This means that the call to Complete will call both SubmitChanges and Complete (if a transaction is active). Starting from the top again the first case handles the completly new order and ends up calling InsertAllOnSubmit. The interesing part is in the else. First I clone the original object (since it is associated with a DataContext) and this method could be an extension method or imnplemented in a base class. Then there is a standard call to Attach with the potentially modified order and the original order. Last, but not least, the call to Merge (the extension) method that will walk through the collection detecting both modified, added and deleted rows. Lets dig into the code.

 

public void Merge<TEntity>(EntitySet<TEntity> current, EntitySet<TEntity> original)
    where TEntity : Domain.BusinessEntity
{
    Merge<TEntity>(GetTable<TEntity>(), current, original);
}

public void Merge<TEntity>(ITable table, EntitySet<TEntity> current, EntitySet<TEntity> original)
    where TEntity : Domain.BusinessEntity
{
    foreach (TEntity entity in current)
    {
        var originalEntity = (from t in original
                              where t.Id == entity.Id
                              select t).FirstOrDefault();

        if (originalEntity == null)
            table.InsertOnSubmit(entity);
        else
            table.Attach(entity, originalEntity);
    }

    foreach (TEntity entity in original)
    {
        var currentEntity = (from t in current
                             where t.Id == entity.Id
                             select t).FirstOrDefault();

        if (currentEntity == null)
        {
            table.Attach(entity);
            table.DeleteOnSubmit(entity);
        }
    }
}

 

What this code does is that it first traverses the current collection and tries to find (by using LINQ :) the corrsponding object in the original collection. If this succeeds it does and Attach(current, orginal); otherwise it calls InsertOnSubmit instead. When this is done it traverses the original collection and sees if it can find any objects in that collection which is not not present in the current collection. In that case it will attach the original entity and then call DeleteOnSubmit. As you can see this code requires all objects to inherit from a base class that has the Id property (or you could solve it with interfaces). This is not POCO, but I belive that it is ok since, at least for me, it is very uncommon that I don't use the same kind of identifier for all objects.

Hope this code helps :)

Published den 20 augusti 2008 18:29 by ericqu
Filed under: ,

Comments

 

Recent Faves Tagged With "poco" : MyNetFaves said:

oktober 7, 2008 14:53
 

Maxi said:

I didn't get your point. I detach and cache my object from the data context. modify it and try to attach it back to another data context. no matter with or without the original entity. It would work if I turn off update checking on all columns but PK. And I am not sure if it works without detaching the object from data context. Does it?
november 3, 2009 10:07
Anonymous comments are disabled
Powered by Community Server, by Telligent Systems