Giuliano Dore
Nov 22, 2020
visibility 3757
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
Optimizely Commerce 14 Now Supports .NET 10

In my previous post, Optimizely CMS 12 Now Fully Supports .NET 10 , I noted one remaining limitation: Commerce 14 (Commerce Connect) did not yet...

Bien Nguyen | Aug 7, 2026

The Top 10 Things I'm Actually Using AI For

A while back I wrote about ReviewPR, an Azure Function that uses AI to review Azure DevOps pull requests. That was one specific use case, but over...

KennyG | Aug 5, 2026

Drag-and-Drop Reordering for Commerce Media Collection in Optimizely Commerce Connect

Optimizely Commerce Connect ships a polished asset editor for the CommerceMediaCollection  property on catalog entries. It lets editors add, remove...

Linh Doan Cuu | Aug 5, 2026

Order tabs with drag and drop V2

I earlier did a very simple Blazor version to be able to sort tabs with drag and drop. I wanted to update it a bit and also display how the tabs ar...

Per Nergård (MVP) | Aug 4, 2026

Optimizely : Missing Language Manager Gadget Fix in CMS 12 After Upgrading to .NET 10

Recently, while upgrading an Optimizely CMS solution to .NET 10, we came across an issue where the Language Manager gadget completely disappeared...

Madhu | Aug 2, 2026 |

Flat or Nested? Weighing Your Content Modeling Options in Optimizely SaaS CMS

When building a headless site on Optimizely SaaS CMS, one of the earliest and most critical design milestones your team will face is defining your...

Vipin Banka | Jul 31, 2026