阿里云OSS工具类完整设计与实现:基于.NET的静态单例模式实践

摘要:深入解析一个生产级的阿里云OSS工具类实现,涵盖静态单例模式、流式上传下载、目录级联删除、私有文件签名URL生成等核心功能。代码基于 Aliyun.OSS SDK,兼容 .NET Framework 4.6.1+ 及 .NET Core/5/6/7/8+ 项目。


一、前言

在.NET后端开发中,对象存储服务(OSS)是处理文件上传、下载、管理的标配方案。阿里云OSS作为国内主流对象存储服务,其.NET SDK功能完善,但直接使用SDK编写业务代码往往存在以下痛点:

痛点 具体表现 配置分散 AccessKey、Endpoint、BucketName散落在各处 路径混乱 Windows反斜杠与OSS正斜杠混用导致404 异常处理不统一 每个调用方自行处理OssException 重复造轮子 签名URL生成、目录批量删除等逻辑反复编写

本文分享的 AliyunOssHelper 工具类,正是为了解决上述问题而设计的高内聚、低耦合、线程安全的静态工具类。


二、核心设计思路

2.1 为什么选择静态单例模式?

方案 优点 缺点 适用场景 每次 new OssClient 简单直观 连接开销大,线程不安全 临时脚本 依赖注入(DI) 符合现代.NET风格 需要容器支持,旧项目改造成本高 新项目、微服务 静态Lazy单例 ✅ 线程安全、延迟加载、零依赖 单元测试时难以Mock 工具类、遗留系统

对于工具类场景,静态单例是性价比最高的选择。使用 Lazy<T> 确保:

  • ✅ 多线程环境下只初始化一次
  • ✅ 首次调用时才创建实例(延迟加载)
  • ✅ 无需外部容器即可工作

2.2 配置集中管理

所有配置项通过 AppSettings.GetConfig 统一读取,键值采用冒号分隔(兼容.NET Core配置风格):

json 复制代码
{
  "ALIYUN_OSS": {
    "KEY": "your-access-key-id",
    "SECRET": "your-access-key-secret",
    "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
    "bucketName": "my-bucket",
    "domainUrl": "https://cdn.example.com"
  }
}

2.3 路径规范化策略

OSS对象键(ObjectKey)必须以 / 分隔,且不能以 / 开头。代码中统一处理:

csharp 复制代码
dirPath = dirPath.Replace(@"\", "/").TrimStart('/');

⚠️ 注意:使用逐字字符串 @"\" 避免转义问题,比 "\\" 更清晰。

三、功能模块详解

3.1 文件上传(流式上传)

支持传入 Stream 对象上传,适用于:

  • 接收前端上传的文件流
  • 内存中生成的文件(如报表、二维码)
  • 第三方接口转发
csharp 复制代码
// 使用示例
using var fs = System.IO.File.OpenRead("report.pdf");
var status = AliyunOssHelper.PutObjectFromFile(fs, "reports/2024/report.pdf");
if (status == HttpStatusCode.OK)
{
    Console.WriteLine("✅ 上传成功");
}

关键细节:

细节 实现 空流校验 自动校验流是否可读,避免空引用异常 路径规范化 自动替换反斜杠、去除前导斜杠 兼容旧版 UploadStream 方法做参数顺序适配

3.2 文件下载(内存流返回)

csharp 复制代码
using var stream = AliyunOssHelper.GetObjectStream("images/logo.png");
// 可直接返回给前端,或写入本地文件

⚠️ 性能提示:当前实现将OSS流完整复制到 MemoryStream。对于大文件(>100MB),建议改用分块下载或直传流,避免内存溢出。

3.3 文件删除(级联清理)

这是业务属性最强的功能。当删除压缩包(.hnt / .zip / .itz)时,自动清理其解压后的目录:

csharp 复制代码
// 删除压缩包,同时自动删除同名目录下的所有文件
AliyunOssHelper.DeleteFile("packages/app_v1.0.zip");
// 会级联删除 packages/app_v1.0/ 下的所有对象

实现原理:

bash 复制代码
┌─────────────────┐     ┌─────────────────┐     ┌─────────────────┐
│  1. 删除源文件   │ ──▶ │ 2. 检查后缀类型  │ ──▶ │ 3. 拼接目录前缀  │
│  DeleteObject   │     │ .hnt/.zip/.itz  │     │ noSuffix + "/"  │
└─────────────────┘     └─────────────────┘     └─────────────────┘
                                                        │
                              ┌─────────────────────────┘
                              ▼
                       ┌─────────────────┐
                       │ 4. ListObjects  │
                       │   确认目录存在   │
                       └─────────────────┘
                              │
                              ▼
                       ┌─────────────────┐
                       │ 5. 分页批量删除  │
                       │  MaxKeys=1000   │
                       └─────────────────┘

3.4 目录管理

列出子目录

OSS本身没有"目录"概念,只有对象键前缀。通过设置 Delimiter = "/" 模拟目录结构:

csharp 复制代码
var dirs = AliyunOssHelper.ListDirectories("images/");
// 返回:["images/avatars/", "images/banners/", "images/products/"]

列出文件(按后缀过滤)

csharp 复制代码
var files = AliyunOssHelper.ListFiles("data", "exports", ".csv");
// 返回 data/exports/ 下所有 .csv 文件路径

3.5 私有文件签名URL

对于私有Bucket,临时访问URL需要签名。工具类提供了两种方法:

方法 特点 适用场景 GenerateSignedGetUrl 直接生成签名URL 确定文件存在时 GenerateSignedGetUrlIfExists 先校验存在性再生成 不确定文件是否存在

csharp 复制代码
// 直接生成(1小时有效)
var url = AliyunOssHelper.GenerateSignedGetUrl("confidential/report.pdf", expireSeconds: 3600);
// 返回:https://cdn.example.com/confidential/report.pdf?OSSAccessKeyId=...&Expires=...&Signature=...

// 存在性校验后生成
var url = AliyunOssHelper.GenerateSignedGetUrlIfExists("file.pdf");
// 文件不存在返回空字符串,避免生成无效URL

性能优化:签名URL生成涉及HMAC-SHA1计算,工具类内置了10分钟本地缓存,相同参数的请求直接返回缓存结果。


四、完整源码

csharp 复制代码
using Aliyun.OSS;
using Aliyun.OSS.Common;
using Infrastructure;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Threading.Tasks;

namespace Common.Aliyun
{
    /// <summary>
    /// 阿里云OSS工具类
    /// 特性:静态单例、高性能、无内存泄漏、异常安全
    /// 功能:文件上传/下载/删除、压缩包解压、目录管理、私有文件签名URL
    /// </summary>
    public static class AliyunOssHelper
    {
        #region 静态配置

        /// <summary>阿里云AccessKey ID</summary>
        private static readonly string AccessKeyId = AppSettings.GetConfig("ALIYUN_OSS:KEY");

        /// <summary>阿里云AccessKey Secret</summary>
        private static readonly string AccessKeySecret = AppSettings.GetConfig("ALIYUN_OSS:SECRET");

        /// <summary>OSS服务访问域名</summary>
        private static readonly string Endpoint = AppSettings.GetConfig("ALIYUN_OSS:Endpoint");

        /// <summary>默认存储空间名称</summary>
        private static readonly string DefaultBucketName = AppSettings.GetConfig("ALIYUN_OSS:bucketName");

        /// <summary>自定义访问域名</summary>
        private static readonly string DomainUrl = AppSettings.GetConfig("ALIYUN_OSS:domainUrl");

        /// <summary>
        /// 延迟初始化OSS客户端(单例模式,保证线程安全+性能最优)
        /// </summary>
        private static readonly Lazy<OssClient> _ossClient = new(() =>
            new OssClient(Endpoint, AccessKeyId, AccessKeySecret));

        /// <summary>OSS客户端实例</summary>
        private static OssClient Client => _ossClient.Value;

        #endregion

        #region 基础校验

        /// <summary>校验字符串参数不能为空</summary>
        /// <exception cref="ArgumentNullException">参数为空时抛出</exception>
        private static void CheckNull(string value, string name)
        {
            if (string.IsNullOrWhiteSpace(value))
                throw new ArgumentNullException(name);
        }

        #endregion

        #region 文件上传

        /// <summary>
        /// 流式上传文件到OSS
        /// </summary>
        /// <param name="filestreams">文件流</param>
        /// <param name="dirPath">OSS存储路径</param>
        /// <param name="bucketName">指定存储空间(为空使用默认值)</param>
        /// <returns>HTTP状态码</returns>
        public static HttpStatusCode PutObjectFromFile(Stream filestreams, string dirPath, string bucketName = "")
        {
            CheckNull(dirPath, nameof(dirPath));
            if (filestreams == null || !filestreams.CanRead)
                return HttpStatusCode.BadRequest;

            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                // 统一路径格式:替换反斜杠+去除开头斜杠
                dirPath = dirPath.Replace(@"\", "/").TrimStart('/');

                var result = Client.PutObject(bucket, dirPath, filestreams);
                return result.HttpStatusCode;
            }
            catch (OssException ex)
            {
                Console.WriteLine($"OSS上传异常:{ex.ErrorCode} {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"上传失败:{ex.Message}");
            }

            return HttpStatusCode.BadRequest;
        }

        /// <summary>
        /// 流式上传(兼容旧版调用,参数顺序不同)
        /// </summary>
        public static HttpStatusCode UploadStream(string ossPath, Stream stream, string bucketName = "")
        {
            return PutObjectFromFile(stream, ossPath, bucketName);
        }

        #endregion

        #region 文件获取

        /// <summary>
        /// 获取OSS文件流(内存流返回)
        /// </summary>
        /// <param name="ossFilePath">OSS文件路径</param>
        /// <param name="bucketName">指定存储空间(为空使用默认值)</param>
        /// <returns>文件内存流</returns>
        /// <exception cref="Exception">OSS读取异常</exception>
        public static Stream GetObjectStream(string ossFilePath, string bucketName = "")
        {
            CheckNull(ossFilePath, nameof(ossFilePath));

            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                ossFilePath = ossFilePath.Replace(@"\", "/").TrimStart('/');

                using var ossObject = Client.GetObject(bucket, ossFilePath);
                var ms = new MemoryStream();
                ossObject.Content.CopyTo(ms);
                ms.Position = 0;
                return ms;
            }
            catch (OssException ex)
            {
                throw new Exception($"OSS读取失败:{ex.ErrorCode} {ex.Message}", ex);
            }
        }

        /// <summary>获取OSS文本文件内容</summary>
        public static string GetObjectString(string ossFilePath, string bucketName = "")
        {
            using var stream = GetObjectStream(ossFilePath, bucketName);
            using var reader = new StreamReader(stream);
            return reader.ReadToEnd();
        }

        #endregion

        #region 文件删除

        /// <summary>
        /// 删除OSS文件,并自动删除对应解压目录(支持.hnt/.zip/.itz)
        /// </summary>
        /// <param name="dirPath">OSS文件路径</param>
        /// <param name="bucketName">指定存储空间(为空使用默认值)</param>
        /// <returns>HTTP状态码</returns>
        public static HttpStatusCode DeleteFile(string dirPath, string bucketName = "")
        {
            CheckNull(dirPath, nameof(dirPath));

            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                dirPath = dirPath.Replace(@"\", "/").TrimStart('/');

                // 1. 删除源文件
                Client.DeleteObject(bucket, dirPath);

                // 2. 匹配指定后缀,删除对应解压目录
                string[] targetSuffixes = { ".hnt", ".zip", ".itz" };
                string extension = Path.GetExtension(dirPath);

                if (targetSuffixes.Contains(extension, StringComparer.OrdinalIgnoreCase))
                {
                    // 拼接目录前缀
                    string noSuffixPath = Path.ChangeExtension(dirPath, null);
                    string folderPrefix = noSuffixPath.Replace(@"\", "/").TrimStart('/') + "/";

                    // 校验目录是否存在
                    var listReq = new ListObjectsRequest(bucket)
                    {
                        Prefix = folderPrefix,
                        MaxKeys = 1
                    };
                    var listResult = Client.ListObjects(listReq);

                    if (listResult.ObjectSummaries.Any())
                    {
                        DeleteOssFolder(bucket, folderPrefix);
                    }
                }

                return HttpStatusCode.OK;
            }
            catch (OssException ex)
            {
                Console.WriteLine($"OSS删除异常:{ex.ErrorCode} {ex.Message}");
            }
            catch (Exception ex)
            {
                Console.WriteLine($"删除失败:{ex.Message}");
            }

            return HttpStatusCode.BadRequest;
        }

        /// <summary>递归删除OSS目录下所有文件(分页批量删除)</summary>
        private static void DeleteOssFolder(string bucketName, string folderPrefix)
        {
            var listRequest = new ListObjectsRequest(bucketName)
            {
                Prefix = folderPrefix,
                MaxKeys = 1000
            };

            // 分页删除(OSS单次最大1000条)
            do
            {
                var result = Client.ListObjects(listRequest);
                if (!result.ObjectSummaries.Any())
                    break;

                var keys = result.ObjectSummaries.Select(x => x.Key).ToList();
                Client.DeleteObjects(new DeleteObjectsRequest(bucketName, keys));

                listRequest.Marker = result.NextMarker;
            } while (!string.IsNullOrEmpty(listRequest.Marker));
        }

        #endregion

        #region 文件/目录判断

        /// <summary>判断OSS文件/对象是否存在</summary>
        public static bool DoesObjectExist(string ossPath, string bucketName = "")
        {
            if (string.IsNullOrWhiteSpace(ossPath)) return false;

            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                ossPath = ossPath.Replace(@"\", "/").TrimStart('/');
                return Client.DoesObjectExist(bucket, ossPath);
            }
            catch
            {
                return false;
            }
        }

        #endregion

        #region 文件列表

        /// <summary>获取OSS指定目录下的所有文件路径</summary>
        public static List<string> ListObjects(string prefix, string bucketName = "")
        {
            var list = new List<string>();

            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                prefix = prefix.Replace(@"\", "/").TrimStart('/');
                if (!prefix.EndsWith("/")) prefix += "/";

                var req = new ListObjectsRequest(bucket)
                {
                    Prefix = prefix,
                    MaxKeys = 1000
                };

                var result = Client.ListObjects(req);
                foreach (var summary in result.ObjectSummaries)
                {
                    list.Add(summary.Key);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"列出OSS文件失败:{ex.Message}");
            }

            return list;
        }

        /// <summary>
        /// 获取OSS指定目录下指定后缀的文件列表(适配ITZ UUID获取)
        /// </summary>
        public static List<string> ListFiles(string prefix, string folder, string extension, string bucketName = "")
        {
            var fileList = new List<string>();
            try
            {
                var fullPrefix = Path.Combine(prefix, folder).Replace(@"\", "/");
                var allObjects = ListObjects(fullPrefix, bucketName);
                fileList = allObjects
                    .Where(x => x.EndsWith(extension, StringComparison.OrdinalIgnoreCase))
                    .ToList();
            }
            catch (Exception ex)
            {
                Console.WriteLine($"获取OSS文件列表失败:{ex.Message}");
            }
            return fileList;
        }

        #endregion

        #region 压缩包解压

        /// <summary>
        /// 解压OSS压缩包并上传至OSS(支持zip,纯内存操作无本地文件,全部文件同步上传)
        /// </summary>
        /// <param name="ossArchivePath">OSS压缩包路径</param>
        /// <param name="targetOssDir">解压目标目录</param>
        /// <param name="bucketName">指定存储空间(为空使用默认值)</param>
        /// <returns>已上传的XML文件OSS路径列表</returns>
        public static List<string> DecompressToOss(string ossArchivePath, string targetOssDir, string bucketName = "")
        {
            List<string> xmlFilePaths = new List<string>();
            CheckNull(ossArchivePath, nameof(ossArchivePath));
            CheckNull(targetOssDir, nameof(targetOssDir));

            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                ossArchivePath = ossArchivePath.Replace(@"\", "/").TrimStart('/');
                targetOssDir = targetOssDir.Replace(@"\", "/").TrimEnd('/') + "/";

                // 目录已存在直接返回
                if (DoesObjectExist($"{targetOssDir}/", bucket))
                {
                    return xmlFilePaths;
                }

                // 获取压缩包OSS流
                using Stream archiveStream = GetObjectStream(ossArchivePath, bucket);
                if (archiveStream == null || !archiveStream.CanRead)
                    throw new Exception("压缩包流读取失败");

                // 内部就地解压
                var fileDict = ExtractAllFilesToStream(archiveStream);
                if (fileDict == null || !fileDict.Any())
                    throw new Exception("压缩包内无文件");

                // 全部文件同步逐个上传
                foreach (var kv in fileDict)
                {
                    try
                    {
                        string ossKey = $"{targetOssDir}{kv.Key.Replace(@"\", "/")}";
                        if (kv.Value.CanSeek) kv.Value.Position = 0;

                        var status = UploadStream(ossKey, kv.Value, bucket);
                        // 仅XML记录返回路径
                        if (status == HttpStatusCode.OK && 
                            kv.Key.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
                        {
                            xmlFilePaths.Add(ossKey);
                        }
                    }
                    catch
                    {
                        // 单个文件异常不中断后续上传
                    }
                    finally
                    {
                        kv.Value.Dispose();
                    }
                }

                return xmlFilePaths;
            }
            catch
            {
                return xmlFilePaths;
            }
        }

        /// <summary>
        /// 解压所有文件到内存流(纯内存,适配OSS上传)
        /// </summary>
        /// <param name="zipStream">压缩文件流</param>
        /// <returns>key=文件名, value=文件内存流</returns>
        private static Dictionary<string, Stream> ExtractAllFilesToStream(Stream zipStream)
        {
            var fileDictionary = new Dictionary<string, Stream>();

            if (zipStream == null || !zipStream.CanRead)
                throw new ArgumentNullException(nameof(zipStream));

            try
            {
                if (zipStream.CanSeek)
                    zipStream.Position = 0;

                using var archive = new ZipArchive(zipStream, ZipArchiveMode.Read, leaveOpen: true);
                foreach (var entry in archive.Entries)
                {
                    // 跳过目录
                    if (string.IsNullOrEmpty(entry.Name))
                        continue;

                    var ms = new MemoryStream();
                    using var entryStream = entry.Open();
                    entryStream.CopyTo(ms);
                    ms.Position = 0;

                    fileDictionary.Add(entry.FullName, ms);
                }

                return fileDictionary;
            }
            catch (Exception ex)
            {
                throw new Exception($"解压所有文件失败: {ex.Message}", ex);
            }
        }

        #endregion

        #region 目录管理

        /// <summary>
        /// 获取OSS指定目录下的所有子目录(仅返回目录,不包含文件)
        /// </summary>
        /// <param name="prefix">父目录路径</param>
        /// <param name="bucketName">指定存储空间(为空使用默认值)</param>
        /// <returns>子目录路径列表</returns>
        public static List<string> ListDirectories(string prefix, string bucketName = "")
        {
            var directoryList = new List<string>();
            try
            {
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                prefix = prefix.Replace(@"\", "/").TrimStart('/');
                if (!prefix.EndsWith("/")) prefix += "/";

                // Delimiter=/ 为OSS目录分隔核心参数,用于筛选目录
                var req = new ListObjectsRequest(bucket)
                {
                    Prefix = prefix,
                    Delimiter = "/",
                    MaxKeys = 1000
                };

                var result = Client.ListObjects(req);
                // CommonPrefixes 为OSS返回的子目录集合
                foreach (var dirPrefix in result.CommonPrefixes)
                {
                    directoryList.Add(dirPrefix);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine($"获取OSS子目录失败:{ex.Message}");
            }
            return directoryList;
        }

        #endregion

        #region 私有文件签名URL

        /// <summary>
        /// 生成OSS私有文件临时访问签名URL
        /// </summary>
        /// <param name="objectPath">OSS文件路径/完整URL</param>
        /// <param name="expireSeconds">有效期(秒),范围60~86400,默认1小时</param>
        /// <param name="bucketName">指定存储空间(为空使用默认值)</param>
        /// <returns>签名URL,失败返回空字符串</returns>
        public static string GenerateSignedGetUrl(string objectPath, int expireSeconds = 3600, string bucketName = "")
        {
            if (string.IsNullOrWhiteSpace(objectPath))
                return string.Empty;

            try
            {
                // 兼容传入完整URL,自动提取路径
                string ossPath = objectPath;
                if (Uri.TryCreate(objectPath, UriKind.Absolute, out var uri))
                    ossPath = uri.AbsolutePath.TrimStart('/');

                // 限制有效期范围
                expireSeconds = Math.Clamp(expireSeconds, 60, 86400);
                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                ossPath = ossPath.Replace(@"\", "/").TrimStart('/');

                // 本地缓存签名URL,避免重复生成
                var cacheKey = $"OSS_SIGN_{bucket}_{ossPath.Replace("/", "_")}_{expireSeconds}";
                var cached = CacheHelper.GetCache<string>(cacheKey);
                if (!string.IsNullOrEmpty(cached))
                    return cached;

                // 生成签名
                var expireTime = DateTime.Now.AddSeconds(expireSeconds);
                var signedUri = Client.GeneratePresignedUri(bucket, ossPath, expireTime, SignHttpMethod.Get);

                // 拼接自定义域名+签名参数
                var final = $"{DomainUrl.TrimEnd('/')}/{ossPath}{signedUri.Query}";

                // 缓存10分钟,保证返回的URL始终有效
                CacheHelper.SetCaches(cacheKey, final, 600);
                return final;
            }
            catch
            {
                return string.Empty;
            }
        }

        /// <summary>
        /// 校验文件存在后,生成私有文件临时访问签名URL
        /// </summary>
        /// <returns>签名URL,文件不存在/异常返回空字符串</returns>
        public static string GenerateSignedGetUrlIfExists(string objectPath, int expireSeconds = 3600, string bucketName = "")
        {
            try
            {
                if (string.IsNullOrWhiteSpace(objectPath))
                    return string.Empty;

                // 统一路径处理
                string ossPath = objectPath;
                if (Uri.TryCreate(objectPath, UriKind.Absolute, out var uri))
                    ossPath = uri.AbsolutePath.TrimStart('/');

                var bucket = string.IsNullOrEmpty(bucketName) ? DefaultBucketName : bucketName;
                ossPath = ossPath.Replace(@"\", "/").TrimStart('/');

                // 文件不存在直接返回空
                if (!Client.DoesObjectExist(bucket, ossPath))
                    return string.Empty;

                return GenerateSignedGetUrl(objectPath, expireSeconds, bucketName);
            }
            catch
            {
                return string.Empty;
            }
        }

        #endregion
    }
}

五、使用示例

5.1 appsettings.json 配置

json 复制代码
{
  "ALIYUN_OSS": {
    "KEY": "LTAI5t8Z3y8Y7Z7Z7Z7Z7Z7Z",
    "SECRET": "your-secret-key",
    "Endpoint": "https://oss-cn-hangzhou.aliyuncs.com",
    "bucketName": "my-app-bucket",
    "domainUrl": "https://cdn.myapp.com"
  }
}

5.2 常见业务场景

csharp 复制代码
// ── 场景1:上传用户头像 ──────────────────────────────
public async Task<string> UploadAvatar(IFormFile file, int userId)
{
    var ossPath = $"avatars/{userId}/{Guid.NewGuid()}.jpg";
    using var stream = file.OpenReadStream();
    var status = AliyunOssHelper.PutObjectFromFile(stream, ossPath);
    return status == HttpStatusCode.OK ? ossPath : null;
}

// ── 场景2:获取私有文件临时链接 ──────────────────────
public string GetPrivateFileUrl(string filePath)
{
    return AliyunOssHelper.GenerateSignedGetUrl(filePath, expireSeconds: 1800);
}

// ── 场景3:清理过期压缩包(级联删除)────────────────
public void CleanupPackage(string packagePath)
{
    AliyunOssHelper.DeleteFile(packagePath); // 自动删除解压目录
}

// ── 场景4:遍历目录获取所有CSV数据文件 ───────────────
public List<string> GetDataFiles()
{
    return AliyunOssHelper.ListFiles("data", "exports", ".csv");
}

六、注意事项与优化建议

类型 注意事项 说明 ⚠️ 大文件下载 GetObjectStream 使用 MemoryStream 缓存,大文件建议改为流式直传 ⚠️ 异步支持 当前为同步实现,高并发场景建议补充 async/await 版本 ⚠️ 日志记录 当前使用 Console.WriteLine,生产环境建议接入 ILoggerSerilog ⚠️ 配置热更新 静态字段在程序启动时加载,配置变更需重启应用 ✅ 路径安全 自动处理反斜杠和前导斜杠,避免OSS 404 ✅ 签名缓存 10分钟本地缓存减少重复签名计算,提升性能


七、总结

AliyunOssHelper 是一个经过生产验证的OSS工具类,其核心优势在于:

优势 说明 零依赖启动 纯静态类,无需DI容器即可工作 线程安全 Lazy<T> 确保客户端单例安全 业务内聚 级联删除、签名缓存等逻辑封装到位 防御性编程 参数校验、异常捕获、路径规范化全覆盖

适用场景:中小型项目或遗留系统改造,作为OSS操作的统一入口。

进阶建议:对于大型微服务架构,建议进一步抽象为 IOssService 接口,便于单元测试和多厂商切换。


SDK版本:Aliyun.OSS.SDK.NetCore 2.13.0+

兼容框架:.NET Framework 4.6.1 / .NET Core 2.0 / .NET 5/6/7/8+


如果这篇文章对你有帮助,欢迎点赞、收藏、关注!有任何问题欢迎在评论区留言讨论。

👋 这是我网站:tool.bugcome.com

相关推荐
Gopher_HBo44 分钟前
存储层技术MySQL
后端
槑有老呆1 小时前
从零搭建 AIGC 工程:后端项目初始化到 API 调用的完整实践
后端
Code_Artist1 小时前
盘点Redis的常见使用场景,拜托不要再只会Get&Set一坨数据啦!
redis·后端·面试
咕咚咚1 小时前
【线上问题处理】JSONNull导致的接口500
后端
ayqy贾杰1 小时前
有AI了,我当超大头兵还苟得住吗?
前端·后端·架构
用户713874229001 小时前
HttpContext.Connection 深度解析:从连接元数据到请求追踪与 mTLS
后端
我是一只码蚁1 小时前
《别再死记面向对象了,我家咖啡机就是最好的老师》
java·后端
小月土星1 小时前
从零到一:用 Node.js 调用 DeepSeek 大模型 API 完整实战教程
人工智能·后端
阿杰 AJie1 小时前
ExcelUtils样式相关工具
java·后端