In my Web API book, in one of the chapters (source here), I’m discussing an in interesting approach towards route localization, using attribute routing.

The whole idea came from the fact that at some point in the past I used to work on a really large application – 70+ language versions, all of which required localizations on the route level.

That approach allowed you to define a single attribute route at action level (as opposed to, well, 70+ routes), and have it auto-translated by the plugged in infrastructure, as long as you provide the mapping to other languages at application startup.

Let’s have a look at how same type of functionality can be built in ASP.NET MVC 6.

Background

That Web API approach to localizing routes relied on the extensibility points around Web API 2 attribute routing – IDirectRouteProvider. In short, you could plug custom logic into the Web API pipeline, and as the framework was creating routes for the first time (at application startup), a single route attribute could be used to create a number of related attribute routes – each for a different language. The only thing needed was to map the route name that’s the defined on the controller level, to the translations of routes defined in some central place

This was very powerful, but unfortunately the extensibility around attribute routing in MVC 6 is completely different – so if you wish to port this type of route localization feature to an MVC 6 application, you will need to rewrite it from scratch and resort to our good friend (if you have been reading this blog recently), IApplicationModelConvention.

Route localization approach

Here’s what we want to achieve in a few simple steps.

1. We want the ability to define a route on the action level, just like we would do that normally for non localized routes. These routes will represent our default routes for our default culture, say en-Ca.

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

publicclassOrdersController:Controller

{

[LocalizedRoute("order",Name="order")]

publicOrder GetAll()

{

//omitted for brevity

}

[LocalizedRoute("order/{id:int}",Name="orderById")]

publicOrder GetById(intid)

{

//omitted for brevity

}

}

The attribute used here is a subclass of the default RouteAttribute, and introduces an extra property for Culture.

C#

1

2

3

4

5

6

7

8

9

publicclassLocalizedRouteAttribute:RouteAttribute

{

publicLocalizedRouteAttribute(stringtemplate):base(template)

{

Culture="en-Ca";

}

publicstringCulture{get;set;}

}

2. We gave our routes distinct names, so at application startup, we’d like to provide translations for those routes from somewhere (config files, data store, doesn’t matter – whatever suits your needs) and have extra routes automagically created for us. The matching of the default routes defined in the code, and the translated ones should happen by the route name – that’s why we needed it in the first place.

Here is a snippet of ASP.NET 5 Startup code that configures ASP.NET MVC 6 and that introduces route localization:

C#

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

publicclassStartup

{

publicvoidConfigureServices(IServiceCollection services)

{

varlocalizedRoutes=newDictionary<string,LocalizedRouteInformation[]>

{

{

"order",new[]

{

newLocalizedRouteInformation("de-CH","auftrag"),

newLocalizedRouteInformation("pl-PL","zamowienie"),

}

},

{

"orderById",new[]

{

newLocalizedRouteInformation("de-CH","auftrag/{id:int}"),

newLocalizedRouteInformation("pl-PL","zamowienie/{id:int"),

}

}

};

services.AddMvc(o=>o.AddLocalizedRoutes(localizedRoutes));

}

publicvoidConfigure(IApplicationBuilder app,IHostingEnvironment env)

{

app.UseMvc();

}

}

So based on the route name, such as order or orderById, we are defining localized versions for Swiss German and Polish localizations. Again, this data could have been read from an external data source, but it’s hardcoded here for simplicity. Afterwards, the route collection is passed into the magical extension method AddLocalizedRoutes which we’ll dive into in a moment. Same applies for another class used in the snippet above, LocalizedRouteInformation.

Overall looks simple and elegant, doesn’t it?

Route localization under the hood

So let’s look deeper, perhaps first at LocalizedRouteInformation. It’s a simple POCO which we’ll use to contain information about local versions of routes:

C#

1

2

3

4

5

6

7

8

9

10

11

publicclassLocalizedRouteInformation

{

publicstringCulture{get;}

publicstringTemplate{get;}

publicLocalizedRouteInformation(stringculture,stringtemplate)

{

Culture=culture;

Template=template;

}

}

The core part of the work will happen, as mentioned earlier, inside a custom IApplicationModelConvention – and we are going to call it LocalizedRouteConvention.

Let me show you the implementation first, and then we can discuss step by step what’s happening inside it.

So the constructor takes a dictionary – where the key will represent a route name, and the value will be an array of LocalizedRouteInformation instances. We have already seen this structure in play in the previous code snippet so it should not be surprising – after all a single route name can have a bunch of different localized versions attached to it.

We discusssed IApplicationModelConvention a few times on this blog before, but in short, it allows you to modify, process and adapt the controllers, actions and even parameters on them, that the framework discovers at application startup. MVC 6 will call the Apply method of your custom convention class, and that’s where you need to do whatever you are trying to achieve with your custom convention.

In our case we loop through all controllers and all of the actions found on the controllers and try to find if any of them have been decorated with LocalizedRouteAttribute. If that’s the case, it means we should intervene and create more routes (all of the localized versions).

We use the Name property defined on LocalizedRouteAttribute to look up LocalizedRouteInformation instances in the dictionary that was used to initialze the convention object. If something is there, for each of the found localizations, we create instances of AttributeRouteModel using the data stored in LocalizedRouteInformation – which will be a new version of an attribute route.

Afterwards we create a new ActionModel representing each new localized route and attach it to the controller. This is something worth remembering – even if you want to have multiple routes pointing to same action in MVC 6, in reality in the semantic model of the app that MVC 6 holds, each of the routes will end up being a separate ActionModel, as if it was a separate action method altogether.

You may have also noticed that we saved the culture into ActionModelProperties and added a LocalizedRouteFilter to each action. This is not mandatory but is an interesting twist that we can throw in. The filter is shown below:

The filter will pick up the culture from the Properties dictionary and set in on the thread. This way your specific localized route will have correct culture attached to the currently executing thread by the time it reaches the controller, which you can use to do all kinds of interesting things.

Finally, we initially used an extension method to bootstrap all of this so this is the last piece of code we have to write. The extension method is shown below:

You should write a tag helper to create a canonical URL meta tag to supplement this.

Miguel

Hello,

Do you need LocalizedRouteAttribute with a Culture property?
I think it would be a better approach to set the Culture of the default routes globally.

And why setting the name of each route?
Why not getting each route by its controller and action name?
Is this possible in ASP.NET 5 instead of getting it by name?

With these two changes it would be easier to set localized routes.

Thank You,
Miguel

TimotyW

Hi,

this is a nice example of the extensibility point provided by the IApplicationModelConvention interface.
However, this approach is somehow hiding the country or language targeted by the specified route.

The routes you may obtain with your approach are clearly providing better indexing capabilities as a general /order with a language parameter (or whatever other hacky workaround):
/orders
/auftrag
/zamowienie

However, we may obtain more transparent routes for the user by including the locale in the URL
/en/orders (or /en/ca/orders)
/de/auftrag (or /de/ch/auftrag)
/pl/zamowienie (or /pl/pl/zamowienie)

In this way we could also try to provide a language switch if the user enters a wrongly formatted URL (i.e catching /de/orders and performing a 301 to /de/auftrag).

What would your recommended approach to support this kind of routing?

http://blog.sasin.eu Assassyn

My question is how to create to url for a localised route?

Edited: I manged to translate the route but I am stirll trying to understand how it works.

seancorn

This appears to be broken in RC2 – ActionModel has dropped the AttributeRouteModel property. It looks like it has moved to a new ‘SelectorAction’, which i guess is assigned via the new Selectors property… I’m trying to figure it out but struggling. Any advice?

Damien

Hello !

You’re solution is very good for me. But there is some issues with last version of ASP.NET Core.