应使用 ifilestorage 接口抽象统一所有后端存储,仅暴露 uploadasync、downloadasync、deleteasync 等语义方法,业务代码不感知具体实现;本地、azure、s3 实现需共用重试、超时、日志策略,并正确管理客户端生命周期(如 blobserviceclient 和 amazons3client 注册为 singleton)。

用 IFileStorage 抽象统一所有后端,别直接 new 具体实现
核心不是“怎么连 Azure”,而是“怎么让业务代码完全不感知存储在哪”。必须定义一个干净的接口,比如 IFileStorage,只暴露 UploadAsync、DownloadAsync、DeleteAsync 这类语义明确的方法。它不包含 ConnectionString、BucketName 等具体配置字段——这些全交给实现类自己处理。
常见错误是把 CloudBlobContainer 或 IAmazonS3 直接塞进业务逻辑里,结果一换存储就得改十几处调用。还有的把本地路径硬编码成 "./uploads",导致测试和生产行为不一致。
- 接口方法参数统一用
string key(如"invoices/2024/invoice.pdf"),不暴露路径拼接逻辑 - 上传时统一接收
Stream,避免byte[]吃内存,也兼容大文件 - 下载返回
Stream而非byte[],由调用方决定是否读取全部内容 - 所有实现都实现
IDisposable或用AsyncDisposable,尤其 Azure SDK 的BlobServiceClient需要显式释放
本地存储用 PhysicalFileStorage,但别真当“开发环境专用”
很多人把本地实现当成临时占位符,上线就删。其实它该是完整可上线的方案:支持并发写入、自动创建目录、按需清理临时文件、带基础权限校验(比如拒绝 ../ 路径遍历)。它的价值不仅是开发调试,更是单元测试的黄金搭档——不需要启容器、不依赖网络、秒级 setup/teardown。
容易踩的坑是直接用 File.WriteAllBytes,这会阻塞线程且无法取消;或者没处理好路径分隔符,在 Linux 容器里挂掉。
- 构造函数只接受一个根目录
string basePath,内部用Path.Combine拼路径 - 上传前用
Path.GetRelativePath校验 key 是否越界(防止../../../etc/passwd) - 用
FileStream的FileShare.Read模式打开文件,允许多个请求同时读同一份 - 在 CI 中用它跑 90% 的文件逻辑测试,比 mock 接口更真实
Azure Blob 和 S3 实现必须共用重试、超时、日志结构
两个云客户端行为差异极大:BlobServiceClient 默认不重试,AmazonS3Client 默认重试 4 次;Azure 的 SAS token 过期是服务端错误,S3 的签名失效却常报 403。如果各自写一套逻辑,不出三个月就会出现“S3 上传成功但 Azure 失败”的诡异现象。
正确做法是在基类(比如 CloudFileStorageBase)里统一封装重试策略、超时设置、结构化日志(含 key、size、duration 字段),再让两个子类只专注协议细节。
- 重试用
ExponentialBackoff,最大间隔不超过 30 秒,避免雪崩 - 每个操作设独立超时(上传 5 分钟、下载 2 分钟),而不是全局
HttpClient.Timeout - 日志中记录原始异常类型(
RequestFailedExceptionvsAmazonS3Exception),方便告警分类 - S3 的
PutObjectRequest必须显式设ContentType,否则浏览器可能下错为binary/octet-stream
DI 注册时用 AddTransient 还是 AddSingleton?看客户端生命周期
IFileStorage 实例本身该是 transient——它只是个门面,不保存状态。但底层客户端不是:Azure 的 BlobServiceClient 和 S3 的 AmazonS3Client 都是线程安全、可复用的,必须注册为 singleton,否则每请求新建连接,很快耗尽 socket。
本地实现例外:如果用了 FileStream 缓存或临时目录锁,它就得是 scoped 或 transient,否则并发写会冲突。
- Azure:注册
BlobServiceClient为 singleton,传入HttpClient实例(也 singleton) - S3:注册
IAmazonS3为 singleton,禁用UseHttpPipelining(.NET 6+ 默认 false) - 本地:注册
PhysicalFileStorage为 transient,避免跨请求共享文件句柄 - 绝对不要在
IFileStorage实现里 newHttpClient,这是经典 socket 耗尽源头
最常被忽略的是 Azure 的 BlobServiceClient 构造成本:它会预热 DNS、建立连接池。如果每次请求都 new 一个,TPS 直接砍半。这点在压测时才暴露,但修复成本很高。










