このサンプルRouteBase実装 をMVC 6で動作するように変換しようとしています ルーティングプロジェクトの例 を実行することでほとんどを解決しましたが、つまずきますメソッドから非同期のTask
を返す方法について説明します。それが実際に非同期であるかどうかは本当に気にしません(その答えを提供できる人に乾杯します)、今のところ私はそれを機能させたいだけです。
発信ルートが機能しています(つまり、ルート値を入力すると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);
}
一致する場合、メソッド全体が最後まで実行されます。ただし、実行が完了すると、Details
コントローラーのCustomPage
メソッドは呼び出されません。ブラウザに空白の白いページが表示されます。
この投稿 で行ったようにWriteAsync
行を追加し、空白のページにHello there
を書き込みますが、MVCがコントローラーを呼び出さない理由がわかりません(以前のバージョンでは、これは問題なく機能しました)。残念ながら、その投稿は、IRouter
またはINamedRouter
を実装する方法を除いて、ルーティングのすべての部分をカバーしていました。
RouteAsync
メソッドを関数にするにはどうすればよいですか?
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;
}
}
services.AddTransient<ICustomRoute, CustomRoute>();
// 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?}");
});
重要な場合は、Beta 5
、DNX 4.5.1
、およびDNX Core 5
を使用しています。
解決
ここで学んだ情報に基づいて、URL双方向マッピングへの単純な主キーに使用できる一般的なソリューションを作成しました この回答では 。主キーのコントローラー、アクション、データプロバイダー、およびデータ型は、MVC6ルーティングに配線するときに指定できます。
@opiantsが言ったように、問題はRouteAsync
メソッドで何もしていないことです。
最終的にコントローラーアクションメソッドを呼び出すことを意図している場合は、デフォルトのMVCルートよりも次のアプローチを使用できます。
デフォルトでは、MVCは内部ターゲット
TemplateRoute
でIRouter
を使用します。 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<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?}");
});
最後に、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を更新
TemplateRoute
はRC2aspnetルーティングでは存在しなくなったようです。
履歴を調査したところ、より大きなリファクタリングの一環として、 RouteBase
in commit 36180ab に名前が変更されました。
それが機能しない主な理由は、RouteAsync
メソッドで何もしていないためです。もう1つの理由は、MVC 6でのルーティングの動作が、以前のMVCルーティングの動作とは大きく異なるため、 ソースコード を参照として使用して最初から作成する方がよいでしょう。現時点でMVC6に取り組んでいる記事。
編集:@Daniel J.G.答えはこれよりもはるかに理にかなっているので、可能であればそれを使用してください。これは他の誰かのユースケースに合うかもしれないので、ここに残しておきます。
これは、beta7を使用した非常に単純な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<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;
GetRequiredService
拡張子を解決するには、必ずMicrosoft.Framework.DependencyInjection
名前空間への参照を追加してください。
その後、以下のように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 ...
});
次に、それをIOCに登録します。
services.AddSingleton<CustomRoute>();
別の「よりクリーンな」アプローチは、おそらくIActionSelector
の異なる実装を作成することです。