在ASP.NET MVC 4中强制异步操作同步执行

6

由于MVC4不支持异步子操作(通过Html.Action),我正在寻找一种方法来强制同步执行子操作。这个限制的一个简单的解决办法是提供所有控制器操作的同步版本:

public class FooAsyncController : Controller {
    public async Task<ActionResult> IndexAsync() {
        var model = await service.GetFoo().ConfigureAwait(false);
        return View(model);
    }   
}

public class FooSyncController : FooAsyncController {
    public ActionResult Index() {
        return IndexAsync().Result; // blocking call
    }   
}

然而,由于我们允许在所有控制器操作上执行子操作请求,因此为每个控制器执行此操作真的很麻烦。

在框架中是否有可扩展性点,我们可以检查操作的返回值,并且如果它返回一个Task<T>并且我们正在处理一个子操作,则强制进行同步调用?


1
首先想到的是创建一个新的扩展方法 Html.ActionAsync,它将简单地调用管道以满足您的需求,但在助手方法体的末尾添加 .Result 调用,从而将异步调用转换为同步调用。这确实是一个有趣的问题。 - Tejs
1
如果是我,我会创建一个新的控制器,覆盖CreateActionInvoker或HandleUnknownAction,并使用反射查看目标控制器以进行同步调用。您只需要实现一次控制器,就可以将其重用于任何异步操作。 - Thinking Sites
@Tejs 是的,这将是理想的解决方案,尽管框架的这个区域不太支持异步。问题似乎出现在 HttpServerUtilityBase.Execute 的深处。@ThikingSites - 为什么不发表一个更详细的答案呢?这听起来像一个很好的解决方法。 - Ben Foster
1个回答

3
经过几个小时的浏览ASP.NET MVC源代码,我能提供的最佳解决方案(除了创建每个控制器操作的同步版本)是在Controller.HandleUnknownAction中手动调用异步Action方法的操作描述符。虽然我对这段代码不是特别满意,但它确实可以工作。思路是故意请求一个无效的操作(以“_”为前缀),这将在控制器上调用HandleUnknownAction方法。在此,我们通过首先从actionName中删除下划线来查找匹配的异步操作,并调用AsyncActionDescriptor.BeginExecute方法。通过立即调用EndExecute方法,我们有效地同步执行操作描述符。
public ActionResult Index()
{
    return View();
}


public async Task<ActionResult> Widget(int page = 10)
{
    var content = await new HttpClient().GetStringAsync("http://www.foo.com")
        .ConfigureAwait(false);
    ViewBag.Page = page;
    return View(model: content);
}

protected override void HandleUnknownAction(string actionName)
{
    if (actionName.StartsWith("_"))
    {
        var asyncActionName = actionName.Substring(1, actionName.Length - 1);
        RouteData.Values["action"] = asyncActionName;

        var controllerDescriptor = new ReflectedAsyncControllerDescriptor(this.GetType());
        var actionDescriptor = controllerDescriptor.FindAction(ControllerContext, asyncActionName)
            as AsyncActionDescriptor;

        if (actionDescriptor != null)
        {
            AsyncCallback endDelegate = delegate(IAsyncResult asyncResult)
            {

            };

            IAsyncResult ar = actionDescriptor.BeginExecute(ControllerContext, RouteData.Values, endDelegate, null);
            var actionResult = actionDescriptor.EndExecute(ar) as ActionResult;

            if (actionResult != null)
            {
                actionResult.ExecuteResult(ControllerContext);
            }
        }
    }
    else
    {
        base.HandleUnknownAction(actionName);
    }
}

视图

<h2>Index</h2>

@Html.Action("_widget", new { page = 5 }) <!-- note the underscore prefix -->

我几乎可以确定有一种更好的方法,通过覆盖 Controller.BeginExecute。默认实现如下所示。想法是立即执行 Controller.EndExecuteCore,尽管到目前为止我还没有成功。

protected virtual IAsyncResult BeginExecute(RequestContext requestContext, AsyncCallback callback, object state)
{
    if (DisableAsyncSupport)
    {
        // For backwards compat, we can disallow async support and just chain to the sync Execute() function.
        Action action = () =>
        {
            Execute(requestContext);
        };

        return AsyncResultWrapper.BeginSynchronous(callback, state, action, _executeTag);
    }
    else
    {
        if (requestContext == null)
        {
            throw new ArgumentNullException("requestContext");
        }

        // Support Asynchronous behavior. 
        // Execute/ExecuteCore are no longer called.

        VerifyExecuteCalledOnce();
        Initialize(requestContext);
        return AsyncResultWrapper.Begin(callback, state, BeginExecuteCore, EndExecuteCore, _executeTag);
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接