I am attempting to convert this sample RouteBase implementation to work with MVC 6. I have worked out most of it by following the example in the Routing project, but I am getting tripped up on how to return the asynchronous Task from the method. I really don't care if it actually is asynchronous (cheers to anyone who can provide that answer), for now I just want to get it functioning.
I have the outgoing routes functioning (meaning ActionLink works fine when I put in the route values). The problem is with the RouteAsync method.
public Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page that matches.
var page = GetPageList()
.Where(x => x.VirtualPath.Equals(requestPath))
.FirstOrDefault();
// If we got back a null value set, that means the URI did not match
if (page != null)
{
var routeData = new RouteData();
// This doesn't work
//var routeData = new RouteData(context.RouteData);
// This doesn't work
//routeData.Routers.Add(this);
// This doesn't work
//routeData.Routers.Add(new MvcRouteHandler());
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = "CustomPage";
routeData.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = page.Id;
context.RouteData = routeData;
// When there is a match, the code executes to here
context.IsHandled = true;
// This test works
//await context.HttpContext.Response.WriteAsync("Hello there");
// This doesn't work
//return Task.FromResult(routeData);
// This doesn't work
//return Task.FromResult(context);
}
// This satisfies the return statement, but
// I'm not sure it is the right thing to return.
return Task.FromResult(0);
}
The entire method runs all the way through to the end when there is a match. But when it is done executing, it doesn't call the Details method of the CustomPage controller, as it should. I just get a blank white page in the browser.
I added the WriteAsync line as was done in this post and it writes Hello there to the blank page, but I can't understand why MVC isn't calling my controller (in previous versions this worked without a hitch). Unfortunately, that post covered every part of routing except for how to implement an IRouter or INamedRouter.
How can I make the RouteAsync method function?
Entire CustomRoute Implementation
using Microsoft.AspNet.Routing;
using Microsoft.Framework.Caching.Memory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class PageInfo
{
// VirtualPath should not have a leading slash
// example: events/conventions/mycon
public string VirtualPath { get; set; }
public int Id { get; set; }
}
public interface ICustomRoute : IRouter
{ }
public class CustomRoute : ICustomRoute
{
private readonly IMemoryCache cache;
private object synclock = new object();
public CustomRoute(IMemoryCache cache)
{
this.cache = cache;
}
public Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page that matches.
var page = GetPageList()
.Where(x => x.VirtualPath.Equals(requestPath))
.FirstOrDefault();
// If we got back a null value set, that means the URI did not match
if (page != null)
{
var routeData = new RouteData();
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
routeData.Values["controller"] = "CustomPage";
routeData.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
routeData.Values["id"] = page.Id;
context.RouteData = routeData;
context.IsHandled = true;
}
return Task.FromResult(0);
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
VirtualPathData result = null;
PageInfo page = null;
// Get all of the pages from the cache.
var pages = GetPageList();
if (TryFindMatch(pages, context.Values, out page))
{
result = new VirtualPathData(this, page.VirtualPath);
context.IsBound = true;
}
return result;
}
private bool TryFindMatch(IEnumerable<PageInfo> pages, IDictionary<string, object> values, out PageInfo page)
{
page = null;
int id;
object idObj;
object controller;
object action;
if (!values.TryGetValue("id", out idObj))
{
return false;
}
id = Convert.ToInt32(idObj);
values.TryGetValue("controller", out controller);
values.TryGetValue("action", out action);
// The logic here should be the inverse of the logic in
// GetRouteData(). So, we match the same controller, action, and id.
// If we had additional route values there, we would take them all
// into consideration during this step.
if (action.Equals("Details") && controller.Equals("CustomPage"))
{
page = pages
.Where(x => x.Id.Equals(id))
.FirstOrDefault();
if (page != null)
{
return true;
}
}
return false;
}
private IEnumerable<PageInfo> GetPageList()
{
string key = "__CustomPageList";
IEnumerable<PageInfo> pages;
// Only allow one thread to poplate the data
if (!this.cache.TryGetValue(key, out pages))
{
lock (synclock)
{
if (!this.cache.TryGetValue(key, out pages))
{
// TODO: Retrieve the list of PageInfo objects from the database here.
pages = new List<PageInfo>()
{
new PageInfo() { Id = 1, VirtualPath = "somecategory/somesubcategory/content1" },
new PageInfo() { Id = 2, VirtualPath = "somecategory/somesubcategory/content2" },
new PageInfo() { Id = 3, VirtualPath = "somecategory/somesubcategory/content3" }
};
this.cache.Set(key, pages,
new MemoryCacheEntryOptions()
{
Priority = CacheItemPriority.NeverRemove,
AbsoluteExpirationRelativeToNow = TimeSpan.FromMinutes(15)
});
}
}
}
return pages;
}
}
CustomRoute DI Registration
services.AddTransient<ICustomRoute, CustomRoute>();
MVC Route Configuration
// Add MVC to the request pipeline.
app.UseMvc(routes =>
{
routes.Routes.Add(routes.ServiceProvider.GetService<ICustomRoute>());
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// Uncomment the following line to add a route for porting Web API 2 controllers.
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});
In case it matters I am using Beta 5, DNX 4.5.1 and DNX Core 5.
Solution
I created a generic solution that can be used for a simple primary key to URL 2-way mapping in this answer based on the information I learned here. The controller, action, data provider, and datatype of the primary key can be specified when wiring it into MVC 6 routing.
As #opiants said, the problem is that you are doing nothing in your RouteAsync method.
If your intention is to end up calling a controller action method, you could use the following approach than the default MVC routes:
By default MVC uses a
TemplateRoute
with an inner target IRouter. In RouteAsync, the TemplateRoute will
delegate to the inner IRouter. This inner router is being set as the
MvcRouteHandler
by the default builder
extensions.
In your case, start by adding an IRouter as your inner target:
public class CustomRoute : ICustomRoute
{
private readonly IMemoryCache cache;
private readonly IRouter target;
private object synclock = new object();
public CustomRoute(IMemoryCache cache, IRouter target)
{
this.cache = cache;
this.target = target;
}
Then update your startup to set that target as the MvcRouteHandler, which has already been set as routes.DefaultHandler:
app.UseMvc(routes =>
{
routes.Routes.Add(
new CustomRoute(routes.ServiceProvider.GetRequiredService<IMemoryCache>(),
routes.DefaultHandler));
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// Uncomment the following line to add a route for porting Web API 2 controllers.
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});
Finally, update your AsyncRoute method to call the inner IRouter, which would be the MvcRouteHandler. You can use the implementation of that method in TemplateRoute as a guide. I have quickly used this approach and modified your method as follows:
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
// Get the page that matches.
var page = GetPageList()
.Where(x => x.VirtualPath.Equals(requestPath))
.FirstOrDefault();
// If we got back a null value set, that means the URI did not match
if (page == null)
{
return;
}
//Invoke MVC controller/action
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(this.target);
// TODO: You might want to use the page object (from the database) to
// get both the controller and action, and possibly even an area.
// Alternatively, you could create a route for each table and hard-code
// this information.
newRouteData.Values["controller"] = "CustomPage";
newRouteData.Values["action"] = "Details";
// This will be the primary key of the database row.
// It might be an integer or a GUID.
newRouteData.Values["id"] = page.Id;
try
{
context.RouteData = newRouteData;
await this.target.RouteAsync(context);
}
finally
{
// Restore the original values to prevent polluting the route data.
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
Update RC2
Looks like TemplateRoute is no longer around in RC2 aspnet Routing.
I investigated the history, and it was renamed RouteBase in commit 36180ab as part of a bigger refactoring.
Primary reason why that doesn't work is because you aren't doing anything in the RouteAsync method. Another reason is that how routing works in MVC 6 is very different to how the previous MVC routing worked so you're probably be better off writing it from scratch using the source code as reference as there are very few articles that tackle MVC 6 at the moment.
EDIT: #Daniel J.G. answer makes much more sense than this so use that if possible. This might fit someone else's use case so I'm leaving this here.
Here's a very simple IRouter implementation using beta7. This should work but you'll probably need to fill in the gaps. You'll need to remove the page != null and replace it with the code below and replace the controllers and actions:
if (page == null)
{
// Move to next router
return;
}
// TODO: Replace with correct controller
var controllerType = typeof(HomeController);
// TODO: Replace with correct action
var action = nameof(HomeController.Index);
// This is used to locate the razor view
// Remove the trailing "Controller" string
context.RouteData.Values["Controller"] = controllerType.Name.Substring(0, controllerType.Name.Length - 10);
var actionInvoker = context.HttpContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
var descriptor = new ControllerActionDescriptor
{
Name = action,
MethodInfo = controllerType.GetTypeInfo().DeclaredMethods.Single(m => m.Name == action),
ControllerTypeInfo = controllerType.GetTypeInfo(),
// Setup filters
FilterDescriptors = new List<FilterDescriptor>(),
// Setup DI properties
BoundProperties = new List<ParameterDescriptor>(0),
// Setup action arguments
Parameters = new List<ParameterDescriptor>(0),
// Setup route constraints
RouteConstraints = new List<RouteDataActionConstraint>(0),
// This router will work fine without these props set
//ControllerName = "Home",
//DisplayName = "Home",
};
var accessor = context.HttpContext.RequestServices.GetRequiredService<IActionContextAccessor>();
accessor.ActionContext = new ActionContext(context.HttpContext, context.RouteData, descriptor);
var actionInvokerFactory = context.HttpContext.RequestServices.GetRequiredService<IActionInvokerFactory>();
var invoker = actionInvokerFactory.CreateInvoker(accessor.ActionContext);
// Render the page
await invoker.InvokeAsync();
// Don't execute the next IRouter
context.IsHandled = true;
return;
Make sure you add a reference to the Microsoft.Framework.DependencyInjection namespace to resolve the GetRequiredService extension.
After that, register the IRouter as per below:
app.UseMvc(routes =>
{
// Run before any default IRouter implementation
// or use .Add to run after all the default IRouter implementations
routes.Routes.Insert(0, routes.ServiceProvider.GetRequiredService<CustomRoute>());
// .. more code here ...
});
Then just register that in your IOC,
services.AddSingleton<CustomRoute>();
Another 'cleaner' approach would probably be to create a different implementation of IActionSelector.
Related
My xunit test is failing because the expected Id isn't updated by the database when unit testing.
How do I mock the id/primary key being created by the database?
[HttpPost]
public async Task<ActionResult<AccountReadDto>> CreateAccountAsync(AccountCreateDto createAccountDto)
{
var account = _mapper.Map<Account>(createAccountDto); // Id is int equal to 0
await _accountRepo.CreateAccountAsync(account); // repo calls db and updates Id with pk
_accountRepo.SaveChanges();
var accountReadDto = _mapper.Map<AccountReadDto>(account);
return CreatedAtRoute(nameof(GetAccountByIdAsync), new { AccountId = accountReadDto.AccountId }, accountReadDto);
}
[Fact]
public async Task CreateAccountAsync_NewAccount_ReturnsAccountReadDto()
{
var expectedAccount = CreateRandomAccount(); // has id
var createDto = _mapperStub.Map<AccountCreateDto>(expectedAccount); // doesn't have id
_repoStub
.Setup(repo => repo.CreateAccountAsync(expectedAccount))
.Returns(Task.CompletedTask);
var controller = new AccountController(_repoStub.Object, _mapperStub);
var actionResult = await controller.CreateAccountAsync(createDto);
var result = (actionResult.Result as CreatedAtRouteResult).Value as AccountReadDto;
result.Should().BeEquivalentTo(
expectedAccount,
options => options.ComparingByMembers<AccountReadDto>().ExcludingMissingMembers() // excluding login info
);
}
This code is not correct:
_repoStub
.Setup(repo => repo.CreateAccountAsync(expectedAccount))
It's setups CreateAccountAsync just for case when you will pass expectedAccount instance as argument. (After mapping it's not the same). Use It.IsAny<T>() to configure method to work with any argument. Then use Callback<T>(Action<T>) to access argument passed to mock call:
_repoStub
.Setup(repo => repo.CreateAccountAsync(It.IsAny<Account>()))
.Callback<Account>(a => a.Id = expectedAccount.Id)
.Returns(Task.CompletedTask);
In that code you configured CreateAccountAsync method to return Task.CompletedTask and modify incoming argument id to expectedAccount.Id.
Returns(Task.CompletedTask) can be ommited. (It's default behaviour)
Additional info about Callback<T>: link
I have the following implementation of my router:
public class TenantUrlResolverRouter : IRouter
{
private readonly IRouter _defaultRouter;
public TenantUrlResolverRouter(IRouter defaultRouter)
{
_defaultRouter = defaultRouter;
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
return _defaultRouter.GetVirtualPath(context);
}
public async Task RouteAsync(RouteContext context)
{
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Values["library"] = "Default";
try
{
context.RouteData = newRouteData;
await _defaultRouter.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
}
Then I define it in Startup.cs:
app.UseMvc(routes =>
{
routes.Routes.Add(
new TenantUrlResolverRouter(routes.DefaultHandler));
routes.MapRoute(
name: "default",
template: "{library=Unknown}/{controller=Home}/{action=Index}/{id?}");
});
But nothing happens, RouteData.Values in RouteContext always empty, I always have Unknown, while it's need to be Default. That's not the problem of the predefined template, because it even worked without the {library=Unknown} and this {library}/{controller=Home}/{action=Index}/{id?} doesn't work too.
What's the problem with this custom IRouter?
You are not providing a complete set of route values. In order to route to an action, you need to provide both controller and action.
Also, you don't have a match condition in the route. A match condition will determine whether the incoming request matches the current route. In the built-in routing, the url and constraints are match conditions. However, in a custom route, you need to put an if block to ensure that any request that doesn't match will pass through to the next registered route.
NOTE: This is the most powerful part of custom routing. The built-in routing can only match against URLs. But with custom routing, you can match anything in the request. For example, you could make the route match only for a certain domain or a certain subdomain. You could even make it match things like posted form values or session state.
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
var segments = requestPath.Split('/');
// Required: Match condition to determine if the incoming request
// matches this route. If not, you should allow the framework to
// match another route by doing nothing here.
if (segments.Length > 0 && segments[0].Equals("libraryname", StringComparison.OrdinalIgnoreCase))
{
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Values["library"] = segments[0];
newRouteData.Values["controller"] = segments[1];
newRouteData.Values["action"] = segments[2];
try
{
context.RouteData = newRouteData;
await _defaultRouter.RouteAsync(context);
}
finally
{
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
}
See this question for more info about implementing IRouter.
I have a situation where a site may need a link to redirect to certain controllers based on database results.
For example:
site.com/abcd
needs to return the result from a Item Controller, which would normally be called as /item/view/123
The key here is that I can't hard code the abcd into the routing. And some links may go to an Item Controller, others may go to an Orders controller.
I've tried a catchall route to a controller than then loads up the desired controller, but the environment is not set so it does not work properly (it can't find the views).
You can get whatever behavior you desire by implementing IRouter as in this answer, including basing your logic on data from an external source (such as a config file or database).
This is much more flexible than a catchall route because it lets you choose the controller on the fly.
public class MyRoute : IRouter
{
private readonly IRouter innerRouter;
public MyRoute(IRouter innerRouter)
{
if (innerRouter == null)
throw new ArgumentNullException("innerRouter");
this.innerRouter = innerRouter;
}
public async Task RouteAsync(RouteContext context)
{
var requestPath = context.HttpContext.Request.Path.Value;
if (!string.IsNullOrEmpty(requestPath) && requestPath[0] == '/')
{
// Trim the leading slash
requestPath = requestPath.Substring(1);
}
if (!requestPath.StartsWith("abcd"))
{
return;
}
//Invoke MVC controller/action
var oldRouteData = context.RouteData;
var newRouteData = new RouteData(oldRouteData);
newRouteData.Routers.Add(this.innerRouter);
newRouteData.Values["controller"] = "Item";
newRouteData.Values["action"] = "View";
newRouteData.Values["id"] = 123;
try
{
context.RouteData = newRouteData;
await this.innerRouter.RouteAsync(context);
}
finally
{
// Restore the original values to prevent polluting the route data.
if (!context.IsHandled)
{
context.RouteData = oldRouteData;
}
}
}
public VirtualPathData GetVirtualPath(VirtualPathContext context)
{
VirtualPathData result = null;
var values = context.Values;
var controller = Convert.ToString(values["controller"]);
var action = Convert.ToString(values["action"]);
var id = Convert.ToString(values["id"]);
if ("Item".Equals(controller) && "View".Equals(action))
{
result = new VirtualPathData(this, "abcd?id=" + id);
context.IsBound = true;
}
// IMPORTANT: Always return null if there is no match.
// This tells .NET routing to check the next route that is registered.
return result;
}
}
Usage
// Add MVC to the request pipeline.
app.UseMvc(routes =>
{
routes.Routes.Add(new MyRoute(
innerRouter: routes.DefaultHandler)
);
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
// Uncomment the following line to add a route for porting Web API 2 controllers.
// routes.MapWebApiRoute("DefaultApi", "api/{controller}/{id?}");
});
The GetVirtualPath should mirror what the RouteAsync does. RouteAsync converts a URL into route values, and the GetVirtualPath should convert the same route data back into the same URL.
The easiest way to accomplish this is to use a data structure to create a two-way mapping between these 2 data points (as in the linked answer) so you don't have to continually change the logic within these 2 methods. This data structure should be cached and not do anything too resource intensive, since every request will use it to determine where to send each URL.
Alternatively, you could create a separate route for each of your individual pieces of logic and register them all at application startup. However, you need to ensure they are registered in the correct order and that each route will only match the correct set of URLs and correct set of RouteValues.
NOTE: For a scenario such as this you should almost never need to use RedirectToAction. Keep in mind redirecting will send an HTTP 302 request to the browser, which tells it to lookup another location on your server. This is unnecessary overhead in most cases because it is much more efficient just to route the initial request to the controller you want.
I need to get the controller name from my route and this I can do if using standard routing code in WebApiConfig.
However, if I am using routing attributes it starts to get a little difficult, especially when trying to version.
Example: If I call an api/terms/bonuses and I have a BonusController and BonusV2Controller and a BonusV3Controller, this code returns the latest controller version 3. That's ok, I can live with that returning the latest and greatest version as a default.
var attributedRoutesData = request.GetRouteData().GetSubRoutes();
var subRouteData = attributedRoutesData.FirstOrDefault();
var actions = (ReflectedHttpActionDescriptor[])subRouteData.Route.DataTokens["actions"];
//This seems to get latest controller name. ie. V2
controllerName = actions[0].ControllerDescriptor.ControllerName;
Now if I request a version 1, for simplicity I'll use a querystring and call api/terms/bonuses?v=2
So this code no longer works (obviously).
How do I get the V2 controller name?
If I abandon routing attributes and just use WebApiConfig routing, this code works happily.
HttpControllerDescriptor controllerDescriptor = null;
var controllers = GetControllerMapping();
var routeData = request.GetRouteData();
var controllerName = (string)routeData.Values["controller"];
UPDATE:
Here is my full selector code.
IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
var attributedRoutesData = request.GetRouteData().GetSubRoutes();
var subRouteData = attributedRoutesData.LastOrDefault(); //LastOrDefault() will get PeopleController, FirstOrDefault will get People{version}Controller which we don't want
var actions = (ReflectedHttpActionDescriptor[])subRouteData.Route.DataTokens["actions"];
var controllerName = actions[0].ControllerDescriptor.ControllerName;
//For controller name without attribute routing
//var controllerName = (string)routeData.Values["controller"];
HttpControllerDescriptor oldControllerDescriptor;
if (controllers.TryGetValue(controllerName, out oldControllerDescriptor))
{
//TODO: Different techniques for handling version api requests.
var apiVersion = GetVersionFromQueryString(request);
//var version = GetVersionFromHeader(request);
//var version = GetVersionFromAcceptHeaderVersion(request);
//var version = GetVersionFromMediaType(request);
if (!String.IsNullOrEmpty(apiVersion))
{
var newControllerName = String.Concat(controllerName, "V", apiVersion);
HttpControllerDescriptor newControllerDescriptor;
if (controllers.TryGetValue(newControllerName, out newControllerDescriptor))
{
return newControllerDescriptor;
}
}
return oldControllerDescriptor;
}
return null;
var subRouteData = request.GetRouteData().GetSubRoutes().LastOrDefault();
if (subRouteData != null && subRouteData.Route != null)
{
var actions = subRouteData.Route.DataTokens["actions"] as HttpActionDescriptor[];
if (actions != null && actions.Length > 0)
{
controllerName = actions[0].ControllerDescriptor.ControllerName;
}
}
At last I found it:
filterContext.ActionContext.ControllerContext.ControllerDescriptor.ControllerName
Goal: test that a given url returns a given controller function.
In the process, I've broken into the routing system and I can't figure out how to test routes (or, for that matter, find the controller corresponding to the route :-/).
Sample code, which doesn't work:
[Test]
public void kick_the_tires()
{
var rc = new RouteCollection();
Infrastructure.RouteRegistry.RegisterRoutes(rc);
// get the route corresponding to name.
var got = rc["name"];
var expected = //What? foo is an internal type that can't be instantiated.
Assert.AreEqual(foo, frob);
}
edit: Using the linked blog post from Simon for the stub class.
[TestCase("/", "~/", "Home", "Index")]
[TestCase("/", "api/command", "Other", "command")]
internal void stub_mocker(string apppath, string route, string expected_controller,\
string expected_action)
{
var rc = new RouteCollection();
Infrastructure.RouteRegistry.RegisterRoutes(rc);
var httpmock = new StubHttpContextForRouting(
appPath: apppath,
requestUrl: route);
// this always returns null for everything but the Index case.
var routeData = rc.GetRouteData(httpmock);
var controller = routeData.Values["controller"];
var action = routeData.Values["action"];
Assert.AreEqual(expected_controller, controller);
Assert.AreEqual(expected_action, action);
}
All you are testing right now is if the routes are added to the collection, by accessing it by the route name, and not if the expected route will return given a virtual path. You need to obtain the route data as returned by the RouteCollection with a HttpContext.
Best way would be to use a mock or a stub for the HttpContext (or HttpContextBase) and call the RouteCollection's GetRouteData(HttpContextBase) method and inspect the route data.
There is a good example of this in Brad Wilson's blog:
http://bradwilson.typepad.com/blog/2010/07/testing-routing-and-url-generation-in-aspnet-mvc.html
Edit:You cannot get a controller instance from the RouteData itself. However, RouteData should give you enough information to know which controller will be instantiated. For example, if you have a controller at MyProject.Controllers.HomeController with an action Home, this should hold true in your test (using xUnit and Moq):
// Prepare
var context = new Mock<HttpContextBase>();
var request = new Mock<HttpRequestBase>();
var response = new Mock<HttpResponseBase>();
var session = new Mock<HttpSessionStateBase>();
var server = new Mock<HttpServerUtilityBase>();
context.SetupGet(c => c.Request).Returns(request.Object);
context.SetupGet(c => c.Response).Returns(response.Object);
context.SetupGet(c => c.Session).Returns(session.Object);
context.SetupGet(c => c.Server).Returns(server.Object);
request.SetupGet(r => r.HttpMethod).Returns("GET");
request.SetupGet(r => r.PathInfo).Returns(String.Empty);
request.SetupGet(r => r.AppRelativeCurrentExecutionFilePath).Returns("~/Home");
var expectedHandler = typeof (HomeController).GetMethod("Index", Type.EmptyTypes);
var data = RouteTable.Routes.GetRouteData(context.Object);
Assert.NotNull(data);
var handler = (MethodInfo) data.DataTokens["actionMethod"];
Assert.Equal(expectedHandler, handler);
I've had prety good experience with MVCContrib's Testhelper
Take a look at this test of testhelper.
Saves a lot of hassles around stubbing HttpContext etc.
Also if you are on MVC4, have a look at this Nuget package which is a fork for MVC4.