Friday, March 26, 2010

WCF RIA and concurrency

I have been asked recently on WCF RIA Services forum about handling concurrency in WCF RIA. Well, the problem is – there is no official information about how to do it ;) There was special method for handling concurrency on server side, but it was deleted in RIA RC bits

Removal of Resolve methods from DomainService

In the previous release, you could provide a method for customizing how concurrency conflicts are resolved. For the RC release, this feature and all related members have been removed. Providing a common method signature for resolving conflicts in different types of DALs could not be supported in this release.

which means we have only one option now – client side concurrency resolution. It’s not trivial but doable.

First of all – define how you will address concurrency server side (in your model). Standard approach is to use concurrency guard field. For MS SQL server it’s usually RowVersion : timestamp

image

then you have to ask your client – what concurrency resolution scenario do we need? Last win? Unlikely ;)

Normal approach is to notify user about concurrency conflict and allow to resolve it. Don’t make this process too complex though – users don’t like complexity 

Consider this code, which implements generic concurrency resolution handler for RIA

Code Snippet
/// <summary>
/// Submit behaviours for DomainContext:SubmitChanges
/// </summary>
public static class SubmitBehaviour
{
    public static void WithConcurrencyResolution<T>(SubmitOperation operation) where T : Entity
    {
        // concurrency conflict
        if (!operation.HasError || !operation.EntitiesInError.Any(c => c.EntityConflict != null)) return;
        
        var result = MessageBox.Show(
            "The entity was modified by other user, overwrite those changes?\r\n(Cancel will refresh your data to the most recent store state)",
            "Concurrency conflict", MessageBoxButton.OKCancel);

        var ctx = (DomainContext)operation.UserState;
        var query = (EntityQuery<T>) ctx.InvokeMethod("GetQuery", null);

        // query for changes only (or current page only)
        var changes = ctx.EntityContainer.GetChanges().Where(c => c.EntityState == EntityState.Modified).OfType<T>();
        var keys = typeof (T).GetProperties().Where(p => p.GetCustomAttributes(typeof (KeyAttribute), true).Count() != 0).ToList();
        query = query.Where(GetWhere(changes, keys));

        if (result == MessageBoxResult.OK)
        {
            ctx.Load(query, LoadBehavior.MergeIntoCurrent, (Action<LoadOperation>) (c => ctx.SubmitChanges(WithConcurrencyResolution<T>, ctx)), null);
        }
        else
        {
            ctx.Load(query, LoadBehavior.RefreshCurrent, null, true);
        }
        operation.MarkErrorAsHandled();
    }

    // make lambda {c => (c.Key1 == value1 && c.Key2 == value2) || (same for change 2)}
    private static Expression<Func<T, bool>> GetWhere<T>(IEnumerable<T> changes, IEnumerable<PropertyInfo> keys)
    {
        Expression currentGroup = null;
        Expression prevGroup = null;
        var param = Expression.Parameter(typeof(T), "c");
        foreach (var change in changes)
        {
            Expression prev = null;
            Expression current = null;
            // composite keys
            foreach (var key in keys)
            {
                var prop = Expression.Property(param, key);
                var value = Expression.Constant(change.GetPropertyValue(key.Name));
                current = Expression.Equal(prop, value);
                if (prev != null)
                {
                    current = Expression.AndAlso(prev, current);
                }
                prev = current;
            }
            currentGroup = current;
            if (prevGroup != null && currentGroup != null)
            {
                currentGroup = Expression.OrElse(prevGroup, currentGroup);
            }
            prevGroup = currentGroup;

        }
        return Expression.Lambda<Func<T, bool>>(currentGroup, new[] { param });
    }
}

The idea behind this voodoo (yes, each time we forced to create expression tree we curse a kitten) code is simple though

1. Submit change set, server side will try to apply it and if there is concurrency error it will return FIRST concurrency error to client

2. If we want to force our changes to be applied – reload changed data with LoadBehavior.MergeIntoCurrent – thus refreshing our RowVersions and keep changes we made, then resubmit until we receive clean response

3. If we want to refresh our data to latest DB state – just reload changed subset of records with LoadBehavior.RefreshCurrent – thus losing our changes

The usage is extremely simple

Code Snippet
private void SaveButtonClick(object sender, RoutedEventArgs e)
{
    orderDomainDataSource.DomainContext.SubmitChanges(SubmitBehaviour.WithConcurrencyResolution<Order>, orderDomainDataSource.DomainContext);
}

I deeply believe this approach is not ideal but at least it works. Now. Out of the box.

PS. RIA team has to consider better approach – client should provide server with resolution mode – server has to resolve accordingly

No comments:

Post a Comment