How to get a first rootnode based on a property value in v16?

Hi,

In a multi site set-up Headless set-up I’ve used this in the past to get the root node of the site. This is now obsolete and I should use the IDocumentNavigationQueryService for this. How can I achieve the same with the new IDocumentNavigationQueryService?

var portalRoot = umbracoContext.Content.GetAtRoot().DescendantsOrSelf(culture)?.FirstOrDefault(r => r.OperatingCompany == operatingCompany);

Thanks,

Richard

Hi Richard,

I think the best approach for this is to use examine rather than trying to navigate content. The new navigation API’s/Services, seem highly efficient for when you aren’t trying to filter by specific things other than type.

Another option is to store your own cache of key nodes with the appropriate values and search that instead.

1 Like

Thanks Nik,

Was hoping for a more easy solution :slight_smile: Will add a helper for now that does the same as Umbraco does and see how we can go in the future. Quite fan of Examine but can also get complex for other devs that take over later on.

Thanks,

Rchard

Your example confuses me a bit, `.FirstOrDefault(r => r.OperatingCompany == operatingCompany)` implies it has been cast to a type that does not occur in the example.

In that case you can call the `TryGetRootKeys`method, then loop through them and call the`TryGetDescendantsKeysOfType` which will give you the nodes of the type you want. Then you will have to fetch the actual nodes from the content cache and check for the value.

Or use Examine as Nik suggests :sweat_smile:

1 Like

Hi Jesper,

This post stripped of in .DescendantsOrSelf<EcommercePortalRoot>(culture) So it was using the models builder which I assume does the TryGetDescendantsKeysOfType?

Thanks,

Richard

@Nik and/or @jemayn When you say “use Examine” what would that look like? To me, just the word “Examine” invokes 25 lines of ceremony, composers and the like, to get certain fields indexed “the correct” way"… and I run away, screaming :sweat_smile:

Would love for it to not have that effect on me :slight_smile:

/Chriztian

I tried writing the approach I would have taken out, it is quite verbose compared to before (could definitely be shortened as well though).

var ecommercePortalKeys = new List<Guid>();
IPublishedContent? portalRoot = null; // exchange for a typed model if needed
var operatingCompany = ""; // whatever

using var umbracoContext = _umbracoContextFactory.EnsureUmbracoContext();

// Gets all keys for nodes in the root
if (_documentNavigationQueryService.TryGetRootKeys(out var rootKeys))
{
    foreach (var rootKey in rootKeys)
    {
        // For each root node, find all decendant keys of the type EcommercePortalRoot
        if (
            _documentNavigationQueryService.TryGetDescendantsKeysOrSelfKeys(
                rootKey,
                EcommercePortalRoot.ModelTypeAlias,
                out var keys
            )
        )
        {
            ecommercePortalKeys.AddRange(keys);
        }
    }

    foreach (var ecommercePortalKey in ecommercePortalKeys)
    {
        var node = umbracoContext.UmbracoContext.Content.GetById(ecommercePortalKey);

        if (node is EcommercePortalRoot ecommercePortalRoot)
        {
            if (ecommercePortalRoot.OperatingCompany == operatingCompany)
            {
                portalRoot = ecommercePortalRoot;
                break;
            }
        }
    }
}

if (portalRoot is not null)
{
    // success
}

That makes sense, there are a lot of nuances..

In it’s essence it is fairly simple - injecting the IExamineManager and then matching the field value along these lines:

if (_examineManager.TryGetIndex(Umbraco.Cms.Core.Constants.UmbracoIndexes.ExternalIndexName, out var index))
{
    var results = index
        .Searcher.CreateQuery(IndexTypes.Content)
        .Field("operatingCompany", "valToMatch".Escape())
        .Execute();

    if (int.TryParse(results.FirstOrDefault()?.Id, out var nodeId))
    {
        // get from content cache based on the nodeId
    }
}

However, that assumes that it is indexed in a way where you can match the value. Examine is case sensitive so that is not always the case. And of course if it is more complex data then it may not work as well as for simple strings/ints.

Additionally Richard’s original example goes through descendants and finds the first match - in examine you would not have a clear indication of the “closest descendant”, my example above would instead take the first result which is the best score match.

Often you’d end up doing custom indexing simply to control what you want to match with later a bit more.

Hi Jesper,

Thanks
TryGetDescendantsKeysOrSelfKeys

I assume this is what Models Builder doesn internally? I love Examine BTW but mostly for overviews.

Best Richard

I am also interested in how we do this now.

Previously, I was able to do something simple like this:

var notFoundPage = umbracoContext.Content?.GetAtRoot().FirstOrDefault(c => c.Name == "Page Not Found");

How would this be achieved now? To me, Examine seems like the wrong choice for something that used to be very simple, or has this easy querying of the content tree been lost due to the move to Hybrid Cache?

(the docs for the UmbracoContext helper are outdated: UmbracoContext helper | Umbraco CMS )

Edit:

I also just noticed that the docs example for implementing custom 404 error pages also still uses the old .getAtRoot() querying too : Implement Custom Error Pages | Umbraco CMS

Hi Owain,
Sorry for delay. We use indeed GetAtRoot,

var node = GetAtRoot(umbracoContext)
    .DescendantsOrSelfOfType(docTypeAlias, culture)
    .SingleOrDefault();

Cheers,
Richard

So here’s the problem. With the hybrid cache, not all content is in the cache to begin with. As soon as you start looking through property values you need to pull that content out of the database and into the cache to actually be able to read those values.

If you want to do performant lookups based on property values, you need to index those property values somehow.

Here you have a choice - roll your own index or use examine.

Umbraco already indexes property values with examine and that is generally fine. It stays up to date with values etc. and it just works. You may need to cache lookups and/or use a slimmer custom index if you’re using it on a critical path to do the same lookup regularly.

Rolling your own is not a lot of work and can be useful where examine isn’t a good fit or you want more control. Use a notification handler to track changes to content, store values in your own persistence layer (database, filesystem etc.) and leverage the hybrid cache to keep it performant.

I’ve always tended to abstract away things like @owainjones.dev’s error page lookup to an IErrorPageService or @rsoeteman’s portal lookup to an IPortalService. l tend to treat values like this as application state and keep in memory (often persisting with IKeyValueService) because lookups tend to scale poorly - that’s not a new Hybrid Cache or even Umbraco limitation, but a limitation of computing generally.

How about a a good old Settings tab on your home page, picking the error page and the whatever page. Performs crazy good (home is presumably always in the cache) and avoids all kinds of unnecessary traversing.

In v17 you can now even hide/set to read-only on any property you want so editors can’t ruin it, but you can still pick a different page as admin if you wanted to.

1 Like

This could be part of my imaginary talk, entitled “The Umbraco Way” :rofl:

1 Like

This can be the best way right up until you don’t have just one home/settings node.

Then you’re back to needing to do lookups again to find out which “home”/“settings” node you need for a given request - especially for things like 404s where, by definition, you don’t have content assigned.

If rearchitecting content structure/types is an option, then that unlocks some more performant options, with less code:

  • Pickers, like @sebastiaan says.
  • 404 pages with predictable types and/or predictable paths (useful if you don’t want to make it a manual thing).
  • I’m sure there are more that I can’t think of right now…

But that’s not always an option.

You may never guess where this one lives in :slight_smile:

But to be honest, this site uses the Content Delivery API which is examine anyway but also cached after the page is retrieved so I don’t bother too much about it. I prefer readability for the future devs in this case.

That said If we weren’t supposed to be using these methods why they are here in the first place? Think my dream will be to have the same methods that we could use before but then abstracted over the Examine Index instead of the Content Cache… V20 maybe?

Oh and for 404 we use SEOChecker of course :wink:

Sometimes I miss the old days indeed, most of the times not… :sweat_smile: