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);
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.
Was hoping for a more easy solution 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.
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.
This post stripped of in .DescendantsOrSelf<EcommercePortalRoot>(culture) So it was using the models builder which I assume does the TryGetDescendantsKeysOfType?
@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
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
}
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.
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?
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
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.
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:
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?