使 ASP.net MVC 5 路由忽略 Action 方法上的异步后缀

Make ASP.net MVC 5 Routing Ignore Async Suffix on Action methods

WebAPI 2 智能处理操作方法上的异步后缀。例如,如果我创建一个默认的 WebAPI 项目,它将路由到正确的操作,而不管后缀如何。即:

http://host/api/controller/action      - SUCCEEDS
http://host/api/controller/actionAsync - SUCCEEDS

但是,如果我使用 MVC 5 创建等效的控制器,则行为会有所不同:

http://host/controller/actionAsync - SUCCEEDS
http://host/controller/action      - FAILS - 404

当 Async 后缀不存在时它会失败并返回 404 的事实令人惊讶。尽管如此,我尝试添加路由来处理它,但仍然失败:

routes.MapRoute(
    name: "DefaultAsync",
    url: "{controller}/{action}Async/{id}",
    defaults: new {controller = "Home", action = "Index", id = UrlParameter.Optional}
    );

这是我用来测试的 MVC 和 WebAPI 控制器(基于具有默认路由的新 MVC/WebAPI 项目):

public class SampleDto { public string Name; public int Age; }

public class SampleMvcController : Controller
{
    public Task<JsonResult> GetJsonAsync()
    {
        // Illustration Only. This is not a good usage of an Async method,
        // in reality I'm calling out to an Async service.
        return Task.FromResult(
            Json(new SampleDto { Name="Foo", Age = 42}, JsonRequestBehavior.AllowGet));
    }
}

public class SampleWebApiController : ApiController
{
    public Task<SampleDto> GetJsonAsync()
    {
        return Task.FromResult(new SampleDto {Name="Bar", Age=24});
    }
}

因为我正在制作一堆异步方法,所以我不想指定操作名称。 routing documentation 表明它可以拾取可以分隔段的文字,但我还没有运气。

更新: 问题是 MVC 检索的 Action 包含 Async 后缀,但控制器上不存在相应的操作(或操作名称)。与动作匹配的片段 MyAction 未将 MyActionAsync 识别为匹配项。

回想起来,这就是这条路线行不通的原因。它尝试将操作标识为以 Async 结尾,但从匹配中使用的操作中删除 Async 后缀,这不是我想要做的。如果我只想创建一个 MyAction 方法(异步但不遵循异步命名约定)并将其映射到相应的 MyAction 方法,这将很有用。

与其尝试将文字与占位符放在一起,您应该进行约束以确保您的操作名称以 Async.

结尾
routes.MapRoute(
    name: "DefaultAsync",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "IndexAsync", id = UrlParameter.Optional },
    constraints: new { action = @".*?Async" }
);

routes.MapRoute(
    name: "Default",
    url: "{controller}/{action}/{id}",
    defaults: new { controller = "Home", action = "Index", id = UrlParameter.Optional }
);

最初我非常失望,因为 AsyncController 类型完全可以开箱即用地完成我们正在寻找的功能。但显然它已经过时了,只在 "backwards compatibility with MVC3."

期间仍然存在

所以我最后做的是制作自定义 AsyncControllerActionInvoker 并将其分配给自定义控制器的 ActionInvoker。 CustomAsyncControllerActionInvoker 重写 BeginInvokeAction 方法以查看是否存在以 "Async" 结尾的用于适当操作的操作方法(例如,您传入 "Index" 它会查找 "IndexAsync")。如果是,请改为调用那个,否则,继续原样。

public class HomeController : CustomController
{
    public async Task<ActionResult> IndexAsync()
    {
        ViewBag.Header = "I am a page header."
        var model = new List<int> {1, 2, 3, 4, 5};
        await Task.Run(() => "I'm a task");
        return View(model);
    }
    public ActionResult About()
    {
        return View();
    }
}

public class CustomController : Controller
{
    public CustomController()
    {
        ActionInvoker = new CustomAsyncControllerActionInvoker();
    }
}

public class CustomAsyncControllerActionInvoker : AsyncControllerActionInvoker
{
    public override IAsyncResult BeginInvokeAction(ControllerContext controllerContext, string actionName, AsyncCallback callback, object state)
    {
        var asyncAction = FindAction(controllerContext, GetControllerDescriptor(controllerContext), $"{actionName}Async");
        return asyncAction != null
            ? base.BeginInvokeAction(controllerContext, $"{actionName}Async", callback, state)
            : base.BeginInvokeAction(controllerContext, actionName, callback, state);
    }
}

唉,导航到 /Home/Index 会正确调用 IndexAsync() 方法并适当地提供 Index.cshtml(不是 IndexAsync.cshtml)视图。同步操作的路由(例如 "About")正常处理。