简体   繁体   English

在ASP.NET 5(vNext)MVC 6中添加自定义IRouter

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

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. 我正在尝试将此示例RouteBase实现转换为与MVC 6一起使用。我已经通过遵循路由项目中的示例来解决大部分问题,但我正在惹恼如何从方法返回异步Task 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). 我有传出路由功能(意味着当我输入路由值时ActionLink工作正常)。 The problem is with the RouteAsync method. 问题在于RouteAsync方法。

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. 但是当它完成执行时,它不会调用CustomPage控制器的Details方法。 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). 我在这篇帖子中添加了WriteAsync行,它将Hello there写入空白页面,但是我无法理解为什么MVC没有调用我的控制器(在以前的版本中这没有顺利工作)。 Unfortunately, that post covered every part of routing except for how to implement an IRouter or INamedRouter . 不幸的是,该帖子涵盖了路由的每个部分,除了如何实现IRouterINamedRouter

How can I make the RouteAsync method function? 如何使RouteAsync方法起作用?

Entire CustomRoute Implementation 整个CustomRoute实现

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 CustomRoute DI注册

services.AddTransient<ICustomRoute, CustomRoute>();

MVC Route Configuration MVC路由配置

// 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 . 如果重要,我使用的是Beta 5DNX 4.5.1DNX 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. 我根据我在这里学到的信息创建了一个通用的解决方案,可以在这个答案中用于URL双向映射的简单主键。 The controller, action, data provider, and datatype of the primary key can be specified when wiring it into MVC 6 routing. 在将其连接到MVC 6路由时,可以指定主键的控制器,操作,数据提供程序和数据类型。

As @opiants said, the problem is that you are doing nothing in your RouteAsync method. 正如@opiants所说,问题是你在RouteAsync方法中什么也没做。

If your intention is to end up calling a controller action method, you could use the following approach than the default MVC routes: 如果您的目的是最终调用控制器操作方法,则可以使用以下方法而不是默认的MVC路由:

By default MVC uses a TemplateRoute with an inner target IRouter . 默认情况下,MVC使用带有内部目标IRouterTemplateRoute In RouteAsync, the TemplateRoute will delegate to the inner IRouter. 在RouteAsync中,TemplateRoute将委托给内部IRouter。 This inner router is being set as the MvcRouteHandler by the default builder extensions . 默认构建器扩展将此内部路由器设置为MvcRouteHandler In your case, start by adding an IRouter as your inner target: 在您的情况下,首先添加一个IRouter作为您的内部目标:

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 : 然后更新您的启动以将该目标设置为MvcRouteHandler ,它已被设置为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 . 最后,更新您的AsyncRoute方法以调用内部IRouter ,它将是MvcRouteHandler You can use the implementation of that method in TemplateRoute as a guide. 您可以在TemplateRoute使用该方法的实现作为指导。 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 更新RC2

Looks like TemplateRoute is no longer around in RC2 aspnet Routing. 看起来在RC2 aspnet路由中不再出现TemplateRoute

I investigated the history, and it was renamed RouteBase in commit 36180ab as part of a bigger refactoring. 我调查了历史,并在提交36180ab中将其重命名为RouteBase ,作为更大的重构的一部分。

Primary reason why that doesn't work is because you aren't doing anything in the RouteAsync method. 这不起作用的主要原因是因为您没有在RouteAsync方法中执行任何RouteAsync 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. 另一个原因是MVC 6中的路由工作方式与以前的MVC路由工作方式有很大不同,因此您可能最好使用源代码作为参考从头开始编写它,因为很少有文章可以解决MVC 6的问题。时刻。

EDIT: @Daniel JG answer makes much more sense than this so use that if possible. 编辑:@Daniel JG的答案比这更有意义,所以尽可能使用它。 This might fit someone else's use case so I'm leaving this here. 这可能适合其他人的用例,所以我将其留在这里。

Here's a very simple IRouter implementation using beta7 . 这里是一个非常简单的IRouter使用β7的实现。 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: 您需要删除page != null并将其替换为下面的代码并替换控制器和操作:

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. 确保添加对Microsoft.Framework.DependencyInjection命名空间的引用以解析GetRequiredService扩展。

After that, register the IRouter as per below: 之后,按以下方式注册IRouter:

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, 然后在你的IOC注册,

services.AddSingleton<CustomRoute>();

Another 'cleaner' approach would probably be to create a different implementation of IActionSelector . 另一种“更清洁”的方法可能是创建一个不同的IActionSelector实现。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM