Отправка асинхронной почты из концентратора SignalR

Мне нужно отправить электронное сообщение в результате вызова хаба SignalR. Я не хочу, чтобы передача выполнялась синхронно, так как я не хочу связывать соединения WebSocket, но я хотел бы, чтобы вызывающий был проинформирован, если это было возможно, если бы были какие-либо ошибки. Я думал, что смогу использовать что-то подобное в хабе (минус обработка ошибок и все остальное, что я хочу сделать):

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

Но вскоре выяснилось, что это не сработает; вызов client.SendMailAsync вызывает следующее:

System.InvalidOperationException: асинхронная операция не может быть запущена в это время. Асинхронные операции могут запускаться только в асинхронном обработчике или модуле или во время определенных событий жизненного цикла страницы.

Дальнейшее исследование и чтение показали мне, что SmtpClient.SendMailAsync является оберткой TAP вокруг методов EAP и что SignalR не позволяет этого.

Мой вопрос в том, есть ли простой способ асинхронной отправки электронных писем непосредственно из метода хаба?

Или мой единственный вариант поместить код отправки электронной почты в другое место? (например, для очереди хабов сообщение служебной шины, то автономная служба может обрабатывать эти сообщения и отправлять электронные письма [хотя у меня также было бы больше работы таким образом, чтобы реализовать уведомление о результатах назад для клиентов-концентраторов); иметь концентратор, сделать HTTP-запрос к веб-сервису, который отправляет электронную почту).

Ответ 1

Аналогичные вопросы здесь и здесь.

Команда SignalR знает о проблеме, но еще не исправила ее. На данный момент похоже, что он войдет в SignalR v3.

Тем временем один быстрый взлом будет следующим:

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);
  }
}

Ответ 2

Насколько я понимаю, оригинальный EAP-стиль SmtpClient.SendAsync (который обернут SendMailAsync как TAP) вызывает SynchronizationContext.Current.OperationStarted/OperationCompleted. Как утверждается, именно это делает хост SignalR недовольным.

Как обходной путь, попробуйте это так (непроверено). Сообщите нам, если это сработает для вас.

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;
    }
}