Giuliano Dore
Nov 22, 2020
visibility 3858
star star star star star
(5 votes)

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

Comments

error Please login to comment.
Latest blogs
Surviving a Dirty Feed. Part 3: Error Handling and Event Batching

Committing a 250,000-row Optimizely catalog import row by row, returning a per-row failure report, and coalescing catalog events into one per batch.

Stanisław Szołkowski | Oct 16, 2026 |

Building UI Extensions for Optimizely SaaS CMS

Introduction Developers have been extending the functionality of Optimizely PaaS CMS for years using addons built in C# and deployed as NuGet...

Mark Stott | Oct 15, 2026

Cheapskate AI Translator: Because Translation Should Not Require a Second Mortgage

There is a particular kind of software project that begins with a detailed architecture document, a product roadmap, and a carefully assembled...

Linh Doan Cuu | Oct 15, 2026

Computed Fields in CMS 13 Content Contracts with Optimizely Graph

Content Contracts in Optimizely CMS 13 allow different content types to share a common structure when querying Optimizely Graph. But what if the...

Mardy | Oct 14, 2026