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;然后在发送邮件时,从字节数组重新创建MemoryStream和Attachment:
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 void为async Task,可以让异常被正确捕获和处理,避免隐藏的bug。 - 使用
await调用异步方法,能更好地控制异步流程,避免线程池资源浪费。
内容的提问来源于stack exchange,提问作者Simant




