World is now on Opti ID! Learn more

Giuliano Dore
Nov 22, 2020
  58
(0 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

Please login to comment.
Latest blogs
Make Global Assets Site- and Language-Aware at Indexing Time

I had a support case the other day with a question around search on global assets on a multisite. This is the result of that investigation. This co...

dada | Jun 26, 2025

The remote server returned an error: (400) Bad Request – when configuring Azure Storage for an older Optimizely CMS site

How to fix a strange issue that occurred when I moved editor-uploaded files for some old Optimizely CMS 11 solutions to Azure Storage.

Tomas Hensrud Gulla | Jun 26, 2025 |

Enable Opal AI for your Optimizely products

Learn how to enable Opal AI, and meet your infinite workforce.

Tomas Hensrud Gulla | Jun 25, 2025 |

Deploying to Optimizely Frontend Hosting: A Practical Guide

Optimizely Frontend Hosting is a cloud-based solution for deploying headless frontend applications - currently supporting only Next.js projects. It...

Szymon Uryga | Jun 25, 2025

World on Opti ID

We're excited to announce that world.optimizely.com is now integrated with Opti ID! What does this mean for you? New Users:  You can now log in wit...

Patrick Lam | Jun 22, 2025

Avoid Scandinavian Letters in File Names in Optimizely CMS

Discover how Scandinavian letters in file names can break media in Optimizely CMS—and learn a simple code fix to automatically sanitize uploads for...

Henning Sjørbotten | Jun 19, 2025 |