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 ge
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
.
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 anIRouter
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.