8

I want to test if a URL is part of the routes defined in the Global.asax. This is what I have:

var TheRequest = HttpContext.Current.Request.Url.AbsolutePath.ToString();
var TheRoutes = System.Web.Routing.RouteTable.Routes;

foreach (var TheRoute in TheRoutes)
{
    if (TheRequest  == TheRoute.Url) //problem here
    {
        RequestIsInRoutes = true;
    }
}

The problem is that I can’t extract the URL from the route. What do I need to change?

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
frenchie
  • 51,731
  • 109
  • 304
  • 510

4 Answers4

9

The problem is that I can't extract the URL from the route.

I disagree. The problem is that you expect to pull the URLs out of the route table and compare them externally. Furthermore, it is unclear what you hope to gain by doing so.

Routing compares the incoming request against business logic to determine if it matches. This is a route's purpose. Moving the matching logic outside of the route is not a valid test because you are not testing the business logic that is implemented by the route.

Not to mention, it is a bit presumptive to assume that a route can only match a URL and nothing else in the request such as form post values or cookies. While the built in routing functionality only matches URLs, there is nothing stopping you from making a constraint or custom route that matches other criteria.

So, in short you need to write unit tests for the business logic in your routes. Any logic that happens outside of your route configuration should be unit tested separately.

There is a great post by Brad Wilson (albeit a bit dated) that demonstrates how to unit test your routes. I have updated the code to work with MVC 5 - here is a working demo using the below code.

IncomingRouteTests.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcRouteTesting;
using System.Web.Mvc;
using System.Web.Routing;

[TestClass]
public class IncomingRouteTests
{
    [TestMethod]
    public void RouteWithControllerNoActionNoId()
    {
        // Arrange
        var context = new StubHttpContextForRouting(requestUrl: "~/controller1");
        var routes = new RouteCollection();
        RouteConfig.RegisterRoutes(routes);

        // Act
        RouteData routeData = routes.GetRouteData(context);

        // Assert
        Assert.IsNotNull(routeData);
        Assert.AreEqual("controller1", routeData.Values["controller"]);
        Assert.AreEqual("Index", routeData.Values["action"]);
        Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]);
    }

    [TestMethod]
    public void RouteWithControllerWithActionNoId()
    {
        // Arrange
        var context = new StubHttpContextForRouting(requestUrl: "~/controller1/action2");
        var routes = new RouteCollection();
        RouteConfig.RegisterRoutes(routes);

        // Act
        RouteData routeData = routes.GetRouteData(context);

        // Assert
        Assert.IsNotNull(routeData);
        Assert.AreEqual("controller1", routeData.Values["controller"]);
        Assert.AreEqual("action2", routeData.Values["action"]);
        Assert.AreEqual(UrlParameter.Optional, routeData.Values["id"]);
    }

    [TestMethod]
    public void RouteWithControllerWithActionWithId()
    {
        // Arrange
        var context = new StubHttpContextForRouting(requestUrl: "~/controller1/action2/id3");
        var routes = new RouteCollection();
        RouteConfig.RegisterRoutes(routes);

        // Act
        RouteData routeData = routes.GetRouteData(context);

        // Assert
        Assert.IsNotNull(routeData);
        Assert.AreEqual("controller1", routeData.Values["controller"]);
        Assert.AreEqual("action2", routeData.Values["action"]);
        Assert.AreEqual("id3", routeData.Values["id"]);
    }

    [TestMethod]
    public void RouteWithTooManySegments()
    {
        // Arrange
        var context = new StubHttpContextForRouting(requestUrl: "~/a/b/c/d");
        var routes = new RouteCollection();
        RouteConfig.RegisterRoutes(routes);

        // Act
        RouteData routeData = routes.GetRouteData(context);

        // Assert
        Assert.IsNull(routeData);
    }

    [TestMethod]
    public void RouteForEmbeddedResource()
    {
        // Arrange
        var context = new StubHttpContextForRouting(requestUrl: "~/foo.axd/bar/baz/biff");
        var routes = new RouteCollection();
        RouteConfig.RegisterRoutes(routes);

        // Act
        RouteData routeData = routes.GetRouteData(context);

        // Assert
        Assert.IsNotNull(routeData);
        Assert.IsInstanceOfType(routeData.RouteHandler, typeof(StopRoutingHandler));
    }
}

OutgoingRouteTests.cs

using Microsoft.VisualStudio.TestTools.UnitTesting;
using MvcRouteTesting;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

[TestClass]
public class OutgoingRouteTests
{
    [TestMethod]
    public void ActionWithAmbientControllerSpecificAction()
    {
        UrlHelper helper = GetUrlHelper();

        string url = helper.Action("action");

        Assert.AreEqual("/defaultcontroller/action", url);
    }

    [TestMethod]
    public void ActionWithSpecificControllerAndAction()
    {
        UrlHelper helper = GetUrlHelper();

        string url = helper.Action("action", "controller");

        Assert.AreEqual("/controller/action", url);
    }

    [TestMethod]
    public void ActionWithSpecificControllerActionAndId()
    {
        UrlHelper helper = GetUrlHelper();

        string url = helper.Action("action", "controller", new { id = 42 });

        Assert.AreEqual("/controller/action/42", url);
    }

    [TestMethod]
    public void RouteUrlWithAmbientValues()
    {
        UrlHelper helper = GetUrlHelper();

        string url = helper.RouteUrl(new { });

        Assert.AreEqual("/defaultcontroller/defaultaction", url);
    }

    [TestMethod]
    public void RouteUrlWithAmbientValuesInSubApplication()
    {
        UrlHelper helper = GetUrlHelper(appPath: "/subapp");

        string url = helper.RouteUrl(new { });

        Assert.AreEqual("/subapp/defaultcontroller/defaultaction", url);
    }

    [TestMethod]
    public void RouteUrlWithNewValuesOverridesAmbientValues()
    {
        UrlHelper helper = GetUrlHelper();

        string url = helper.RouteUrl(new
        {
            controller = "controller",
            action = "action"
        });

        Assert.AreEqual("/controller/action", url);
    }

    static UrlHelper GetUrlHelper(string appPath = "/", RouteCollection routes = null)
    {
        if (routes == null)
        {
            routes = new RouteCollection();
            RouteConfig.RegisterRoutes(routes);
        }

        HttpContextBase httpContext = new StubHttpContextForRouting(appPath);
        RouteData routeData = new RouteData();
        routeData.Values.Add("controller", "defaultcontroller");
        routeData.Values.Add("action", "defaultaction");
        RequestContext requestContext = new RequestContext(httpContext, routeData);
        UrlHelper helper = new UrlHelper(requestContext, routes);
        return helper;
    }
}

Stubs.cs

using System;
using System.Collections.Specialized;
using System.Web;

public class StubHttpContextForRouting : HttpContextBase
{
    StubHttpRequestForRouting _request;
    StubHttpResponseForRouting _response;

    public StubHttpContextForRouting(string appPath = "/", string requestUrl = "~/")
    {
        _request = new StubHttpRequestForRouting(appPath, requestUrl);
        _response = new StubHttpResponseForRouting();
    }

    public override HttpRequestBase Request
    {
        get { return _request; }
    }

    public override HttpResponseBase Response
    {
        get { return _response; }
    }

    public override object GetService(Type serviceType)
    {
        return null;
    }
}

public class StubHttpRequestForRouting : HttpRequestBase
{
    string _appPath;
    string _requestUrl;

    public StubHttpRequestForRouting(string appPath, string requestUrl)
    {
        _appPath = appPath;
        _requestUrl = requestUrl;
    }

    public override string ApplicationPath
    {
        get { return _appPath; }
    }

    public override string AppRelativeCurrentExecutionFilePath
    {
        get { return _requestUrl; }
    }

    public override string PathInfo
    {
        get { return ""; }
    }

    public override NameValueCollection ServerVariables
    {
        get { return new NameValueCollection(); }
    }
}

public class StubHttpResponseForRouting : HttpResponseBase
{
    public override string ApplyAppPathModifier(string virtualPath)
    {
        return virtualPath;
    }
}

With that out of the way, back to your original question.

How to determine if the URL is in the route table?

The question is a bit presumptive. As others have pointed out, the route table does not contain URLs, it contains business logic. A more correct way to phrase the question would be:

How to determine if an incoming URL matches any route in the route table?

Then you are on your way.

To do so, you need to execute the GetRouteData business logic in the route collection. This will execute the GetRouteData method on each route until the first one of them returns a RouteData object instead of null. If none of them return a RouteData object (that is, all of the routes return null), it indicates that none of the routes match the request.

In other words, a null result from GetRouteData indicates that none of the routes matched the request. A RouteData object indicates that one of the routes matched and it provides the necessary route data (controller, action, etc) to make MVC match an action method.

So, to simply check whether a URL matches a route, you just need to determine whether the result of the operation is null.

[TestMethod]
public void EnsureHomeAboutMatches()
{
    // Arrange
    var context = new StubHttpContextForRouting(requestUrl: "~/home/about");
    var routes = new RouteCollection();
    RouteConfig.RegisterRoutes(routes);

    // Act
    RouteData routeData = routes.GetRouteData(context);

    // Assert
    Assert.IsNotNull(routeData);
}

Note also that generating routes is a separate task from matching incoming routes. You can generate outgoing URLs from routes, but it uses a completely different set of business logic than matching incoming routes. This outgoing URL logic can (and should) be unit tested separately from the incoming URL logic as demonstrated above.

NightOwl888
  • 55,572
  • 24
  • 139
  • 212
  • I like the simplicity of your answer. – Nkosi May 13 '16 at 17:53
  • Can't I just get a list of all the URLs in the route table? – frenchie May 13 '16 at 18:16
  • @frenchie, technically there is no list of URLs in the route table. There is a list of URL templates that it uses to match against incoming URLs. – Nkosi May 13 '16 at 18:35
  • NightOwl888. ran some unit tests against your answer and it throws null reference errors as it can't create virtual paths for the urls. – Nkosi May 13 '16 at 18:39
  • Can you give me an example URL that throws the exception? Are you using absolute URLs or relative? – NightOwl888 May 13 '16 at 19:35
  • Can I somehow extract the URLs in a List?? – frenchie May 13 '16 at 19:44
  • `Can't I just get a list of all the URLs in the route table?` - Yes, you could use the `GetVirtualPath` method to get the URLs, but only if you provide the route values for each one (controller, action, area, id, etc) in order to generate those URLs. However, using outgoing URL matching does not tell you whether the URL matches anything in the route table - the outgoing URL logic can be completely different from the request matching logic - they are 2 entirely different methods (`GetVirtualPath` and `GetRouteData`) that should be, but don't necessarily have to be mirror images of each other. – NightOwl888 May 13 '16 at 19:45
  • I used the same paths you had in your usage example. – Nkosi May 14 '16 at 01:40
  • 1
    @Nkosi - I changed my answer to one that demonstrates exactly how to test routing business logic including how to stub out the HTTP context so it doesn't blow up under unit tests. – NightOwl888 May 14 '16 at 11:41
  • @NightOwl888, yep, that's the route (pardon the pun) I took. Someone provided a link in the comments that lead me to the same conclusion you provided. Good answer. – Nkosi May 14 '16 at 11:48
  • 3
    You know.. I wish stack overflow had an option that gives you something like a way to award people for "FreakingAwesomeAnswers" once a while.. cause you sir.. deserve one of those for this answer! But all I can do is +1 – Piotr Kula Mar 16 '17 at 11:11
  • @NightOwl888 you should try to slow down a bit with the amount of edits before it raises any flags with the mods. – Nkosi Feb 04 '18 at 18:36
  • @Nkosi - Gotcha. – NightOwl888 Feb 04 '18 at 18:42
2

I don't know if this is what you want is the requested route, if that is the case you can get it from the current request:

var route = HttpContext.Current.Request.RequestContext.RouteData.Route;
Arturo Menchaca
  • 15,783
  • 1
  • 29
  • 53
2

You could try checking the current context against route table

var contextBase = HttpContext.Current.Request.RequestContext.HttpContext;
var data = RouteTable.Routes.GetRouteData(contextBase);
if (data != null) {
    //Route exists
}

Using the above as a basis of creating a service

public interface IRouteInspector {
    bool RequestIsInRoutes();
}

public interface IHttpContextAccessor {
    HttpContextBase HttpContext { get; }
}

public interface IRouteTable {
    RouteCollection Routes { get; }
}

public class RouteInspector : IRouteInspector {
    private IRouteTable routeTable;
    private IHttpContextAccessor contextBase;

    public RouteInspector(IRouteTable routeTable, IHttpContextAccessor contextBase) {
        this.routeTable = routeTable;
        this.contextBase = contextBase;
    }

    public bool RequestIsInRoutes() {
        if (routeTable.Routes.GetRouteData(contextBase.HttpContext) != null) {
            //Route exists
            return true;
        }
        return false;
    }
}

And here is test class showing how it is used.

[TestClass]
public class RouteTableUnitTests : ControllerUnitTests {
    [TestMethod]
    public void Should_Get_Request_From_Route_Table() {
        //Arrange                
        var contextBase = new Mock<IHttpContextAccessor>();
        contextBase.Setup(m => m.HttpContext)
            .Returns(HttpContext.Current.Request.RequestContext.HttpContext);
        var routeTable = new Mock<IRouteTable>();
        routeTable.Setup(m => m.Routes).Returns(RouteTable.Routes);
        var sut = new RouteInspector(routeTable.Object, contextBase.Object);
        //Act
        var actual = sut.RequestIsInRoutes();
        //Assert
        Assert.IsTrue(actual);
    }
}

There is room for refactoring and improvements but it's a start.

Nkosi
  • 235,767
  • 35
  • 427
  • 472
  • There has to be a simpler way to do it; just to check if 2 strings are equal but I can't figure out how to extract the URLs out of the routes. – frenchie May 12 '16 at 17:45
  • Routes are not as simple as string as you have to map them to templates. Plus the string are being matched within the framework's `GetRouteData`. Instead of trying to recreate functionality that already exists within the framework. Use what is already there – Nkosi May 12 '16 at 17:50
  • Ok, then how can I get a List with all the URLs in the route table? – frenchie May 12 '16 at 20:16
  • The route table has a list of URL templates, not full URLs. – Nkosi May 12 '16 at 20:30
0

This is what I ended up doing:

string TheRequest = HttpContext.Current.Request.Url.AbsolutePath.ToString();

foreach (Route r in System.Web.Routing.RouteTable.Routes)
{
    if (("/" + r.Url) == TheRequest)
    {
        //the request is in the routes
    }
}

It's hacky but it works in 3 lines.

frenchie
  • 51,731
  • 109
  • 304
  • 510
  • `but it works` - Unless you use URLs like `{controller}/{action}/{id}` that will never match any real URL or until the day you start putting other subclasses of `RouteBase` into your route table than `Route`, or any number of other reasons. As I mentioned, this test is invalid - it is not testing the business logic implemented by the route to see if it matches. – NightOwl888 Oct 22 '17 at 07:02