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

我正在尝试将此示例RouteBase实现转换为与MVC 6一起使用。我已经通过遵循路由项目中的示例来解决大部分问题,但是我正在了解如何从方法返回异步Task 。 我真的不在乎它是否实际上是异步的(对能够提供该答案的任何人欢呼),现在我只想让它运转起来。

我有传出路线function(意味着当我输入路线值时, ActionLink工作正常)。 问题出在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); } 

当匹配时,整个方法一直运行到最后。 但是当它完成执行时,它不会调用CustomPage控制器的Details方法。 我只是在浏览器中看到一个空白的白页。

我在这篇文章中添加了WriteAsync行,并将Hello there写入空白页面,但我无法理解为什么MVC没有调用我的控制器(在以前的版本中这没有顺利)。 不幸的是,该post涵盖了路由的每个部分,除了如何实现IRouterINamedRouter

如何使RouteAsync方法起作用?

整个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 pages, IDictionary 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 GetPageList() { string key = "__CustomPageList"; IEnumerable 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() { 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注册

 services.AddTransient(); 

MVC路由配置

 // Add MVC to the request pipeline. app.UseMvc(routes => { routes.Routes.Add(routes.ServiceProvider.GetService()); 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?}"); }); 

如果重要,我使用的是Beta 5DNX 4.5.1DNX Core 5

我根据我在这里学到的信息,创建了一个通用解决方案,可以在此答案中用于URL双向映射的简单主键。 在将其连接到MVC 6路由时,可以指定主键的控制器,操作,数据提供程序和数据类型。

正如@opiants所说,问题是你在RouteAsync方法中什么都不做。

如果您的目的是最终调用控制器操作方法,则可以使用以下方法而不是默认的MVC路由:

默认情况下,MVC使用带有内部目标IRouterTemplateRoute 。 在RouteAsync中,TemplateRoute将委托给内部IRouter。 默认构建器扩展将此内部路由器设置为MvcRouteHandler 。 在您的情况下,首先添加一个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; } 

然后更新您的启动以将该目标设置为MvcRouteHandler ,它已被设置为routes.DefaultHandler

 app.UseMvc(routes => { routes.Routes.Add( new CustomRoute(routes.ServiceProvider.GetRequiredService(), 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?}"); }); 

最后,更新您的AsyncRoute方法以调用内部IRouter ,它将是MvcRouteHandler 。 您可以在TemplateRoute使用该方法的实现作为指导。 我很快就使用了这种方法并修改了你的方法如下:

 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; } } } 

更新RC2

看起来在RC2 aspnet路由中不再出现TemplateRoute

我调查了历史,并在提交36180ab中将其重命名为RouteBase ,作为更大的重构的一部分。

这不起作用的主要原因是因为您没有在RouteAsync方法中执行任何RouteAsync 。 另一个原因是MVC 6中的路由工作方式与以前的MVC路由工作方式有很大不同,因此您可能最好使用源代码作为参考从头开始编写它,因为很少有文章可以解决MVC 6的问题。时刻。

编辑:@Daniel JG的答案比这更有意义所以尽可能使用它。 这可能适合其他人的用例,所以我要离开这里。

这是一个使用IRouter的非常简单的IRouter实现。 这应该可行但你可能需要填补空白。 您需要删除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(); var descriptor = new ControllerActionDescriptor { Name = action, MethodInfo = controllerType.GetTypeInfo().DeclaredMethods.Single(m => m.Name == action), ControllerTypeInfo = controllerType.GetTypeInfo(), // Setup filters FilterDescriptors = new List(), // Setup DI properties BoundProperties = new List(0), // Setup action arguments Parameters = new List(0), // Setup route constraints RouteConstraints = new List(0), // This router will work fine without these props set //ControllerName = "Home", //DisplayName = "Home", }; var accessor = context.HttpContext.RequestServices.GetRequiredService(); accessor.ActionContext = new ActionContext(context.HttpContext, context.RouteData, descriptor); var actionInvokerFactory = context.HttpContext.RequestServices.GetRequiredService(); var invoker = actionInvokerFactory.CreateInvoker(accessor.ActionContext); // Render the page await invoker.InvokeAsync(); // Don't execute the next IRouter context.IsHandled = true; return; 

确保添加对Microsoft.Framework.DependencyInjection命名空间的引用以解析GetRequiredService扩展。

之后,按以下方式注册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()); // .. more code here ... }); 

然后在你的IOC注册,

 services.AddSingleton(); 

另一种“更清洁”的方法可能是创建一个不同的IActionSelector实现。