Routing in Umbraco Part 2: Content Finders
Tags: umbraco, dotnet, routing, cms
Published: 2024-06-03 17:39:42

The [previous article](https://dev.to/hartviglarsen/routing-in-umbraco-part-1-url-segments-4lmj) in this series demoed how to adjust segments for your Umbraco nodes using an `IUrlSegmentProvider`. When an altered segment is returned in an `IUrlSegmentProvider` it becomes a _native_ segment that Umbraco can handle out of the box and no further URL manipulation/rewrite is required. 

But.. what if you wanted to change the path? Say, for example, you have a blog and you want to automatically have the year of the blog post's creation inserted in the URL, such as `/blog/hello-world` should become `/blog/2024/hello-world`. This is where an `IContentFinder` comes in.

When you alter part of a node's _path_ and not simply the _segment_, Umbraco will need some help mapping the [inbound request](https://docs.umbraco.com/umbraco-cms/reference/routing/request-pipeline) to the node, as the new path assigned to the node is not natively known by Umbraco.

This article will show how to provide a new path for a node and how to return that node for the inbound request.


## Changing the path

Much like an `IUrlSegmentProvider` Umbraco has an `IUrlProvider` that can be implemented for change the URL for your nodes.

The interface has two methods: `GetUrl()` and `GetOtherUrls()`. The latter will let your return other URLs to display in the Backoffice on the node's _Info_ Content App.

Below is an example for `GetUrl()`.

```c#
public class BlogPostUrlProvider : IUrlProvider
{
    public UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
    {

    }

    public IEnumerable<UrlInfo> GetOtherUrls(int id, Uri current)
    {

    }
}
```

However, instead of implementing `IUrlProvider`, Umbraco ships with a `DefaultUrlProvider` with a _virtual_ method for both `GetOtherUrls()` and `GetUrl()` that can be overwritten.

```c#
public class BlogPostUrlProvider : DefaultUrlProvider
{
    public BlogPostUrlProvider(IOptionsMonitor<RequestHandlerSettings> requestSettings, 
        ILogger<DefaultUrlProvider> logger, 
        ISiteDomainMapper siteDomainMapper, 
        IUmbracoContextAccessor umbracoContextAccessor, 
        UriUtility uriUtility, 
        ILocalizationService localizationService) : base(requestSettings, logger, siteDomainMapper, umbracoContextAccessor, uriUtility, localizationService)
    { }
}
```

> Note that `ILocalizationService` is removed in V15. Unfortunately DefaultUrlProvider does not yet have a constructor that accepts ILanguageService. If you want to skip changing the injected interface as part of a V15 upgrade, consider doing a full `IUrlProvider` implementation. Inspiration can be found on [Github](https://github.com/umbraco/Umbraco-CMS/blob/contrib/src/Umbraco.Core/Routing/DefaultUrlProvider.cs).

Change the blog post's `UrlInfo` in `GetUrl()` like so:

```c#
public override UrlInfo? GetUrl(IPublishedContent content, UrlMode mode, string? culture, Uri current)
{
    if (content.ContentType.Alias != "blogPost") return null;
    
    var segment = content.UrlSegment;
    if (segment is null) return null;
    
    var currentUrlInfo = base.GetUrl(content, mode, culture, current);
    if (currentUrlInfo is null) return null;
    
    // To add the post's created year one could 
    // simply replace the post's segment in the 
    // URL with year/segment.
    var currentUrl = currentUrlInfo.Text;
    var customUrl = currentUrl.Replace(segment,
        $"{content.CreateDate.Year}/{segment}");

    return new UrlInfo(customUrl, true, culture);
}
```

Register the `BlogPostUrlProvider`. `Insert<T>()` accepts an `int index`. When no index is provided it defaults to 0.

```c#
public class BlogPostComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.UrlProviders().Insert<BlogPostUrlProvider>();
    }
}
```

Alternatively, if your solution has multiple providers, the provider could be inserted before a specific provider:

```c#
builder.UrlProviders().InsertBefore<DefaultUrlProvider, BlogPostUrlProvider>();
```

The following methods are available for registering your provider. The same goes for `ContentFinders()`:

```c#
builder.UrlProviders().Append<T>();
builder.UrlProviders().Insert<T>(int index = 0);
builder.UrlProviders().Remove<T>();
builder.UrlProviders().InsertAfter<TAfter,T>();
builder.UrlProviders().InsertBefore<TBefore, T>();
```

If you attempt to view the URL in the Backoffice you will notice that it is not working. This is because we have yet to change the inbound request.

![Blog post with broken URL](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/k03hbx3crr70w8t109or.png)

## Handling the inbound request

Umbraco will go through all registered `IContentFinder`s until one of them returns true, after which no further `IContentFinder`s are executed.

The goal of an `IContentFinder` is therefore to set an `IPublishedContent` for the request and return `true`, so Umbraco can load the node and stop executing the rest of the `IContentFinder`s.

Create a `BlogPostContentFinder` that implements `IContentFinder` and inject `IUmbracoContextAccessor`.

```c#
public class BlogPostContentFinder : IContentFinder
{
    private readonly IUmbracoContextAccessor _contextAccessor;

    public BlogPostContentFinder(IUmbracoContextAccessor contextAccessor)
    {
        _contextAccessor = contextAccessor;
    }

    public Task<bool> TryFindContent(IPublishedRequestBuilder request)
    { 

    }
}
```

In `TryFindContent()` attempt to find a content node by (in this case) the _route_. Provided there is a match, set it as the published content and return `true`.

```c#
public Task<bool> TryFindContent(IPublishedRequestBuilder request)
{ 
    // Get the path for the current request
    var path = request.Uri.GetAbsolutePathDecoded();
    
    // In this example site there will not be other URLs using four digits
    // between two slashes, so they can easily be removed safely like so.
    // E.g. /blog/2024/hello-world -> /blog/hello-world
    var url = Regex.Replace(path, "/[0-9]+/", "/");
    
    if (!_contextAccessor.TryGetUmbracoContext(out var context)) return Task.FromResult(false);
    
    // Attempt to find the content
    var content = context.Content?.GetByRoute(url);
    if (content is null) return Task.FromResult(false);

    // Set it as the published content for the request
    request.SetPublishedContent(content);
    
    // Return true to let Umbraco know a match was found
    return Task.FromResult(true);
}
```

Register the `BlogPostContentFinder`:

```c#
public class BlogPostComposer : IComposer
{
    public void Compose(IUmbracoBuilder builder)
    {
        builder.UrlProviders().Insert<BlogPostUrlProvider>();
        builder.ContentFinders().Append<BlogPostContentFinder>();
    }
}
```

Build your solution and publish a blog post and everything should work:


![Blog post with updated URL in Umbraco](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1znlw0gll4sb0axpog72.png)


![Blog post with updated URL in the frontend](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ne2vasx7guxj9hv0408s.png)