[ASP.NET Core MVC Pipeline] Routing Middleware — Custom IRouter

Lucas Araujo | Azure Coder
4 min readJul 9, 2017

--

What if you want to add some custom logic to the routing system?

There are two main components where we can inject our code to bend some rules on the framework. The first one is the IRouter.

Middleware Pipeline - Routing
Middleware Pipeline

In our flow the IRouter is represented by the “Matching to Route Entry” box and that is what it is. The role of the IRouter in the Routing system is to match something on a request, usually on the URI, and point it to the right IRouteHandler. We are going to learn more about the RouteHandler component in a future post.

Why would I do that?

As previously said, the role of the IRouter in the pipeline is to evaluate the request and pass it to the IRouteHandler that is the best fit, but what might be mentioned is that you can also use the IRouter to make modifications to the incoming request. In this way, whenever you have a request that matches a certain criteria you can add, remove or edit some information and let the default RouteHandler do the rest.

In my experience, that is the most common case. I don’t see complete custom implementation of the Routing system so frequently.

The Basics

A bit different from the custom middleware, which is entirely implemented through conventions, the custom Router must implement an Interface called IRouter. This interface is quite simple:

namespace Microsoft.AspNetCore.Routing
{
public interface IRouter
{
VirtualPathData GetVirtualPath(VirtualPathContext context);
Task RouteAsync(RouteContext context);
}
}

The first method, GetVirtualPath, is used internally by the framework to generate urls based on this route in methods like the HTML.ActionLink one, the second method, RouteAsync, is where our logic will really reside.

Our class should start with something like this:

public class CustomRouter : IRouter
{
private IRouter _defaultRouter;

public CustomRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}

public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}

public Task RouteAsync(RouteContext context)
{
throw new NotImplementedException();
}
}

Routing our Requests

The concept behind the RouteAsync method is quite simple and straightforward, we do all the checking necessary to see if the provided route matches our IRouter context and, if it does, we execute the needed logic and inform the Routing middleware that the request has been handled.

public class CustomRouter : IRouter
{
private IRouter _defaultRouter;

public CustomRouter(IRouter defaultRouteHandler)
{
_defaultRouter = defaultRouteHandler;
}

public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}

public async Task RouteAsync(RouteContext context)
{
var headers = context.HttpContext.Request.Headers;
var path = context.HttpContext.Request.Path.Value.Split('/');

// Look for the User-Agent Header and Check if the Request comes from a Mobile
if (headers.ContainsKey("User-Agent") &&
headers["User-Agent"].ToString().Contains("Mobile"))
{
var action = "Index";
var controller = "";
if (path.Length > 1)
{
controller = path[1];
if (path.Length > 2)
action = path[2];
}

context.RouteData.Values["controller"] = $"Mobile{controller}";
context.RouteData.Values["action"] = action;

await _defaultRouter.RouteAsync(context);
}
}
}

We are doing the checking part on the if statement where we analyze if the request comes from a Mobile User-Agent (there are certainly better and more secure ways to do this but it is enough for the purpose of the demonstration) and if our Router finds that the request comes from such an agent, we apply some logic to change the controller and action values in our RouteData, therefore ensuring that our request will be redirected to the right Action.

If everything works as expected, then we send our modified context to the Default RouteHandler, and let it process it. The RouteHandler will set the value for the Handler property in our context, and it will let the framework know that the request has been Handled by our Router and no longer need to be processed by other Routers.

Hooking up to the Routing Middleware

Now we have a nice and functional Custom Router but if you run the application you will see that it is never touched by your request and that is because we never hooked our Router to the Routing Middleware.

The Routing Middleware has a stack of Routers that it will use to try and match a request, hence we need to add our Custom Router to this Stack. Let’s do it.

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseStaticFiles();

app.UseMvc(routes =>
{
routes.Routes.Add(new CustomRouter(routes.DefaultHandler));

routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}

Now we add a new Router to the Routes collection and we passed the Default Handler, which will be used by our Routing flow.

And that is all we have to do to make our Router capture every request that comes from a mobile User-Agent and redirect them to specific controllers/actions.

To be completely honest, creating a custom Router will usually be a bit of an overkill as you can achieve most of the desired behaviors through the usage of easier components on the pipeline (including the middleware), but I think that is important to go through all the major components that we can customize.

What do you think? Sounds like something that you can use in your application?

Thanks for Reading! Tot ziens :)

MVC Pipeline Series

Sources

Originally published at The Azure Coder.

--

--