You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

.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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.07 10:42:30