C#异步,执行停止,发生了什么?

时间:2016-08-12 15:43:39

标签: c# asp.net asynchronous

我实际上知道自己在做什么的一些时刻;这不是其中之一。

我试着理解为什么执行特定的异步操作时代码的执行会停止。它在同步执行时起作用,在异步执行时不起作用。

这一切都发生在MVC应用程序中;

  • AccountController通过AccountService创建一个新帐户,以保持我的控制器的清洁程度。
  • AccountService我们做了一些像用户帐户(ASP.NET身份)这样的事情,这很好用。没什么特别的。
  • 然后,我尝试发送电子邮件"欢迎使用您的新帐户"一切都出错了。

我不是特别寻找解决方案,我对发生的事情更感到好奇。

当我在EmailSender.Send(message);方法中执行await CreatePrimaryUser时,account.AccountOwnerId = userId;行永远不再执行。没有例外。看起来一切都很顺利,但方法才停止执行。

我哪里出错了?

的AccountController

[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
public ActionResult Register(AccountViewModel model)
{
    if (ModelState.IsValid)
    {
        try
        {
            BusinessServiceFacade.GetAccountService(RavenMasterSession).Create(model.ToModel(), model.SubscriptionPlanId);
            return Redirect(string.Format("http://{0}.localhost:6257/Admin/GettingStarted",model.Name));
        }
        catch(AccountTakenException)
        {
            ModelState.AddModelError("", "Account name already taken");
        }
        catch (CustomException e)
        {
            ModelState.AddModelError("", "Error:" + e);
        }
    }
    return View(model);
}

GetAccountService创建

public async Task<Account> Create(Account account, string subscriptionPlanId)
{
    if (IsAccountNameTaken(account.Name))
        throw new AccountTakenException();

    RavenMasterSession.Store(account); // register account in MasterDB
    CreateTenantDatabase(account); // create tenantDB

    var userId = await CreatePrimaryUser(account); // create account owner (first user)
    account.AccountOwnerId = userId;

    var stripeAccountResult = BusinessServiceFacade.GetStripeService(RavenMasterSession).CreateCustomer(account); // register account in Stripe
    account.CustomerId = stripeAccountResult.CustomerId;

    AddSubscription(account, subscriptionPlanId); // add subscription to account

    return account;
}    

CreatePrimaryUser

private async Task<string> CreatePrimaryUser(Account account)
{
    var user = new User()
    {
        UserName = account.AccountOwner.Email,
        FirstName = account.AccountOwner.FirstName,
        LastName = account.AccountOwner.LastName,
        Email = account.AccountOwner.Email
    };
    RavenContext.GetTenantSession().Store(user);

    var result = await UserManager.CreateAsync(user);
    if (result.Succeeded)
    {
        var message = new MailMessage();
            message.To.Add("recipient@domain.com");
            message.Subject = "Thank you for joining ... ";
            message.Body = string.Format("Your account is available at http://{0}.localhost:6257/Admin/GettingStarted", account.Name);

        await EmailSender.Send(message);
    }
    else
    {
        RavenContext.GetTenantSession().Delete(user);
        var stringList = String.Concat(result.Errors.ToArray());
        throw new CustomException(stringList);
    }
    return user.Id;
}

EmailSender

public static async Task SendSES(MailMessage message)
{
    const string FROM = "from@domain.com";
    message.From = new MailAddress(FROM);

    const string SMTP_USERNAME = "user";   
    const string SMTP_PASSWORD = "pass";  
    const string HOST = "email-smtp.eu-west-1.amazonaws.com";
    const int PORT = 587;

    using (SmtpClient client = new SmtpClient(HOST, PORT))
    {                
        client.Credentials = new System.Net.NetworkCredential(SMTP_USERNAME, SMTP_PASSWORD);
        client.EnableSsl = true;

        try
        {
            //client.Send(message);
            await client.SendMailAsync(message);
        }
        catch (Exception ex)
        {
            throw ex;
        }
    }
}

1 个答案:

答案 0 :(得分:3)

这是在某处混合异步同步的症状,这将导致完全如您所述的死锁。像下面一样更改你的调用代码,所以它一直是异步,否则使用async没有什么好处。像这样更改它没有问题,因为你正在使用支持动作签名的MVC。

[HttpPost]
[ValidateAntiForgeryToken]
[AllowAnonymous]
// change to async
public async Task<ActionResult> Register(AccountViewModel model)
{
    if (ModelState.IsValid)
    {
        try
        {
            var service = BusinessServiceFacade.GetAccountService(RavenMasterSession);

            // broken apart so you can see the await call
            await service.Create(model.ToModel(), model.SubscriptionPlanId);

            return Redirect(string.Format("http://{0}.localhost:6257/Admin/GettingStarted",model.Name));
        }
        catch(AccountTakenException)
        {
            ModelState.AddModelError("", "Account name already taken");
        }
        catch (CustomException e)
        {
            ModelState.AddModelError("", "Error:" + e);
        }
    }
    return View(model);
}

旁注1

重新抛出异常只写throw;而不是throw ex;。稍后执行操作会在异常中重置调用堆栈,这使得以后很难进行调试。要保留异常状态/调用堆栈,请使用前者(最佳实践并始终建议使用)!

try {
    // something that could throw exception
} catch(Exception ex) {
    // do something with the exception like logging or retry or whatever
    // if the code does nothing but a rethrow then remove the whole try/catch as it has no added value
    throw;
}

旁注2

对于非操作方法(那些未在Mvc或Web API控制器上定义的方法),如果方法是异步方法,则应使用Async作为命名约定来后缀该方法。这使您可以轻松查看代码中的哪个位置可以使用await或其他任务机制来执行代码。您可以看到Microsoft也使用FCL中内置的异步方法执行此操作。它不是必需的(没有什么可以强制执行),但这是一种很好的做法,因为它使代码更具可读性。

  • Create变为CreateAsync
  • CreatePrimaryUser变为CreatePrimaryUserAsync
  • SendSES变为SendSESAsync