5

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).

lethek
  • 445
  • 6
  • 14
  • Not that this will help you, but it is return (await client.SendMailAsync(message)); isn't it? – csgero Jun 24 '14 at 06:44
  • No, it isn't. `Task` returning method is the equivalent of `void` in synchronous methods. `await` inside a method will propagate the `Task` upwards, no need to `return` – Yuval Itzchakov Jun 24 '14 at 06:48
  • Show how you call `DoSomething`. Do you call it without `await` from another, client-callable SingalR hub method? – noseratio Jun 24 '14 at 11:46
  • @Noseratio The DoSomething method is called directly from a Javascript hub proxy (in browser). – lethek Jun 25 '14 at 01:37

2 Answers2

7

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);
  }
}
Community
  • 1
  • 1
Stephen Cleary
  • 437,863
  • 77
  • 675
  • 810
4

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;
    }
}
noseratio
  • 59,932
  • 34
  • 208
  • 486
  • Thanks Noseratio, your workaround works perfectly, though the WithNoContext method needed to be made public. – lethek Jun 25 '14 at 01:55
  • @lethek, glad it helped. Yep, it was a quick and untested type-up; for purity, it also makes sense to move `SynchronizationContext.SetSynchronizationContext(null)` out of the `try`. – noseratio Jun 25 '14 at 01:58
  • Although your answer works exactly the same way as Stephen's and came first, I've decided to accept his answer instead as I personally prefer the style of that approach and he's linked the issue he raised with the SignalR team which provides more detail on the problem and its future. – lethek Jun 25 '14 at 01:59
  • @lethek, it just occurred to me though, that Stephen's approach might be restoring the original synchronization context on a completely different thread (most likely, a random IOCP thread), after the `await`. I'm not sure if this is a big deal, but it'd be great if @StephenCleary could comment on this. – noseratio Jun 25 '14 at 02:16
  • Good point... I lack sufficient understanding of async await internals and sync contexts to tell if there's any potential problem if that were to happen. – lethek Jun 25 '14 at 02:26