This post covers some ways you can improve the testability and reduce framework coupling when configuring routing in an ASP.NET MVC application.
Don’t declare your routes in Global.asax
Most of the ASP.NET MVC tutorials you find on the web declare routes within Global.asax.
A better approach is to store your routes in separate registries that can be tested/instantiated indepedently:
public interface IRouteRegistry
{
void RegisterRoutes(RouteCollection routes);
int Priority { get; }
}
public class MyRouteRegistry : IRouteRegistry
{
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute("",
"{controller}/{action}/{id}}",
new { controller = "Home", action = "Index", Id = UrlParameter.Optional });
}
public int Priority { get { return 0; } }
}
In your web application you can register the routes like so:
protected void Application_Start()
{
AreaRegistration.RegisterAllAreas();
RegisterGlobalFilters(GlobalFilters.Filters);
new MyRouteRegistry().RegisterRoutes(RouteTable.Routes);
}
An even better approach is to use the IoC tool of your choice to automatically locate and initialize route registries. This is a great way of supporting a pluggable routing configuration.
For example, with StructureMap:
ObjectFactory.Initialize(cfg =>
{
cfg.Scan(scan =>
{
scan.TheCallingAssembly();
scan.AddAllTypesOf<IRouteRegistry>();
});
});
ObjectFactory.GetAllInstances<IRouteRegistry>()
.OrderBy(r => r.Priority)
.ToList()
.ForEach(r => r.RegisterRoutes(RouteTable.Routes));
Avoid unecessary framework coupling
I recently answered a question on StackOverflow about how to test complex route constraints. Here’s the code in question:
public class CountryRouteConstraint : IRouteConstraint {
private readonly ICountryRepository<Country> _countryRepo;
public CountryRouteConstraint(ICountryRepository<Country> countryRepo) {
_countryRepo = countryRepo;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection) {
//do the database look-up here
//return the result according the value you got from DB
return true;
}
}
The problem with this code is that the validation logic is performed directly within the CountryRouteConstraint
class. In order to test the validation we would need to set up our routes and create a mock HttpContext. This is unecessary coupling and you’ll end up wasting time testing framework code. The same principle can be applied to other MVC components such as custom model binders and action filters.
Instead, move the validation logic into a separate class that can be tested in isolation:
public interface ICountryValidator
{
bool IsValid(string country);
}
public class CountryValidator : ICountryValidator
{
public bool IsValid(string country)
{
return true;
}
}
The CountryRouteConstraint
can be updated like so:
public class CountryRouteConstraint : IRouteConstraint
{
private readonly ICountryValidator countryValidator;
public CountryRouteConstraint(ICountryValidator countryValidator)
{
this.countryValidator = countryValidator;
}
public bool Match(HttpContextBase httpContext, Route route, string parameterName, RouteValueDictionary values, RouteDirection routeDirection)
{
object country = null;
values.TryGetValue("country", out country);
return countryValidator.IsValid(country as string);
}
}
Now there is no real need to test the RouteConstraint since its doing little more than retrieving a value from a dictionary and passing it to the validator.
Dependency injection for complex Route Constraints
Sometimes (like in the above example) you need access to external services within a Route Constraint. An “easy” approach is to simply call DependencyResolver
or your IoC tool directly when declaring your routes:
routes.MapRoute(
"Countries",
"countries/{country}",
new {
controller = "Countries",
action = "Index"
},
new {
country = new CountryRouteConstraint(
DependencyResolver.Current.GetService<ICountryValidator>()
)
}
);
Unfortunately this can make testing your routes difficult. A better approach is to use a factory to create IRouteConstraint
instances:
public interface IRouteConstraintFactory
{
IRouteConstraint Create<TRouteConstraint>()
where TRouteConstraint : IRouteConstraint;
}
Here’s an implementation that uses StructureMap to create the route constraint instance:
public class StructureMapRouteConstraintFactory : IRouteConstraintFactory
{
private readonly IContainer container;
public StructureMapRouteConstraintFactory(IContainer container)
{
this.container = container;
}
public IRouteConstraint Create<TRouteConstraint>()
where TRouteConstraint : IRouteConstraint
{
return container.GetInstance<TRouteConstraint>();
}
}
An advantage of registering route registries using an IoC tool (see above) is that dependencies will be injected automatically. In this case, we can inject an instance of IRouteConstraintFactory
that can be used to create our route constraints:
public class MyRouteRegistry : IRouteRegistry
{
private readonly IRouteConstraintFactory routeConstraintFactory;
public MyRouteRegistry(IRouteConstraintFactory routeConstraintFactory)
{
this.routeConstraintFactory = routeConstraintFactory;
}
public void RegisterRoutes(RouteCollection routes)
{
routes.MapRoute(
"Countries",
"countries/{country}",
new {
controller = "Countries",
action = "Index"
},
new {
country = routeConstraintFactory.Create<CountryRouteConstraint>()
}
);
}
public int Priority { get { return 0; } }
}
This makes testing routes much easier since we can just create a fake factory that satisfies our test requirements:
public class TestRouteConstraintFactory : IRouteConstraintFactory
{
public IRouteConstraint Create<TRouteConstraint>()
where TRouteConstraint : IRouteConstraint
{
return new CountryRouteConstraint(new FakeCountryValidator());
}
}
Reusing Route Constraints
Route Constraints should be stateless so you can resuse common route constraints.
We make heavy use of two custom route constraints, PageNumberRouteConstraint
and SlugRouteConstraint
.
In our route registry we create two static instances of the constraints that can then be used by any of our routes:
private static IRouteConstraint PageNumberConstraint = new PageNumberRouteConstraint();
private static IRouteConstraint SlugConstraint = new SlugRouteConstraint();
For convenience we added the following extension method:
public static Route WithConstraint(this Route route, string key, IRouteConstraint contraint)
{
route.Constraints.Add(key, contraint);
return route;
}
We can then chain on constraint instances like so:
routes.MapRoute("",
"projects/page/{page}",
new { controller = "Projects", action = "List" }
)
.WithConstraint("page", PageNumberConstraint);
Optimizing ASP.NET MVC Routing
As a sidenote I would recommend checking out Sam Safron’s article on Optimizing ASP.NET MVC3 Routing, especially if you make use of Regex constraints. We saw significant performance improvements in fabrik after making these changes.