Include children to n levels

Developer
Jun 2, 2009 at 3:17 PM

In the latest version that Rupert updated, he has allowed the possibility to update create and delete the children EntitySets of the Entity that is being updated (the includeChildren parameter on the Update method in the GenericRepository). This works well for any children Entities to a single level.

For the purposes of the project I'm working on, I have a more complex situation:

Quiz has QuizResults and QuizQuestions

QuizQuestions have QuizAnswers

This means we have a tree to 2 levels for the situation:

Quiz --> QuizQuestions --> QuizAnswers

For the purposes of what I'm trying to achieve, I'm looking to serialise the entire Entity tree as a JSON object, modify it client side and pass all changes back for revision as a single call. I've made some changes to the current revision of the GenericDataLayer to facilitate this, and I'd like to discuss this before submitting the changes back to Rupert for inclusion.

Firstly, in order to serialise the Entity tree I need, I use the DataLoadOptions on the DataContext for the particular Quiz entity I'm getting. This is then converted to JSON using a WCF service I've created. After changes have been made client side, I pass the object back to the service, and it converts it successfully back to a modified Quiz entity. I then perform the update.

Currently, with the includeChildren parameter set to true, it successfully updates all the relevant Quiz, QuizResult and QuizQuestion entities, but it leaves the QuizAnswers out because it stops itself from recursion in the ObjectUpdater class at this point within the UpdateChildCollection method:

            //Now update all the destination entities
            //TODO: could optimise this
            for (int i = 0; i < sourceCollection.Count; i++)
            {
                Update(sourceCollection[i], destinationCollection[i], false); // includeChildren is false => No recursion
            }

The changes I wanted to introduce are as follows:

1. Allow recursion to occur.
2. Introduce the idea of a maximum level of recursion so that the updating can be physically stopped before it goes too far.

With this in mind, I've added another overload for the Update method in the GenericRepository and the StaticGenericRepository (and the IGenericRepository obviously), which takes an int parameter for the maxLevel:

        public static void Update(TEntity entity, bool submitChanges, bool includeChildren, int maxLevels)

This new parameter then gets filtered down to the ObjectUpdater. In here, I've also added another overload to the Update method to include the maxLevel.

Additionally, I've added an includeChildren parameter to the UpdateChildCollection method, so that we can choose to update the children of the Entity that is being updated. Here is the meat of the code changes within ObjectUpdater (Note the decision making process on the Invoke call to UpdateChildCollection - this is the important part for the recursion to occur):

        public void Update(object source, object destination, bool includeChildren, int maxLevel)
        {
            foreach (PropertyInfo pi in _mapping.GetDatabaseProperties(source.GetType()))
            {
                pi.SetValue(destination, pi.GetValue(source, null), null);
            }
            if (includeChildren)
            {
                int currentLevel = 0;
                while (currentLevel < maxLevel)
                {
                    currentLevel++;
                    foreach (var md in _mapping.GetAssociationPropertiesMetaData(source.GetType()))
                    {
                        //call UpdateChildCollection as it's generic and we don't know the type at
                        //compile time we need to call it as follows:
                        MethodInfo method = typeof(ObjectUpdater).GetMethod("UpdateChildCollection");
                        MethodInfo generic = method.MakeGenericMethod(new Type[] { md.Association.OtherType.Type });
                        PropertyInfo pi = _mapping.GetPropertyInfoFromMetaData(source.GetType(), md);
                        object s = pi.GetValue(source, null);
                        object d = pi.GetValue(destination, null);
                        // Here's the magic for the recursion to occur:
                        // test if currentLevel != maxLevel and pass that to the UpdateChildCollection method
                        generic.Invoke(this, new object[] { s, d, ((currentLevel != maxLevel)?true:false) });
                    }
                }
            }
        }

        public void UpdateChildCollection<T>(EntitySet<T> sourceCollection, EntitySet<T> destinationCollection, bool includeChildren) where T: class, new()
        {
            //get a list of all the ids in the 2 collections
            var sourceIds = sourceCollection.Select(x => _mapping.GetPrimaryKeyValue(x));
            var destinationIds = destinationCollection.Select(x => _mapping.GetPrimaryKeyValue(x));
           
            //get the ids of the entities to be added and deleted
            var additions = from s in sourceIds
                            where (destinationIds.Contains(s) == false)
                            select s;
            var deletions = (from d in destinationIds
                            where (sourceIds.Contains(d) == false)
                            select d).ToList();
           

            //delete all the entities that are in the destination but not in the source
            while (deletions.Count > 0)
            {
                var entityToRemove = _mapping.GetEntityByPrimaryKeyValue<T>(destinationCollection, deletions[0]);
                destinationCollection.Remove(entityToRemove);
                _dataContext.GetTable(typeof(T)).DeleteOnSubmit(entityToRemove);
                deletions.RemoveAt(0);
            }

            //Add all the entities that are in the source but not the destination
            foreach (var item in additions)
            {
                T newEntity = new T();
                T entityToAdd = _mapping.GetEntityByPrimaryKeyValue<T>(sourceCollection, item);
                Update(entityToAdd, newEntity, false);
                destinationCollection.Add(newEntity);
            }
            //Now update all the destination entities
            //TODO: could optimise this
            for (int i = 0; i < sourceCollection.Count; i++)
            {
                // Have removed the hard-coding of 'false' on the method call here in order
                // to allow recursion to happen. IncludeChildren is passed from the Update method
                // in which we test to see if we've reached the max levels.
                Update(sourceCollection[i], destinationCollection[i], includeChildren);
            }
        }

I need to do a hell of a lot more testing on this change to make sure that there are as few undesirable effects as possible. It's worth noting that there is the possibility to go infinite here by trying to update a triangle of related entities with more than 2 levels - but I'm hoping that the programmer using this will take that into account before just applying an update to everything. Descretion is called for in these cases. :)

Hoping for a good discussion!

 

Developer
Jun 18, 2009 at 2:28 PM

Have checked in this change ... hopefully it'll work ok for everyone. :)