Wednesday, March 24, 2010

It is all about CRUD.

I have been developing line of business (LOB) applications for more than 10 years by now, and I can tell that in vast majority of cases it is all about Search and basic data manipulations or simply CRUD (CRUD stands for Create, Update, Delete). Yes, sometimes you need to do funky stuff like workflows, but 90% is still CRUD, Search, CRUD, Search, CRUD and Search…
And in real life this CRUD is not simple one/two tables (favourite for demoing stuff) – one UI screen to do data manipulations. It is usually very complex object graphs (or composites) on the server with complex UI as a frontend. Lots of interrelated tabs, lists, sub lists, one-to-many, many-to-many and all the hell of relational modelling behind.
Let’s see how latest bits of WCF RIA (RC at the moment) address simple (and not so simple) CRUD operations over composites.
As WCF RIA developer you have to be aware of Compositional Hierarchies Simple speaking, you can define a "part-of" relationship inside you metadata by using [Composition] attribute. This will affect client-side generated entities behaviour (changes to leaf element will pop up changed flag thru object graph to root element) and server side (you can use ChangeSet.GetAssociatedChanges to retrieve associated changes). There is great post about it here
Ok, enough theory, let’s give it a go. Consider this object graph

clip_image001
Pretty simple, right? Just two levels of hierarchy, each element of List1 has two sub lists. (In my current project we have 3 levels of sub lists on average and up to 10 lists per hierarchy). Anyway, EF model is created, metadata is defined as

Code Snippet
[MetadataTypeAttribute(typeof(Metadata))]
public partial class Root
{
    internal sealed class Metadata
    {
        // Metadata classes are not meant to be instantiated.
        private Metadata() { }

        [Include, Composition]
        public EntityCollection<List1> List1;
    }
}

[MetadataTypeAttribute(typeof(Metadata))]
public partial class List1
{
    internal sealed class Metadata
    {
        // Metadata classes are not meant to be instantiated.
        private Metadata() {}

        [Include, Composition]
        public EntityCollection<List2> List2;

        [Include, Composition]
        public EntityCollection<List3> List3;
    }
}

Ok, next step is to create DomainServiceclip_image002

Great, almost 200 lines of code were generated for ComplexCompositeService class. We need to slightly tweak it to load the whole object graph though

Code Snippet
public IQueryable<Root> Get()
{
    return this.ObjectContext.Roots.Include("List1.List2").Include("List1.List3");
}

Now it’s time to quickly model simplest UI for basic CRUD. It looks like this

ui

Will it work? Of course NOT! ;) And the reason is – almost 200 lines of generated code. Have I told you I hate code generators and usual cr@p they produce? In this particular example generated code is

  • ugly formatted
  • repetitive
  • doesn’t work

what a wonderful combination ;) However let’s explore why it doesn’t work and how to fix it. Do simple test – add new element into List1 and press Save. Boom! Exception fires here

Code Snippet
public void UpdateRoot(Root currentRoot)
{
    this.ObjectContext.Roots.AttachAsModified(currentRoot, this.ChangeSet.GetOriginal(currentRoot));
}

unfortunately, this.ChangeSet.GetOriginal(currentRoot) is null for root element (it makes sense because it wasn’t physically modified, it was just marked as modified when we added new sub elements to composite) and AttachAsModified fails. Ok, it’s easy to fix

Code Snippet
public void InsertRoot(Root root)
{
    if ((root.EntityState != EntityState.Detached))
    {
        this.ObjectContext.ObjectStateManager.ChangeObjectState(root, EntityState.Added);
    }
    else
    {
        this.ObjectContext.Roots.AddObject(root);
    }
}

Great! Now we can save! Don’t be too excited though ;( Make test a bit harder – add two new lines to List1. Boom! Same place, different exception ;) Now it’s EF refuses to attach root element with two new sub items (we’ll discuss this amazing EF behaviour [I’d rather call it nasty bug] in later posts). Ok, I am tired of fighting these endless problems. The bottom line is – the code generated by RIA project template is useless for composites and complex CRUD.
But don’t run and hide! There is solution. And this solution is as simple as this

Code Snippet
[EnableClientAccess]
public class ComplexCompositeService : CompositeServiceBase<Root, TestEntities>
{
    // yes, it's empty!!! and you have got all CRUD + Search for free!
}

yes, zero lines of code is all you need! ;)
ok, let me explain how this magic works.

Code Snippet
/// <summary>
/// Provides base CRUD for type T (object graph root) + common Query method [Get] with whole object graph scope inclusion
/// Child sets of T have to be marked with [Include, Composition]
/// </summary>
/// <typeparam name="T">Root type</typeparam>
/// <typeparam name="T1">ObjectContext</typeparam>
public class CompositeServiceBase<T, T1> : DomainServiceBase<T1>  
    where T : EntityObject
    where T1 : ObjectContext, IObjectContext, new()

the basic idea is simple – generalise base CRUD operations and base querying based on composition scope defined by Include and Composition attributes.
First of all for composites you can expose only methods for your composite's root entity Update, Insert, Delete and using ChangeSet.GetAssociatedChanges apply the whole changeset to EF ObjectContext

Code Snippet
public virtual void Insert(T current)
{
    AttachAndApplyChanges(current);
}

public virtual void Update(T current)
{
    AttachAndApplyChanges(current);
}

public virtual void Delete(T current)
{
    AttachAndApplyChanges(current);
}

private void AttachAndApplyChanges<TElement>(TElement current) where TElement : EntityObject
{
    ObjectContext.ContextOptions.LazyLoadingEnabled = false;
    ObjectContext.AddObject(current); // attach the whole graph as added
    ApplyChangesToGraph(current);
}

// recursively traverse the changeset and apply it to ObjectContext
internal void ApplyChangesToGraph<TElement>(TElement current) where TElement : EntityObject
{
    var childSets = GetChildSets(current.GetType());
    var self = GetType().GetMethod("ApplyChangesToGraph", BindingFlags.Instance | BindingFlags.NonPublic);

    foreach (var set in childSets)
    {
        var expr = CreateLambda(current, set);
        
        foreach (var item in ChangeSet.GetAssociatedChanges(current, expr))
        {
            var apply = self.MakeGenericMethod(item.GetType());
            apply.Invoke(this, new[] { item });
        }
    }

    ApplyState(current);
}

private void ApplyState(EntityObject current)
{
    var state = EntityState.Unchanged;
    var operation = ChangeSet.GetChangeOperation(current);
    if (operation == ChangeOperation.Insert) return;
    if (operation == ChangeOperation.Update) state = EntityState.Modified;
    else if (operation == ChangeOperation.Delete) state = EntityState.Deleted;

    if (current.EntityState == EntityState.Detached) ObjectContext.AddObject(current); // deleted entities

    ObjectContext.ObjectStateManager.ChangeObjectState(current, state);
}

private static IEnumerable<PropertyInfo> GetChildSets(Type elementType)
{
    // explore buddy classes for metadata
    var metadataClassType = elementType.GetCustomAttributes(typeof(MetadataTypeAttribute), true).Cast<MetadataTypeAttribute>().First().MetadataClassType;
    var metadataChildSets = metadataClassType.GetMembers().Where(p => p.GetCustomAttributes(typeof(CompositionAttribute), true).Count() != 0);
    var childSets = elementType.GetProperties().Where(p => metadataChildSets.Select(m => m.Name).Contains(p.Name));
    return childSets;
}

private static Expression<Func<TElement, object>> CreateLambda<TElement>(TElement current, PropertyInfo set) where TElement : EntityObject
{
    // make lambda {c => c.CompositionPropertyName}
    var param = Expression.Parameter(current.GetType(), "c");
    var prop = Expression.Property(param, set);
    var expr = Expression.Lambda<Func<TElement, object>>(prop, new[] { param });
    return expr;
}

It might seems as a bit of voodoo code, but actually it’s quite simple – we just recursively traverse change set and apply proper entity state to EF unit of work

Secondly – common Get

Code Snippet
public virtual IQueryable<T> Get()
{
    // auto-include child sets based on [Composition] attribute
    BuildCompositionScope(typeof(T), string.Empty);
    var setProperty = ObjectContext.GetType().GetProperties().Single(pi => pi.PropertyType == typeof(ObjectSet<T>));
    var query = (ObjectQuery<T>)setProperty.GetValue(ObjectContext, null);
    foreach (var scope in _compositionScope) query = query.Include(scope);
    return query;
}

private readonly List<string> _compositionScope = new List<string>();

private void BuildCompositionScope(Type elementType, string scope)
{
    var childSets = GetChildSets(elementType);
    if (childSets.Count() == 0)
    {
        if (scope.Length != 0) _compositionScope.Add(scope.Remove(0, 1)); // add final path
        return;
    }
    foreach (var set in childSets)
    {
        // list vs prop
        var type = set.PropertyType.IsGenericType ? set.PropertyType.GetGenericArguments()[0] : set.PropertyType;
        BuildCompositionScope(type, scope + "." + set.Name);
    }
}

again – simple idea – instead of doing ObjectContext.Roots.Include("List1.List2").Include("List1.List3");
we build eager loading scope automatically based on composition scope definition metadata
And the best part is – it works. For any change set, any object graph. Out of the box.

…to be continued (how to extend base CRUD of CompositeServiceBase with your custom logic)

3 comments:

  1. Sergey,

    The
    ObjectContext.AddObject(current); // attach the whole graph as added

    API has changed. Can you suggest how do we work out the EntitySetName?

    Chui

    ReplyDelete
  2. Hi Chui and Sergey,

    I'm running into the same issue. Were you able to find a solution?

    Thanks,
    Brandon

    ReplyDelete
  3. http://blog.zoolutions.se/post/2010/04/05/Generic-Repository-for-Entity-Framework-for-Pluralized-Entity-Set.aspx

    ReplyDelete