21

I'm trying to set up email confirmation for an ASP.NET MVC5 website, based on the example AccountController from the VS2013 project template. I've implemented the IIdentityMessageService using SmtpClient, trying to keep it as simple as possible:

public class EmailService : IIdentityMessageService
{
    public async Task SendAsync(IdentityMessage message)
    {
        using(var client = new SmtpClient())
        {
            var mailMessage = new MailMessage("[email protected]", message.Destination, message.Subject, message.Body);
            await client.SendMailAsync(mailMessage);
        }
    }
}

The controller code that is calling it is straight from the template (extracted into a separate action since I wanted to exclude other possible causes):

public async Task<ActionResult> TestAsyncEmail()
{
    Guid userId = User.Identity.GetUserId();
    
    string code = await UserManager.GenerateEmailConfirmationTokenAsync(userId);
    var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = userId, code = code }, protocol: Request.Url.Scheme);
    await UserManager.SendEmailAsync(userId, "Confirm your account", "Please confirm your account by clicking <a href=\"" + callbackUrl + "\">here</a>");

    return View();
}

However I'm getting odd behavior when the mail fails to send, but only in one specific instance, when the host is somehow unreachable. Example config:

<system.net>
    <mailSettings>
        <smtp deliveryMethod="Network">
            <network host="unreachablehost" defaultCredentials="true" port="25" />
        </smtp>
    </mailSettings>
</system.net>

In that case, the request appears to deadlock, never returning anything to the client. If the mail fails to send for any other reason (e.g. host actively refuses connection) the exception is handled normally and I get a YSOD.

Looking at the Windows event logs, it seems that an InvalidOperationException is thrown around the same timeframe, with the message "An asynchronous module or handler completed while an asynchronous operation was still pending."; I get that same message in a YSOD if I try to catch the SmtpException in the controller and return a ViewResult in the catch block. So I figure the await-ed operation fails to complete in either case.

As far as I can tell, I am following all the async/await best practices as outlined in other posts on SO (e.g. HttpClient.GetAsync(...) never returns when using await/async), mainly "using async/await all the way up". I've also tried using ConfigureAwait(false), with no change. Since the code deadlocks only if a specific exception is thrown, I figure the general pattern is correct for most cases, but something is happening internally that makes it incorrect in that case; but since I'm pretty new to concurrent programming, I've a feeling I could be wrong.

Is there something I'm doing wrong ? I can always use a synchronous call (ie. SmtpClient.Send()) in the SendAsync method, but it feels like this should work as is.

10
  • 1
    Take a look at Stephen Cleary's answer on catching an exception on a void method(SendMailAsync). Async Void Methods are problem childs sometimes. Commented Feb 4, 2015 at 23:19
  • 1
    @ErikPhilips - I don't see any async void methods in the sample (either implemented or called) - did you mean some particular line? Commented Feb 4, 2015 at 23:42
  • 1
    As workaround you may try to manually resolve the host and fail earlier... Also look at the source for insights - hopefully it would be helpful... Commented Feb 4, 2015 at 23:46
  • 2
    I remember a related question with a workaround... there it is: Sending async mail from SignalR hub Commented Feb 6, 2015 at 0:46
  • 1
    @regexen, try my WithNoContext from here, see if it makes any difference. Commented Feb 8, 2015 at 11:04

2 Answers 2

15

Try this implementation, just use client.SendMailExAsync instead of client.SendMailAsync. Let us know if it makes any difference:

public static class SendMailEx
{
    public static Task SendMailExAsync(
        this System.Net.Mail.SmtpClient @this,
        System.Net.Mail.MailMessage message,
        CancellationToken token = default(CancellationToken))
    {
        // use Task.Run to negate SynchronizationContext
        return Task.Run(() => SendMailExImplAsync(@this, message, token));
    }

    private static async Task SendMailExImplAsync(
        System.Net.Mail.SmtpClient client, 
        System.Net.Mail.MailMessage message, 
        CancellationToken token)
    {
        token.ThrowIfCancellationRequested();

        var tcs = new TaskCompletionSource<bool>();
        System.Net.Mail.SendCompletedEventHandler handler = null;
        Action unsubscribe = () => client.SendCompleted -= handler;

        handler = async (s, e) =>
        {
            unsubscribe();

            // a hack to complete the handler asynchronously
            await Task.Yield(); 

            if (e.UserState != tcs)
                tcs.TrySetException(new InvalidOperationException("Unexpected UserState"));
            else if (e.Cancelled)
                tcs.TrySetCanceled();
            else if (e.Error != null)
                tcs.TrySetException(e.Error);
            else
                tcs.TrySetResult(true);
        };

        client.SendCompleted += handler;
        try
        {
            client.SendAsync(message, tcs);
            using (token.Register(() => client.SendAsyncCancel(), useSynchronizationContext: false))
            {
                await tcs.Task;
            }
        }
        finally
        {
            unsubscribe();
        }
    }
}
Sign up to request clarification or add additional context in comments.

13 Comments

That one works; the exception is caught as one would normally expect, bubbles up the call stack and I get a YSOD. Seems like a whole lot of code to do something that seemed so simple (!), but I can see how it can get complicated fast. Anyway marking as accepted since it does solve it. Thanks for all your help!
Seems to work without the Task.Yield, according to a quick test.
By your current implementation you're still 'wasting' a thread while doing the IO operation... @BornToCode I'm not. Task.Run(() => FuncAsync()) is basically doing the same as Task.Run(async () => await FuncAsync()) but without added async state machine micro-overhead. In both cases, the same Task.Run override is used.
@BornToCode, no Task.Run won't block anything for functions that return a Task like SendMailExImplAsync (unless such function has a blocking wait inside, which mine doesn't). If you like to learn more, have a look at Task.Run implementation.
@Gary, create a CancellationTokenSource with timeout you want and pass its token to SendMailExImplAsync
|
3

On .Net 8 this did not solve my issue, and I did not see any exceptions being thrown either.

Finally adding

message.BodyEncoding = System.Text.Encoding.UTF8;

Seems to fix some underlying issue...

2 Comments

SmtpClient is deprecated on modern .NET anyway, so you shouldn't be using it on .NET 8.
@EtiennedeMartel Thanks, I was not aware of that. It is kind of hidden away in the remarks section of the docs. They should mark the class with an obsolete attribute, that way it would be noticed.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.