In this post, I show how you can use the 'middleware as filters' feature of ASP.NET Core 1.1.0 to easily add request localisation based on url segments.

The end goal we are aiming for is to easily specify the culture in the url, similar to the way Microsoft handle it on their public website. If you navigate to https://microsoft.com, then you'll be redirected to https://www.microsoft.com/en-gb/ (or similar for your culture)

Microsoft home page with localised url

Using URL parameters is one of the approaches to localisation Google suggests as it is more user and SEO friendly than some of the other options.

Localisation in ASP.NET Core 1.0.0

The first step to localising your application is to associate the current request with a culture. Once you have that, you can customise the strings in your request to match the culture as required.

Localisation is already perfectly possible in ASP.NET Core 1.0.0 (and the subsequent patch versions). You can localise your application using the RequestLocalizationMiddleware, and you can use a variety of providers to obtain the culture from cookies, querystrings or the Accept-Language header out of the box.

It is also perfectly possible to write your own provider to obtain the culture from somewhere else, from the url for example. You could use the RoutingMiddleware to fork the pipeline, and extract a culture segment from it, and then run your MVC pipeline inside that fork, but you would still need to be sure to handle the other fork, where the cultured url pattern is not matched and a culture can't be extracted.

While possible, this is a little bit messy, and doesn't necessarily correspond to the desired behaviour. Luckily, in ASP.NET Core 1.1.0, Microsoft have added two features that make the process far simpler: middleware as filters, and the RouteDataRequestCultureProvider.

In my previous post, I looked at the middleware as filters feature in detail, showing how it is implemented; in this post I'll show how you can put the feature to use.

The other piece of the puzzle, the RouteDataRequestCultureProvider, does exactly what you would expect - it attempts to identify the current culture based on RouteData segments. You can use this as a drop-in provider if you are using the RoutingMiddleware approach mentioned previously, but I will show how to use it in the MVC pipeline in combination with the middleware as filters feature. To see how the provider can be used in a normal middleware pipeline, check out the tests in the localisation repository on GitHub.

Setting up the project

As I mentioned, these features are all available in the ASP.NET Core 1.1.0 release, so you will need to install the preview version of the .NET core framework. Just follow the instructions in the announcement blog post.

After installing (and fighting with a couple of issues), I started by scaffolding a new web project using

dotnet new -t web  

which creates a new MVC web application. For simplicity I stripped out most of the web pieces and added a single ValuesController, That would simply write out the current culture when you hit /Values/ShowMeTheCulture:

public class ValuesController : Controller  
{
    [Route("ShowMeTheCulture")]
    public string GetCulture()
    {
        return $"CurrentCulture:{CultureInfo.CurrentCulture.Name}, CurrentUICulture:{CultureInfo.CurrentUICulture.Name}";
    }
}

Adding localisation

The next step was to add the necessary localisation services and options to the project. This is the same as for version 1.0.0 so you can follow the same steps from the docs or my previous posts. The only difference is that we will add a new RequestCultureProvider.

First, add the Microsoft.AspNetCore.Localization.Routing package to your project.json. You may need to update some other packages too to ensure the versions align. Note that not all the packages will necessarily be 1.1.0, it depends on the latest versions of the packages that shipped.

{
  "dependencies": {
    "Microsoft.NETCore.App": {
      "version": "1.1.0",
      "type": "platform"
    },
    "Microsoft.AspNetCore.Mvc": "1.1.0",
    "Microsoft.AspNetCore.Routing": "1.1.0",
    "Microsoft.AspNetCore.Server.Kestrel": "1.0.1",
    "Microsoft.Extensions.Configuration.EnvironmentVariables": "1.0.0",
    "Microsoft.Extensions.Configuration.Json": "1.0.0",
    "Microsoft.Extensions.Options": "1.1.0",
    "Microsoft.Extensions.Logging": "1.0.0",
    "Microsoft.Extensions.Logging.Console": "1.0.0",
    "Microsoft.Extensions.Logging.Debug": "1.0.0",
    "Microsoft.AspNetCore.Localization.Routing": "1.1.0"
  },

You can now configure the RequestLocalizationOptions in the ConfigureServices method of your Startup class:

public void ConfigureServices(IServiceCollection services)  
{
    // Add framework services.
    services.AddMvc();

    var supportedCultures = new[]
    {
        new CultureInfo("en-US"),
        new CultureInfo("en-GB"),
        new CultureInfo("de"),
        new CultureInfo("fr-FR"),
    };

    var options = new RequestLocalizationOptions()
    {
        DefaultRequestCulture = new RequestCulture(culture: "en-GB", uiCulture: "en-GB"),
        SupportedCultures = supportedCultures,
        SupportedUICultures = supportedCultures
    };
    options.RequestCultureProviders = new[] 
    { 
         new RouteDataRequestCultureProvider() { Options = options } 
    };

    services.AddSingleton(options);
}

This is all pretty standard up to this point. I have added the cultures I support, and defined the default culture to be en-GB. Finally, I have added the RouteDataRequestCultureProvider as the only provider I will support at this point, and registered the options in the DI container.

Adding localisation to the urls

Now we've setup our localisation options, we just need to actually try and extract the culture from the url. As a reminder, we are trying to add a culture prefix to our urls, so that /controller/action becomes /en-gb/controller/action or /fr/controller/action. There are a number of ways to achieve this, but if your are using attribute routing, one possibility is to add a {culture} routing parameter to your route:

[Route("{culture}/[controller]")]
public class ValuesController : Controller  
{
    [Route("ShowMeTheCulture")]
    public string GetCulture()
    {
        return $"CurrentCulture:{CultureInfo.CurrentCulture.Name}, CurrentUICulture:{CultureInfo.CurrentUICulture.Name}";
    }
}

With the addition of this route, we can now hit the urls defined above, but we're not yet doing anything with the {culture} segment, so all our requests use the default culture:

The default culture is used even though the url indicates a culture

To actually convert that value to a culture we need the middleware as filters feature.

Adding localisation using a MiddlewareFilter

In order to extract the culture from the RouteData we need to run the RequestLocalisationMiddleware, which will use the RouteDataRequestCultureProvider. However, in this case, we can't run it as part of the normal middleware pipeline.

Middleware can only use data that has been added by preceding components in the pipeline, but we need access to routing information (the RouteData segments). Routing doesn't happen till the MVC middleware runs, which we need to run to extract the RouteData segments from the url. Therefore, we need request localisation to happen after action selection, but before the action executes; in other words, in the MVC filter pipeline.

To use a MiddlewareFilter, use first need to create a pipeline. This is like a mini Startup file in which you Configure an IApplicationBuilder to define the middleware that should run as part of the pipeline. You can configure several middleware to run in this way.

In this case, the pipeline is very simple, as we literally just need to run the RequestLocalisationMiddleware:

public class LocalizationPipeline  
{
    public void Configure(IApplicationBuilder app, RequestLocalizationOptions options)
    {
        app.UseRequestLocalization(options);
    }
}

We can then apply this pipeline using a MiddlewareFilterAttribute to our ValuesController:

[Route("{culture}/[controller]")]
[MiddlewareFilter(typeof(LocalizationPipeline))]
public class ValuesController : Controller  
{
    [Route("ShowMeTheCulture")]
    public string GetCulture()
    {
        return $"CurrentCulture:{CultureInfo.CurrentCulture.Name}, CurrentUICulture:{CultureInfo.CurrentUICulture.Name}";
    }
}

Now if we run the application, you can see the culture is resolved correctly from the url:

Culture being resolved correctly

And there you have it. You can now localise your application using urls instead of querystrings or cookie values. There is obviously more to getting a working solution together here. For example you need to provide an obvious route for the user to easily switch cultures. You also need to consider how this will affect your existing routes, as clearly your urls have changed!

Optional RouteDataRequestCultureProvider configuration

By default, the RouteDataRequestCultureProvider will look for a RouteData key with the value culture when determining the current culture. It also looks for a ui-culture key for setting the UI culture, but if that's missing then it will fallback to culture, as you can see in the previous screenshots. If we tweak the ValuesController, RouteAttribute to be

Route("{culture}/{ui-culture}/[controller]")]  

then we can specify the two separately:

Specifying a different UI culture

When configuring the provider, you can change the RouteData keys to something other that culture and ui-culture if you prefer. It will have no effect on the final result, it will just change the route tokens that are used to identify the culture. For example, we could change the culture RouteData parameter to be lang when configuring the provider:

options.RequestCultureProviders = new[] {  
    new RouteDataRequestCultureProvider() 
        { 
            RouteDataStringKey = "lang",
            Options = options
        } 
    };

We could then write our attribute routes as

Route("{lang}/[controller]")]  

Summary

In this post I showed how you could use the url to localise your application by making use of the MiddlewareFilter and RouteDataRequestCultureProvider that are provided in ASP.NET Core 1.1.0. I will write a couple more posts on using this approach in practical applications.

If you're interested in how the ASP.NET team implemented the feature, then check out my previous post. You can also see an example usage on the announcement page and on Hisham's blog.