XYS.MinIO.Client
3.0.0
dotnet add package XYS.MinIO.Client --version 3.0.0
NuGet\Install-Package XYS.MinIO.Client -Version 3.0.0
<PackageReference Include="XYS.MinIO.Client" Version="3.0.0" />
<PackageVersion Include="XYS.MinIO.Client" Version="3.0.0" />
<PackageReference Include="XYS.MinIO.Client" />
paket add XYS.MinIO.Client --version 3.0.0
#r "nuget: XYS.MinIO.Client, 3.0.0"
#:package XYS.MinIO.Client@3.0.0
#addin nuget:?package=XYS.MinIO.Client&version=3.0.0
#tool nuget:?package=XYS.MinIO.Client&version=3.0.0
XYS.MinIO.Client
MinIO 对象存储客户端。3.0.0 起为可注入的实例客户端,2.x 的静态 MinioHelper 已移除。
底层依赖 Minio .NET SDK 6.0.5。
安装与配置
xys.config.json:
{
"minio": {
"endpoint": "10.0.0.5",
"port": 9000,
"accessKey": "your-access-key",
"secretKey": "<Crypto.Protect 后的密文>",
"useSsl": false,
"region": "",
"timeoutSeconds": 60
}
}
secretKey 经 XYS.Utils.Sys 的 Crypto.Unprotect 解密,与 connectionStrings 的约定一致。
timeoutSeconds 目前不生效(原因见"已知限制"第 1 条),配置它不会报错,也不会产生效果。
关于 ENC(...) 的强度:Crypto.Protect 在未设置 XYS_CRYPTO_KEY 环境变量时使用内置默认密钥,
只是混淆级保护——持有程序集的人可以反解。要获得真正的加密,请在宿主环境设置 XYS_CRYPTO_KEY。
另外 Crypto.Unprotect 对非 ENC(...) 的值原样返回,所以直接写明文 secretKey 也能跑通,
且不会有任何提示——请自行确认配置文件里写的是密文。
用法
依赖注入:
services.AddMinioStorage(); // 读 xys.config.json
services.AddMinioStorage(o => { o.Endpoint = "10.0.0.5"; o.Port = 9000;
o.AccessKey = "ak"; o.SecretKey = "sk"; });
不用容器:
using (var storage = new MinioStorage(MinioOptions.FromConfig()))
{
await storage.MakeBucketAsync("reports");
using (var fs = File.OpenRead(@"D:\a.pdf"))
{
await storage.PutObjectAsync("reports", "2026/a.pdf", fs, fs.Length, "application/pdf");
}
var meta = await storage.GetObjectToFileAsync("reports", "2026/a.pdf", @"D:\b.pdf");
Console.WriteLine(meta.Size);
var url = await storage.PresignedGetObjectAsync("reports", "2026/a.pdf", 600);
}
注册为单例。 IMinioClient 内部持有 HttpClient,按调用创建实例会耗尽套接字端口。
AddMinioStorage 已按单例注册。
异常
本库除参数校验(ArgumentNullException / ArgumentException / ObjectDisposedException)外
不包裹异常。Minio SDK 的异常原样上抛,可按类型分支:
try { await storage.StatObjectAsync("reports", "missing.pdf"); }
catch (ObjectNotFoundException) { /* 对象不存在 */ }
catch (BucketNotFoundException) { /* 桶不存在 */ }
catch (MinioException) 不是一张安全网。 凭据 / 签名错误抛的 AuthorizationException
直接派生自 Exception,并不是 MinioException 的子类;一部分操作在凭据错误时抛的
甚至是 NullReferenceException(见"已知限制"第 2 条)。所以只要凭据错误也需要走同一个
分支,兜底就必须写 catch (Exception):
try { await storage.PutObjectAsync(...); }
catch (BucketNotFoundException) { /* 业务可预期的错误 */ }
catch (Exception ex) { /* 凭据错误会落到这里,而不是 MinioException 分支 */ }
已知限制(来自 Minio SDK 6.0.5,非本库)
以下均为实测确认的上游行为,测试环境:Minio .NET SDK 6.0.5 + MinIO 服务端
RELEASE.2025-09-07T16-13-09Z。本库不包裹它们,如实转达。
1. timeoutSeconds 完全不生效
MinioOptions.Timeout 和配置里的 timeoutSeconds 都不起作用,两处原因叠加:
MinioClientExtensions.Build()无条件执行HttpClient.Timeout = TimeSpan.FromMinutes(30);RequestExtensions.ExecuteTaskAsync里用来做超时的两个CancellationTokenSource是using声明、作用域只到if块结束,在真正await发出请求之前就已被Dispose, 于是那个令牌永远不会触发。
这两处在 6.0.4 与 6.0.5 之间逐字未变。要控制超时只能自己传 CancellationToken。
2. 凭据错误时的异常类型不统一,catch (MinioException) 兜不住
凭据 / 签名错误会可靠地抛异常(不会静默成功),但类型分两种:
| 操作 | 凭据错误时抛出 |
|---|---|
ListBucketsAsync、ListObjectsAsync、PutObjectAsync、RemoveObjectAsync |
AuthorizationException |
BucketExistsAsync、MakeBucketAsync、StatObjectAsync、GetObjectAsync、GetObjectToFileAsync |
NullReferenceException |
两种都不是 MinioException 的子类——AuthorizationException 直接派生自 Exception,
NullReferenceException 是 SystemException。
根因(ILSpy 反编译 Minio.dll 6.0.5 核实):错误响应带 S3 的 XML 响应体时走
MinioClient.ParseErrorFromContent,能正确造出 AuthorizationException;而 HEAD 类请求
的错误响应没有响应体,走 ParseErrorNoContent → ParseWellKnownErrorNoContent,
那里第一行判断写的是:
if (response.StatusCode.ToString().Contains("NotFound", StringComparison.Ordinal)
|| response.Exception.ToString().Contains("NotFound", StringComparison.Ordinal))
纯状态码错误(没有传输层异常)时 response.Exception 为 null,第二个操作数直接空引用。
6.0.4 该处写的是 HttpStatusCode.NotFound == response.StatusCode,没有这次解引用——
这是 6.0.5 引入的回归。副作用是同一方法里那条本该产出 AccessDeniedException 的
Forbidden 分支在这条路径上根本走不到。
第 2 类里的 MakeBucketAsync 是本库自己的连带:它为了实现幂等会先调 BucketExistsAsync。
3. GetPolicyAsync 在桶没有策略时抛 UnexpectedMinioException
桶从未设过策略、或策略已被 RemovePolicyAsync 删除时,GetPolicyAsync 不返回 null,
而是抛出笼统的 UnexpectedMinioException——SDK 没有为这种情况准备专用异常类型。
要把"没有策略"和真正的故障区分开,只能看错误码:
try { policy = await storage.GetPolicyAsync("reports"); }
catch (MinioException ex) when (ex.Response?.Code == "NoSuchBucketPolicy") { policy = null; }
4. MinioException.Response 在无响应体的错误上为 null
上面第 3 条能读到 ex.Response.Code,是因为那条错误带 XML 响应体。
走 ParseWellKnownErrorNoContent 的错误(例如 RemoveBucketAsync 删不存在的桶时的
BucketNotFoundException)ex.Response 是 null——6.0.5 把 6.0.4 的
ex.Response = errorResponse 改成了 response.Exception = ex,构造好的
ErrorResponse 被丢弃了。所以读 ex.Response 一律要判空。
5. PutObjectFromFileAsync 上传后不立即释放本地文件句柄
要等垃圾回收才释放,6.0.5 未修复。若需要在上传后立刻删除或移动该文件,
请改用 PutObjectAsync 自行控制流的生命周期。
与 AWS S3 语义一致、无需特殊处理的部分
这些实测过,行为正常,列出来是为了免得被上面几条吓到:
| 场景 | 行为 |
|---|---|
RemoveBucketAsync 删不存在的桶 |
抛 BucketNotFoundException |
ListObjectsAsync 列不存在的桶 |
抛 BucketNotFoundException |
StatObjectAsync / GetObjectToFileAsync 读不存在的对象 |
抛 ObjectNotFoundException |
CopyObjectAsync 源对象不存在 |
抛 ObjectNotFoundException |
RemoveObjectAsync 删不存在的对象 |
不抛(S3 的 DELETE Object 本就幂等) |
RemovePolicyAsync 删本来就没有的策略 |
不抛 |
RemoveIncompleteUploadAsync 目标不存在 |
不抛 |
已取消的 CancellationToken |
抛 OperationCanceledException |
目标框架
net462、netcoreapp3.1、net8.0。
从 2.x 迁移
2.x 的所有方法是 MinioHelper 上的静态方法,首参为 MinioClient;
3.0 全部改为 IMinioStorage 上的实例方法。
| 2.x | 3.0 |
|---|---|
MinioHelper.GetClient(ip, port, ak, sk) |
new MinioStorage(options) / AddMinioStorage() |
MakeBucket |
MakeBucketAsync(幂等:桶已存在返回 false 而非抛异常) |
ListBuckets |
ListBucketsAsync |
BucketExists |
BucketExistsAsync |
RemoveBucket |
RemoveBucketAsync |
ListObjects |
ListObjectsAsync |
ListIncompleteUploads |
ListIncompleteUploadsAsync |
GetPolicy / SetPolicy |
GetPolicyAsync / SetPolicyAsync(策略 JSON 改由调用方传入)、新增 RemovePolicyAsync |
FGetObject(..., fileName, sse) |
GetObjectToFileAsync |
FGetObject(..., callback, sse) |
GetObjectAsync(callback) |
GetObjectAsync(..., fileName) |
GetObjectToFileAsync(与 FGetObject 合并) |
GetObjectAsync(..., callback) |
GetObjectAsync(callback)(与 FGetObject 合并) |
GetObjectAsync(..., offset, length, callback) |
GetObjectAsync(offset, length, callback) |
FPutObject(..., fileName, contentType) |
PutObjectFromFileAsync |
PutObjectAsync(..., filePath, ...) |
PutObjectFromFileAsync(与 FPutObject 合并) |
PutObjectAsync(..., Stream, size, ...) |
PutObjectAsync |
StatObject |
StatObjectAsync |
CopyObject |
CopyObjectAsync |
RemoveObject / RemoveObjects |
RemoveObjectAsync / RemoveObjectsAsync |
RemoveIncompleteUpload |
RemoveIncompleteUploadAsync |
PresignedGetObject / PresignedPutObject |
PresignedGetObjectAsync / PresignedPutObjectAsync |
PresignedPostPolicy |
已移除 |
桶通知相关三个 private 方法 |
已移除 |
静默的行为变化(对照表之外,容易漏)
方法改了名容易发现,下面这些不会报错但行为变了:
| 项 | 2.x | 3.0 |
|---|---|---|
| 建桶区域默认值 | loc = "us-east-1" |
region = null,不调用 WithLocation |
| 预签名有效期默认值 | expiresInt = 1000 秒 |
expirySeconds = 3600 秒 |
| 预签名有效期上限 | 无校验 | 超过 604800 秒(7 天)抛 ArgumentException |
| 上传大小 | 原样透传给 SDK | size < 0 抛 ArgumentException |
| 桶名 / 对象键 | 无校验 | 空或纯空白抛 ArgumentException |
| 下载到本地文件 | 目标文件已存在时先 File.Delete |
直接覆盖;下载失败时原有目标文件不被破坏 |
表里"上传大小"那行有实际影响:Minio SDK 用 ObjectSize = -1 表示"长度未知的流式上传",
本库的校验封死了这条路。若你需要上传不可 seek 的流(网络流、压缩流)且不想先缓冲到内存,
当前版本做不到——请提 issue。
其他行为变化:
- 返回值不再是
Task<bool>/Tuple<bool, T>。2.x 里那个flag恒为true(所有false分支都是throw)。3.0 直接返回数据。 - 三个
Get重载返回ObjectMetadata——SDK 本就返回它,2.x 丢掉了。 - 全部方法(两个预签名方法除外)接收
CancellationToken。 - 不再向控制台输出任何内容;需要日志时注入
ILogger<MinioStorage>。 (2.x 依赖的 SDK 6.0.4 自己会在ExecuteTaskAsync里Console.WriteLine异常消息, 6.0.5 已删除该行,所以现在整条链路都不会往控制台写东西。)
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp3.1 is compatible. |
| .NET Framework | net462 is compatible. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
-
.NETCoreApp 3.1
- Minio (>= 6.0.5)
- XYS.Utils.Sys (>= 2.6.0)
-
.NETFramework 4.6.2
- Minio (>= 6.0.5)
- XYS.Utils.Sys (>= 2.6.0)
-
net8.0
- Minio (>= 6.0.5)
- XYS.Utils.Sys (>= 2.6.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.