Thursday, March 13, 2008

Disconnected LINQ To SQL Tips Part 2



How to Enumerate the Entity Tree Graph


I really, really like the way that the standard IEnumerator interface works, and in conjunction with "yield" keyword and a little, I came up with a cunning plan that would help me with my Disconnected model.

Basically, I needed a way to find all objects that were changed tracked in an entity tree, otherwise it would be very manual and an awful lot of code to re-attach entity. It's not so much of a problem when you have just a parent and some children like so:

Customer1
-> Order1
-> Order2
-> Order3


Here you could just re-attach you're objects, first with the custmer and then in a nice little loop attch the orders.


This will be fairly light and not a lot of code, but I thought that in a lot of circumstances the tree would be a lot more complicated like this:


Customer1
-> Order1
->->OrderDetails1
->->OrderDetails2
-> Order2
->->OrderDetails3
->->OrderDetails4
->Order3
->Order4


This starts to look a little complicated because now you have to write a lot of code that loops through each level of the tree attaching as necessary... and you can start to imagine trees that may be 20 entities deep and all of the place. Yuk.


So, basically with a little help from reflection and with the use of the base class, I could put togther a nice little function that would traverse the enitre tree in one list. This is useful for a lot of reasons, not just for finding changed objects, but you would now have the ability to find an object or objects in the tree without hardcoding paths in your linq statements.


Of course, I've only done parent->child relationships and ignore foriegn key ones (Child <- Parent) to avoid overflowing the stack.

This code is in the base class for all entities that will allow you to enumerate against a tree of entities. Please note the following:

  • Yes, it will work in connected mode as well, i.e. when a datacontext is attached.

  • You can query it with LINQ

  • -> var temp = from c in customer.GetEntityHierarchy().OfType() select c);
  • If you're wondering why I've put it in a private class instead of just exposing IEnumerator/IEnumerable on the entity it's self - it's because at runtime it seems to fail because I think Microsoft have put some sort of runtime check on it, perhaps because they want to reserve the IEnumerator for implementation later on.




using System;
using System.Collections;
using System.Collections.Generic;
using System.Data.Linq;
using System.Data.Linq.Mapping;
using System.Linq;
using System.Text;
using System.ComponentModel;
using System.Runtime.Serialization;
using System.Reflection;
 
namespace LINQEntityBaseExample1
{
    public abstract class LINQEntityBase
    {
        // stores the property info for associations
        private Dictionary<string, PropertyInfo> _entityAssociationProperties 
                    = new Dictionary<string, PropertyInfo>(); 
        //used to hold the private class that allows entity hierarchy to be enumerated
        private EntityHierarchy _entityHierarchy; 
        
        /// <summary>
        /// Constructor!
        /// </summary>
        protected LINQEntityBase()
        {
            // Note: FindAssociations() finds association property info's 
            // using reflection (where IsForeignKey !=true)
            // Have left this function out just to keep this short.
            _entityAssociationProperties = FindAssociations();
            // pass in the current object and it's property associations
            _entityHierarchy = new EntityHierarchy(this, _entityAssociationProperties);
        }
 
        /// <summary>
        /// This method flattens the hierachy of objects into a single list that can be queried by linq
        /// </summary>
        /// <returns></returns>
        public IEnumerable<LINQEntityBase> GetEntityHierarchy()
        {
            return (from t in _entityHierarchy
                    select t);
        }
 
        /// <summary>
        /// This class is used internally to implement IEnumerable, so that the hierarchy can
        /// be enumerated by LINQ queries.
        /// </summary>
        private class EntityHierarchy : IEnumerable<LINQEntityBase>
        {
            private Dictionary<string, PropertyInfo> _entityAssociationProperties;
            private LINQEntityBase _entityRoot;
 
            public EntityHierarchy(LINQEntityBase EntityRoot, Dictionary<string, PropertyInfo> EntityAssociationProperties)
            {
                _entityRoot = EntityRoot;
                _entityAssociationProperties = EntityAssociationProperties;
            }
 
            // implement the GetEnumerator Type
            public IEnumerator<LINQEntityBase> GetEnumerator()
            {
                // return the current object
                yield return _entityRoot;
 
                // return the children (using reflection)
                foreach (PropertyInfo propInfo in _entityAssociationProperties.Values)
                {
                    // Is it an EntitySet<> ?
                    if (propInfo.PropertyType.IsGenericType && propInfo.PropertyType.GetGenericTypeDefinition() == typeof(EntitySet<>))
                    {
                        // It's an EntitySet<> so lets grab the value, loop through each value and
                        // return each value as an EntityBase.
                        IEnumerator entityList = (propInfo.GetValue(_entityRoot, null) as IEnumerable).GetEnumerator();
 
                        while (entityList.MoveNext() == true)
                        {
                            if (entityList.Current.GetType().IsSubclassOf(typeof(LINQEntityBase)))
                            {
                                LINQEntityBase currentEntity = (LINQEntityBase)entityList.Current;
                                foreach (LINQEntityBase subEntity in currentEntity.GetEntityHierarchy())
                                {
                                    yield return subEntity;
                                }
                            }
                        }
                    }
                    else if (propInfo.PropertyType.IsSubclassOf(typeof(LINQEntityBase)))
                    {
                        //Ask for these children for their section of the tree.
                        foreach (LINQEntityBase subEntity in (propInfo.GetValue(_entityRoot, null) as LINQEntityBase).GetEntityHierarchy())
                        {
                            yield return subEntity;
                        }
                    }
                }
            }
 
            // implement the GetEnumerator type
            IEnumerator IEnumerable.GetEnumerator()
            {
                return this.GetEnumerator();
            }
        }
 
    }
 
}

No comments: