Localized routing using ASP.NET Core MVC 2

This is one implementation of localized routes using ASP.NET Core 2 MVC. [Attributes] are used to control the localized route data for the controllers and actions for simplicity.
Source code: https://github.com/saaratrix/asp.net-core-mvc-localized-routing

Abstract

A localized routing solution using ASP.NET Core 2.1 MVC. The implementation uses attributes on the controllers and actions to determine the localization for each culture. A convention is added in Startup.cs to iterate over all the controllers and actions to set their cultural routes based on the [LocalizationRoute] attributes.

Example usage for a controller for cultures english (default), finnish and swedish:

  1. // Routes for each culture:
  2. // Default: /Home - / for the Index action
  3. // Finnish: /fi/koti - /fi for the Index action.
  4. // Swedish: /sv/Hem - /sv for the Index action.
  5. [LocalizationRoute("fi", "koti")]
  6. [LocalizationRoute("sv", "Hem", "Hemma")] // The link text for <a>linktext</a> will be Hemma
  7. public class HomeController : LocalizationController
  8. {
  9. public IActionResult Index()
  10. {
  11. return View();
  12. }
  13. // Routes for each culture:
  14. // Default: /Home/About
  15. // Finnish: /fi/koti/meistä
  16. // Swedish: /sv/Hem/om
  17. [LocalizationRoute("fi", "meistä")]
  18. [LocalizationRoute("sv", "om")]
  19. public IActionResult About()
  20. {
  21. return View();
  22. }
  23. }
  24.  

Example usage of the cms-culture tag helper in the views for the anchor element:

  1. // Generated html: <a href="/Home/About">Home</a>
  2. <a asp-controller="home" asp-action="about" cms-culture="en">Home</a>
  3. // Generated html: <a href="/fi/koti/meistä">Meistä</a>
  4. <a asp-controller="home" asp-action="about" cms-culture="fi">Home</a>
  5. // Generated html: <a href="/sv/Hem/om">Hemma</a>
  6. <a asp-controller="home" asp-action="about" cms-culture="sv">Home</a>
  7. // Leaving cms-culture="" empty will use the current request culture.
  8. // If user is at /fi/... then finnish culture is used so the generated html would be:
  9. // <a href="/fi/koti/meistä">Meistä</a>
  10. <a asp-controller="home" asp-action="about" cms-culture="">Home</a>
  11.  

Introduction and features

In January 2016 I wrote a blog post detailing how I did localized routing for ASP.NET Core 1.0 MVC Release candidate 1. A reader wrote a comment saying things had changed and some things were deprecated so I decided to update the localized routing solution. This is the updated project meant for ASP.NET Core 2.1 MVC.

When I started out the project in 2016 I tried looking at other solutions and the main features that I wanted were:

  • Multiple cultures/languages.
  • Easy to set up cultures and to modify the routes. Which I believe was achieved because each controller is edited individually for a good overview and fine tuning.
  • The url should start with the culture but not for default culture.
  • Actions and controllers should be localized/translated. For example where Controller = Home and Action = About it has the following routing for english and finnish culture:
    • English: /home/about
    • Finnish: /fi/koti/meistä
  • Default controller and default action aren't required in the url. As an example we're using default controller and default action.
    • English: /
    • Finnish: /fi
    As second example we're only using default action and the controller's name is Example.
    • English: /example
    • Finnish: /fi/esimerkki


Similar Projects

With these features in mind I started looking around the internet for existing implementations to use. The solution closest to what I wanted I found here: www.strathweb.com/2015/11/localized-routes-with-asp-net-5-and-mvc-6/. I did not like this for 2 reasons.
1. Their way of initializing the routes was not how I wanted to do it. Editing the routes in a large dictionary seemed clunky and could get confusing for many controllers and actions.

The way I came up with looks like this for example an action called About.
  1. [LocalizationRoute("fi", "meistä")]
  2. [LocalizationRoute("sv", "om")]
  3. public IActionResult About()
  4. {
  5. return View();
  6. }

2. From what I could see it did not have the cultural part at the start of the url. The culture in their solution from what I understood is stored in a property of the action.

I also looked at other examples which were for mvc 5 or earlier. The examples had things I either didn't understand due to lack of knowledge, or features that it was missing. So in the end I decided to write my own solution and thus here is how I solved localized routing!

Here is my implementation of the localized routing. In my example I am using 3 cultures, en, fi, sv where en is the default culture.

Startup.cs

The Startup.cs is a file that comes with a new ASP.NET project. This file is where initialization and configuring the ASP.NET app happens. For the localized routing to work we need to set it up in Startup.cs.

Localized Routes

The first part in Startup.cs is to tell the LocalizationRouteDataHandler what the default culture is and what cultures we're going to use. We do this in the ConfigureServices method.
Then we add the convention LocalizationRouteConvention. Which is what will iterate over all controllers and actions to generate the localized routes based on the [LocalizationRoute] attributes.
We also configure the RequestLocalizationOptions which is what sets the culture for every http request.
In the ConfigureServices() method we're also setting up using Resource files and IViewLocalizer for translating the view texts.

Request Localization

In Startup.cs in the method Configure() the RequestLocalization for each http request is enabled through this code:

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. var localizationOptions = app.ApplicationServices.GetService<IOptions<RequestLocalizationOptions>>();
  4. app.UseRequestLocalization(localizationOptions.Value);
  5. // ... Other code below here ...
  6. }
  7.  

UrlCultureProvider.cs

This is the class that was added in Startup.cs for every http request to set the culture through the RequestCultureProvider. The code below is what made sure UrlCultureProvider is used for every http request.

  1. options.RequestCultureProviders = new List<IRequestCultureProvider>()
  2. {
  3. new UrlCultureProvider(options.SupportedCultures)
  4. };
  5.  

The default RequestProviders that ASP.NET uses are header, cookie and query string provider. We're only interested in the url which is why a new list is created. You can read more about setting the culture for each request on this website. I looked at the source code of ASP.NET localization to write the UrlCultureProvider. My implementation ONLY supports 2 letters to figure out the culture. So the UrlCultureProvider supports en but not en-US.

LocalizationRouteAttribute.cs

The attribute is attached to each controller or their actions to create the routes for each culture. It does not inherit from the ASP.NET RouteAttribute. The LocalizationRoute attribute is used by the LocalizationRouteConvention to create the routes for each culture for the controllers and actions.

LocalizationController.cs

The LocalizationController is a base controller that sets ViewData["Culture"] = CurrentCulture for every action so you don't have to do that for every single action. It also sets the ViewData["controller"] = ControllerName; and ViewData["action"] = ActionName;. Which is used in the partial view _CultureSelector.cshtml when changing culture so a user stays on the same page. A controller does not need to inherit from LocalizationController. The LocalizationRouteConvention will still generate the localized routes for every controller. The code for the LocalizationController:

CultureActionLinkTagHelper.cs

The <a> tag helper class. The CultureActionLinkTagHelper will based on the controller, action and culture generate the localized url. It also overwrites the link text which is the innerText for the anchor tag. <a>linkText</a>
TagHelper is a new cool feature for ASP.NET Core which allows you to modify the HTMLElements. Here's more information about them: https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.1

How to use

The CultureActionLinkTagHelper is meant to be easy to use. The code is the same as you'd normally write when generating a link except you also add the cms-culture attribute. There are two ways to use it. cms-culture="culture" or leave the value empty and it'll automatically select current culture. cms-culture=""
Here are two examples where [LocalizationRoute("fi", "koti")] was used and the current culture is finnish.

  1. <a asp-controller="home" asp-action="index" cms-culture="fi">Home</a>
  2. <a asp-controller="home" asp-action="index" cms-culture="">Home</a>
  3.  

The snippet above would output the following html output: <a href="/fi/koti">Koti</a>
The code for the CultureActionLinkTagHelper:

If you don't want the link text to be overwritten there is an extra attribute cms-keep-link. If that attribute is true cms-keep-link="true" or by changing the default behavior CultureActionLinkTagHelper.KeepLinkDefault = true. If the default behavior is changed you need to write cms-keep-link="false" to make the TagHelper overwrite the link text instead.

LocalizationTagHelperUtility

LocalizationTagHelperUtility is a class that is used by CultureActionLinkTagHelper and CultureFormLinkTagHelper to get the LocalizationUrlResult data. The LocalizationUrlResult is a struct that contains the localized url and localized link name.

CultureFormLinkTagHelper.cs

The CultureFormLinkTagHelper is for <form> tags and works the same as the CultureActionLinkTagHelper except it doesn't have any link text logic.

LocalizationRouteConvention.cs

This is the heart in setting up all the routing. It iterates over all controllers and all the actions for each controller. It checks if a controller or an action has the [LocalizationRoute] attribute. The convention also checks if the action already has a Selector with an AttributeRouteModel. An example where an action has a selector is for HttpPost or HttpGet with parameters.

Adding route data to the LocalizationRouteDataHandler

For all the controllers & actions that the convention iterates over it will add the route data to the LocalizationRouteDataHandler. If the route data wasn't added to the LocalizationRouteDataHandler then we couldn't generate the localized urls based on a controller name, action name and culture.

The data added to the LocalizationRouteDataHandler is stored in a static Dictionary so it's easy to query based on controller name or action name. The reason for choosing a static Dictionary is because it needs to be shared between all ASP.NET requests. However keys are only added during the LocalizedRouteConvention logic so it should be thread safe and offer better performance over a ConcurrentDictionary.

Code for LocalizedRouteConvention.cs

Here is the entire code for the LocalizedRouteConvention.cs file:

LocalizationRouteDataHandler.cs

This is the class where everything is stored so the TagHelpers can find out what url and link text to generate. The localized url comes from the method GetUrl() GetUrl will return a LocalizationUrlResult struct which has the localized data.

LocalizationUrlResult

How GetUrl works

The declaration for GetUrl is: public static LocalizationUrlResult GetUrl(string controller, string action, string culture).
So it requires a controller, action and culture as input. It then converts controller and action to lowercase so it's case insensitive. Then it checks if route data exists for the controller and action. It also checks if the controller or action are default controller or action. If they are defaults then the url is just the culture: /culture/. Or if action is default then the url is culture + localized controller only /culture/controller

Code for LocalizationDataHandler.cs

Other files

Some other small files mainly used for data structures.

CultureControllerRouteData.cs

This class is for the static dictionary ControllerRoutes in the LocalizationRouteDataHandler. The CultureActionRouteData Actions dictionary keys are the action method names for the controller in lowercase.

CultureActionRouteData.cs

For the dictionary Actions in the CultureControllerRouteData. The UrlData dictionary keys are the cultures. So en, fi or sv in my implementation.

The ParametersData is a sorted string array of all parameters for the action. If the action has no parameters then ParametersData is null.

CultureUrlRouteData.cs

The class used in the CultureActionRouteData UrlData dictionary value. It has the localized action route and link text. The controller route part is from CultureControllerRouteData.Names

Examples

In the example project on github the cultures are en, fi and sv. Where en is the default culture. Here are some examples for two controllers, global error handling, a culture change partial view and usage of IViewLocalizer for the view.

HomeController

This is the default controller with 3 localized routes in total. Note that the finnish route koti is lowercase and the swedish Hem isn't. Both are reachable in any upper/lower case combination but the route that is written in the [LocalizedRoute] attribute is what is used when generating the url.

ExampleController

A controller using parameterised HttpGet and HttpPost for a form.

IViewLocalizer

The project uses IViewLocalizer to do translations for the view texts. It requires some setup to set the IViewLocalizer up. To use the IViewLocalizer I had to install the NuGet package Microsoft.AspNetCore.Mvc.Localization.
There's more information about localizing texts here: docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.1

Startup.cs

The code required in Startup.cs for IViewLocalizer

_ViewImports.cshtml

I chose to inject an IViewLocalizer for all the views in the _ViewImports.cshtml So that an instance called Localizer is always available.

  1. @using Microsoft.AspNetCore.Mvc.Localization
  2. @inject IViewLocalizer Localizer
  3.  

_Layout.cshtml

A short snippet from the default layout edited to use the localized routing. This is the default navbar in a new ASP.NET project edited.

Global Errorhandling

Added a global error handler so the 404 errors e.t.c. are localized. It's done by using a controller and set the error handling in Startup.cs It doesn't have any [LocalizationRoute] attributes because in Startup.cs any errors are re-executed to the ErrorController. So that the url stays the same that made error happen.

Error setup in Startup.cs

The code used in Startup.cs to enable the error handling.

  1. public void Configure(IApplicationBuilder app, IHostingEnvironment env)
  2. {
  3. // ... Other code above or below
  4. app.UseExceptionHandler("/Error");
  5. app.UseStatusCodePagesWithReExecute("/Error/{0}");
  6. }
  7.  

Error view

The razor layout for the error view. The view uses the IViewLocalizer Localizer instance that's injected in the _ViewImports.cshtml

  1. @{
  2. ViewData["Title"] = Localizer["Error"];
  3. }
  4. <h2>@Localizer["An error has occured"]</h2>
  5.  

Culture changing partial

This is an example of changing the culture and keep the controller and action that you're currently on as a user. It also tries to maintain the route parameters.

Changes upgrading from ASP.NET Core release candidate 1 to 2.1.2

Here's a brief list of changes done to upgrade from the old blog post. A lot changed with ASP.NET Core from beta to 2.0! :) This is a link to the github pull request changes. Some of the changes are from upgrading from Visual Studio 2015 to 2017. Many changes also come from upgrading the default ASP.NET authentication template.

  • Updated namespaces from using Microsoft.AspNet.Mvc; to using Microsoft.AspNetCore.Mvc;
  • LocalizedRouteConvention.cs had the most breaking changes. AttributeRoutes property removed from ControllerModel. AttributeRouteModel property removed from ActionModel. Instead a property called Selectors which is an IList<SelectorModel> has been added to both controller and action models. The ActionModel is being cloned for each culture to set up the routing for each action. But since it is cloned it also copies the Selectors list which meant it needs to be cleared or it would have duplicate routes.
  • Startup.cs had some compilation errors changes as well. The constructor for RequestLocalizationOptions had changed. And how the Convention was added, instead of calling Insert() you now call Add()!

Conclusion

It works! I haven't tested this against other cultures like russian, japanese characters if it works as nice there. But for me this works and I hope it does for you too if you use it.

Also thank you readers that have contacted me about issues with the localized routing so that I can improve it for everyone.

Changes made after initial version

When updating from ASP.NET Core 1 to 2 it caused some regressions and loss of feature but they should be resolved since version 2.0.2.

Here is a list of major changes made to fix bugs or other improvements after this was posted on 2017.12.27.
The full changelog can be found here: github.com/saaratrix/asp.net-mvc-core-1.0-localized-routing/blob/master/changelog.md.

2.0.2

  • Fixed bugs and regressions from upgrading ASP.NET Core 1 to 2. A reader called Jesus reported some of the bugs that were solved.
  • Added examples for localized error handling, IViewLocalizer, culture selector.

2.0.1

  • Fixed a bug from upgrading to 2.0 reported by a comment written by Ruedi with [HttpGet] and [HttpPost] The bug was with the new Selectors and they were cleared in the convention when copying the ActionModel which as a side effect removed the [HttpGet] and [HttpPost] constraints. So the solution that Ruedi in the comment gave fixed the bug by adding the action constraints back to the selectors.
  • Improved routing generated from anchor TagHelper when using asp-route-parameter. This was done through updating the LocalizedRouteConvention.cs so that it used the variable string route = attribute.Route + parameterTemplate;. Instead of just using the attribute.Route

References

The references used in this article.

  1. https://github.com/saaratrix/asp.net-core-mvc-localized-routing
  2. https://www.strathweb.com/2015/11/localized-routes-with-asp-net-5-and-mvc-6/
  3. https://www.jerriepelser.com/blog/how-aspnet5-determines-culture-info-for-localization
  4. https://docs.microsoft.com/en-us/aspnet/core/mvc/views/tag-helpers/intro?view=aspnetcore-2.1
  5. https://docs.microsoft.com/en-us/aspnet/core/fundamentals/localization?view=aspnetcore-2.1