65.9K
CodeProject 正在变化。 阅读更多。
Home

SmtpClient.SendMailAsync:CancellationToken 支持

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (3投票s)

2017 年 8 月 2 日

CPOL

1分钟阅读

viewsIcon

16062

为 SendMailAsync 方法添加取消支持

引言

自从 .NET 4.5 以来,SmtpClient 类 提供了 TPL 友好的 SendMailAsync 方法,以便更容易地从 async 方法异步发送电子邮件。不幸的是,没有任何重载支持传递 CancellationToken,这使得取消操作变得困难。

我最近遇到了这个缺点,在一个配置为在用户断开连接时取消的异步 ASP.NET MVC 操作中。如果这发生在发送电子邮件时,应用程序会记录一个带有消息的异常:“异步模块或处理程序在异步操作仍在挂起时完成”。这是因为该操作正在被取消,但发送电子邮件的任务没有被取消。

Using the Code

以下扩展方法添加了对传递 CancellationToken 的支持。当取消时,它将调用 SendAsyncCancel 方法 来取消操作。

using System;
using System.ComponentModel;
using System.Net.Mail;
using System.Threading;
using System.Threading.Tasks;

namespace System.Net.Mail
{
    /// <summary>
    /// Extension methods for the <see cref="SmtpClient"/> class.
    /// </summary>
    public static class SmtpClientExtensions
    {
        /// <summary>
        /// Sends the specified message to an SMTP server for delivery as an asynchronous operation.
        /// </summary>
        /// <param name="client">
        /// The <see cref="SmtpClient"/> instance.
        /// </param>
        /// <param name="message">
        /// The <see cref="MailMessage"/> to send.
        /// </param>
        /// <param name="cancellationToken">
        /// The <see cref="CancellationToken"/> to monitor for cancellation requests.
        /// </para>
        /// <returns>
        /// A <see cref="Task"/> representing the asynchronous operation.
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <para><paramref name="client"/> is <see langword="null"/>.</para>
        /// <para>-or-</para>
        /// <para><paramref name="message"/> is <see langword="null"/>.</para>
        /// </exception>
        public static Task SendMailAsync(
            this SmtpClient client, 
            MailMessage message, 
            CancellationToken cancellationToken)
        {
            if (client == null) throw new ArgumentNullException(nameof(client));
            if (message == null) throw new ArgumentNullException(nameof(message));
            if (!cancellationToken.CanBeCanceled) return client.SendMailAsync(message);

            var tcs = new TaskCompletionSource<object>();
            var registration = default(CancellationTokenRegistration);
            SendCompletedEventHandler handler = null;
            handler = (sender, e) =>
            {
                if (e.UserState == tcs)
                {
                    try
                    {
                        if (handler != null)
                        {
                            client.SendCompleted -= handler;
                            handler = null;
                        }
                    }
                    finally
                    {
                        registration.Dispose();

                        if (e.Error != null)
                        {
                            tcs.TrySetException(e.Error);
                        }
                        else if (e.Cancelled)
                        {
                            tcs.TrySetCanceled();
                        }
                        else
                        {
                            tcs.TrySetResult(null);
                        }
                    }
                }
            };

            client.SendCompleted += handler;

            try
            {
                client.SendAsync(message, tcs);
                registration = cancellationToken.Register(client.SendAsyncCancel);
            }
            catch
            {
                client.SendCompleted -= handler;
                registration.Dispose();
                throw;
            }

            return tcs.Task;
        }
    }
}

使用它非常简单

using (var message = new MailMessage())
{
    InitializeMessage(message);
    
    var cts = new CancellationTokenSource();
    cts.CancelAfter(TimeSpan.FromSeconds(2));
    
    var smtp = new SmtpClient();
    await smtp.SendMailAsync(message, cts.Token).ConfigureAwait(false);
}

关注点

网上有各种更简单的版本 - 例如,Matt Benic 在 2016 年发布的一个。但是,我希望代码尽可能接近 现有方法的源代码

正如 Todd Aspeotis 在 Matt 版本评论中指出的那样,有必要在 CancellationToken 上注册回调之前调用 SendAsync;否则,如果令牌已经取消,SendAsyncCancel 方法将被过早调用。

历史

  • 2017-08-02:初始版本
© . All rights reserved.