Views: 454
Number of votes: 5
Average rating:

Navigate the content tree like a pro with the GetDescendentOrSelf function

In the 'Navigate the content tree like a pro with the GetAncestorOrSelf function' article, we started discussing about how important it is to be able to navigate a content tree and the importance of performance.

With the new set of functions, we can now navigate all the way to the top, but what if we need to navigate all the way to the bottom of the tree to retrieve a specific page ?

As you know now, the native IContentLoader.GetDescendents function doesn't use the cache and calls the database every time we call the function, can we imagine adding a function like this one on a page where we get 10.000 users hourly ? 😱

IContentLoader helper table

That's why I came out with my own cached implementation of GetDescendents. It's a very simple tree flattening algorith: we set a 'pivot' as being a list of items. The first version being a list of one item (the starting point) and we start collecting all the children of those items with the GetChildren function from contentLoader as this function is using the cache.

Every time we collect the children, we move to the level below, and we can set the pivot to the new list that we retrieved. We can stop going through the tree once there's no more child nodes.

 public static IEnumerable<IContent> GetDescendents(this IContentLoader contentLoader,
            IContent content,
            int maxLevel = defaultMaxLevel)
        {
            var toReturn = new List<IContent>();

            IEnumerable<IContent> pivot = new List<IContent>
            {
                content
            };

            for(var i = 0; i < maxLevel; i++)
            {
                var children = pivot.SelectMany(x => contentLoader.GetChildren<IContent>(x.ContentLink));

                if (!children.Any())
                    break;

                toReturn.AddRange(children);

                pivot = children;
            }

            return toReturn;
        }

And that's it, folks.

If I am being honest, the heavy lifting was done thanks to the SelectMany function, especially useful for flattening lists like the ones we are receiving at each level.

But now I realise I missed something. If we want to find a specific descendent item, we might not want to load all the descendents. we might want to filter before we load the next set of descendents.

So what do we need ? Like the GetAncestorOrSelf function, we need a predicate, we need to filter before we "swap" our pivot and we need to return a single object instead of a list:

public static IContent GetDescendentOrSelf(this IContentLoader contentLoader,
            IContent content,
            Func<IContent, bool> predicate,
            int maxLevel = defaultMaxLevel)
        {
            if (predicate.Invoke(content))
                return content;

            IEnumerable<IContent> pivot = new List<IContent>
            {
                content
            };

            for (var i = 0; i < maxLevel; i++)
            {
                var children = pivot.SelectMany(x => contentLoader.GetChildren<IContent>(x.ContentLink));

                if (!children.Any())
                    break;

                var res = children.FirstOrDefault(predicate);

                //we can return the object if the value isn't the default one nor null
                if(EqualityComparer<T>.Default.Equals(res, default(T))) {
                   return res;
                }

                pivot = children;
            }

            //we couldnt find a result so we just return the default value for the return
            return default;

Some notes regarding this function:

  • As we want to test descendents and the current object, I run the predicate before getting inside the loop.
  • We can combine FirstOrDefault with our predicate for a very efficient expression.
  • It's possible for struct to inherit from interfaces. It is not recommended, but it's possible. Because of that scenario & because we are building a function that should cover all scenarios, it's better to return default instead of null.

I hope this was helpful and I am looking forward to your comments 😊

Nov 22, 2020

Please login to comment.