Skip to content

Instantly share code, notes, and snippets.

@amenayach
Last active May 13, 2020 23:30
Show Gist options
  • Save amenayach/2db33610fc24ddf1f280e9833113d033 to your computer and use it in GitHub Desktop.
Save amenayach/2db33610fc24ddf1f280e9833113d033 to your computer and use it in GitHub Desktop.
A CSharp class that builds a nested tree out of a flat collection
public sealed class Nester<T, TSortKey> : IDisposable
{
private readonly IEnumerable<T> flatList;
private Func<T, T, bool> parentChildPredicate;
private PropertyInfo nestingPropertyInfo;
private Func<T, TSortKey> sortingKeySelector;
private Func<T, bool> rootLevelPredicate;
private Nester(IEnumerable<T> flatList,
Func<T, bool> rootLevelPredicate,
Func<T, T, bool> parentChildPredicate,
Expression<Func<T, IEnumerable<T>>> nestingField,
Func<T, TSortKey> sortingKeySelector)
{
this.flatList = flatList;
this.rootLevelPredicate = rootLevelPredicate;
this.parentChildPredicate = parentChildPredicate;
this.sortingKeySelector = sortingKeySelector;
if (!(nestingField.Body is MemberExpression memberSelectorExpression))
{
throw new ArgumentException(nameof(nestingField));
}
nestingPropertyInfo = memberSelectorExpression.Member as PropertyInfo;
if (nestingPropertyInfo == null)
{
throw new ArgumentException(nameof(nestingField));
}
}
private IEnumerable<T> Nest()
{
if (flatList == null || !flatList.Any())
{
return null;
}
var rootLevels = flatList
.Where(rootLevelPredicate)
.OrderBy(sortingKeySelector)
.Select(rootLevel =>
{
var nestedData = ConstructLevels(rootLevel, flatList);
nestingPropertyInfo.SetValue(rootLevel, nestedData, null);
return rootLevel;
})
.ToArray();
return rootLevels;
}
private IEnumerable<T> ConstructLevels(T detail, IEnumerable<T> remainCourseDetails)
{
var result = remainCourseDetails?.Where(m => parentChildPredicate(detail, m)).OrderBy(sortingKeySelector).ToArray();
var rest = remainCourseDetails?.Where(m => !parentChildPredicate(detail, m)).OrderBy(sortingKeySelector).ToArray();
if ((rest?.Any() ?? false) && (result?.Any() ?? false))
{
result = result
.Select(innerDetail =>
{
var nestedData = ConstructLevels(innerDetail, rest);
nestingPropertyInfo.SetValue(innerDetail, nestedData, null);
return innerDetail;
})
.ToArray();
}
return result;
}
public void Dispose()
{
this.rootLevelPredicate = null;
this.parentChildPredicate = null;
this.nestingPropertyInfo = null;
this.sortingKeySelector = null;
}
/// <summary>
/// Nests the specified flat list.
/// </summary>
/// <param name="flatList">The flat list.</param>
/// <param name="rootLevelPredicate">The root level predicate. ex: m => string.IsNullOrWhiteSpace(m.ParentId)</param>
/// <param name="parentChildPredicate">The parent child predicate. ex: (p, c) => c.ParentId == p.Id</param>
/// <param name="nestingField">The nesting field. ex: c => c.Details</param>
/// <param name="sortingKeySelector">The sorting key selector. ex: m => m.Order</param>
/// <returns></returns>
public static IEnumerable<T> Nest(IEnumerable<T> flatList,
Func<T, bool> rootLevelPredicate,
Func<T, T, bool> parentChildPredicate,
Expression<Func<T, IEnumerable<T>>> nestingField,
Func<T, TSortKey> sortingKeySelector)
{
using var nester = new Nester<T, TSortKey>(flatList, rootLevelPredicate, parentChildPredicate, nestingField, sortingKeySelector);
return nester.Nest();
}
}
@amenayach
Copy link
Author

amenayach commented May 13, 2020

Usage, given a class CourseDetail for example:

using System.Collections.Generic;

public class CourseDetail
{
	public string Id { get; set; }
	public string ParentId { get; set; }
	public int Order { get; set; }
	public IEnumerable<CourseDetail> Details { get; set; }
}

Given a collection of course details:

var details = new[] {
	new CourseDetail { Id = "1", ParentId = null, Order = 0 },
	new CourseDetail { Id = "2", ParentId = null, Order = 0 },
	new CourseDetail { Id = "3", ParentId = "2" , Order = 1 },
	new CourseDetail { Id = "4", ParentId = "3" , Order = 1 },
	new CourseDetail { Id = "33", ParentId = "3" , Order = 0 },
	new CourseDetail { Id = "5", ParentId = "1" , Order = 0 },
	new CourseDetail { Id = "6", ParentId = null, Order = 0 },
	new CourseDetail { Id = "7", ParentId = "4", Order = 0 },
};

The tree can be generated as following:

var nested = Nester<CourseDetail, int>.Nest(details,
	m => string.IsNullOrWhiteSpace(m.ParentId),
	(p, c) => c.ParentId == p.Id,
	c => c.Details,
	m => m.Order);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment