XiHan.Framework.ObjectStorage.Abstractions
对象存储的抽象包:容器、键与元数据,IBlobStore(批量写入与删除、按键读取、范围读取、按前缀分页列举),可选的分片上传 IBlobMultipartStore 与预签名 IBlobUrlSigner,以及命名存储实例的注册表 BlobStores。业务代码只依赖本包,存储后端由 Provider 决定。
| 项 | 内容 |
|---|---|
| 包 ID | XiHan.Framework.ObjectStorage.Abstractions |
| 环层 | Ring 2(抽象) |
| 能力 | 定义 objectstorage.blob@1、objectstorage.multipart@1、objectstorage.presign@1,由 ObjectStorage.FileSystem(默认实现)、ObjectStorage.S3 与 Hosting.AspNetCore 提供 |
| 特性入口 | — |
| 配置节 | XiHan:ObjectStorage:Stores、XiHan:ObjectStorage:StoreHealthProbeIntervalSeconds |
| 依赖 | XiHan.Framework.Kernel、XiHan.Framework.Localization.Abstractions;Microsoft.Extensions.Configuration.Abstractions、Microsoft.Extensions.DependencyInjection.Abstractions |
| AOT | 兼容 |
| 稳定性 | Preview |
概述
IBlobStore 是字节流级的契约,Provider 实现六个操作:批量写入、按键读取、范围读取、只读元数据、批量删除与按前缀列举。批量操作按顺序逐项执行、逐项返回结果,两项之间检查取消,已完成的项不回滚;单条写法的扩展方法在 BlobStoreExtensions 里,都转调批量方法。写入是原子的,读取方要么看到旧对象,要么看到完整的新对象。
标识在构造时校验:容器名 ContainerName 兼容 S3 桶名,键 BlobKey 是 / 分段的路径,不合法抛出 ArgumentException,不会进入 Provider。基础设施失败(IO、网络)以 OBJECTSTORAGE_UNAVAILABLE_001 的失败结果返回,不抛出。
分片上传与预签名是两个可选能力,接口独立:存储没有原生预签名时,宿主可以代为收发(Hosting.AspNetCore 的 HostedBlobUrlFeature)。命名存储实例(ADR-0050)让一个应用同时用多个存储:每个装配了的存储包登记一个 IBlobStoreProvider(被别的存储顶替时也登记),注册表 BlobStores 按定义建实例、按名缓存、定义变了就替换。缺省的 IBlobStore、IBlobMultipartStore、IBlobUrlSigner 必须落在同一存储上,存储包装配时以 BlobStoreSelection.ProvidesDefaultInstance 核对,只约束被要求的契约(ADR-0075、ADR-0082)。
何时使用
业务代码注入 IBlobStore、IBlobMultipartStore、IBlobUrlSigner 或 BlobStores 时引用本包;具体后端装 ObjectStorage.FileSystem 或 ObjectStorage.S3。实现新的存储后端时实现本包的接口,并通过 ObjectStorage.Conformance。用法讲解见对象存储。
安装与启用
dotnet add package XiHan.Framework.ObjectStorage.Abstractions --prerelease当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。Profiles.Full 已引用本包与文件系统实现。本包没有特性,装配任一存储包后即可注入上述接口。
配置
| 键 | 类型 | 缺省 | 说明 |
|---|---|---|---|
XiHan:ObjectStorage:Stores:<name>:Provider | string | — | 命名实例的 Provider 名:filesystem 或 s3,不区分大小写 |
XiHan:ObjectStorage:Stores:<name>:<设置> | string | — | 该 Provider 的设置,键同其配置节;未知键报 OBJECTSTORAGE_VALIDATION_003 |
XiHan:ObjectStorage:StoreHealthProbeIntervalSeconds | 正数 | 15 | 命名实例健康探测的间隔,也是单次探测的超时 |
{
"XiHan": {
"ObjectStorage": {
"StoreHealthProbeIntervalSeconds": 30,
"Stores": {
"archive": { "Provider": "filesystem", "RootPath": "/var/lib/app/archive" }
}
}
}
}主要类型
| 类型 | 说明 |
|---|---|
Keys.ContainerName | 容器名,^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$;IsValid、Validate,可从 string 隐式转换 |
Keys.BlobKey | 键:/ 分段,1 到 1024 个字符,每段非空且不是 . / ..,不含空白、控制字符与 \,首段不是保留段 ReservedSegment(.xihan,框架自己的对象),区分大小写;IsValid、IsReserved、IsValidPrefix、Validate,可从 string 隐式转换 |
Blobs.IBlobStore | PutAsync(container, uploads)、GetAsync(container, key)、GetAsync(container, key, range)、HeadAsync、DeleteAsync(container, keys)、ListAsync(container, query) |
Blobs.BlobStoreExtensions | PutAsync(container, key, stream 或 bytes, options)、DeleteAsync(container, key)、ExistsAsync、ReadAllBytesAsync |
Blobs.BlobUpload | 一项写入:键、内容流、选项 |
Blobs.BlobPutOptions | ContentType(缺省 application/octet-stream)、Metadata(名 ^[a-z0-9][a-z0-9-]{0,63}$)、Overwrite(缺省 true);Default |
Blobs.BlobPutResult / BlobDeleteResult | 逐项结果:写入为元数据或错误;删除为是否存在过或错误 |
Blobs.BlobMetadata | 键、长度、内容类型、ETag、最后修改时间、自定义元数据 |
Blobs.BlobContent | 元数据、内容流与可选的实际范围 Range;IAsyncDisposable,释放时关闭流 |
Blobs.BlobRange / BlobContentRange | 范围请求(起点与可选长度)与实际范围(起点、长度、总长、Last) |
Listing.BlobQuery / BlobPage | 列举条件(Prefix、PageSize 1–1000 缺省 1000、ContinuationToken)与一页结果;Validate() 核对页大小,Provider 在列举开头调用,越界返回 OBJECTSTORAGE_VALIDATION_005 |
Multipart.IBlobMultipartStore | StartAsync、UploadPartAsync、ListPartsAsync、CompleteAsync、AbortAsync |
Multipart.BlobMultipartUpload | 一次分片上传:容器、键、上传标识、是否覆盖;可持久化后续传 |
Multipart.BlobPart / BlobMultipart | 分片(编号、ETag、长度);常量 MaxPartNumber = 10000、MinPartBytes = 5 MiB |
Signing.IBlobUrlSigner | SignAsync(container, key, request) → BlobSignedUrl |
Signing.BlobUrlRequest | ForDownload、ForUpload、ForPart;DefaultExpiresIn = 30 分钟,MaxExpiresIn = 7 天 |
Signing.BlobSignedUrl | 地址、方法、失效时刻与客户端必须发送的请求头 |
Signing.BlobContentDisposition | Attachment(fileName):下载文件名的统一写法,非 ASCII 文件名带 filename* |
Stores.BlobStores | 命名实例注册表:GetAsync(name)、GetOrAddAsync(definition)、RemoveAsync(name)、Names、Providers;同时是健康项贡献者 |
Stores.BlobStoreDefinition | 实例名(^[a-z0-9][a-z0-9-]{0,63}$)、Provider 名与设置;Describes(other) 判断定义是否相同 |
Stores.IBlobStoreProvider | 存储包的命名实例工厂:Name、CreateAsync(definition)、ProbeAsync(store) |
Stores.BlobStoreSelection | ProvidesDefaultInstance(node):被选为对象存储返回 true(缺省存储);没被选为对象存储、却被要求提供分片上传或预签名(有依赖或经配置指定)时抛 OBJECTSTORAGE_VALIDATION_004;两者都只是兜底选出时返回 false,存储包按被顶替处理 |
Stores.BlobStoreServiceCollectionExtensions | AddBlobStoreProvider<TProvider>(configuration):存储包装配时登记 Provider,并确保注册表只登记一次 |
ObjectStorageCapabilities / ObjectStorageErrors / ObjectStorageMessages | 能力 ID、错误码与消息键常量,ObjectStorageMessages.Catalog 是本包的消息目录 |
使用示例
写入、读取、列举与删除:
using XiHan.Framework.ObjectStorage.Abstractions.Blobs;
using XiHan.Framework.ObjectStorage.Abstractions.Listing;
var put = await store.PutAsync("invoices", "2026/10/inv-1.pdf", stream, new BlobPutOptions { ContentType = "application/pdf" }, ct);
var page = await store.ListAsync("invoices", new BlobQuery { Prefix = "2026/10/", PageSize = 100 }, ct);
var deleted = await store.DeleteAsync("invoices", ["2026/10/inv-1.pdf", "2026/10/inv-2.pdf"], ct); // 逐项:存在过 / 不存在 / 失败
await using var content = (await store.GetAsync("invoices", "2026/10/inv-1.pdf", ct)).ValueOrThrow();
await content.Content.CopyToAsync(destination, ct);断点续传与限时下载链接:
using XiHan.Framework.ObjectStorage.Abstractions.Multipart;
using XiHan.Framework.ObjectStorage.Abstractions.Signing;
// 服务端发起,客户端按片直传,服务端完成
var upload = (await multipart.StartAsync("videos", key, new BlobPutOptions { ContentType = "video/mp4" }, ct)).ValueOrThrow();
var partUrl = (await signer.SignAsync("videos", key, BlobUrlRequest.ForPart(upload, 1), ct)).ValueOrThrow();
var done = await multipart.CompleteAsync(upload, parts, ct);
// 限时下载链接(缺省 30 分钟)
var link = (await signer.SignAsync("invoices", key, BlobUrlRequest.ForDownload(downloadFileName: "发票.pdf"), ct)).ValueOrThrow().Url;实现自己的存储后端时,用一致性套件验证;测试项目引用 XiHan.Framework.ObjectStorage.Conformance:
using XiHan.Framework.ObjectStorage.Abstractions.Blobs;
using XiHan.Framework.ObjectStorage.Abstractions.Keys;
using XiHan.Framework.ObjectStorage.Conformance;
namespace Storage.Tests;
public sealed class AzureBlobStoreConformanceTests : BlobStoreConformance
{
// 返回被测的存储;实现了 IBlobMultipartStore 时分片用例一并执行,否则跳过
protected override IBlobStore CreateStore() => new AzureBlobStore(TestSettings.ConnectionString);
// 绕过契约直接写一个对象(键可以在保留前缀下),用来核对列举跳过框架自己的对象
protected override Task PlantOutsideContractAsync(ContainerName container, string key, byte[] content)
=> TestSettings.UploadRawAsync(container.Value, key, content);
}AzureBlobStore 与 TestSettings 代表应用自己的实现与测试设置,框架没有提供 Azure Blob Provider。命名实例的 Provider 另继承 BlobStoreProviderConformance,实现 CreateProvider()、ValidSettings() 与 OtherValidSettings()。
行为与限制
- 写入:已存在且
Overwrite = false的项返回OBJECTSTORAGE_CONFLICT_001,不写入;不覆盖写入是原子的,多个写入方同时写同一个新键只有一个成功(一致性套件核对,ADR-0089);配额或单对象大小超限返回OBJECTSTORAGE_UNAVAILABLE_002,不写入。空列表返回空,重复的键按顺序各执行一次。 - 删除:不存在的项成功并报告为不存在;同一批里重复的键第二次报告不存在。
- 列举:键的序数顺序;续页标记不透明,原样带回。不符合键文法的对象(保留前缀
.xihan下的框架对象,如 S3 的条件写入探测键;S3 的目录占位)跳过,所以一页可能少于PageSize条、甚至为空而仍带续页标记,取到续页标记为空为止(ADR-0090)。 - 范围读取:长度为空读到末尾,超出末尾截断;起点不在对象内(含空对象)返回
OBJECTSTORAGE_VALIDATION_001;BlobMetadata.Length仍是总长。 - 分片上传:编号 1–10000,可乱序、可重传;完成时清单编号严格升序、ETag 相符、非末片至少 5 MiB;完成前对象不可见;放弃是幂等的。
- 预签名:有效期 1 秒到 7 天,签名在本地计算,不检查对象是否存在。
- 命名实例:同名同定义复用;定义变了替换并释放旧实例,替换时仍在旧实例上的调用可能失败;Provider 未登记或设置不合法、后端建不出来、建实例时的探测发现实例不能用(如 S3 探明服务不认条件写入;Provider 抛出携带错误码的
FrameworkException,注册表原样返回该错误码)时,已有的同名实例不受影响。每个实例一条健康项objectstorage.store.<name>,还没探过为 Started,探测失败为 Degraded。
| 错误码 | 常量 | 含义 |
|---|---|---|
OBJECTSTORAGE_NOTFOUND_001 | BlobNotFound | 对象不存在 |
OBJECTSTORAGE_NOTFOUND_002 | UploadNotFound | 分片上传不存在或已结束 |
OBJECTSTORAGE_NOTFOUND_003 | StoreNotFound | 命名存储实例不存在 |
OBJECTSTORAGE_VALIDATION_001 | RangeNotSatisfiable | 读取范围的起点不在对象内 |
OBJECTSTORAGE_VALIDATION_002 | PartsInvalid | 分片清单不合法 |
OBJECTSTORAGE_VALIDATION_003 | StoreDefinitionInvalid | 命名实例的定义不合法 |
OBJECTSTORAGE_VALIDATION_004 | StoreSelectionInvalid | 缺省存储的能力选择不一致:被要求的分片上传或预签名与对象存储不在同一存储(装配时抛出,启动失败) |
OBJECTSTORAGE_VALIDATION_005 | PageSizeInvalid | 列举的页大小越界(HTTP 400,不在构造时抛出) |
OBJECTSTORAGE_DENIED_001 | SignatureInvalid | 预签名地址无效或已过期 |
OBJECTSTORAGE_DENIED_002 | ProbeDenied | 条件写入探测被存储拒绝(403):启动或建实例时即失败;启动时连不上、之后才被拒的,健康项 Failed、后台照常重试 |
OBJECTSTORAGE_CONFLICT_001 | BlobExists | 对象已存在且不允许覆盖 |
OBJECTSTORAGE_UNAVAILABLE_001 | StoreUnavailable | 对象存储不可达或 IO 失败,reason 只写异常类型的全名,完整异常进日志(ADR-0115) |
OBJECTSTORAGE_UNAVAILABLE_002 | QuotaExceeded | 配额或单对象大小超限 |
OBJECTSTORAGE_UNAVAILABLE_003 | ConditionalWriteUnavailable | 不覆盖写入在本存储上无法保证:服务不认条件写入(启动时探明即启动失败或不建命名实例,之后探明的不覆盖写入一律返回它),或关闭了条件写入 |
OBJECTSTORAGE_UNAVAILABLE_004 | ConditionalWriteUnconfirmed | 不覆盖写入暂不可用:存储是否认条件写入尚未确认(启动时连不上,或之后的探测被拒绝、缺桶;HTTP 503,探测通过后自动恢复) |
相关
- 对象存储
- ObjectStorage.FileSystem
- ObjectStorage.S3
- Hosting.AspNetCore
- Conformance 一致性套件
- 规范:RFC-0012《对象存储》、ADR-0049、ADR-0050
