views:

783

answers:

2

I have implemented a DAL using Rob Conery's spin on the repository pattern (from the MVC Storefront project) where I map database objects to domain objects using Linq and use Linq to SQL to actually get the data.

This is all working wonderfully giving me the full control over the shape of my domain objects that I want, but I have hit a problem with concurrency that I thought I'd ask about here. I have concurrency working but the solution feels like it might be wrong (just one of those gitchy feelings).

The basic pattern is:

private MyDataContext _datacontext
private Table _tasks;

public Repository(MyDataContext datacontext)
{
    _dataContext = datacontext;
}

public void GetTasks()
{
    _tasks = from t in _dataContext.Tasks;

    return from t in _tasks
        select new Domain.Task
        {
            Name = t.Name,
            Id = t.TaskId,
            Description = t.Description                              
        };
}

public void SaveTask(Domain.Task task)
{
    Task dbTask = null;

    // Logic for new tasks omitted...

    dbTask = (from t in _tasks
        where t.TaskId == task.Id
        select t).SingleOrDefault();

    dbTask.Description = task.Description,
        dbTask.Name = task.Name,

    _dataContext.SubmitChanges();
}

So with that implementation I've lost concurrency tracking because of the mapping to the domain task. I get it back by storing the private Table which is my datacontext list of tasks at the time of getting the original task.

I then update the tasks from this stored Table and save what I've updated

This is working - I get change conflict exceptions raised when there are concurrency violations, just as I want.

However, it just screams to me that I've missed a trick.

Is there a better way of doing this?

I've looked at the .Attach method on the datacontext but that appears to require storing the original version in a similar way to what I'm already doing.

I also know that I could avoid all this by doing away with the domain objects and letting the Linq to SQL generated objects all the way up my stack - but I dislike that just as much as I dislike the way I'm handling concurrency.

A: 

I would look at trying to utilise the .Attach method by passing the 'original' and 'updated' objects thus achieving true optimistic concurrency checking from LINQ2SQL. This IMO would be preferred to using version or datetime stamps either in the DBML objects or your Domain objects. I'm not sure how MVC allows for this idea of persisting the 'original' data however.. i've been trying to investigate the validation scaffolding in the hope that it's storing the 'original' data.. but i suspect that it is as only as good as the most recent post (and/or failed validation). So that idea may not work.

Another crazy idea i had was this: override the GetHashCode() for all of your domain objects where the hash represents the unique set of data for that object (minus the ID of course). Then, either manually or with a helper bury that hash in a hidden field in the HTML POST form and send it back to your service layer with your updated domain object - do the concurrency checking in your service layer or data layer (by comparing the original hash with a newly extracted domain object's hash) but be aware that you need to be checking for and raising concurrency exceptions yourself. It's nice to use the DMBL functions but the idea of abstracting away the data layer is so to not depend on the particular implementation's features etc. So having full control of the optimistic concurrency checking on your domain objects in your service layer (for example) seems like a good approach to me.

cottsak
A: 

I worked through this and found the following solution. It works in all the test cases I (and more importantly, my testers!) can think of.

I am using the .Attach() method on the datacontext, and a TimeStamp column. This works fine for the first time that you save a particular primary key back to the database but I found that the datacontext throws a System.Data.Linq.DuplicateKeyException "Cannot add an entity with a key that is already in use."

The work around for this I created was to add a dictionary that stored the item I attach the first time around and then every subsequent time I save I reuse that item.

Example code is below, I do wonder if I've missed any tricks - concurrency is pretty fundamental so the hoops I'm jumping through seem a little excessive.

Hopefully the below proves useful, or someone can point me towards a better implementation!

private Dictionary<int, Payment> _attachedPayments;

public void SavePayments(IList<Domain.Payment> payments)
    {
        Dictionary<Payment, Domain.Payment> savedPayments =
            new Dictionary<Payment, Domain.Payment>();

        // Items with a zero id are new
        foreach (Domain.Payment p in payments.Where(p => p.PaymentId != 0))
        {
            // The list of attached payments that works around the linq datacontext  
            // duplicatekey exception
            if (_attachedPayments.ContainsKey(p.PaymentId)) // Already attached
            {
                Payment dbPayment = _attachedPayments[p.PaymentId];                    
                // Just a method that maps domain to datacontext types
                MapDomainPaymentToDBPayment(p, dbPayment, false);
                savedPayments.Add(dbPayment, p);
            }
            else // Attach this payment to the datacontext
            {
                Payment dbPayment = new Payment();
                MapDomainPaymentToDBPayment(p, dbPayment, true);
                _dataContext.Payments.Attach(dbPayment, true);
                savedPayments.Add(dbPayment, p);
            }
        }

        // There is some code snipped but this is just brand new payments
        foreach (var payment in newPayments)
        {
            Domain.Payment payment1 = payment;
            Payment newPayment = new Payment();
            MapDomainPaymentToDBPayment(payment1, newPayment, false);
            _dataContext.Payments.InsertOnSubmit(newPayment);
            savedPayments.Add(newPayment, payment);
        }

        try
        {
            _dataContext.SubmitChanges();
            // Grab the Timestamp into the domain object
            foreach (Payment p in savedPayments.Keys)
            {
                savedPayments[p].PaymentId = p.PaymentId;
                savedPayments[p].Timestamp = p.Timestamp;
                _attachedPayments[savedPayments[p].PaymentId] = p;
            }
        }
        catch (ChangeConflictException ex)
        {
            foreach (ObjectChangeConflict occ in _dataContext.ChangeConflicts)
            {
                Payment entityInConflict = (Payment) occ.Object;

                // Use the datacontext refresh so that I can display the new values
                _dataContext.Refresh(RefreshMode.OverwriteCurrentValues, entityInConflict);
                _attachedPayments[entityInConflict.PaymentId] = entityInConflict;

            }
            throw;
        }

    }
David Hall