Improve the Custom Localized MVC Routing with Fluent API<!-- --> | <!-- -->Patrick Desjardins Blog
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Improve the Custom Localized MVC Routing with Fluent API

Posted on: January 4, 2016

Fluent API is a pattern that wrap existing code to create you some helper to build a complex object. This is used by Entity Framework when configuring entities or used by a lot of Mocking framework. It's more verbose, more explicit about what we can do or not. You can recognize Fluent API because they have a form of almost sentence like : yourObject.WhenStock(typeA).IsHigher().Than(100).Then(doThis);

In this article, we will add a Fluent API on top of the custom localized code. Why I am saying "on top"? Because the code previous coded will still work and because it will not interfere at all with the existing code, we will work with this one without touching this one. But first, let's see what we had and what we will have after. Let's use this complex route that can look like this in English and like that in French:

  • /Administrator/investigate/reject/1
  • /Administrateur/investigation/rejeter/1

This is the standard ASP.Net MVC route in English only. To have it in French, you would have to repeat that with French string but it wouldn't have handled the culture thread problem.

1context.MapRoute( Constants.Areas.ADMINISTRATOR + "_OrdersController_Reject" , "Administrator/Investigate/Reject/{" + Constants.RoutePortionName.ID + "}"
2 , new RouteValueDictionary {{ Constants.RoutePortionName.ACTION, "Reject" }, { Constants.RoutePortionName.CONTROLLER, "Orders" }}
3 , new RouteValueDictionary {{ Constants.RoutePortionName.ID,@"\\d+"}, });

In the previous article, we described a way to defined constraints, values, localized tokens and Area-Controller-Action. For our example here, this would look like the following code.

1var areaRoutes = new List<AreaTranslation> {
2new AreaTranslation(Constants.Areas.ADMINISTRATION
3 ,new List<LocalizedSection>{
4 new LocalizedSection(LocalizedSection.EN, "administrator")
5 ,new LocalizedSection(LocalizedSection.FR, "administrateur")
6 }
7 , new List<ControllerSectionLocalized> {
8 new ControllerSectionLocalized("Orders"
9 ,new List<LocalizedSection>{
10 new LocalizedSection(LocalizedSection.EN, "Orders")
11 ,new LocalizedSection(LocalizedSection.FR, "Ordres")
12 ,new List<ActionSectionLocalized> {
13 new ActionSectionLocalized("Reject"
14 ,new List<LocalizedSection>{
15 new LocalizedSection(LocalizedSection.EN, "Reject")
16 ,new LocalizedSection(LocalizedSection.FR, "rejeter")
17 } , null
18 , new RouteValueDictionary { { Constants.RoutePortionName.ID, @"\\d+" } } , Constants.Areas.ADMINISTRATION + "/{investi}/{action}/{" + Constants.RoutePortionName.ID + "}" , new Dictionary<string, List<LocalizedSection>> {{"investi"
19 , new List<LocalizedSection>{
20 new LocalizedSection(LocalizedSection.EN, "investigate")
21 , new LocalizedSection(LocalizedSection.EN, "investigation")}
22 }
23 };
24 )
25 } )
26 })
27};

This is the final form with the Fluent API that we will discuss.

1var areaRoutes = FluentLocalizedRoute
2 .BuildRoute()
3 .ForBilingualArea(Constants.Areas.ADMINISTRATION, "administrator", "administrateur")
4 .WithBilingualController("Orders", "Orders", "Ordres")
5 .WithBilingualAction("Reject", "Reject", "rejeter")
6 .WithConstraints(Constants.RoutePortionName.ID, @"\\d+")
7 .WithUrl(Constants.Areas.ADMINISTRATION + "/{investi}/{action}/{" + Constants.RoutePortionName.ID + "}")
8 .WithTranslatedTokens("investi", "investigate", "investigation")
9 .ToAreaList();

I think it's pretty clear the benefit of using Fluent API in that particular case. However, do not get me wrong, Fluent API is not ideal in a lot of situation. First, as you will see, it add a lot of code if you want to have your Fluent API to restrict what operation you can do depending of which action you use. Second, it also creates additional codes to unit test, to support and to improve in the future.

There is many different ways to use the "starting point" with a Fluent API. You could create an extension method that let you use an existing object to be enhance. Or, you can use a static object like a builder. I am using the second idea because I am not enhancing an existing object but create a new collection of objects. However, I could have extend the RouteCollection for example to do the same. The first class is the entry point, could have been simply flatten out directly inside the RouteBuilder. I built it this way to allow me to add later more route builder.

1public static class FluentLocalizedRoute {
2 public static RouteBuilder BuildRoute() {
3 return new RouteBuilder();
4 }
5}

The second one is the builder itself. I have two possibilities. One to create a list of route that doesn't have areas and one that has area support.

1public class RouteBuilder: IRouteBuilder {
2 public List<ControllerSectionLocalized> ControllerList { get; }
3 public List<AreaSectionLocalized> AreaList { get; }
4
5 public RouteBuilder() {
6 this.ControllerList = new List<ControllerSectionLocalized>();
7 this.AreaList = new List<AreaSectionLocalized>();
8 }
9
10 public IRouteBuilderController ForBilingualController(string controllerName, string controllerEnglishLocalizedString, string controllerFrenchLocalizedString) {
11 var controllerSectionLocalized = new ControllerSectionLocalized(controllerName, new List<LocalizedSection>{ new LocalizedSection(LocalizedSection.EN,controllerEnglishLocalizedString) ,new LocalizedSection(LocalizedSection.FR, controllerFrenchLocalizedString) } ,null); this.ControllerList.Add(controllerSectionLocalized); if (this.AreaList.Any()) {
12 this.AreaList.Last().ControllerTranslations.Add(controllerSectionLocalized);
13 }
14 var rbc = new RouteBuilderController(controllerSectionLocalized, this);
15 return rbc;
16 }
17
18 public IRouteBuilderArea ForBilingualArea(string areaName, string areaEnglishLocalizedString, string areaFrenchLocalizedString) {
19 var areaLocalized = new AreaSectionLocalized(areaName, new List<LocalizedSection>{
20 new LocalizedSection(LocalizedSection.EN,areaEnglishLocalizedString)
21 ,new LocalizedSection(LocalizedSection.FR, areaFrenchLocalizedString)
22 } , null);
23 this.AreaList.Add(areaLocalized);
24 var rbc = new RouteBuilderArea(areaLocalized, this);
25 return rbc;
26 }
27}

So far, we can do :

1var areaRoutes = FluentLocalizedRoute.BuildRoute()
2 .ForBilingualArea(Constants.Areas.CONTEST, Constants.Areas.CONTEST, Constants.Areas.CONTEST)
3 ...

You will notice that every methods return an Interface from that point of this article. This is how you define what will appears when you cascade the instruction. It also filters what you can do or not. For example, in the previous code, the ForBilingualController returns a IRouteBuilderController, and the ForBilingualArea returns IRouteBuilderArea . Hence, you cannot do the same actions. Here is what you can do in both case.

1public interface IRouteBuilderController { IRouteBuilderAction WithBilingualAction(string actionName, string actionEnglishLocalizedString, string actionFrenchLocalizedString); } public interface IRouteBuilderArea { IRouteBuilderController WithBilingualController(string controllerName, string controllerEnglishLocalizedString, string controllerFrenchLocalizedString); }

It's similar but not the same. The controller lets you create an action, while the area let you create a controller. As you can see, the return is IRouteBuilderController which converge both path to the controller. That make sense since Area > Controller > Action. This is interesting because it will help us limiting the number of interface. However, keep it mind that a rich API will probably have 1 interface per underlying method. The reason is that each method you have or property you want to set may have some restriction. For example, we will see that once you have setup an action, you can create an Url, a constraint or a value but not in the controller, neither in the area.

Both Area and Controller builder takes care of defining the interface contract. It's pretty limited what they can do. They can only localize their name and define the collection of child (one will be controllers and the other one actions).

1public class RouteBuilder: IRouteBuilder {
2 public List<ControllerSectionLocalized> ControllerList { get; }
3 public List<AreaSectionLocalized> AreaList { get; }
4
5 public RouteBuilder() {
6 this.ControllerList = new List<ControllerSectionLocalized>();
7 this.AreaList = new List<AreaSectionLocalized>();
8 }
9
10 public IRouteBuilderController ForBilingualController(string controllerName, string controllerEnglishLocalizedString, string controllerFrenchLocalizedString) {
11 var controllerSectionLocalized = new ControllerSectionLocalized(controllerName, new List<LocalizedSection>{
12 new LocalizedSection(LocalizedSection.EN,controllerEnglishLocalizedString) ,
13 new LocalizedSection(LocalizedSection.FR, controllerFrenchLocalizedString) } ,null);
14 this.ControllerList.Add(controllerSectionLocalized);
15 if (this.AreaList.Any()) {
16 this.AreaList.Last().ControllerTranslations.Add(controllerSectionLocalized);
17 }
18 var rbc = new RouteBuilderController(controllerSectionLocalized, this);
19 return rbc;
20 }
21
22 public IRouteBuilderArea ForBilingualArea(string areaName, string areaEnglishLocalizedString, string areaFrenchLocalizedString) {
23 var areaLocalized = new AreaSectionLocalized(areaName, new List<LocalizedSection>{
24 new LocalizedSection(LocalizedSection.EN,areaEnglishLocalizedString)
25 ,new LocalizedSection(LocalizedSection.FR, areaFrenchLocalizedString)
26 } , null);
27 this.AreaList.Add(areaLocalized);
28 var rbc = new RouteBuilderArea(areaLocalized, this);
29 return rbc;
30 }
31}
32
33public class RouteBuilderArea : IRouteBuilderArea {
34
35 private readonly AreaSectionLocalized currentControllerSection;
36 private readonly RouteBuilder routeBuilder;
37
38 public RouteBuilderArea(AreaSectionLocalized controllerSection, RouteBuilder routeBuilder) {
39 this.currentControllerSection = controllerSection;
40 this.routeBuilder = routeBuilder;
41 }
42
43 public IRouteBuilderController WithBilingualController(string controllerName, string controllerEnglishLocalizedString, string controllerFrenchLocalizedString) {
44 if (this.currentControllerSection.ControllerTranslations == null) {
45 this.currentControllerSection.ControllerTranslations = new List<ControllerSectionLocalized>();
46 }
47
48 var controllerSectionLocalized = new ControllerSectionLocalized(controllerName, new List<LocalizedSection>{
49 new LocalizedSection(LocalizedSection.EN,controllerEnglishLocalizedString) ,
50 new LocalizedSection(LocalizedSection.FR, controllerFrenchLocalizedString)
51 }, null);
52
53 if (this.routeBuilder.AreaList.Any()) {
54 this.routeBuilder.AreaList.Last().ControllerTranslations.Add(controllerSectionLocalized);
55 }
56 this.currentControllerSection.ControllerTranslations.Add(controllerSectionLocalized);
57 return new RouteBuilderController(controllerSectionLocalized, routeBuilder);
58 }
59
60}

The action interface is getting interesting because of the number of possible action. When the controller is defined, you can do several actions for an action. You can define default value, constraints, url, defines token, create a new action or create a new controller and of course outputting the whole list.

1public interface IRouteBuilderAction : IRouteBuilderAction_Defaults, IRouteBuilderAction_Constraints, IRouteBuilderAction_Url, ITranslatedTokens, IRouteBuilderAction_ToList {
2
3}
4
5public interface IRouteBuilderAction_Defaults: IRouteBuilderAction_Constraints, IRouteBuilderAction_Url, IRouteBuilderAction_ToList { IRouteBuilderAction_Defaults WithDefaultValues(object values); }
6
7public interface IRouteBuilderAction_Constraints: IRouteBuilderAction_Url, IRouteBuilderAction_ToList { IRouteBuilderAction_Constraints WithConstraints(object constraints); IRouteBuilderAction_Constraints WithConstraints(string constraintName, object constraint); }
8
9public interface IRouteBuilderAction_Url: IRouteBuilderAction_ToList, IRouteBuilder {
10
11IRouteBuilderAction_ToList WithUrl(string url); IRouteBuilderAction_ToList UseEmptyUrl(); IRouteBuilderAction_ToList UseDefaulUrl(); }
12
13public interface IRouteBuilderAction_ToList: IRouteBuilder, IAndAction, ITranslatedTokens { List<ControllerSectionLocalized> ToList(); List<AreaSectionLocalized> ToListArea(); }
14
15public interface IAndAction { IRouteBuilderController And(); } public interface ITranslatedTokens { IRouteBuilderAction_ToList WithTranslatedTokens(string tokenKey, string english, string french); }

So for each scenario, you end up with interfaces, and also it's implementation. Most of these implementation return an interface that the Action class will inherit, thus we can return the same class. This is the reason that in the implementation, RouteBuilderAction , you will see that a lot of methods return this. This allows to chain actions on the same object.

1public class RouteBuilderAction : IRouteBuilderAction
2 , IRouteBuilderAction_Defaults
3 , IRouteBuilderAction_Constraints
4 , IRouteBuilderAction_Url
5 , IRouteBuilderAction_ToList {
6 private List<ActionSectionLocalized> listActions = new List<ActionSectionLocalized>();
7 private ControllerSectionLocalized currentControllerSection;
8 private readonly ActionSectionLocalized currentAction;
9 private RouteBuilderController routeBuilderController;
10 private RouteBuilder routeBuilder;
11
12public RouteBuilderAction(ControllerSectionLocalized controllerSection , ActionSectionLocalized currentAction , RouteBuilder routeBuilder , RouteBuilderController routeBuilderController) {
13 currentControllerSection = controllerSection;
14 this.currentAction = currentAction;
15 this.routeBuilder = routeBuilder;
16 this.routeBuilderController = routeBuilderController;
17}
18
19public List<ControllerSectionLocalized> ToList() { return routeBuilder.ControllerList; }
20
21public List<AreaSectionLocalized> ToListArea() { return routeBuilder.AreaList; }
22
23public IRouteBuilderAction_ToList UseEmptyUrl() {
24 currentAction.Url = string.Empty;
25 return this;
26}
27
28public IRouteBuilderAction_ToList UseDefaulUrl() {
29 currentAction.Url = "{area}/{controller}/{action}";
30 return this;
31}
32
33public IRouteBuilderAction_Constraints WithConstraints(object constraints) {
34 if (currentAction.Constraints == null) {
35 currentAction.Constraints = new RouteValueDictionary();
36 }
37 var rvd = currentAction.Constraints as RouteValueDictionary;
38
39 if (rvd != null) {
40 var c = constraints as RouteValueDictionary;
41 if (c == null) {
42 c = new RouteValueDictionary(constraints);
43 }
44 c.ToList().ForEach(x => rvd.Add(x.Key, x.Value));
45 }
46 this.currentAction.Constraints = rvd;
47 return this;
48}
49
50public IRouteBuilderAction_Constraints WithConstraints(string constraintName, object constraint) {
51 if (currentAction.Constraints == null) {
52 currentAction.Constraints = new RouteValueDictionary();
53 }
54 var rvd = currentAction.Constraints as RouteValueDictionary;
55 if (rvd != null) {
56 rvd.Add(constraintName, constraint);
57 }
58 return this;
59}
60
61public IRouteBuilderAction_Defaults WithDefaultValues(object values) {
62 currentAction.Values = values;
63 return this;
64}
65
66public IRouteBuilderAction_ToList WithUrl(string url) {
67 currentAction.Url = url;
68 return this;
69}
70
71public IRouteBuilderController And() {
72 AddInActionList();
73 return this.routeBuilderController;
74}
75
76private void AddInActionList() {
77 if (currentAction != null) {
78 listActions.Add(currentAction);
79 }
80}
81
82public IRouteBuilderController ForBilingualController(string controllerName, string controllerEnglishLocalizedString, string controllerFrenchLocalizedString) {
83 AddInActionList();
84 return routeBuilder.ForBilingualController(controllerName, controllerEnglishLocalizedString, controllerFrenchLocalizedString);
85}
86
87public IRouteBuilderArea ForBilingualArea(string areaName, string areaEnglishLocalizedString, string areaFrenchLocalizedString) {
88 AddInActionList();
89 return routeBuilder.ForBilingualArea(areaName, areaEnglishLocalizedString, areaFrenchLocalizedString);
90}
91
92public IRouteBuilderAction WithBilingualAction(string actionName, string actionEnglishLocalizedString, string actionFrenchLocalizedString) {
93 AddInActionList();
94 return routeBuilderController.WithBilingualAction(actionName, actionEnglishLocalizedString, actionFrenchLocalizedString);
95}
96
97public IRouteBuilderAction_ToList WithTranslatedTokens(string tokenKey, string english, string french) {
98
99 if (currentAction != null) {
100 if (this.currentAction.Tokens == null) { this.currentAction.Tokens = new Dictionary<string, List<LocalizedSection>>(); }
101 var tokenToAdd = new Dictionary<string, List<LocalizedSection>>();
102 if (this.currentAction.Tokens.Keys.Any(g => g == tokenKey)) {
103 //Already exist, tbd what we do here, for now nothing
104 } else {
105 this.currentAction.Tokens.Add(tokenKey, new List<LocalizedSection>() {
106 new LocalizedSection(LocalizedSection.EN,english),
107 new LocalizedSection(LocalizedSection.FR, french)
108 });
109 }
110 }
111 return this;
112 }
113}

The Fluent API is not cheap to create, neither something to do on everything. However, in that particular scenario the benefices are tremendous. Not only the code is way shorter to write but it is also limit the duplication by having every collections and properties instantiated in a single place.