XiHan Framework 处于预览阶段

查看获取方式
跳转到内容

XiHan.Framework.ObjectStorage.Abstractions ​

对象存储的抽象包:容器、键与元数据,IBlobStore(批量写入与删除、按键读取、范围读取、按前缀分页列举),可选的分片上传 IBlobMultipartStore 与预签名 IBlobUrlSigner,以及命名存储实例的注册表 BlobStores。业务代码只依赖本包,存储后端由 Provider 决定。

项内容
包 IDXiHan.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。用法讲解见对象存储。

安装与启用 ​

bash
dotnet add package XiHan.Framework.ObjectStorage.Abstractions --prerelease

当前是预览版,安装要带 --prerelease;从源码或本地包源引用见快速上手。Profiles.Full 已引用本包与文件系统实现。本包没有特性,装配任一存储包后即可注入上述接口。

配置 ​

键类型缺省说明
XiHan:ObjectStorage:Stores:<name>:Providerstring—命名实例的 Provider 名:filesystem 或 s3,不区分大小写
XiHan:ObjectStorage:Stores:<name>:<设置>string—该 Provider 的设置,键同其配置节;未知键报 OBJECTSTORAGE_VALIDATION_003
XiHan:ObjectStorage:StoreHealthProbeIntervalSeconds正数15命名实例健康探测的间隔,也是单次探测的超时
json
{
  "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.IBlobStorePutAsync(container, uploads)、GetAsync(container, key)、GetAsync(container, key, range)、HeadAsync、DeleteAsync(container, keys)、ListAsync(container, query)
Blobs.BlobStoreExtensionsPutAsync(container, key, stream 或 bytes, options)、DeleteAsync(container, key)、ExistsAsync、ReadAllBytesAsync
Blobs.BlobUpload一项写入:键、内容流、选项
Blobs.BlobPutOptionsContentType(缺省 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.IBlobMultipartStoreStartAsync、UploadPartAsync、ListPartsAsync、CompleteAsync、AbortAsync
Multipart.BlobMultipartUpload一次分片上传:容器、键、上传标识、是否覆盖;可持久化后续传
Multipart.BlobPart / BlobMultipart分片(编号、ETag、长度);常量 MaxPartNumber = 10000、MinPartBytes = 5 MiB
Signing.IBlobUrlSignerSignAsync(container, key, request) → BlobSignedUrl
Signing.BlobUrlRequestForDownload、ForUpload、ForPart;DefaultExpiresIn = 30 分钟,MaxExpiresIn = 7 天
Signing.BlobSignedUrl地址、方法、失效时刻与客户端必须发送的请求头
Signing.BlobContentDispositionAttachment(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.BlobStoreSelectionProvidesDefaultInstance(node):被选为对象存储返回 true(缺省存储);没被选为对象存储、却被要求提供分片上传或预签名(有依赖或经配置指定)时抛 OBJECTSTORAGE_VALIDATION_004;两者都只是兜底选出时返回 false,存储包按被顶替处理
Stores.BlobStoreServiceCollectionExtensionsAddBlobStoreProvider<TProvider>(configuration):存储包装配时登记 Provider,并确保注册表只登记一次
ObjectStorageCapabilities / ObjectStorageErrors / ObjectStorageMessages能力 ID、错误码与消息键常量,ObjectStorageMessages.Catalog 是本包的消息目录

使用示例 ​

写入、读取、列举与删除:

csharp
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);

断点续传与限时下载链接:

csharp
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:

csharp
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_001BlobNotFound对象不存在
OBJECTSTORAGE_NOTFOUND_002UploadNotFound分片上传不存在或已结束
OBJECTSTORAGE_NOTFOUND_003StoreNotFound命名存储实例不存在
OBJECTSTORAGE_VALIDATION_001RangeNotSatisfiable读取范围的起点不在对象内
OBJECTSTORAGE_VALIDATION_002PartsInvalid分片清单不合法
OBJECTSTORAGE_VALIDATION_003StoreDefinitionInvalid命名实例的定义不合法
OBJECTSTORAGE_VALIDATION_004StoreSelectionInvalid缺省存储的能力选择不一致:被要求的分片上传或预签名与对象存储不在同一存储(装配时抛出,启动失败)
OBJECTSTORAGE_VALIDATION_005PageSizeInvalid列举的页大小越界(HTTP 400,不在构造时抛出)
OBJECTSTORAGE_DENIED_001SignatureInvalid预签名地址无效或已过期
OBJECTSTORAGE_DENIED_002ProbeDenied条件写入探测被存储拒绝(403):启动或建实例时即失败;启动时连不上、之后才被拒的,健康项 Failed、后台照常重试
OBJECTSTORAGE_CONFLICT_001BlobExists对象已存在且不允许覆盖
OBJECTSTORAGE_UNAVAILABLE_001StoreUnavailable对象存储不可达或 IO 失败,reason 只写异常类型的全名,完整异常进日志(ADR-0115)
OBJECTSTORAGE_UNAVAILABLE_002QuotaExceeded配额或单对象大小超限
OBJECTSTORAGE_UNAVAILABLE_003ConditionalWriteUnavailable不覆盖写入在本存储上无法保证:服务不认条件写入(启动时探明即启动失败或不建命名实例,之后探明的不覆盖写入一律返回它),或关闭了条件写入
OBJECTSTORAGE_UNAVAILABLE_004ConditionalWriteUnconfirmed不覆盖写入暂不可用:存储是否认条件写入尚未确认(启动时连不上,或之后的探测被拒绝、缺桶;HTTP 503,探测通过后自动恢复)

相关 ​

Released under The MIT License