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)
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:
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:
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:
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.