Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sending async mail from SignalR hub

I need to send an email as a result of a SignalR hub invocation. I don't want the send to execute synchronously, as I don't want to tie up WebSocket connections, but I would like the caller to be informed, if possible, if there were any errors. I thought I'd be able to use something like this in the hub (minus error handling and all other things that I want it to do):

public class MyHub : Hub {
    public async Task DoSomething() {
        var client = new SmtpClient();
        var message = new MailMessage(/* setup message here */);
        await client.SendMailAsync(message);
    }
}

But soon discovered that it won't work; the client.SendMailAsync call throws this:

System.InvalidOperationException: An asynchronous operation cannot be started at this time. Asynchronous operations may only be started within an asynchronous handler or module or during certain events in the Page lifecycle.

Further investigation and reading has shown me that SmtpClient.SendMailAsync is a TAP wrapper around EAP methods, and that SignalR does not allow that.

My question is, is there any simple way to asynchronously send the emails directly from a hub method?

Or is my only option to place the email sending code elsewhere? (e.g. have the hub queue a service-bus message, then a stand-alone service could handle those messages and send the emails [though I'd also have more work this way to implement notification of results back to the hub's clients]; or have the hub make an HTTP request to a webservice that does the email sending).

like image 409
lethek Avatar asked Jun 24 '14 01:06

lethek


2 Answers

My understanding is, the original EAP-style SmtpClient.SendAsync (which is wrapped by SendMailAsync as TAP) calls SynchronizationContext.Current.OperationStarted/OperationCompleted. Allegedly, this is what makes the SignalR host unhappy.

As a workaround, try it this way (untested). Let us know if it works for you.

public class MyHub : Hub {
    public async Task DoSomething() {
        var client = new SmtpClient();
        var message = new MailMessage(/* setup message here */);
        await TaskExt.WithNoContext(() => client.SendMailAsync(message));
    }
}

public static class TaskExt
{
    static Task WithNoContext(Func<Task> func)
    {
        Task task;
        var sc = SynchronizationContext.Current;
        try
        {
            SynchronizationContext.SetSynchronizationContext(null);
            task = func();
        }
        finally
        {
            SynchronizationContext.SetSynchronizationContext(sc);
        }
        return task;
    }
}
like image 39
noseratio Avatar answered Oct 18 '22 04:10

noseratio


Similar questions here and here.

The SignalR team is aware of the issue, but haven't fixed it yet. At this point it looks like it'll go into SignalR v3.

In the meantime, one quick hack would be this:

public async Task DoSomething() {
  using (new IgnoreSynchronizationContext())
  {
    var client = new SmtpClient();
    var message = new MailMessage(/* setup message here */);
    await client.SendMailAsync(message);
  }
}

public sealed class IgnoreSynchronizationContext : IDisposable
{
  private readonly SynchronizationContext _original;
  public IgnoreSynchronizationContext()
  {
    _original = SynchronizationContext.Current;
    SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
  }
  public void Dispose()
  {
    SynchronizationContext.SetSynchronizationContext(_original);
  }
}
like image 127
Stephen Cleary Avatar answered Oct 18 '22 04:10

Stephen Cleary