You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

ASP.NET Core 3.1使用SmtpClient发邮件附件报"无法访问已关闭的流"求助

解决"Cannot access a closed Stream."错误的方案

我一眼就看出问题出在哪了——你在Message构造函数里直接用file.OpenReadStream()创建Attachment,但这个流是和请求中的IFormFile绑定的。当你的Index方法执行完返回View后,ASP.NET Core会自动清理请求相关的资源,包括这个文件流。而你的SendEmail方法是用Task.Factory.StartNew异步执行的,等真正开始发送邮件的时候,那个流早就被关闭了,自然就会抛出Cannot access a closed Stream错误。

解决方案步骤:

1. 修改Message类,存储文件内容而非直接绑定请求流

我们需要把上传的文件内容复制到独立的存储(比如字节数组)里,脱离原来的请求流。修改后的Message类如下:

public class Message
{
    public List<string> Recipients { get; set; }
    public string Subject { get; set; }
    public string Content { get; set; }
    // 用元组存储文件字节数组和文件名,避免依赖外部流
    public List<(byte[] FileBytes, string FileName)> Attachments { get; set; }

    public Message(IEnumerable<string> to, string subject, string content, IFormFileCollection attachments)
    {
        Recipients = to.ToList();
        Subject = subject;
        Content = content;
        Attachments = new List<(byte[], string)>();

        foreach (var file in attachments)
        {
            if (file.Length > 0)
            {
                using (var memoryStream = new MemoryStream())
                {
                    // 将文件内容复制到内存流
                    file.CopyTo(memoryStream);
                    // 转换成字节数组存储
                    Attachments.Add((memoryStream.ToArray(), file.FileName));
                }
            }
        }
    }
}

2. 修复EmailSender的异步逻辑并重新创建附件

首先要避免使用async void(这种方法的异常无法被正常捕获),改成async Task;然后在发送邮件时,从字节数组重新创建MemoryStreamAttachment

public class EmailSender
{
    // 改成异步方法,方便调用方await
    public async Task SendEmailAsync(Message message)
    {
        await SendMailAsync(message);
    }

    // 用async Task替代async void
    private async Task SendMailAsync(Message message)
    {
        try
        {
            using (MailMessage mailMessage = new MailMessage())
            {
                mailMessage.IsBodyHtml = true;
                mailMessage.Subject = message.Subject;
                mailMessage.SubjectEncoding = Encoding.UTF8;
                mailMessage.Body = message.Content;
                mailMessage.BodyEncoding = Encoding.UTF8;
                mailMessage.From = new MailAddress("From email");

                foreach (var recipient in message.Recipients)
                {
                    mailMessage.To.Add(new MailAddress(recipient));
                }

                if (message.Attachments != null && message.Attachments.Any())
                {
                    foreach (var (fileBytes, fileName) in message.Attachments)
                    {
                        // 从字节数组创建新的内存流,这个流是独立的,不会被外部关闭
                        var attachmentStream = new MemoryStream(fileBytes);
                        mailMessage.Attachments.Add(new Attachment(attachmentStream, fileName));
                    }
                }

                using (SmtpClient smtpClient = new SmtpClient("Host name", 587))
                {
                    NetworkCredential smtpUserInfo = new NetworkCredential("UserName", "Password");
                    smtpClient.UseDefaultCredentials = false;
                    smtpClient.Credentials = smtpUserInfo;
                    await smtpClient.SendMailAsync(mailMessage);
                }
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("SendMailAsync exception: " + ex);
        }
        finally
        {
            Console.WriteLine("SendMailAsync done");
        }
    }
}

3. 修改Controller中的调用逻辑

HomeController里使用await调用异步方法,确保流程更可控:

[HttpPost]
public async Task<IActionResult> Index(string id)
{
    var files = Request.Form.Files.Any() ? Request.Form.Files : new FormFileCollection();
    List<string> recipients = new List<string> { "abc@gmail.com", "xyz@gmail.com"};
    // 改用await调用异步发送方法
    await _emailSender.SendEmailAsync(new Message(recipients, "Test Subject", "<h1>Welcome</h1>", files));
    return View();
}

为什么这样改?

  • 把文件内容复制到字节数组后,我们就脱离了请求上下文的流,即使请求结束,文件内容依然保存在内存中,发送邮件时可以正常访问。
  • 替换async voidasync Task,可以让异常被正确捕获和处理,避免隐藏的bug。
  • 使用await调用异步方法,能更好地控制异步流程,避免线程池资源浪费。

内容的提问来源于stack exchange,提问作者Simant

火山引擎 最新活动