.NET Core依赖注入:每次请求是否生成新SmtpClient实例?
首先直接回答你的核心疑问:
- 用
AddTransient注册IMailer<BaseModel>时,每次从DI容器获取IMailer实例(包括同一个请求内多次获取)都会生成全新的Mailer,同时也会新建一个SmtpClient——因为你在注册的工厂方法里直接new了SmtpClient,每次创建Mailer都会执行这段逻辑。 - 改成
AddScoped后,同一个请求内只会有一个Mailer实例,对应的SmtpClient也是同一个;但不同请求之间会各自创建新的实例。
连续发邮件出错的原因
你的Mailer<T>实现了IDisposable,并且在Dispose里销毁了_smtpClient。如果在同一个请求中,你多次调用SendEmail,或者DI容器提前触发了Mailer的Dispose(比如某些框架的生命周期钩子),后续再用这个已被释放的SmtpClient发邮件就会报错。
另外,旧的System.Net.Mail.SmtpClient本身有复用缺陷:如果SMTP服务器主动关闭了连接,复用同一个实例再次发送时,客户端不会自动重建连接,直接抛出错误。
修复方案
这里给你几个可行的解决思路:
方案1:每次发送都用新的SmtpClient
修改Mailer的设计,不在构造函数中持有SmtpClient,而是每次发送时从DI容器获取新实例,用完自动释放:
public sealed class Mailer<T> : IMailer<T> where T : BaseModel { private readonly IServiceProvider _serviceProvider; private readonly string _contentRootPath; private readonly bool _forceSandbox; public Mailer(IServiceProvider serviceProvider, string contentRootPath, bool isLive) { _serviceProvider = serviceProvider; _contentRootPath = contentRootPath; _forceSandbox = !isLive; } public async Task<bool> SendEmail(T model, string template) { using var smtpClient = _serviceProvider.GetRequiredService<SmtpClient>(); Email.DefaultSender = new SmtpSender(smtpClient); Email.DefaultRenderer = new RazorRenderer(); // 以下保持原发送逻辑不变 if (template.Contains(".cshtml")) template = template.Replace(".cshtml", ""); var email = Email .From(model.From.Email, model.From.Name.NullOrEmptyTo(model.From.Email)) .To(EmailAddressHelper.GetAddressesFromModel(model.Recipients, _forceSandbox)) .Subject(model.Subject) .UsingTemplateFromFile($@"{_contentRootPath}\views\{template}.cshtml", model) .Attach(EmailAttachmentHelper.GetAttachmentsFromModel(model.Attachments)); if (model.CcRecipients != null && model.CcRecipients.Any()) email.CC(EmailAddressHelper.GetAddressesFromModel(model.CcRecipients, _forceSandbox)); if (model.BccRecipients != null && model.BccRecipients.Any()) email.BCC(EmailAddressHelper.GetAddressesFromModel(model.BccRecipients, _forceSandbox)); email.Data.Headers = EmailHeaderHelper.GetHeaders(model.UniqueID, model.Headers); var result = await email.SendAsync(); return result.Successful; } }
同时修改注册代码,单独把SmtpClient注册为Transient:
public static void AddSmtpMailer(this IServiceCollection services, IHostingEnvironment environment) { // 单独注册SmtpClient services.AddTransient<SmtpClient>(_ => environment.IsDevelopment() ? new SmtpClient { PickupDirectoryLocation = @"c:\temp", DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory } : new SmtpClient { Host = "mail.someplace.com", Port = 25, EnableSsl = true, Credentials = new NetworkCredential("user", "password") } ); // 注册Mailer,让DI自动注入依赖 services.AddTransient<IMailer<BaseModel>, Mailer<BaseModel>>(); }
这样每次发送都会用全新的SmtpClient,用完通过using自动释放,彻底避免复用导致的问题。
方案2:安全复用SmtpClient(需注意线程安全)
如果想复用SmtpClient提升性能,可以把它注册为Singleton,同时修改Mailer不要在Dispose中销毁它:
// 注册SmtpClient为单例 services.AddSingleton<SmtpClient>(_ => environment.IsDevelopment() ? new SmtpClient { PickupDirectoryLocation = @"c:\temp", DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory } : new SmtpClient { Host = "mail.someplace.com", Port = 25, EnableSsl = true, Credentials = new NetworkCredential("user", "password") } ); // 修改Mailer的Dispose方法 public void Dispose() { // 不要销毁_smtpClient,由DI容器管理其生命周期 }
⚠️ 注意:System.Net.Mail.SmtpClient的实例方法不是线程安全的,多个请求同时发邮件时必须加锁,否则会出现并发错误:
// 在Mailer中加锁 private static readonly object _sendLock = new object(); public async Task<bool> SendEmail(T model, string template) { lock(_sendLock) { // 发送逻辑放在锁内 Email.DefaultSender = new SmtpSender(_smtpClient); // ... 其余代码不变 var result = await email.SendAsync(); return result.Successful; } }
方案3:切换到MailKit(推荐)
System.Net.Mail.SmtpClient已经被微软标记为过时,官方推荐使用MailKit库。它的SmtpClient设计更健壮,支持自动重连、连接池,能完美解决连续发送的问题,而且API更友好。
总结
你的问题本质是:
AddScoped模式下,同一个请求复用的SmtpClient被Mailer的Dispose提前销毁,导致后续发送失败;- 旧
SmtpClient的连接管理缺陷放大了这个问题。
选上面任意一个方案,都能解决你连续发送邮件出错的问题。
内容的提问来源于stack exchange,提问作者Simon

