为什么带有async/await方法的属性永远不会返回?

4

好的,请跟着我一起来,这可能需要一些解释。我有一个简单的账户控制器,如下所示:

[RoutePrefix("api/account")]
[Authorize]
[HmacAuthentication]
public class AccountController : ApiController
{
    public async Task<IHttpActionResult> Register(UserModel userModel)
    {
        if (!this.ModelState.IsValid)
        {
            return this.BadRequest(this.ModelState);
        }

        IdentityResult result = await this._userService.RegisterUser(userModel);

        var errorResult = this.GetErrorResult(result);
        if (errorResult != null)
        {
            return errorResult;
        }

        return this.Ok();
    }
}
属性在这里:
public class HmacAuthenticationAttribute : Attribute, IAuthenticationFilter
{
    public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {
        ...

        var isValid = this.IsValidRequest(req, appId, incomingBase64Signature, nonce, requestTimeStamp);

        if (isValid.Result)
        {
            ...
        }
        ...
    }

    private async Task<bool> IsValidRequest(
        HttpRequestMessage req, 
        string appId, 
        string incomingBase64Signature, 
        string nonce, 
        string requestTimeStamp)
    {
        ...
        var user = await this.UserService.FindUser(userId); // this never gets a return value
        ...
    }
}

UserService 中调用的方法如下:
public async Task<ApplicationUserModel> FindUser(int id)
{
    var user = await this._userBusiness.FindAsync(id);
    return this.MapToModel(user);
}

在商务舱中是这样的:

public async Task<ApplicationUser> FindAsync(int id)
{
    var result = await this._userManager.FindByIdAsync(id);
    return result;
}

我遇到的问题是,当调用 Register 方法时,HmacAuthentication 属性会触发并执行 AuthenticateAsync 过滤器方法。在 IsValidRequest 中查找用户的调用实际上从未返回任何值,如果我尝试通过 Postman 发送请求,它将永远无法完成。
请问是否有人能帮忙解决这个问题?

你对 isValid 做了什么?你在任何地方对它进行了 await 吗? - sstan
1
你能否判断代码是否卡在 isValid.Result 这一行?如果是,那么你可能遇到了死锁问题。请阅读这里,了解为什么通常不建议使用 Task.Result 等待任务完成。 - sstan
有时候变量不是什么好东西,它会让你忘记await... - Ryan Chu
@sstan 谢谢,我只是假设上面那行代码有问题,因为调试器从来没有执行到那里,链接和下面的答案都很有帮助。 - Neil Stevens
2个回答

5
public async Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)
    {
        ...

        var isValid = await this.IsValidRequest(req, appId, incomingBase64Signature, nonce, requestTimeStamp);

        if (isValid)
        {
            ...
        }
        ...
    }

正如编译器所建议的那样,您只能在标记为async的方法内部使用await关键字。因此,我已相应地更新了AuthenticateAsync的签名。另请注意,现在您只需检查isValid而不是执行isValid.Result,因为布尔值的值将在await行之后可用。
这可能有点令人困惑,因为IAuthenticationFilter接口没有指定async(接口无法指定,它们只能表明该方法将返回一个任务)。由实现者确定方法是否仅返回任务或提供异步以便在方法体内等待值。

我尝试过了,但是我得到了一个编译时错误:“'await'运算符只能在标记有'async'修饰符的方法或lambda中使用”。我调用的方法位于框架接口上:“public Task AuthenticateAsync(HttpAuthenticationContext context, CancellationToken cancellationToken)”。 - Neil Stevens
我之前不知道你可以在不是自己的方法上使用async修饰符,我仍在努力理解await/async,但这确实起到了作用。同时感谢@sstan上面的评论,它确实被阻止在isValid.Result上,我以为是前一行因为我在调试时从来没有走到那里。 - Neil Stevens
你只能在返回Tasks的接口方法上使用它。async/await肯定需要一点时间来理解,但它非常强大,一旦你掌握了它,你会想知道自己以前是如何编码的 :) - Jesse Carter

4
接受的答案在解决问题方面是正确的,但没有解释为什么会发生这种情况。
这个:
var isValid = this.IsValidRequest(
                  req, 
                  appId, 
                  incomingBase64Signature, 
                  nonce, 
                  requestTimeStamp);

if (isValid.Result)

你的代码出现了死锁。你正在同步地阻塞异步方法。这是因为编译器在看到一个async方法时,会生成一个状态机,将第一个await之后的所有内容都包装为一个续集。编译器还会查看是否涉及任何同步上下文,如果有,则尝试将续集调度回AspNetSynchronizationContext,但由于你的.Result调用而导致其被阻塞,从而造成死锁。
这就是为什么你不应该阻塞异步代码的原因。

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