【问题标题】:Awaited Async method not returning before result is required在需要结果之前等待的异步方法未返回
【发布时间】:2015-06-13 16:23:18
【问题描述】:

我无法解决我遇到的异步/等待问题。

简而言之,我有一个控制器,装饰有一个属性。 该属性从一个 i/o 密集型进程(filesystem / db / api etc...)中获取指定的内容

然后它将返回的内容设置为 ViewBag 上的 Dictionary<string, string>

然后,在一个视图中,我可以做这样的事情来检索内容:

@(ViewBag.SystemContent["Common/Footer"])

我遇到的问题是,它第一次运行时,内容没有返回,并且通过字符串索引检索值的调用失败,因为它不存在。
按F5,没问题。

控制器动作非常简单:

[ProvideContent("Common/Footer")]
public class ContactDetailsController : Controller
{
    public async Task<ActionResult> Index()
    {
        //omitted for brevity - awaits some other async methods
        return View();
    }
}

属性

public override async void OnActionExecuting(ActionExecutingContext filterContext)
{
    if (filterContext.Result is ViewResult)
    {
        var localPath = filterContext.RouteData.Values["controller"] + "/" + filterContext.RouteData.Values["action"];

        if (!_useControllerActionAsPath)
            localPath = _path;

        var viewResult = filterContext.Result as ViewResult;

        //this seems to go off and come back AFTER the view requests it from the dictionary
        var content = await _contentManager.GetContent(localPath);

        if (viewResult.ViewBag.SystemContent == null)
            viewResult.ViewBag.SystemContent = new Dictionary<string, MvcHtmlString>();

        viewResult.ViewBag.SystemContent[localPath] = new DisplayContentItem(content, localPath);
    }

编辑

在我的属性中更改以下行:

var content = await _contentManager.GetContent(localPath);

var content = Task.Factory.StartNew(() =>
            manager.GetContent(localPath).Result, TaskCreationOptions.LongRunning).Result;

解决了这个问题,但我觉得这与我在 Stephen Clearys 博客上读到的所有内容都背道而驰......

【问题讨论】:

  • 我不是异步专家,但是您正在定义内容并立即使用它,您不能将其更改为同步调用吗?
  • 有内容,我在 awaiting _contentmanager.GetContent - 这是异步的.....即使我最后用 .Result 阻塞,也是一样的结果结果
  • 我怀疑问题在于您正在用异步方法覆盖同步方法。如果您需要它在操作执行之前运行完成,为什么要让它异步?
  • 为什么我做了哪个异步?被覆盖的方法?
  • 是的,Controller.OnActionExecuting 看起来像一个同步方法。如果你让你的异步,那么当 Index 操作完成时它仍然可以运行。

标签: c# async-await


【解决方案1】:

我不是 100% 熟悉 ASP.Net MVC 堆栈以及所有这些是如何工作的,但我会尝试一下。

OnActionExecuting() 文档说:

在调用操作方法之前调用。

由于您覆盖了以前的同步方法并使其异步,因此期望该代码是完整的,并为下一个执行步骤做好准备。

理论执行路径:

public void ExecuteAction()
{
 OnActionExecuting();

 OnActionExecution();

 OnActionExecuted();
}

由于您覆盖了 OnActionExecuting() 方法,因此执行堆栈基本上仍然相同,但是下一个要执行的代码(ExecuteAction()OnActionExecuted() 以及任何称为 ExecuteAction() 的代码)期望同步调用制作完成,因此据他们所知,一切都很好,可以继续运行。

基本上,这归结为OnActionExecuting() 不是异步方法,并且没有任何期望。 (MVC 6 也不是异步的。)

OnActionExecuting() 之后被同步调用的东西,它是顺序调用,正在引用viewResult.ViewBag.SystemContent,因此它没有得到你想要的值。正如你在标题中所说的那样,

在需要结果之前未返回等待的异步方法。

使用 Tasks 的关键在于您无法保证何时完成任务,但可以保证它完成。

可能的解决方案:

  • GetContent() 呼叫移出该事件。
  • 存储为GetContent() 创建的任务,找到下一个使用viewResult.ViewBag.SystemContent 的位置并检查任务是否完成或等待完成。
  • 为 GetContent() 方法添加超时间隔。 (有很多不同的方法可以做到这一点。MSDN Docs for Task class 这不会解决你的问题。

编辑:在控制器中存储任务的代码示例

[ProvideContent("Common/Footer")]
public class ContactDetailsController : Controller
{

/*
 * BEGINNING OF REQUIRED CODE BLOCK
 */
    private Task<string> _getContentForLocalPathTask; 
    private string _localPath;

/*
 * END OF REQUIRED CODE BLOCK
 */
    public async Task<ActionResult> Index()
    {
        //omitted for brevity - awaits some other async methods
/*
 * BEGINNING OF REQUIRED CODE BLOCK
 */
        string content = await _getContentForLocalPath;

        viewResult.ViewBag.SystemContent[_localPath] = new DisplayContentItem(content, _localPath);            
/*
 * END OF REQUIRED CODE BLOCK
 */

         return View();
    }

public override async void OnActionExecuting(ActionExecutingContext filterContext)
{
    if (filterContext.Result is ViewResult)
    {
        var localPath = filterContext.RouteData.Values["controller"] +
                         "/" + filterContext.RouteData.Values["action"];

        if (!_useControllerActionAsPath)
            localPath = _path;

        var viewResult = filterContext.Result as ViewResult;

/*
 * BEGINNING OF REQUIRED CODE BLOCK
 */
       _localPath = localPath;

       _getContentForLocalPathTask = _contentManager.GetContent(localPath);
/*
 * END OF REQUIRED CODE BLOCK
 */

        if (viewResult.ViewBag.SystemContent == null)
            viewResult.ViewBag.SystemContent = new Dictionary<string, MvcHtmlString>();

    }
}

【讨论】:

  • +1。 async 的第一条规则是避免async void。在这种情况下,OnActionExecuting 在遇到await 时返回到 ASP.NET,从而导致请求管道继续。我建议将异步操作存储在 Task&lt;T&gt;(即,在您的控制器类型中)和 awaiting 在您的控制器操作中执行该任务。
  • @StephenCleary - 不确定我是否遵循:将任务存储在控制器操作中?我的 ProvideContent 属性当前位于 Controller 本身,而不是 ActionMethod - 我愿意接受将这个想法合并的建议,但这似乎是一种合乎逻辑的方法?
  • @Alex 我已经用斯蒂芬所指的内容更新了我的答案。
  • @Cameron 谢谢,但这没有意义 - OnActionExecuting 是属性的一部分,但你有它在控制器体内?我可能不得不考虑使内容检索同步,因为我继承了这一点,并且上述更改是一个相当大的更改(编辑使用的每个控制器)
  • 在这里阅读@StephenCleary 的答案 - stackoverflow.com/questions/12482338/… - 所以我想在这种情况下阻止异步调用是正确的做法?
猜你喜欢
  • 2013-02-02
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2014-11-26
  • 1970-01-01
  • 1970-01-01
  • 2017-07-12
  • 2020-02-10
相关资源
最近更新 更多