.NET Core 3.1中SmtpClient回调写入数据库遇已释放对象错误
问题分析与解决方案
你遇到的核心问题是:Transient生命周期的EmailSender被释放后,其注入的DbContext也跟着被回收,但邮件发送的异步回调触发时还试图使用这个已释放的上下文。直接改成AddScoped会在非请求上下文(比如后台任务)调用时报错,改成AddSingleton则会导致DbContext线程安全问题,所以需要针对性调整。
下面提供三种可行的解决方案,你可以根据业务场景选择:
方案一:使用DbContext工厂创建独立上下文实例
这个方案的核心是:不在EmailSender中直接注入DbContext,而是注入IDbContextFactory<DataBaseAppContext>,在异步回调中创建全新的上下文实例,用完后手动释放,完全脱离原有的生命周期限制。
步骤1:注册DbContext工厂
在Startup.cs中添加DbContext工厂的注册(可以保留原有的AddDbContext供其他服务使用):
services.AddDbContextFactory<DataBaseAppContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
步骤2:修改EmailSender实现
public class EmailSender : IEmailSender { private readonly IDbContextFactory<DataBaseAppContext> _contextFactory; // 提前保存邮件参数,避免异步闭包的变量捕获问题 private string _targetEmail; private string _emailSubject; private string _emailMessage; public EmailSender(IDbContextFactory<DataBaseAppContext> contextFactory) { _contextFactory = contextFactory; } public Task SendEmailAsync(string subject, string message, string email) { _emailSubject = subject; _emailMessage = message; _targetEmail = email; return ExecuteEmailSend(); } private Task ExecuteEmailSend() { try { // 初始化邮件发送相关对象(此处替换为你的实际配置) MailAddress fromAddress = new MailAddress("your-sender@example.com"); SmtpClient smtp = new SmtpClient { Host = "smtp.example.com", Port = 587, EnableSsl = true, Credentials = new NetworkCredential("your-sender@example.com", "your-password") }; MailMessage mail = new MailMessage { From = fromAddress, IsBodyHtml = true, Subject = _emailSubject, Body = _emailMessage }; mail.To.Add(_targetEmail); smtp.SendCompleted += OnSendCompleted; return smtp.SendMailAsync(mail); } catch (Exception ex) { // 同步记录发送失败日志 using(var context = _contextFactory.CreateDbContext()){ context.EmailLogger.Add(new EmailLogger { Email = _targetEmail, Subject = _emailSubject, Message = _emailMessage, DateTime = DateTime.Now, IsSuccess = false, ErrorMessage = ex.Message }); context.SaveChanges(); } throw; } } private void OnSendCompleted(object sender, AsyncCompletedEventArgs e) { using(var context = _contextFactory.CreateDbContext()){ var log = new EmailLogger { Email = _targetEmail, Subject = _emailSubject, Message = _emailMessage, DateTime = DateTime.Now, IsSuccess = true }; if (e.Cancelled) { log.IsSuccess = false; log.ErrorMessage = "邮件发送被取消"; } else if (e.Error != null) { log.IsSuccess = false; log.ErrorMessage = e.Error.Message; } context.EmailLogger.Add(log); context.SaveChanges(); } // 释放SmtpClient资源 ((SmtpClient)sender).Dispose(); } }
方案二:同步等待邮件发送完成,直接记录日志
如果你的业务场景允许等待邮件发送完成再返回(不介意请求响应时间稍长),可以去掉异步回调,直接await邮件发送操作,在同一个方法内完成日志记录,此时DbContext还在EmailSender的生命周期内(只要调用方在请求范围内,比如Controller)。
修改EmailSender的核心逻辑
public async Task SendEmailAsync(string subject, string message, string email) { try { // 初始化邮件发送相关对象(替换为你的实际配置) MailAddress fromAddress = new MailAddress("your-sender@example.com"); SmtpClient smtp = new SmtpClient { Host = "smtp.example.com", Port = 587, EnableSsl = true, Credentials = new NetworkCredential("your-sender@example.com", "your-password") }; MailMessage mail = new MailMessage { From = fromAddress, IsBodyHtml = true, Subject = subject, Body = message }; mail.To.Add(email); // 等待邮件发送完成 await smtp.SendMailAsync(mail); // 记录成功日志 var successLog = new EmailLogger { Email = email, Subject = subject, Message = message, DateTime = DateTime.Now, IsSuccess = true }; _context.EmailLogger.Add(successLog); await _context.SaveChangesAsync(); smtp.Dispose(); } catch (Exception ex) { // 记录失败日志 var errorLog = new EmailLogger { Email = email, Subject = subject, Message = message, DateTime = DateTime.Now, IsSuccess = false, ErrorMessage = ex.Message }; _context.EmailLogger.Add(errorLog); await _context.SaveChangesAsync(); throw; } }
方案三:使用后台任务队列处理日志记录
如果不想等待邮件发送完成,也不想直接在回调中操作DbContext,可以把日志信息放入队列,由后台服务异步处理写入数据库,完全解耦邮件发送和日志存储的生命周期。
步骤1:创建日志队列服务
public interface IEmailLogQueue { void EnqueueLog(EmailLogger log); Task<EmailLogger> DequeueAsync(CancellationToken cancellationToken); } public class EmailLogQueue : IEmailLogQueue { private readonly ConcurrentQueue<EmailLogger> _queue; private readonly SemaphoreSlim _signal; public EmailLogQueue() { _queue = new ConcurrentQueue<EmailLogger>(); _signal = new SemaphoreSlim(0); } public void EnqueueLog(EmailLogger log) { if (log == null) throw new ArgumentNullException(nameof(log)); _queue.Enqueue(log); _signal.Release(); } public async Task<EmailLogger> DequeueAsync(CancellationToken cancellationToken) { await _signal.WaitAsync(cancellationToken); _queue.TryDequeue(out var log); return log; } }
步骤2:创建后台日志处理服务
public class EmailLogProcessor : BackgroundService { private readonly IEmailLogQueue _logQueue; private readonly IServiceProvider _serviceProvider; public EmailLogProcessor(IEmailLogQueue logQueue, IServiceProvider serviceProvider) { _logQueue = logQueue; _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var log = await _logQueue.DequeueAsync(stoppingToken); // 创建服务范围,获取Scoped的DbContext(避免Singleton服务直接使用DbContext的线程安全问题) using(var scope = _serviceProvider.CreateScope()){ var context = scope.ServiceProvider.GetRequiredService<DataBaseAppContext>(); context.EmailLogger.Add(log); await context.SaveChangesAsync(stoppingToken); } } } }
步骤3:注册服务到Startup.cs
services.AddSingleton<IEmailLogQueue, EmailLogQueue>(); services.AddHostedService<EmailLogProcessor>(); // 保留原有的DbContext Scoped注册 services.AddDbContext<DataBaseAppContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"))); services.AddTransient<IEmailSender, EmailSender>();
步骤4:修改EmailSender实现
public class EmailSender : IEmailSender { private readonly IEmailLogQueue _logQueue; private string _targetEmail; private string _emailSubject; private string _emailMessage; public EmailSender(IEmailLogQueue logQueue) { _logQueue = logQueue; } public Task SendEmailAsync(string subject, string message, string email) { _emailSubject = subject; _emailMessage = message; _targetEmail = email; return ExecuteEmailSend(); } private Task ExecuteEmailSend() { try { // 初始化邮件发送相关对象(替换为你的实际配置) MailAddress fromAddress = new MailAddress("your-sender@example.com"); SmtpClient smtp = new SmtpClient { Host = "smtp.example.com", Port = 587, EnableSsl = true, Credentials = new NetworkCredential("your-sender@example.com", "your-password") }; MailMessage mail = new MailMessage { From = fromAddress, IsBodyHtml = true, Subject = _emailSubject, Body = _emailMessage }; mail.To.Add(_targetEmail); smtp.SendCompleted += OnSendCompleted; return smtp.SendMailAsync(mail); } catch (Exception ex) { _logQueue.EnqueueLog(new EmailLogger { Email = _targetEmail, Subject = _emailSubject, Message = _emailMessage, DateTime = DateTime.Now, IsSuccess = false, ErrorMessage = ex.Message }); throw; } } private void OnSendCompleted(object sender, AsyncCompletedEventArgs e) { var log = new EmailLogger { Email = _targetEmail, Subject = _emailSubject, Message = _emailMessage, DateTime = DateTime.Now, IsSuccess = true }; if (e.Cancelled) { log.IsSuccess = false; log.ErrorMessage = "邮件发送被取消"; } else if (e.Error != null) { log.IsSuccess = false; log.ErrorMessage = e.Error.Message; } _logQueue.EnqueueLog(log); ((SmtpClient)sender).Dispose(); } }
方案选择建议
- 简单场景优先选方案一:实现成本低,直接解决上下文生命周期问题。
- 对响应时间敏感的高并发场景选方案三:完全解耦邮件发送和日志存储,不影响请求响应速度。
- 对响应时间要求不高的场景选方案二:代码逻辑最简洁,无需额外依赖。
内容的提问来源于stack exchange,提问作者ToTa
相关产品推荐
相关产品推荐

