This is part tree, in what was originally planned to be a two-part post series ;).
In parts one and two, I explored ways to creating something similar to Ruby’s acts_as_tree plugin in C#. The result was a set of extension methods; working title: hierarchy extensions.

In this post, I’ll define additional overloads, and I’ll introduce a version for Lightspeed ORM (well actually a draft version).

Parameterless hierarchy extension methods

Ruby’s Acts_as_tree method can be called without any parameters. By default ‘parent_id’ is used as parent id column name, and there is no need to define the id property, since ActiveRecord primary key column is selected automatically.

In C# we can achieve the same thing with our hierarchy extension methods. In the example below, I’ll define a new Parent() overload, that takes no parameters. As id property, it takes the primary key property of the entity. As parent id property the method looks for a property named “ParentId”.

/// <summary>
/// Retrieve parent entity of current DmModel entity. Entity class must contain "ParentId" property.
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of id and parentId</typeparam>
/// <param name="node">Entity this method is called on</param>
/// <returns>Parent entity</returns>
public static TEntity Parent<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node) where TEntity : DmModel
{
    var allNodes = ((TEntity)node).Context.Query<TEntity>();
    TEntity entity = (TEntity)node;
    PropertyInfo idProperty = entity.PrimaryKeyProperty;
    PropertyInfo parentIdProperty = typeof(TEntity).GetProperty("ParentId");
    if (parentIdProperty == null)
        throw new Exception("Property 'ParentId' missing in entity");
    return Parent<TEntity, TProperty>(node, allNodes, idProperty, parentIdProperty);
}

To make this work, some changes need to be made in the previously defined set of extension methods. This is the new complete set of extension methods:

/// <summary>
/// Marker interface for hierarchy extension methods.
/// </summary>
public interface IHierarchyNode<TEntity> { }

public static class TreeExtensions {

/// <summary>
/// Retrieves child entities of current DmModel entity. from the database
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of Id's</typeparam>
/// <param name="node">Entity that implements the <see cref="IHierarchyNode{T}"/> interface</param>
/// <param name="propertyNameId">Selector for the property that holds the entity id</param>
/// <param name="propertyNameParentId">Selector for property that holds the parent id</param>
/// <returns>Child entities</returns>
public static IEnumerable<TEntity> Children<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node,
  Expression<Func<TEntity, TProperty>> idProperty,
  Expression<Func<TEntity, TProperty>> parentIdProperty) where TEntity : DmModel
{
    var allNodes = ((TEntity)node).Context.Query<TEntity>();
    return Children<TEntity, TProperty>(node, allNodes, idProperty, parentIdProperty);
}

/// <summary>
/// Retrieves child entities of current entity from a <see cref="IQueryable"/> datasource.
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of id and parentId</typeparam>
/// <param name="node">Entity that implements the <see cref="IHierarchyNode{T}"/> interface</param>
/// <param name="propertyNameId">Selector for the property that holds the entity id</param>
/// <param name="propertyNameParentId">Selector for property that holds the parent id</param>
/// <returns>Child entities</returns>
public static IEnumerable<TEntity> Children<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node,
  IQueryable<TEntity> allNodes,
  Expression<Func<TEntity, TProperty>> idProperty,
  Expression<Func<TEntity, TProperty>> parentIdProperty) where TEntity : class
{
    PropertyInfo idPropertyInf = GetPropertyInfo<TEntity, TProperty>(idProperty);
    PropertyInfo parentIdPropertyInf = GetPropertyInfo<TEntity, TProperty>(parentIdProperty);
    return Children<TEntity, TProperty>(node, allNodes, idPropertyInf, parentIdPropertyInf);
}

/// <summary>
/// Retrieves child entities of current entity from a <see cref="IQueryable"/> datasource.
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of id and parentId</typeparam>
/// <param name="node">Entity that implements the <see cref="IHierarchyNode{T}"/> interface</param>
/// <param name="propertyNameId">Selector for the property that holds the entity id</param>
/// <param name="propertyNameParentId">Selector for property that holds the parent id</param>
/// <returns>Child entities</returns>
private static IEnumerable<TEntity> Children<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node,
  IQueryable<TEntity> allNodes,
  PropertyInfo idProperty,
  PropertyInfo parentIdProperty) where TEntity : class
{
    ParameterExpression parameter = Expression.Parameter(typeof(TEntity), "e");
    Expression<Func<TEntity, bool>> predicate;

    // Left hand side            
    Expression left = Expression.Property(parameter, parentIdProperty);

    // Right hand side            
    object idValue = idProperty.GetValue(node, null);
    Expression right = Expression.Constant(idValue);

    predicate = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(left, right), parameter);
    var result = allNodes.Where(predicate);
    return result;
}

/// <summary>
/// Retrieve parent entity of current DmModel entity. Entity class must contain "ParentId" property.
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of id and parentId</typeparam>
/// <param name="node">Entity this method is called on</param>
/// <returns>Parent entity</returns>
public static TEntity Parent<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node) where TEntity : DmModel
{
    var allNodes = ((TEntity)node).Context.Query<TEntity>();
    TEntity entity = (TEntity)node;
    PropertyInfo idProperty = entity.PrimaryKeyProperty;
    PropertyInfo parentIdProperty = typeof(TEntity).GetProperty("ParentId");
    if (parentIdProperty == null)
        throw new Exception("Property 'ParentId' missing in entity");
    return Parent<TEntity, TProperty>(node, allNodes, idProperty, parentIdProperty);
}

/// <summary>
/// Retrieve parent entity
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of id and parentId</typeparam>
/// <param name="node">Current node</param>
/// <param name="allNodes">Flat collection of all tree nodes</param>
/// <param name="idProperty">Property that contains the primary key value</param>
/// <param name="parentIdProperty">Property that contains the parent id value</param>
/// <returns>Parent entity</returns>
private static TEntity Parent<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node,
  IQueryable<TEntity> allNodes,
  PropertyInfo idProperty,
  PropertyInfo parentIdProperty) where TEntity : class
{
    ParameterExpression parameter = Expression.Parameter(typeof(TEntity), "e");
    Expression<Func<TEntity, bool>> predicate;

    // Left hand side            
    Expression left = Expression.Property(parameter, idProperty);

    // Right hand side            
    object rightValue = parentIdProperty.GetValue(node, null);
    Expression right = Expression.Constant(rightValue);

    predicate = Expression.Lambda<Func<TEntity, bool>>(Expression.Equal(left, right), parameter);
    IEnumerable<TEntity> e = allNodes.Where(predicate);
    return e.FirstOrDefault();
}

/// <summary>
/// Retrieve parent entity
/// </summary>
/// <typeparam name="TEntity">Entity class</typeparam>
/// <typeparam name="TProperty">Property type of id and parentId</typeparam>
/// <param name="node">Current node</param>
/// <param name="allNodes">Flat collection of all tree nodes</param>
/// <param name="idProperty">Property that contains the primary key value</param>
/// <param name="parentIdProperty">Property that contains the parent id value</param>
/// <returns>Parent entity</returns>
public static TEntity Parent<TEntity, TProperty>(
  this IHierarchyNode<TEntity> node,
  IQueryable<TEntity> allNodes,
  Expression<Func<TEntity, TProperty>> idProperty,
  Expression<Func<TEntity, TProperty>> parentIdProperty) where TEntity : class
{  
    PropertyInfo idPropInf = GetPropertyInfo<TEntity, TProperty>(idProperty);            
    PropertyInfo parentIdPropInf = GetPropertyInfo<TEntity, TProperty>(parentIdProperty);
    return Parent<TEntity, TProperty>(node, allNodes, idPropInf, parentIdPropInf);
}

private static PropertyInfo GetPropertyInfo<TEntity, TProperty>(Expression<Func<TEntity, TProperty>> property)
{
    if (property.Body.NodeType != ExpressionType.MemberAccess)
        throw new ArgumentException("The property selector expression has an incorrect format");
    MemberExpression memberAccess = property.Body as MemberExpression;
    PropertyInfo propInfo = memberAccess.Member as PropertyInfo;
    if (propInfo == null)
        throw new ArgumentException(string.Format("Expression '{0}' refers to a field, not a property.", property.ToString()));
    return propInfo;
}

}

No shocking changes. Just more convenience for lazy programmers thanks to the parameterless overload of Parent() and Children() methods.

The code above is based on my personal DynamicModel ORM, because it allows me to create readable examples quickly. However, the methods can easy be adapted for any ORM that implements the IQueryable interface, for example Lightspeed

Lightspeed version of hierarchy extension

Mindscape Lightspeed is a commercial .NET ORM with many features I miss in other ORM’s. Most notably:

  • Migrations
  • LINQ support
  • Visual Studio designer support
  • Flexibility in underlying database. Painless switching between underlaying DBMS. Something that is possible in theory in most ORM’s, such as Entity Framework, but in often not implemented well.

(See also this post)

As a proof of concept, I created a Lightspeed ORM version of the hierarchy extension methods, which you can download here. Example usage:

var child = unitOfWork.FindById<Customer>(23);
var parent = child.Parent<Customer, int>(unitOfWork);

Disclaimer: this code has not been thoroughly tested!

Leave a Reply





Human Verification