Building XML based routing system using XRouter
Contents
Defining routes in xml files
Routes are part of an application and they are defined through code in the Application_Start event. Routes not changes frequently but whenever they requires a change we need to modify the code. In applications where we need frequent changes in routing configuration, defining routes in code is not a flexible option and a better idea would be to place them in xml files or database.
I've created a simple API named XRouter that helps you to define routes in xml files. XRouter helps you to map, ignore and redirect requests quite easily. It also helps you to manage areas in a flexible way. The main advantage is whenever you change the routes, the routing table is automatically updated and you don't need to restart the application.
XRouter
In XRouter API, the routes has to be defined in an xml file named routes.config that should be placed in the application root. If your application has areas then you can define routes either in multiple files (one per area) or in a single file.
In the simple case (application without areas) the routes.config file looks as shown in the below listing.
<routes> <ignore pattern="{resource}.axd/{*pathInfo}" /> <map name="archive" pattern="blog/archive/{date}" controller="blog" action="archive"> <params> <param name="date" type="date" /> </params> </map> <map name="default" pattern="{controller}/{action}/{id}" controller="blog" action="index"> <params> <param name="id" optional="true" /> </params> </map> </routes>
Listing 1. Sample xml file (routes.config)
The routes element contains mainly three elements: map, ignore and redirect. The map and ignore elements may contain one or more child elements.
The map element
The map element is used to map incoming requests to controllers and actions. The map element also contains child elements to specify default values, constraints, namespaces and datatokens.
<map ..> <params /> <constraints /> <namespaces /> <tokens /> </map>
Listing 2. Structure of the map element
<map name="default" pattern="{controller}/{action}"> </map>
Listing 3. A simple map element
Other interesting attributes of the map element are.
controller - Used to specify the default controller for the route.
action - Used to specify the default action for the route.
method - This is a route constraint and used to specify the http methods (comma separated list) that can be accepted by the route.
<map pattern="/comment/post" controller="comment" action="post" method="POST" />
Listing 4. Specifying http methods
routefiles - The routing system as default not handle requests that matches a physical file in the server. You can override this by setting the routefiles attribute to true.
<map pattern="/premium/images/{filename}" controller="premium" action="index" routefiles="true" />
Listing 5. Setting routefiles to true to handle requests that matches physical files
constraint - This attribute helps to specify a custom constraint for the route. Any class that implements IRouteConstraint is called as custom constraint. If you want to specify a collection of custom constraints then you have to go for the child element constraints.
<map pattern="/contact" constraint="Namespace.CustomConstraint, Assembly" .. />
Listing 6. Specifying custom constraint
disabled - This property is used to temporarily disable the route.
The params element
This is the child element of map element. The params element is used to specify the default values for the parameters in the pattern. The params element can contain one or more param elements. Along with the defaults, you can also specify constraints for parameters.
<map name="default" pattern="{controller}/{action}/{id}" controller="blog" action="index"> <params> <param name="id" optional="true" /> </params> </map>
Listing 7. Specifying details about parameters using params element
Other interesting attributes of the param element are.
value - You can specify a default value for the parameter through the value attribute.
<map pattern="home/{state}" controller="home" action="index"> <params> <param name="state" value="CO" /> </params> </map>
Listing 8. Specifying default value for a parameter
type - You can specify the data type of the parameter through this attribute. Actually this is a route constraint and if the parameter not matches the data type the request will be ignored. Currently you can specify only two values for the type attribute: int or date.
<map name="archive" pattern="blog/archive/{date}" controller="blog" action="archive"> <params> <param name="date" type="date" /> </params> </map>
Listing 9. Specifying data type for the parameter
constraint - You can specify a regular expression constraint for the parameter using this attribute. If the parameter not matches the regular expression the request will be ignored.
<map name="archive" pattern="blog/archive/{y}/{m}/{d}" controller="blog" action="archive"> <params> <param name="y" constraint="\d{4}" /> <param name="m" constraint="\d{2}" /> <param name="d" constraint="\d{2}" /> </params> </map>
Listing 10. Specifying regular expression constraints for parameters
The constraints element
The constraints for a route can be specified at different levels. You can specify at the map element using the method and constraint attributes. You can specify at the parameter level using type and constraint attributes. If you want to specify a collection of custom route constraints then you have to go for constraints element which is the child element of map element.
<map pattern="/contact" controller="contact" action="index"> <constraints> <constraint name="ipconstraint" value="Namespace.IPConstraint, Assembly" /> <constraint name="cultureconstraint" value="NamespaceCultureConstraint, Assembly" /> </constraint> </map>
Listing 11. Specifying collection of custom route constraints
You can temporarily disable a constraint by setting the disabled attribute as true.
The namespaces and tokens elements
The namespaces of the controllers that going to handle the requests mapped by the route can be specified using the namespaces element. The tokens element is used to pass additional values to the route.
<map pattern="/support/{action}" controller="support"> <namespaces> <ns>Namespace1</ns> <ns>Namespace2</ns> </namespaces> <tokens> <token name="param1" value="23 /> </tokens> </map>
Listing 12. Passing namespaces and datatokens
The ignore element
The incoming requests can be ignored using the ignore element. You can specify constraints to ignore element same as the map element either by using the constraint attribute or constraints element.
<ignore pattern="/contact/{user}" constraint="Namespace.CustomConstraint, Assembly"> <constraints /> </ignore>
Listing 13. ignore element
The ignore element also supports the method and disabled attributes like the map element.
The redirect element
The MVC framework provides extension methods for only mapping and ignoring requests but we can also achieve redirecting requests in routing by wrapping our custom route and handlers. The XRouter helps you to perform simple level redirections and hopefully I'll extend it more in feature.
<redirect pattern="blog/category/{id}" to ="~/blog/tag" permanent="true" />
Listing 14. redirect element
The redirect element has a disabled attribute which is used to temporarily disable the redirection.
Managing areas in applications
If your application has areas then you can define routes in applications in two ways: either by creating a separate route file for each area (each file should be placed in the corresponding area) or by creating a single route file that contains the route definitions for all the areas which should be typically placed in the application's root. Note that the name of all the route files should be routes.config.
Sample route file for a single area
<area name="Support"> <namespaces> <ns>Areas.Support.Controllers</ns> </namespaces> <routes> <map name="support_default" pattern="support/{controller}/{action}/{id}" controller="home" action="index"> <params> <param name="id" optional="true" /> </params> </map> </routes> </area>
Listing 15. Sample route file for a single single area
Sample route file for all areas
<areas> <area name="Support"> <namespaces> <ns>Areas.Support.Controllers</ns> </namespaces> <routes> <map name="support_default" pattern="support/{controller}/{action}/{id}" controller="home" action="index"> <params> <param name="id" optional="true" /> </params> </map> </routes> </area> <area name="Contact"> <namespaces> <ns>Areas.Contact.Controllers</ns> </namespaces> <routes> <map name="contact_default" pattern="contact/{controller}/{action}/{id}" controller="home" action="index"> <params> <param name="id" optional="true" /> </params> </map> </routes> </area> <area> <routes> <ignore pattern="{resource}.axd/{*pathInfo}" /> <map name="archive" pattern="blog/archive/{date}" controller="blog" action="archive"> <params> <param name="date" type="date" /> </params> </map> <map name="default" pattern="{controller}/{action}/{id}" controller="blog" action="index"> <params> <param name="id" optional="true" /> </params> </map> </routes> </area> </areas>
Listing 16. Sample route file for all areas
If an application has a default non-area then that has to be specified at last ignoring the name attribute.
The area element
The area element contains a single attribute called name which should be unique for each area. It contains couple of child elements namespaces and routes. The namespaces element is used to specify the namespaces of the controllers that's going to handle the requests in that area. The routes element is the same one that we saw earlier which contains the map, ignore and redirect elements.
<area name=""> <namespaces /> <routes /> </area>
Listing 17. The structure of area element
Configuring routes in Application_Start
To configure the routes in the application all you have to do is call the Configure method of the RouteConfigurator class in the Application_Start event.
protected void Application_Start() { RouteConfigurator.Configure(); }
Listing 18. Configuring the routes from Application_Start event
Auto-update of RouteTable
The routes we define in the xml files are ultimately stored in RouteTable. Whenever you change the route file(s), the RouteTable will be updated automatically. A FileSystemWatcher is used internally to monitor the route file(s) for changes. I'm aware that people has faced some issues with FileSystemWatcher in production and definitely I'll extend the monitor class to track the changes based on a timer based mechanism.
Storing routes in database
XRouter supports to store routes in xml files but you can easily extend the API to store the routes in database by implementing couple of interfaces: IRouteSource and IRouteMonitor.
IRouteSource
public interface IRouteSource { List<RouteItem> GetRouteItems(); }
Listing 19. IRouteSource interface
This interface is used to fetch the routes from the corresponding source. If the routes are defined in database, you have to implement this interface and in the GetRouteItems method you should read the routes from the corresponding tables and returns them as a collection of RouteItems.
IRouteMonitor
public interface IRouteMonitor { void Monitor(IRouteSource routeSource, Action<RouteCollection, IEnumerable<RouteItem>> action); }
Listing 20. IRouteMonitor interface
This interface is used to monitor the source (xml, database) for any changes in the routes and call the passed action. Finally to pass the custom implementations of IRouteSource and IRouteMonitor to RouteConfigurator, you should use another overloaded version Configure method.
RouteConfigurator.Configure(IRouteSource routeSource, IRouteMonitor routeMonitor);
Listing 21. Passing custom implementations to RouteConfigurator