从零开始:c#如何优雅的操作临时文件/数据?以ASP文件下载为例

news/2025/9/24 14:46:09/文章来源:https://www.cnblogs.com/luojin765/p/19109217

从零开始:c#如何优雅的操作临时文件/数据?以ASP文件下载为例

在程序开发中,我们经常需要处理临时文件,例如:

  • 安全替换大文件:先将内容写入临时文件,成功后再替换目标文件,避免写入过程中断导致数据损坏。
  • 进程间数据传递:临时文件作为中间媒介,实现不同进程之间的数据交换。
  • Web文件下载:将动态生成的数据写入临时文件,并提供给用户下载。

本文将以 ASP.NET Core 中的文件下载 场景为例,带你一步步实现更优雅的临时文件处理方案。

一、理解核心概念:Stream(流)

文件操作离不开Stream)的概念。你可以把Stream想象成一根水管,数据就像水一样,可以从一端流入,从另一端流出。

Stream是C#中用来处理数据的一种方式。它就像是一个管道,数据可以通过这个管道流动。你可以把数据从一个地方(比如硬盘上的文件)读出来,也可以把数据写到另一个地方(比如内存或者网络)。

使用Stream的好处是,它提供了一种统一的方式来处理不同类型的数据传输。在这个过程中,数据就像水管里的水一样源源不断的流动着,处理完的数据咱们可以舍弃,继续接收后面的即可,这样就可以实现传输例如磁盘上的大文件的信息,解决了一次性加载时的内存占用问题。

二 简单基础的实现 lv.1 (入门参考)

了解了流的基本概念后,我们先来看一个在ASP.NET Core中实现文件下载接口的基础代码。这是一个最直接的实现。为了快速达成目标,我们在指定文件夹创建了一个随机名称的临时文件,写入数据后,返回文件流供下载。

如果这段代码能成功运行,恭喜你!你已经实现了一个基础的Web文件下载接口。用户将得到一个文本文件,内容为查询参数的值。


// 一个简单的控制器实现
public class DownloadController:Controller
{string path = "d:\\tmp";// 一个简单的带参数的Get实现[HttpGet("DownloadFileMemory")]public IResult DownloadFileMemory(string query){try{                //创建一个新的文件流,可写(写入文件数据)、可读(读取数据最终给用户下载)FileStream fs = new FileStream(path+"\\"+Guid.NewGuid(), FileMode.CreateNew, FileAccess.ReadWrite);// 一个简单的流写入对象,以文本写入为例using (StreamWriter writer = new StreamWriter(fs, leaveOpen: true)){writer.Write(query);}fs.Position = 0;// 返回下载的文件,并将文件名重设为"sample.txt"return Results.File(fs, "application/octet-stream", "sample.txt");  // 流回自带关闭}catch (Exception ex){throw new Exception(ex.Message);}}
}

这是一个最基础的实现,刚接触代码时,为了简单粗暴实现目标,通常直接在指定的文件夹创建一个随机名称的文件,然后写入信息,返回Stream。上面的代码如果您能跑通,那么可以恭喜了!咱们已经实现了一个基础的web文件下载接口,能下载得到一个txt文本文件,里面写着query取值的文本。
这里有几个需要注意的点:

  • FileStream 不能加using,因为using将导致fs对象在方法结束时立刻触发Dispose()导致流关闭,但此时用户端请求的流还未开始执行传输;
  • StreamWriter 必须加参数leveOpen:true, 否则流会随着writer对象提前关闭,导致后续传输出错,如果不想用这个参数也可以把leveOpen:trueusing都去掉,加上writer.Flush(),托管对象自动回收;
  • fs.Position=0必须有,因为writer.Write()执行完已将流的位置Position写到了末尾,如果不重置,那么返回的将是一个空的Stream
  • `Return Results.File(),ASP.NET Core 框架在文件传输完成后,会自动关闭流,无需我们手动处理。

这个实现有个明显问题:方法执行后,临时文件会一直留在磁盘上,随着调用次数增加,会造成大量垃圾文件, 得定期手动清理。那么,如何避免临时文件残留呢?

三 避免临时文件残留 lv.2 (优化进阶)

当确认临时文件不再需要时,我们应在操作完成后立即删除它。对于文件下载场景,难点在于必须保证文件内容已成功传输给用户后,才能删除文件。即:在文件传输完成之前,数据流不能被破坏。

3.1 思路A 使用FileShare.Delete

这种方法利用了操作系统的一个特性:允许在文件仍被打开时将其标记为删除。具体做法是,在创建文件流时,设置其共享模式为“允许删除”。这样,我们就可以立即调用删除命令。此时,文件并不会立刻从磁盘上消失,而是会等到最后一个打开它的程序(即我们的下载进程)关闭文件流后,才被系统真正清理。

具体操作:将FileStream的FileShare属性设为Delete,然后在后续操作中直接用File.Delete()删除掉文件即可,代码如下:

// 前面的其他代码不变,省略// codes ...// 由于文件可被即时删除了,所以路径的指定不再重要// 可以方便的用系统自带方法直接生成一个空白的临时文件,并返回该文件路径var tmppath = Path.GetTempFileName()FileStream fs = new FileStream(tmpPath, FileMode.OpenOrCreate, FileAccess.ReadWrite,FileShare.Delete); // 增加标志位参数,可供其他进程删除// writer 的代码,和之前一致, 省略// codes ...System.IO.File.Delete(tmppath); // 直接删除return Results.File(fs, "application/octet-stream", "sample.txt");// 后面面的其他代码不变,省略// codes ...

可能一开始会觉得奇怪,为什么文件删除都执行了,还能继续读取数据? 这是因为 Windows 和 .NET 中,文件删除是一个“延迟删除”操作。也就是说:当你用 FileShare.Delete 打开一个文件流时,其他进程(或同一进程)可以“标记”该文件为删除。但实际上,文件并不会立即从磁盘上消失,直到最后一个打开该文件的句柄被关闭。所以,只要你还持有文件流(FileStream)未关闭,你就可以继续读取数据,即使文件已经被“删除”。

3.2 思路B 使用MemoryStream

如果待下载的数据量不大,使用内存流是更简单、更高效的方案。内存流将数据完全保存在内存中,不再涉及磁盘I/O操作。当下载完成、流被关闭后,所占用的内存会被垃圾回收器自动释放,从根本上杜绝了文件残留的问题。这是一种非常干净利落的解决方案,特别适合生成小型报表、文本内容或图片等场景。

[HttpGet("DownloadFileMemory")]public IResult DownloadFileMemory(string query){try{                MemoryStream ms = new MemoryStream();using (StreamWriter writer = new StreamWriter(ms, leaveOpen: true)){writer.Write(query);}ms.Position = 0;return Results.File(ms, "application/octet-stream", "sample.txt");}catch (Exception ex){throw new Exception(ex.Message);}}

四 走向优雅 lv.3 (设计一个通用方案)

虽然上述两种优化方案已经能解决特定问题,但在复杂的实际项目中,我们可能需要一个更统一、更强大的解决方案。例如:

  • 流来源多样:数据可能来自磁盘文件、内存流,甚至是非托管内存,流本身自带的信息甚少。
  • 生命周期管理复杂:某些流需要缓存复用,某些则需要立即销毁。
  • 规避潜在风险FileShare.Delete 模式可能使文件在预期之外被删除,增加调试难度。

因此,一个理想的设计是创建一个通用的 TempDataStream 类。这个类旨在:
统一接口:无论底层是文件流还是内存流,对使用者来说都是同一个流类型。
自动化管理:在流关闭时,能根据预设策略自动执行清理工作(如删除临时文件、释放非托管内存等)。
灵活可控:允许明确指定某个临时流是否需要被销毁。

我们可以通过封装(Decorator Pattern)来实现它。让 TempDataStream 类继承 Stream 基类,并在内部持有一个真正的流实例(如 FileStreamMemoryStream)。TempDataStream 重写所有流操作方法(如 Read, Write, Seek 等),将其转发给内部持有的流实例。最关键的是,在其 Dispose 方法中,除了关闭内部流,还执行我们自定义的清理逻辑。

    public class DownloadController:Controller{[HttpGet("DownloadFile")]public IResult DownloadFile(){TempDataStream tempStream = new TempDataStream(destoryOnDispose:true);tempStream.Write([1, 2, 3, 3]);return Results.File(tempStream, "application/octet-stream", "sample.txt");                   }}

还希望他能兼容其他类型的流,作为统一的临时数据对象:

public Stream GetStream()
{Stream stream;  // 定义包装的内部Streamif(data.lengthM<1024){// 使用 MemoryStream 暂存到内存// stream=...}else{// stream 使用 FileStream 暂存到文件// stream=...}//else{// 非托管内存的stream//  stream=...    //}return new TempDataStream(stream,destoryOnDispose:true);
}

假如忘记Dispose,他应该也可以自动按设定来关闭或销毁处理这个临时的流。

五 TempDataStream的实现

主要实现其实很简单,直接重写一个TempDataStream类继承Stream,然后内部嵌套一个来自外部的Stream,通过控制TempDataStream类生命周期的行为来实现对外部Stream的处理即可。

相关初步的代码实现如下:

 public class TempDataStream : Stream{private Stream _stream;  // 引入外部的Streampublic string FilePath { get; }  // 存储一个路径方便执行文件删除private readonly bool destoryOnDispose = true; // 判定要不要在关闭时进行摧毁// 包装引入外部`Stream`public TempDataStream(Stream stream, string filePath,bool destoryOnDispose = true){this.destoryOnDispose = destoryOnDispose;FilePath = filePath;this._stream = stream;}// 自动创建新的临时文件public TempDataStream(bool destoryOnDispose = true){this.destoryOnDispose = destoryOnDispose;FilePath = Path.GetTempFileName();_stream = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read );  //可选配 | FileShare.Delete}// 指定磁盘上的某一个文件作为流读入public TempDataStream(string filePath, bool destoryOnDispose = true){this.destoryOnDispose = destoryOnDispose;FilePath = filePath;_stream = new FileStream(FilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.ReadWrite); //可选配 | FileShare.Delete}public override void Flush(){_stream.Flush();}public override int Read(byte[] buffer, int offset, int count){return _stream.Read(buffer, offset, count);}public override long Seek(long offset, SeekOrigin origin){return _stream.Seek(offset, origin);}public override void SetLength(long value){_stream.SetLength(value);}public override void Write(byte[] buffer, int offset, int count){_stream.Write(buffer, offset, count);}public override bool CanRead => _stream.CanRead;public override bool CanSeek => _stream.CanSeek;public override bool CanWrite => _stream.CanWrite;public override long Length => _stream.Length;public override long Position { get => _stream.Position; set => _stream.Position = value; }private bool disposedValue;public override void Close(){Console.WriteLine("Closedl ");Dispose(true);}[MethodImpl(MethodImplOptions.Synchronized)]protected override void Dispose(bool disposing){if (!disposedValue){if (disposing){}// 全部作为非托管对象释放,重复执行也无所谓_stream?.Dispose();if (!string.IsNullOrEmpty(FilePath)){try{if (destoryOnDispose && File.Exists(FilePath)){File.Delete(FilePath);}}finally{_stream = null;}}disposedValue = true;}}~TempDataStream(){Dispose(false);}}

六、最后

感谢您的耐心阅读,希望各位从零开始的新朋友和老朋友有所收获!如果你对这篇文章的内容有任何建议或想法,欢迎随时交流!所有实现的代码以在上述章节完整提供。如果你觉得有用,欢迎去浏览一些本公众号的其他其他项目,点个 Star ⭐️支持一下! https://github.com/LdotJdot

P.S. 虽然现在AI是强大的工具,但那种为一个方案苦思冥想、最终灵光一现的顿悟感,以及亲手将代码调试成功的巨大成就感,是任何提示词都无法直接给予的。这恰恰是编程中最迷人的部分,是真正属于我们自己的成长。希望大家能享受不断实践、深入原理的过程,那才是通往前方之路的坚实阶梯。

欢迎关注公众号“萤火初芒“,更多分享等你来看:

image

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/915834.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

KDL - 金山云数据湖系统参数

KDL - 金山云数据湖系统参数 ${flow.name} ${flow.id} ${job.name} ${job.id} ${biz.date} #20250924 ${yyyyMM} #202509

表情网站源码网站里面内外链接如何做

最好的种树是十年前,其次是现在。歌谣 每天一个前端小知识 提醒你改好好学习了 知乎博主 csdn博主 b站博主 放弃很容易但是坚持一定很酷 我是歌谣 喜欢就一键三连咯 你得点赞是对歌谣最大的鼓励 编写一个函数来查找字符串数组中的最长公共前缀。 如果不存在公共前缀&…

wordpress搭建个人网站费用wordpress 8080

我们每天都在开发Java,每天都在使用JDK,那么我们了解JDK的发展史吗,这篇文章将带你深入了解JDK的发展史。 JDK(Java Development Kit)是Java开发者工具包,是用于编写Java程序和运行Java程序的软件开发工具集。自从1995年Java语言首次发布以来,JDK已经经历了数十年的发展…

内力网站建设深圳常平网站建设制作公司

哲哲是一位硬核游戏玩家。最近一款名叫《达诺达诺》的新游戏刚刚上市&#xff0c;哲哲自然要快速攻略游戏&#xff0c;守护硬核游戏玩家的一切&#xff01; 为简化模型&#xff0c;我们不妨假设游戏有 N 个剧情点&#xff0c;通过游戏里不同的操作或选择可以从某个剧情点去往另…

大丰网站建设公司鞍山网站制作开发

由于最近比较忙&#xff0c;所以本周搞了一个相对简单的验证码&#xff0c;就是抖音Tiktok的滑块验证码&#xff0c;这也是接到客户的一个需求。这种验证码通常在电脑端登录抖音、巨量引擎的的时候出现。 首先看一下最终的效果&#xff1a; 验证码识别过程 1、利用爬虫采集图…

网上做外贸都有哪些网站做网站要多少

主要是安装一些插件&#xff0c;c/c开发常用的插件有如下几个&#xff1a; 插件名称功能C/CC 和 C的编译环境C/C SnippetsC/C重用代码块C/C Advanced LintC/C静态检测Code Runner代码运行Include AutoComplete自动头文件包含Rainbow Brackets彩虹花括号&#xff0c;有助于阅读…

建设网站建设什么挣钱织梦技术网站模版

0.按照步骤&#xff0c;快速进行python的开发准备工作 1. Python解释器的下载 下载地址 https://www.python.org/ 选择对应你的系统的安装包 2.记得勾选这里将python加入你的路径中 3.有如下四个程序表明安装成功 4.点击上图中的第二个程序打开窗口检查解释器能否正常工作 输…

乌海做网站网站开发服务费入什么科目

我们知道C的类应当是先定义&#xff0c;然后使用。但在处理相对复杂的问题、考虑类的组合时&#xff0c;很可能遇到俩个类相互引用的情况&#xff0c;这种情况称为循环依赖。 例如&#xff1a; class A { public:void f(B b);//以B类对象b为形参的成员函数//这里编译错位&…

房建设计网站excel+表格+做的网站

k8s subPathExpr作用 场景&#xff1a; 对于一个deployment或者job拉起的服务&#xff0c;所有pod都是一样的配置&#xff0c;如果都挂载了宿主机的同一个目录&#xff0c;那么就会互相干扰&#xff0c;我们希望挂载相同目录&#xff0c;且在这个目录下&#xff0c;每个pod建立…

答题互动网页收藏

<!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"&…

常见问题解决 --- windows软件运行报错MSVCP140 ATOMIC WAIT.dI

常见问题解决 --- windows软件运行报错MSVCP140 ATOMIC WAIT.dI解决方法: https://aka.ms/vs/17/release/vc_redist.x64.exe 下载安装即可 其他参考: https://www.dll-files.com/msvcp140_atomic_wait.dll.htmlhttps…

芯脉:面向高速接口的SoC架构与完整性设计<3> - 教程

芯脉:面向高速接口的SoC架构与完整性设计<3> - 教程pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas&q…

spring boot实现MCP服务器,及其cursor测试利用的手段

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

vscode插件开发,打包后不生效问题解决

原因:使用AI生成的vscode 插件代码,打包使用的tsc,没有将三方依赖打包进去,导致安装插件后,插件无法激活,仅注册了快捷键 解决:对比使用 yo生成的ts+pnpm 插件项目,切换为esbuild打包 注意发布的话,使用 @vsco…

streamlit构建dashboard

1.python numpy pandas plotly库三者不同的侧重点 | 内容1 | 内容2 | | 内容3 | 内容4 | | 内容5 | 内容6 |

力扣 338题 比特位计数

动态规划 1.奇数中1的个数,是它上一个数1个个数+1,如2(10),3(11),4(100),5(101) 2.偶数中1的个数,是它除以2后的那个数的1的个数,如2(10),4(100),8(1000),6(110),12(1100) 3.因此,dp[i]…

技术前瞻与个人发展 - 构建终身学习的手艺体系——AI大模型:从0手搓到∞——AI、大模型时代,如何学习?

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

企业服务管理是做什么的?-ManageEngine卓豪

什么是企业服务管理软件呢?企业服务管理软件提供了一个统一的系统,可以整合IT、人力资源、设施和薪资等垂直业务部门的服务运营。这使得最终用户能够从单个控制台发现和访问服务,服务提供商可以从单个工作区中受益,…

wordpress免费云储存上海seo及网络推广

题目&#xff1a;桌上有 n 堆力扣币&#xff0c;每堆的数量保存在数组 coins 中。我们每次可以选择任意一堆&#xff0c;拿走其中的一枚或者两枚&#xff0c;求拿完所有力扣币的最少次数。 示例 1&#xff1a; 输入&#xff1a;[4,2,1]输出&#xff1a;4解释&#xff1a;第一…

站内关键词排名软件建设美妆企业网站

数据分区&#xff08;Data Partitioning&#xff09; 数据分区是指将一个大规模的数据集按某种规则划分成多个子集&#xff0c;并将这些子集存储到不同的存储节点上。这种方式不仅能提高查询效率&#xff0c;还能减轻单一节点的负担&#xff0c;使系统更容易扩展。 数据分区的…