Giuliano Dore
Nov 22, 2020
visibility 3768
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
Copies vs. Connections: Modeling Content Areas for Multilingual Success

Starting With the Simple Question Most multilingual content modeling conversations start with a deceptively simple question from a client: "If we s...

Vipin Banka | Aug 23, 2026

Content Transfer coming to Optimizely SaaS CMS

Back in June I released Content Transfer on the Optimizely nuget feed – today I’m releasing version 1.0 , because we’ve had it running out in the...

Matt Pallatt | Aug 23, 2026

Improving Optimizely Product Information in LLM Tools

Tools like Claude Code can be great for working with Optimizely CMS, but when they index outdated or incomplete information about developing a...

Nicholas Sideras | Aug 21, 2026 |

How to Get a Bacpac Out of DXP (And Never Do It By Hand Again)

Every Optimizely DXP project eventually runs into the same task: getting production data onto a local machine so you can actually troubleshoot...

KennyG | Aug 20, 2026