Imlementing a Custom IRouter in ASP.NET 5 (vNext) MVC 6

前端 未结 2 464
情话喂你
情话喂你 2020-12-05 18:41

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

相关标签:
2条回答
  • 2020-12-05 19:12

    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.

    0 讨论(0)
  • 2020-12-05 19:16

    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.

    0 讨论(0)
提交回复
热议问题