Bits of Xperience: The Hidden Cost of IPageUrlRetriever.Retrieve
Tags: xperience, kentico, aspnetcore, dotnet
Published: 2021-06-28 13:19:38

---
title: Bits of Xperience: The Hidden Cost of IPageUrlRetriever.Retrieve
published: true
description: Learn why we need to be careful when using convenient APIs that hide their true cost and how you can optimize the retrieval of Page URLs in Kentico Xperience.
tags: xperience, kentico, aspnetcore, dotnet
series: Bits of Xperience
cover_image: https://dev-to-uploads.s3.amazonaws.com/i/91a47vt9de7x1jkcfrl8.jpg
---

There's a lot of new, helpful types and methods in Kentico Xperience 13.0... but it can sometimes be difficult to know when each should be used 🤔.

Let's look at the simple (or is it?) example of the `IPageUrlRetriever` interface and its `Retrieve()` method.

## 📚 What Will We Learn?

- What `IPageUrlRetriever` does
- The multiple overloads of the `Retrieve()` method
- The hidden difference between each overload
- The best way to retrieve Page URLs

## 🎣 What is the IPageUrlRetriever?

Using Kentico Xperience's [Content Tree based routing](https://docs.xperience.io/developing-websites/implementing-routing/content-tree-based-routing) is the option most developers choose. It enables the ability to have some Pages in the Content Tree not participate in URL generation, and also ensures that [culture is included in URLs](https://docs.xperience.io/developing-websites/implementing-routing/content-tree-based-routing#Contenttreebasedrouting-GenerationMultilingualURLgenerationonmultilingualsites) based on the site's settings.

All of this means that generating URLs correctly can get a little tricky 😅!

Fortunately, Kentico Xperience helps us out by providing the `IPageUrlRetriever` interface which has 1 method, `Retrieve()`.

This method returns an instance of the `PageUrl` type which is defined as:

```csharp
//
// Encapsulates page relative path and absolute URL.
public class PageUrl
{
    public PageUrl();

    //
    // Relative path (starting with ~/) of the page.
    public string RelativePath { get; set; }

    //
    // Absolute URL of the page.
    public string AbsoluteUrl { get; set; }
}
```

We can use this `PageUrl` instance to render links to other Pages in our Razor Views.

However, there are multiple overloads of the `Retrieve()` method and they have very different use-cases 😮.

## 🧙🏽‍♀️ Which Method Do We Choose?

Here are all the overloads of `IPageUrlRetriever.Retrieve()`, with their XML doc comments summary included:

```csharp
// Retrieves URL for given page
PageUrl Retrieve(TreeNode page, bool keepCurrentCulture = true);

// Retrieves URL for the given page in the given culture.
PageUrl Retrieve(TreeNode page, string cultureCode);

// Retrieves URL for a page based on given properties.
PageUrl Retrieve(string nodeAliasPath, bool keepCurrentCulture = true);

// Retrieves URL for a page based on given properties.
PageUrl Retrieve(string nodeAliasPath, string cultureCode, string siteName = null);
```

It looks like Kentico Xperience is giving us a lot of flexibility here. We can either supply a full `TreeNode` instance or, just the `nodeAliasPath`.

That's convenient! If we know there's always an `/About` Page in the Content Tree, we can make a call like:

```csharp
PageUrl pageUrl = retriever.Retrieve("/About");
```

This will give us access to the correctly generated Page URL, _and_ we don't have to query for the `TreeNode`, which means one less database round-trip 😃 (or does it?)

So, we probably feel pretty confident 😤 in using `Retrieve(TreeNode node, ...)` when we have the `TreeNode` instance anyway, and using `Retrieve(string nodeAliasPath, ...)` when we only know where in the Content Tree the Page is or when we get the Node Alias Path from some other Page's field.

## ⚔ Retrieve(string) vs Retrieve(TreeNode)

In this situation Xperience asks for what it needs but lets us provide less, however [there's no free lunch](https://en.wikipedia.org/wiki/There_ain%27t_no_such_thing_as_a_free_lunch) and the convenience provided to us has a cost 🤨!

The `IPageUrlRetriever` is implemented by the `Kentico.Content.Web.Mvc.PageUrlRetriever` internal class. When we call `Retrieve(string nodeAliasPath, ...)` the `PageUrlRetriever` uses `IPageSystemDataContextRetriever.Retrieve()` internally to get the 'page data' that matches the `nodeAliasPath` we provided:

```csharp
/// <summary>
/// Provides an interface for retrieving the page based on given parameters for system purposes.
/// </summary>
public interface IPageSystemDataContextRetriever
{
    // ...
    
   TreeNode Retrieve(SiteInfoIdentifier site, string nodeAliasPath, string cultureCode, bool latest);
}
```

`IPageSystemDataContextRetriever.Retrieve()` is fortunately cached for 10 minutes, so repeated uses of `IPageUrlRetrieve.Retrieve(string nodeAliasPath, ...)` won't result in multiple database calls, but the first call absolutely does hit the database because it needs more information, than what we provided, to generate the correct URL.

This is an important point to understand... Kentico Xperience provides _many_ different ways to accomplish the same goal, which is a good thing because we can choose the right one for our use-case. At the same time, if we choose the wrong approach for our use-case, we might end up taking a performance hit we didn't intend 😬!

How bad can it get? Let's say we are generating URLs for 100 products displayed on a Page using `IPageUrlRetriever.Retrieve(string nodeAliasPath, ...)`. This means we are executing **at least** 100 database queries just to get URLs! Add on to this all the querying we did to get the Product information and images! Ooof 😖!

This is commonly known at the [N + 1 Querying Problem](https://stackoverflow.com/a/97253/939634) and is often seen with [Object-Relational Mapping tools](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping) like [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/) or ... Kentico Xperience's APIs 😋.

## 🔥 More Pitfalls 💣

Let say we've avoided the N + 1 query by using the alternative overload of `IPageUrlRetriever.Retrieve(TreeNode node, ...)` so that Kentico Xperience doesn't have to go and fetch all the nodes independently.

> If we still have to get our Pages from `string nodeAliasPath` values, we can use the `WhereIn(string columnName, ICollection<string> values)` method defined on `WhereConditionBase` to query for all `TreeNode` objects that match set of `nodeAliasPath` values we have. This would be a big query, but at least it's 1 query and not 100.

Since we've found the correct API for our use-case, we should be all set now, right? `git commit` and deploy 😎!

![Ship falling into the water](https://media.giphy.com/media/143vPc6b08locw/giphy.gif)

Unfortunately, we could now run into a second problem 😫.

In Kentico Xperience MVC (compared to older Portal Engine sites), the `nodeAliasPath` is not the true URL even if parts of it match a URL for a Page. Instead, all generated URL values are stored in the `CMS_PageUrlPath` table 🤓 in the database and the actual path is in the `PageUrlPathUrlPath` column (what a tongue twister!)

Without this data, we cannot generate valid Page URLs.

This means that when we pass a `TreeNode` to `IPageUrlRetriever.Retrieve(TreeNode node, ...)`, internally it has to check if the `PageUrlPathUrlPath` field is in the `TreeNode`'s internal data set of field/value pairs. If the value is not populated, then Kentico Xperience has to query the database for it:

```csharp
PageUrlPathInfo.Provider.Get()
  .WhereEquals("PageUrlPathNodeID", page.NodeID)
  .WhereEquals("PageUrlPathCulture", cultureCode);
```

In addition to having this field populated, the culture of the `TreeNode` retrieved from the database needs to match the culture of the URL we are trying to generate.

If either the `PageUrlPathUrlPath` is missing or the cultures don't match, we have to make yet another database call for the URL data, and as far as I can tell, this query **is not cached** 😨.

So we're in another situation where we could be making an additional 100 database queries (and if we were using `nodeAliasPath` that means at least 200 database calls!) to get Page URLs.

## 🏆 Optimal URL Generation

Fortunately, there's a nice extension method `WithPageUrlPaths()`, in the `CMS.DocumentEngine.Routing` namespace, for `IDocumentQuery` that ensures the `CMS_PageUrlPath` table is joined when querying for our `TreeNode`s 🤩.

If we had a collection of `NodeGUID` values (or `string nodeAliasPath` values) that referenced Pages in the Content Tree that we wanted to generate URLs for, I think this would be the best approach:

```csharp
Guid[] linkedNodeGuids = // ...

var linkedDocuments = await DocumentHelper
    .GetDocuments<TreeNode>()
    .WithPageUrlPaths()
    .WhereIn(nameof(TreeNode.NodeGUID), linkNodeGuids)
    .GetEnumerableTypedResultAsync(cancellationToken: token);

List<(TreeNode Node, PageUrl URL)> nodesAndURLs = linkedDocuments
    .Select(node => (node, urlRetriever.Retrieve(node)))
    .ToList();
```

With our `nodesAndURLs` array of tuples we have all the data we need to create links to all those Pages 👏!

Of course, if we instead use Kentico Xperience's `IPageRetriever` service, the `.WithPageUrlPaths()` extension gets applied for us automatically:

```csharp
Guid[] linkedNodeGuids = // ...

var linkedDocuments = await pageRetriever.RetrieveAsync<TreeNode>(
    q => q.WhereIn(nameof(TreeNode.NodeGUID), linkNodeGuids),
    cancellationToken: token);

List<(TreeNode Node, PageUrl URL)> nodesAndURLs = linkedDocuments
    .Select(node => (node, urlRetriever.Retrieve(node)))
    .ToList();
```

Isn't that nice 😁!

## Conclusion

The convenience of the Kentico Xperience libraries help us developers create the applications our businesses need, quickly and with a lot of flexibility 💪🏽.

That flexibility can come at a cost that we might not notice during local development or when a site isn't under heavy load.

Caching helps solve a lot of inevitable performance limitations and mistakes, but it's best if we can make the right choices the first time (especially if its only the difference of 1 method overload vs another 😉).

When trying to get URLs for Pages, especially in bulk, our best choice is to use `IPageUrlRetriever.Retrieve(TreeNode node, ...)` and then make sure the `TreeNode` being passed was retrieved from the database using a `DocumentQuery` that called `.WithPageUrlPaths()` with the correct culture... otherwise we might end up causing N + 1 (or worse!) querying against the database.

The `IPageRetriever.RetrieveAsync()` method can at least make sure `.WithPageUrlPaths()` is applied to our query, so we don't have to remember to do it 😄.

If there are any other APIs you have questions about, let me know in the comments below.

As always, thanks for reading 🙏!

## References

- [Kentico Xperience - Content Tree Routing](https://docs.xperience.io/developing-websites/implementing-routing/content-tree-based-routing)
- [There's No Free Lunch](https://en.wikipedia.org/wiki/There_ain%27t_no_such_thing_as_a_free_lunch)
- [N + 1 Querying](https://stackoverflow.com/a/97253/939634)
- [Object-Relational Mapping](https://en.wikipedia.org/wiki/Object%E2%80%93relational_mapping)
- [Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/)

---

We've put together a list over on [Kentico's GitHub account](https://github.com/Kentico/Home/blob/master/RESOURCES.md) of developer resources. Go check it out!

If you are looking for additional Kentico content, checkout the Kentico or Xperience tags here on DEV.

{% tag kentico %}

{% tag xperience %}

Or my [Kentico Xperience blog series](https://dev.to/seangwright/series), like:

- [Kentico Xperience Xplorations](https://dev.to/seangwright/series/8185)
- [Kentico Xperience MVC Widget Experiments](https://dev.to/seangwright/series/9483)
- [Kentico Xperience Design Patterns](https://dev.to/seangwright/series/10963)