EPiServer simple address with injected URL slug in multi language website

So you have successfully injected a custom slug at the start of the URL, and gotten it to work in an EPiServer installation with multiple languages. However, now the simple address feature is broken. It only returns 404 Not Found errors.

In this article we will continue the example with the injected region slug from the previous posts. I worked on simple address support together with Martin Lindström from Knowit.

Making simple addresses work with custom routing in EPiServer

The EPiServer functionality responsible for feeding you the correct content when you surf to a page via a simple address is located in a class called SimpleAddress. This is what we will have to extend, and we will do so by creating another class called RegionalSimpleAddress.

Since EPiServer injects both the concrete class SimpleAddress as well as it’s interface ISimpleAddressResolver in the source code, we will need to replace both injections.

For(typeof(EPiServer.Web.Internal.SimpleAddress))
  .ClearAll()
  .Use(typeof(RegionalSimpleAddress));

For(typeof(EPiServer.Web.ISimpleAddressResolver))
  .ClearAll()
  .Use(typeof(RegionalSimpleAddress));

EPiServer also injects the class by retrieving all concrete instances of the ISimpleAddressResolver interface, so it will not be enough to just inherit from their concrete class.

RegionalSimpleAddress.cs

public class RegionalSimpleAddress : SimpleAddress, ISimpleAddressResolver
{
  [Obsolete("EPi marked this obsolete.")]
  public RegionalSimpleAddress(
    IContentRepository contentRepository,
    ServiceAccessor<IPageQuickSearch> pageQuickSearch,
    ISiteDefinitionResolver siteDefinitionResolver,
    ISiteDefinitionRepository siteDefinitionRepository,
    IVirtualPathResolver virtualPathResolver) 
  : base(contentRepository, pageQuickSearch, siteDefinitionResolver, siteDefinitionRepository, virtualPathResolver)
  {
  }

  public RegionalSimpleAddress(
    IContentRepository contentRepository,
    ServiceAccessor<IPageQuickSearch> pageQuickSearch,
    ISiteDefinitionResolver siteDefinitionResolver,
    ISiteDefinitionRepository siteDefinitionRepository,
    IVirtualPathResolver virtualPathResolver,
    IContentLanguageAccessor contentLanguageAccessor)
  : base(contentRepository, pageQuickSearch, siteDefinitionResolver, siteDefinitionRepository, virtualPathResolver, contentLanguageAccessor)
  {
  }

What we will have to do in the four methods in this class is basically the same. We need to take the input UrlBuilder object, which may contain our injected region slug, and clean it. This URL may also contain a language code which EPiServer would have been able to handle itself, if our region slug wasn’t in there.

So since an URL without an injected custom region slug is called national, we have created a method called NationalPathFrom, which takes an UrlBuilder object. This clean URL will then be passed to EPiServer’s own implementation of the ISimpleAddressResolver, allowing it to find the correct content.

Note that we will be hiding the base implementations of some methods.

  public new static object SimpleAddressToInternal(UrlBuilder url)
  {
    string nationalPath = NationalPathFrom(url);
    url.Path = nationalPath;

    return EPiServer.Web.Internal.SimpleAddress .SimpleAddressToInternal(url);
  }

  public new static bool SimpleAddressToInternal(UrlBuilder url, ref object internalObject)
  {
    string nationalPath = NationalPathFrom(url);
    url.Path = nationalPath;

    return EPiServer.Web.Internal.SimpleAddress .SimpleAddressToInternal(url, ref internalObject);
  }

  public new SimpleAddressResolveResult Resolve(UrlBuilder url, SimpleAddressResolveContext simpleAddressContext)
  {
    string nationalPath = NationalPathFrom(url);
    url.Path = nationalPath;
    SimpleAddressResolveResult result = base.Resolve(url, simpleAddressContext);

    return result;
  }

  public override bool TryResolveAsSimpleAddress(UrlBuilder url, SimpleAddressResolveContext context, out object internalObject)
  {
    string nationalPath = NationalPathFrom(url);
    url.Path = nationalPath;

    return base.TryResolveAsSimpleAddress(url, context, out internalObject);
  }

We do not really know how the friendly URL coming into our SimpleAddress implementation may look. The only thing that we are sure of is that it may or may not contain an injected region slug as the first or second element.

So a simple address regionalized in Stockholm may either look like /Stockholm/simple-address or if we’re not on the default language /so-SO/Stockholm/simple-address (so-SO is the language tag for Somali).

However, how can we be sure that the URL does not contain the region name somewhere else? Well, since it’s up to the editor, we cannot. Therefore we can’t just remove the instances of known regions from the URL.

You will likely need to implement your own version of the cleaning code, but here is what we came up with.

  private static string NationalPathFrom(UrlBuilder url)
  {
    string[] slugs = url.Path.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries);

    // Example: /region/simple-address
    var hasRegionNoLanguage = slugs.TryGetRegionAtPosition(0, out Region temp);

    // Example: /so-SO/region/simple-address
    var hasRegionWithLanguage = slugs.TryGetRegionAtPosition(1, out temp);

    if (!hasRegionNoLanguage && !hasRegionWithLanguage)
    {
      // Incoming simple address does not seem to have region slug in first or second element.
      return url.Path;
    }

    int position = hasRegionNoLanguage ? 0 : 1;
    slugs = slugs.Where((val, index) => index != position).ToArray();

    string deregionalizedPath = $"/{string.Join("/", slugs)}";

    return deregionalizedPath;
  }
}

The URL is split into an array containing all the slugs, without any empty entries. We know that either index zero or index one may contain a region slug, so we use a small extention method (below) for checking. You can ignore the out parameter if you’d like. We are using the same code elsewhere, where we actually need it.

If the URL does not contain a recognizable region slug in it’s first or second element, we know that it’s already a national URL (a clean one, that EPiServer will understand).

On the other hand, if we do have a region slug, we remove it and put the URL back together again, leaving the possible language code intact

StringArrayExtensions.cs

public static class StringArrayExtensions
{
  public static bool TryGetRegionAtPosition(this string[] arrayOfSlugs, int position, out Region region)
  {
    region = null;
    if (position >= arrayOfSlugs.Length)
    {
      return false;
    }

    IRegionRepository repository = ServiceLocator.Current.GetInstance<IRegionRepository>();
    Region national = repository.GetNationalRegion();
    region = repository.GetRegionBySlugOrNull(arrayOfSlugs[position]);

    return region != null && region != national;
  }
}

Above is just an example of what the extension method may look like. Our code wants the region if there is one, and it is not the national one (which would have an empty URL slug element). Crude, but simple.