NOTE: A reader notified me of some breaking changes in newer ASP.NET Core versions so there is an updated blog post here for version: ASP.NET Core 2.1 MVC
A working localized routing example for ASP.NET Core MVC 1.0 release candidate 1.
The solution uses attributes on the controllers & actions to determine the localized routing for each different culture.
A convention is added to the MvcOptions in startup.cs to iterate over all the controllers & actions to set their routes based on the localized attributes.
I was looking at the new asp.net 5 and I wanted to play around with multiple languages as routing.
NOTE: This was done on RC 1 so might be changes later on.
I had prior to this never done localized routing so I'm bound to have overlooked or done something that can be improved on.
The features that I wanted in my localized routing are:
Multiple Cultures (Languages)
Use database for the different translations in a view. (This part is not implemented)
The url starts with the Culture, eg: /en/home/index
Actions and controllers should be localized
Like home == koti for English and finnish.
Default culture does not use the /culture/ prefix
If en is default culture then /home/index is the url and not /en/home/index
Default controller and default action are empty strings
So if home controller and index action are defaults the url would be / and for finnish /fi/
With these goals in mind I started looking around the web for solutions to implement. The solution closest to what I needed 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. His way of initializing the routes. Was not how I wanted to do it.
var localizedRoutes =newDictionary<stringLocalizedRouteInformation=[]>()
2. From what I could see it did not have the cultural part at the start of the url. The culture in that example is stored in a property of the action.
I also looked at other examples they are for mvc 5 or earlier but also had things I either didn't understand due to lack of knowledge or things that it was missing. So in the end I decided to write my own solution and thus here is how I solved localized routing!
The startupc.cs file that comes with any asp.net 5 setup.
The method bodies of ConfigureServices and Configure has more code that is added by asp.net as default.
I'm just showing the code I wrote for the localization.
The first part in startup.cs is to tell the localization data handler what the default culture is and what the supported cultures are.
Then as normal code would do is add the mvc framework to the service but here in addition we add the LocalizedRouteConvention that will iterate
over all the controller and actions setting up their routing and localized routes.
It will also add the routes to a dictionary used by the CultureTagHelper to get the localized href and linktext when generating the html <a> links.
This is the class added in the Request Localization section above. Asp.net on every request uses RequestProviders to set the culture.
The default ones that asp.net have are header, cookie and querystring provider.
You can read more about it on this website.
I looked at the source code of asp.net localization to write my UrlCultureProvider. My implementetation ONLY supports 2 letters to figure out the culture.
So it does not for example support en-US but only en.
So this UrlCultureProvider supports en but not en-US.
/// <summary>
/// Determines the culture information for a request via the value of the start of a url.
The attribute is attached to each controller or their action to set the cultural route.
It does not inherit from RouteAttribute.
It's used by the LocalizedRouteConvention to create the routes for the controllers & actions.
The localization controller is a base controller that sets ViewData["Culture"] = CurrentCulture for every action so you don't have to do that for every single action.
This is also where I in the future would put the code to fetch translated strings for the view from example the database or a resource file.
The code for the localization controller:
The <a> tag helper class.
Currently the tag helper overwrites any link text even for default culture.
One improvement could be to check if it has text and if it does don't overwrite it.
Tag helper is the new cool thing from asp.net and what this particular helper does is that it listens to the attribute asp-culture to fetch the route for the input culture so for example:
This is the heart in setting up all the routing.
It iterates over all controllers and all the actions in each controller.
It checks if the controller or action has the LocalizedRouteAttribute.
If the controller or action has one the convention adds the localized route.
The convention also checks if the action already has a RouteAttribute for potential parameters.
If the action's RouteAttribute contains parameters it adds them to each localized route aswell.
For all the controllers & actions that the convention finds it will add that data to the datahandler.
To later by the CultureActionLinkTagHelper generate anchor tags like this based on culture:
<ahref="route">linkText</a>
The added data in the datahandler is stored in a ConcurrentDictionary so it's easy to query based on controller name or action name.
The reason for choosing a ConcurrentDictionary is because it's a static variable shared between all asp.net request threads as I do not know how to share it otherwise.
I could not find a way to share it inside a Tag Helper.
This is the class where everything is stored so the CultureActionLinkTagHelper can find out what url and link text to generate.
If you look at the tag helper it will ask the datahandler for a url through GetUrl().
GetUrl itself will return a LocalizedUrlResult struct. that the tag helper will use.
publicstructLocalizedUrlResult
{
/// <summary>
/// The actual url => /home/about
/// </summary>
publicstringUrl;
/// <summary>
/// The inner html for the anchor tag.
/// </summary>
publicstringLinkName;
}
The datahandler will query the dictionary if the controller & action exists.
The input should be what the controller & actions are called in code.
So for the HomeController and action About it would be:
home - about
It automatically makes the input lowercase.
So HomE is okay aswell.
If the datahandler finds the controller and action it will first check if the culture is the default culture.
If culture is default the method will do another if to check if the controller and the action are the defaults.
If controller or action is default it will be an empty string instead of the original name.
This is so that the url points towards eg: localhost:80/ instead of localhost:80/Home/Index.
If the culture isn't default culture the function will query the dictionary for its localization based on the culture input.
If the dictionary doesn't contain the culture it will return a result for the default culture.
After calculating what the url and link text is it will return the LocalizedUrlResult
For the dictionary in the datahandler.
The CultureActionData keys are the action names in the default culture.
Meaning the method name in the controller class.
publicclassCultureControllerData
{
/// <summary>
/// Different controllernames in different cultures
In my project I had the cultures en, fi and sv. Where en is the default culture. Here is an example of the home controller and an extra controller I made for testing.
It works! I havn't tested this against other cultures like russian, japanese characters if it works as nice there. But for me this works fine and I hope it does for you too if you implement it.
I did not in my example implement buttons to change between cultures.
If you want to just change between cultures it's easy to add a link to the root of each culture, for my project it'd be: / , /fi/, /sv/ .
If you want something more fancy where you keep the current controller & action location you need to get the current controller & action and then input that like this:
There also needs to be an extra argument to tell the tag helper not to overwrite the image in my example as the current functionality would do that.
I'm fairly certain the LocalizedRouteAttribute could inherit from RouteAttribute and instead of creating new RouteAttributes the templates could be updated instead by adding the parameters for example.
What can be improved upon regarding existing functionality a potential redirect if you're at like /fi/koti/enAction to redirect the user to /fi/koti/fiAction.
After writing this piece I realise that it's best if I set up github so I can link to the source in its entirety! :) I will do that for future writings and I'll edit this post in future with a github link.
It will also make this writing a lot shorter plus I can focus on the keyparts instead of showing the whole source.
The web is certainly missing posts such as yours, great job. "I am going to try a different approach in a few weeks with my new knowledge about routing in asp.net", can you please elaborate?
Hi, I did try it again and I failed. I'll update the blogpost to reduce any further confusion. So thanks for that and I will try in future to be more clear on what the different approach would have been.
What I tried was to extend the asp.net's route class (IRouter) and change the behavior of the lookup of what route to choose depending on Culture there. I'm sorry I don't remember all the details as it was few months ago and I didn't take notes as it failed.
The web is certainly missing posts such as yours, great job. "I am going to try a different approach in a few weeks with my new knowledge about routing in asp.net", can you please elaborate?
ReplyDeleteHi, I did try it again and I failed. I'll update the blogpost to reduce any further confusion. So thanks for that and I will try in future to be more clear on what the different approach would have been.
DeleteWhat I tried was to extend the asp.net's route class (IRouter) and change the behavior of the lookup of what route to choose depending on Culture there. I'm sorry I don't remember all the details as it was few months ago and I didn't take notes as it failed.
Hi haestflod. Thanks for your helpful post.
ReplyDeleteThere were some changes in the ASP.NET Core since the release: https://github.com/aspnet/Mvc/issues/4043
`AttributeRoutes` is removed from `ControllerModel` class and `AttributeRouteModel` is removed from `ActionModel` class.