Sparkdo.Manifest
2.0.0-preview.2
dotnet add package Sparkdo.Manifest --version 2.0.0-preview.2
NuGet\Install-Package Sparkdo.Manifest -Version 2.0.0-preview.2
<PackageReference Include="Sparkdo.Manifest" Version="2.0.0-preview.2" />
<PackageVersion Include="Sparkdo.Manifest" Version="2.0.0-preview.2" />
<PackageReference Include="Sparkdo.Manifest" />
paket add Sparkdo.Manifest --version 2.0.0-preview.2
#r "nuget: Sparkdo.Manifest, 2.0.0-preview.2"
#:package Sparkdo.Manifest@2.0.0-preview.2
#addin nuget:?package=Sparkdo.Manifest&version=2.0.0-preview.2&prerelease
#tool nuget:?package=Sparkdo.Manifest&version=2.0.0-preview.2&prerelease
Sparkdo.Manifest
Sparkdo.Manifest 是 Sparkdo 应用与生态包的强类型声明层。它让应用或包通过普通 C# API 描述能力开关、依赖注入、选项、生命周期、调用管道、可观测性、资源和扩展协议,再由编译器把这些声明转换为确定、可合并、可校验的 Manifest 事实。
本文既面向使用 Sparkdo 的应用开发者,也面向编写 Sparkdo 生态包和第三方库集成的维护者。
阅读路线
| 目标 | 建议阅读 |
|---|---|
| 在应用中编写 Manifest | “应用快速开始”及各功能章节 |
| 编写可复用生态包 | “生态包快速开始”“能力覆盖与引用关系” |
| 集成 MassTransit 等第三方库 | “第三方服务集成协议” |
| 排查编译或合并失败 | “编译期约束”“产物与排障” |
| 查找某个 API 的用途 | “公共 API 速查” |
模块定位
负责什么
Sparkdo.Manifest 负责提供以下声明契约:
IManifest和IManifestDefinition:Manifest 根入口。ICapabilityManifestBuilder:能力覆盖与禁用;包依赖默认由项目引用图自动推导。IServiceManifestBuilder:服务注册、替换、移除、开放泛型和装饰。IOptionsManifestBuilder<TOptions>:选项声明、默认值、绑定、校验和应用覆盖。IPublishManifestBuilder:发布可见性配置。ILifecycleManifestBuilder:启动、就绪、关闭任务及其顺序。IConventionManifestBuilder:基于强类型选择器的批量服务注册与属性注入。IInvocationManifestBuilder:调用中间件、适用条件和顺序。IInstrumentationManifestBuilder:观测源、包含、排除和元数据抑制。IResourceManifestBuilder:资源来源、挂载、暴露、禁用和能力要求。IExtensionManifestBuilder:底层扩展点和命名空间委托。ManifestServiceIntegrationAttribute:第三方服务集成入口契约。ManifestIntegrationTerminalAttribute:第三方集成终结方法契约。Types、Methods、Properties、Fields、Events、Constructors:强类型选择器工厂。
不负责什么
本包只提供声明 API,不负责:
- 执行
Define中的普通运行时代码。 - 访问 Roslyn 符号或解析选择器。
- 生成 Manifest 清单片段或应用蓝图。
- 合并包默认配置与应用显式配置。
- 创建
IServiceProvider、启动宿主或执行生命周期任务。 - 读取密码、令牌、连接字符串等运行期机密。
这些职责分别由 Sparkdo.Compiler、Sparkdo.Compilation、Sparkdo.Build、Sparkdo.Runtime 和 Sparkdo.Hosting 承担。
flowchart LR
A["应用或生态包中的 IManifest"] --> B["Sparkdo.Compiler 静态编译"]
B --> C["包清单片段或应用蓝图"]
C --> D["Sparkdo.Compilation 合并与解析"]
D --> E["生成的应用引导代码"]
E --> F["Sparkdo.Hosting 与 Sparkdo.Runtime 执行"]
安装与集成
应用快速开始
应用项目应引用 Sparkdo.App,它会传递引入 Manifest API、编译器、构建目标和宿主能力,并自动把项目角色设置为 Application。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Sparkdo.App" Version="你的版本" />
</ItemGroup>
</Project>
在应用程序集中定义一个具体 IManifest:
using Sparkdo.Manifest;
namespace Contoso.Orders;
internal sealed class AppManifest : IManifest
{
public void Define(IManifestDefinition manifest)
{
manifest.ConfigureServices(static services =>
{
services.Add<IOrderService, OrderService>(SparkdoServiceLifetime.Scoped);
});
manifest.ConfigureOptions<OrderOptions>(static options =>
{
options.Declare();
options.Bind("Orders");
options.Validate<OrderOptionsValidator>();
});
manifest.ConfigureLifecycle(static lifecycle =>
{
lifecycle.OnStartup<DatabaseMigrationTask>("database-migration");
});
}
}
使用生成的应用入口启动:
using Sparkdo;
using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
shutdown.Cancel();
};
var builder = SparkdoApplication.CreateBuilder(args);
await using var app = await builder.BuildAsync(shutdown.Token);
await app.RunAsync(shutdown.Token);
Sparkdo.App 默认使用程序集名称作为 SparkdoApplicationId。只有需要稳定的跨程序集应用标识时,才在项目文件中显式设置:
<PropertyGroup>
<SparkdoApplicationId>contoso.orders</SparkdoApplicationId>
</PropertyGroup>
生态包快速开始
生态包应引用 Sparkdo.Package。它会把项目角色设置为 Package,默认使用 PackageId 作为 SparkdoPackageId,并在打包时把 Manifest 清单片段放入 NuGet 包的 buildTransitive 目录。
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<PackageId>Contoso.Orders</PackageId>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Sparkdo.Package"
Version="你的版本"
PrivateAssets="all" />
</ItemGroup>
</Project>
包内 Manifest 声明默认能力和默认服务:
using Sparkdo.Manifest;
namespace Contoso.Orders.Manifest;
internal sealed class OrdersPackageManifest : IManifest
{
public void Define(IManifestDefinition manifest)
{
manifest.ConfigureServices(static services =>
{
services.TryAdd<IOrderSerializer, DefaultOrderSerializer>(
SparkdoServiceLifetime.Singleton);
});
}
}
生态包对其他包的依赖通过项目文件中的 PackageReference 或 ProjectReference 表达。Sparkdo 构建链会根据引用关系收集对应清单片段并推导依赖,包 Manifest 不需要重复调用 DependsOn。
打包时运行:
dotnet pack -c Release -p:PackageVersion=1.0.0
应用只需引用该 NuGet 包。包内清单片段会自动进入应用编译,应用可以禁用能力、替换默认服务或追加显式配置。
何时直接引用 Sparkdo.Manifest
通常不应只引用 Sparkdo.Manifest。单独引用它只能获得声明类型,不会自动获得编译器、构建目标、清单片段打包或宿主启动能力。
- 可执行应用:引用
Sparkdo.App。 - 可复用生态包:引用
Sparkdo.Package。 - 仅编写抽象库或工具:确认不需要生成与运行能力后,才直接引用
Sparkdo.Manifest。
Manifest 编写模型
根入口
推荐直接实现 IManifest:
public sealed class FeatureManifest : IManifest
{
public void Define(IManifestDefinition manifest)
{
}
}
Manifest 抽象基类提供空的虚拟 Define,适合确实需要基类复用的场景:
public sealed class FeatureManifest : Manifest
{
public override void Define(IManifestDefinition manifest)
{
manifest.ConfigurePublish(static publish =>
publish.VisibilityProfile("minimal"));
}
}
优先直接实现 IManifest,这样最容易看清项目唯一的声明入口。
编译期约束
Manifest 是编译期领域专用语言,不是运行期配置回调。编写时必须遵守:
- 每个项目只能有一个可发现的具体
IManifest。 Define必须是同步、无返回值、签名精确的实例方法。- 不要给
Define增加重载,也不要改为async。 - 配置调用必须直接位于真实的
IManifestDefinition路径下。 - 不要用普通帮助方法包装 Manifest 终结调用。
- 不要在
Define中依赖运行期分支、循环、服务解析或外部状态。 - 不捕获外部变量时,显式使用
staticLambda。 - 参数应使用编译器可证明的常量、类型、枚举或受支持的选择器表达式。
- 包清单片段中导出的类型必须对应用程序集可访问。
- 任何不能静态理解的调用都会产生闭锁诊断,而不是在运行时静默忽略。
推荐:
private const string CapabilityId = "Contoso.Orders";
public void Define(IManifestDefinition manifest)
{
manifest.ConfigureCapabilities(static capabilities =>
capabilities.Disable(CapabilityId));
}
避免:
public void Define(IManifestDefinition manifest)
{
var capabilityId = ReadCapabilityIdAtRuntime();
manifest.ConfigureCapabilities(capabilities =>
capabilities.Disable(capabilityId));
}
能力覆盖与引用关系
引用依赖自动推导
生态包之间的依赖以项目引用图为唯一常规来源:
- NuGet 依赖使用
PackageReference表达。 - 同一仓库或解决方案中的源码依赖使用
ProjectReference表达。 - Sparkdo 构建链从还原图和项目引用中收集依赖包的清单片段,应用合并时据此形成能力关系。
- 包 Manifest 不应把项目引用再次写成
DependsOn,否则会形成重复且可能漂移的依赖来源。
例如,生态包需要 Sparkdo.Json 时,只需在项目文件中引用它:
<ItemGroup>
<PackageReference Include="Sparkdo.Json" Version="你的版本" />
</ItemGroup>
ConfigureCapabilities 的常规用途是让应用禁用引用图中已经存在的默认能力:
manifest.ConfigureCapabilities(static capabilities =>
{
capabilities.Disable("Contoso.LegacyFeature");
});
禁用能力会同时过滤该能力拥有的默认服务、选项、生命周期、资源和扩展事实。如果其他有效能力仍要求该能力,解析阶段会报告冲突,而不会静默生成不完整的应用。
ICapabilityManifestBuilder.DependsOn 仍是公开的底层显式依赖入口,只应在引用图无法表达且扩展协议明确要求时使用。普通应用和生态包不应调用它,也不应把它作为补充 PackageReference 或 ProjectReference 的手段。
合并优先级
Manifest 合并遵循以下基本方向:
- 包清单片段提供默认事实。
- 应用 Manifest 提供显式事实。
- 能力禁用先过滤失效的包默认事实。
- 各领域解析器再处理替换、移除、去重、冲突和顺序。
不要依赖项目引用或 NuGet 恢复的偶然枚举顺序。需要顺序时,应使用生命周期依赖、调用中间件顺序或第三方集成协议中的稳定键显式表达。
服务配置
ConfigureServices 描述服务图,不会立即修改 IServiceCollection。
| 方法 | 用途 |
|---|---|
Add<TService, TImplementation>(lifetime) |
添加封闭泛型或普通服务 |
TryAdd<TService, TImplementation>(lifetime) |
仅在尚无有效注册时添加默认实现 |
Replace<TService, TImplementation>(lifetime) |
替换某个服务契约的有效实现 |
Remove<TService>() |
移除某个服务契约 |
AddOpenGeneric(serviceType, implementationType, lifetime) |
添加开放泛型服务 |
TryAddOpenGeneric(serviceType, implementationType, lifetime) |
尝试添加开放泛型默认服务 |
Decorate<TService, TDecorator>(lifetime) |
为服务添加装饰器,可选覆盖装饰器生命周期 |
支持的生命周期为 Singleton、Scoped 和 Transient。
manifest.ConfigureServices(static services =>
{
services.Add<IOrderService, OrderService>(SparkdoServiceLifetime.Scoped);
services.TryAdd<IClock, SystemClock>(SparkdoServiceLifetime.Singleton);
services.Replace<IOrderSerializer, JsonOrderSerializer>(
SparkdoServiceLifetime.Singleton);
services.AddOpenGeneric(
typeof(IRepository<>),
typeof(EfRepository<>),
SparkdoServiceLifetime.Scoped);
services.Decorate<IOrderService, AuditedOrderService>();
});
应用需要撤销包默认注册时:
manifest.ConfigureServices(static services =>
{
services.Remove<ILegacyOrderService>();
});
Manifest 服务配置只接受类型和生命周期事实,不支持运行期实例、任意工厂委托或在声明阶段调用 BuildServiceProvider()。需要第三方构建器配置时,使用后文的服务集成协议。
选项配置
ConfigureOptions<TOptions> 按“选项类型 + 可选名称”管理选项事实。
| 方法 | 当前用途 |
|---|---|
Declare(name) |
显式声明默认或命名选项 |
Default(object, name, sensitive, publicDefault) |
写入可序列化的编译期默认值 |
Bind(configurationPath, name) |
绑定配置节 |
Validate<TValidator>(name) |
添加选项校验器 |
RemoveDefault(name) |
移除包提供的默认值 |
DisableBinding(name) |
禁用包提供的配置绑定 |
Default(Action<TOptions>, name) |
当前编译器不能序列化任意委托体,不应在 Manifest DSL 中使用 |
Configure(Action<TOptions>, name) |
当前编译器不能把运行期配置委托降级为 Manifest 事实,不应在 Manifest DSL 中使用 |
默认选项:
manifest.ConfigureOptions<OrderOptions>(static options =>
{
options.Declare();
options.Default(
new OrderOptions
{
BatchSize = 100,
Features = new[] { "audit", "metrics" }
},
sensitive: false,
publicDefault: true);
options.Bind("Orders");
options.Validate<OrderOptionsValidator>();
});
命名选项:
manifest.ConfigureOptions<OrderOptions>(static options =>
{
options.Declare("background");
options.Default(
new OrderOptions { BatchSize = 20 },
name: "background",
sensitive: false,
publicDefault: true);
options.Bind("Orders:Background", "background");
options.Validate<OrderOptionsValidator>("background");
});
应用撤销包默认值或绑定:
manifest.ConfigureOptions<OrderOptions>(static options =>
{
options.RemoveDefault();
options.DisableBinding();
});
sensitive: true 只表示默认值应按敏感数据处理,不代表可以把真实密码、令牌或连接字符串写进 Manifest。机密仍应来自运行期配置提供程序或机密存储。
如果第三方库只提供 Action<TOptions> 配置入口,应把该委托配置放在运行期引导或服务集成的 Apply 阶段,而不是放进 Manifest 声明。
发布可见性
ConfigurePublish 用于在 Manifest 中选择更窄的发布可见性配置。
manifest.ConfigurePublish(static publish =>
{
publish.VisibilityProfile("minimal");
});
Manifest 只能在项目或发布配置允许的范围内收窄可见性,不能用代码扩大构建侧已经限定的可见性。具体配置名称由当前项目的发布配置约定决定。
生命周期
ConfigureLifecycle 声明三个阶段的任务:
OnStartup<TTask>:应用构建完成后的启动任务。OnReady<TTask>:应用进入就绪阶段前后的任务。OnShutdown<TTask>:应用关闭阶段的任务。
每个任务可设置:
id:同一阶段内稳定的逻辑标识。timeoutMilliseconds:非负超时时间。cancellationPolicy:cooperative或none。failurePolicy:fail-fast或continue-and-report。
默认取消策略为 cooperative。启动与就绪阶段默认采用 fail-fast,关闭阶段默认采用 continue-and-report。
manifest.ConfigureLifecycle(static lifecycle =>
{
lifecycle.OnStartup<DatabaseMigrationTask>(
id: "database-migration",
timeoutMilliseconds: 30_000,
cancellationPolicy: "cooperative",
failurePolicy: "fail-fast");
lifecycle.OnStartup<CacheWarmupTask>(
id: "cache-warmup",
timeoutMilliseconds: 10_000);
lifecycle.Before<DatabaseMigrationTask>("cache-warmup", stage: "startup");
lifecycle.OnReady<HealthPublicationTask>("health-publication");
lifecycle.OnShutdown<DrainMessagesTask>(
id: "drain-messages",
failurePolicy: "continue-and-report");
});
顺序方法:
| 方法 | 用途 |
|---|---|
Before<TTask>(taskId, stage) |
要求 TTask 对应任务在指定任务之前执行 |
After<TTask>(taskId, stage) |
要求 TTask 对应任务在指定任务之后执行 |
Disable<TTask>(id, stage) |
禁用某个包默认生命周期任务 |
依赖目标不存在、顺序形成环或策略值无效时,解析阶段会闭锁失败。
强类型选择器
选择器描述“哪些类型或成员适用”,由编译器转换为选择器文档,再由解析器匹配目标事实。选择器对象本身不执行反射扫描。
类型选择器
| API | 匹配范围 |
|---|---|
Types.All() |
全部可见类型 |
Types.Implementations<T>() |
实现 T 的类型 |
Types.AssignableTo<T>() |
可赋值给 T 的类型 |
Types.HasAttribute<TAttribute>() |
带指定特性的类型 |
方法选择器
| API | 匹配范围 |
|---|---|
Methods.PublicAsync() |
公共异步方法 |
Methods.Async() |
异步方法 |
Methods.AsyncStream() |
异步流方法 |
Methods.HasAttribute<TAttribute>() |
带指定特性的方法 |
Methods.DeclaringType(selector) |
方法源码声明类型满足给定类型选择器的方法;继承 slot 不改写为派生接口 |
Methods.ContractType(selector) |
方法所在的有效已注册服务契约满足给定类型选择器;继承 slot 保留实际 serviceType |
Methods.ImplementationType(selector) |
方法对应的有效已注册实现满足给定类型选择器;同时检查实现基类 |
ContractType 与 ImplementationType 面向解析后的有效服务图,而不是全部可见类型。若同一基础接口方法同时通过两个兄弟服务契约暴露,契约类型选择只作用于实际命中的服务契约,不会把中间件复制到另一个兄弟契约。方法自身或基础契约上的元数据仍会展开到所有实际暴露该 slot 的已注册服务契约。
其他成员选择器
Properties.HasAttribute<TAttribute>()Fields.HasAttribute<TAttribute>()Events.HasAttribute<TAttribute>()Constructors.HasAttribute<TAttribute>()
组合
所有 Selector 都可以使用 And、Or 和 Not 组合:
var handlers = Types
.Implementations<IOrderHandler>()
.And(Types.HasAttribute<AutoRegisterAttribute>());
var invocableMethods = Methods
.PublicAsync()
.Or(Methods.AsyncStream())
.And(Methods.HasAttribute<RemoteCallableAttribute>().Not());
选择器表达式应直接写在 Manifest 配置中。不要把选择结果当成运行期集合使用。
约定配置
ConfigureConventions 对选择器匹配到的目标批量生成服务或属性注入事实。
批量服务注册
manifest.ConfigureConventions(static conventions =>
{
conventions.AddServices(
Types.Implementations<IOrderHandler>(),
SparkdoServiceLifetime.Scoped);
});
属性注入
manifest.ConfigureConventions(static conventions =>
{
conventions.InjectProperties(
Properties.HasAttribute<InjectClockAttribute>(),
typeof(IClock),
required: true);
});
字符串选择器重载仍为兼容性保留,但已经标记为过时。新代码必须使用 ISelector 强类型表达式,以获得符号校验、确定性和更好的演进能力。
调用中间件
ConfigureInvocation 把实现 IInvocationMiddleware 的中间件应用到符合选择器条件的方法。
manifest.ConfigureInvocation(static invocation =>
{
invocation.UseMiddleware<ValidationMiddleware>(
pipelineId: "application",
useWhen: Methods.HasAttribute<ValidateAttribute>()
.Or(Methods.ContractType(Types.HasAttribute<ValidateAttribute>()))
.Or(Methods.ImplementationType(Types.HasAttribute<ValidateAttribute>())),
disableWhen: Methods.HasAttribute<SkipValidationAttribute>(),
configure: static order =>
{
order.RequireInterceptableTargets();
order.Order(100);
order.Before<MetricsMiddleware>();
order.After<AuthorizationMiddleware>();
});
});
参数含义:
| 参数 | 用途 |
|---|---|
pipelineId |
稳定的调用管道标识 |
useWhen |
启用中间件的方法选择器 |
disableWhen |
可选的排除选择器 |
configure |
当前中间件的严格拦截要求、基础顺序和相对于其他中间件的前后顺序 |
Before<TMiddleware> 和 After<TMiddleware> 只声明顺序边,不创建中间件实例。中间件必须由有效服务图提供,顺序不能形成环。
Order(int) 只声明同一管道内的稳定基础顺序;未声明时按 0 处理。Before<TMiddleware> 和 After<TMiddleware> 是硬边,会覆盖基础顺序。该配置仅由 Sparkdo.Compiler 在编译期处理,运行时不会执行 Manifest 扩展方法。
RequireInterceptableTargets() 适合授权、权限等不能接受静默跳过的安全中间件。启用后,选择器命中同步方法、以 concrete service 注册而无法生成 interface decorator 的方法,或无法唯一映射到服务契约的方法时,解析阶段会闭锁失败。未启用时,Invocation v1 仍会按兼容行为跳过不支持的同步返回形态。
可观测性声明
ConfigureInstrumentation 维护观测源的有效集合和元数据抑制规则。
manifest.ConfigureInstrumentation(static instrumentation =>
{
instrumentation.Source("Contoso.Orders");
instrumentation.Include("Contoso.Orders");
instrumentation.Exclude("Contoso.Orders.Legacy");
instrumentation.Suppress("Contoso.Orders", "user.id");
});
| 方法 | 用途 |
|---|---|
Source(name) |
声明观测源 |
Include(name) |
把已声明源纳入有效集合 |
Exclude(name) |
从有效集合排除源 |
Suppress(name, metadataKey) |
抑制指定源的某个元数据键 |
Include、Exclude 和 Suppress 引用的源应先通过 Source 声明,否则解析阶段会报告无效源。
资源声明
ConfigureResources 描述资源来源和虚拟资源视图。
支持的来源类型:
| 枚举值 | 用途 |
|---|---|
Embedded |
程序集嵌入资源 |
PackageAsset |
NuGet 包携带的资源 |
Generated |
编译或生成阶段产生的资源 |
Physical |
物理文件来源 |
完整示例:
manifest.ConfigureResources(static resources =>
{
resources.Source(
id: "assets",
kind: SparkdoResourceSourceKind.PackageAsset,
location: "content/site.css",
contentHash: "已知内容哈希");
resources.Use("Sparkdo.VirtualFileSystem", static use =>
{
use.Sources(
SparkdoResourceSourceKind.PackageAsset,
SparkdoResourceSourceKind.Generated);
use.Require(SparkdoResourceRequirement.PackageAssets);
use.Require(SparkdoResourceRequirement.GeneratedResources);
});
resources.Mount("assets", "static");
resources.Expose("static", "site");
});
| 方法 | 用途 |
|---|---|
Source |
声明资源来源及可选内容哈希 |
Mount |
把来源挂载到虚拟路径 |
Expose |
用稳定名称暴露路径 |
Disable |
禁用某个资源操作标识 |
Use |
声明消费资源的能力、允许来源类型和必需产物 |
应用可以撤销包默认暴露:
manifest.ConfigureResources(static resources =>
{
resources.Disable("expose:site");
});
资源位置不是机密存储。远程凭据、签名令牌等运行期信息不得写入 location 或扩展负载。
底层扩展点
ConfigureExtensions 用于无法通过已有强类型领域表达的低层协议扩展。普通应用通常不需要直接使用。
manifest.ConfigureExtensions(static extensions =>
{
extensions.Point(
extensionId: "Contoso.Orders",
name: "application-bootstrap-contribution",
schemaMajor: 1,
schemaMinor: 0,
requiredFeature: "sparkdo.extension.v1",
payload: "{\"contributionTypeIdentity\":\"Contoso.Orders.Generated.OrdersBootstrapContribution\"}",
namespaceName: "Contoso.Orders");
extensions.DelegateNamespace(
ownerExtensionId: "Contoso.Orders",
namespaceName: "Contoso.Orders.Transport",
delegatedExtensionId: "Contoso.Orders.Transport.RabbitMq");
});
扩展负载必须具备稳定架构版本、确定性序列化和显式所需特性。不要在负载中放入任意程序集限定类型名、运行期回调或机密。服务类第三方集成优先使用下一节的强类型服务集成协议。
第三方服务集成协议
许多第三方库使用 AddXxx(Action<TBuilder>) 配置模式。直接把这类运行期委托塞进 Manifest 会失去可合并性、确定性和 AOT 安全。服务集成协议把它拆成四个部分:
UseXxx(Action<TBuilder>):应用和包使用的强类型入口。TBuilder:聚合多次调用的领域构建器。- 带
ManifestIntegrationTerminalAttribute的终结方法:可序列化的配置步骤。 Apply(IServiceCollection, TBuilder):全部步骤完成后执行一次的运行期引导。
定义集成构建器
以下示例演示如何让 MassTransitManifestBuilder 接受多次追加配置。示例聚焦 Manifest 聚合边界,实际包可在 Apply 中把最终计划映射到对应版本的 MassTransit API。
using Sparkdo.Manifest;
namespace Contoso.MassTransit;
public sealed class MassTransitManifestBuilder
{
private readonly List<Type> _consumerTypes = [];
private readonly Dictionary<string, string> _transportSections =
new(StringComparer.Ordinal);
[ManifestIntegrationTerminal("consumer.add", schemaMajor: 1)]
public void AddConsumer<TConsumer>()
{
_consumerTypes.Add(typeof(TConsumer));
}
[ManifestIntegrationTerminal("transport.use", schemaMajor: 1)]
public void UseTransport(string profileKey, string configurationSection)
{
_transportSections[profileKey] = configurationSection;
}
internal MassTransitIntegrationPlan Build()
{
return new MassTransitIntegrationPlan(
_consumerTypes.Distinct().ToArray(),
new Dictionary<string, string>(_transportSections, StringComparer.Ordinal));
}
}
public sealed record MassTransitIntegrationPlan(
IReadOnlyList<Type> ConsumerTypes,
IReadOnlyDictionary<string, string> TransportSections);
这个构建器明确规定了领域合并语义:
AddConsumer<TConsumer>采用累加并去重。UseTransport以profileKey为稳定键,后一次配置覆盖同键值。- 多总线或多配置档由稳定键表达,而不是靠多次
UseMassTransit隐式创建多个构建器。
定义入口和单次应用
using Microsoft.Extensions.DependencyInjection;
using Sparkdo.Manifest;
namespace Contoso.MassTransit;
public static class MassTransitManifestServiceExtensions
{
[ManifestServiceIntegration(
"Contoso.MassTransit",
schemaMajor: 1,
bootstrapMethod: nameof(Apply),
schemaMinor: 0)]
public static void UseMassTransit(
this IServiceManifestBuilder services,
Action<MassTransitManifestBuilder> configure)
{
throw new InvalidOperationException(
"UseMassTransit 只能由 Sparkdo.Compiler 在 Manifest 中处理。");
}
public static void Apply(
IServiceCollection services,
MassTransitManifestBuilder builder)
{
ArgumentNullException.ThrowIfNull(services);
ArgumentNullException.ThrowIfNull(builder);
var plan = builder.Build();
services.AddSingleton(plan);
// 在这里把 plan 映射到实际的 services.AddMassTransit(...) 调用。
// 连接字符串和凭据应由运行期配置读取,不能保存在 plan 或 Manifest 中。
}
}
入口方法的方法体不会作为普通运行时代码执行;编译器识别它并把内部终结调用写入清单片段。主动抛出异常可以防止使用者在 Manifest 之外误调用。
包默认配置
using Sparkdo.Manifest;
namespace Contoso.MassTransit.Manifest;
internal sealed class MassTransitPackageManifest : IManifest
{
public void Define(IManifestDefinition manifest)
{
manifest.ConfigureServices(static services =>
{
services.UseMassTransit(static massTransit =>
massTransit.UseTransport("default", "Messaging:Default"));
});
}
}
应用多重配置
using Contoso.MassTransit;
using Sparkdo.Manifest;
namespace Contoso.Orders;
internal sealed class AppManifest : IManifest
{
public void Define(IManifestDefinition manifest)
{
manifest.ConfigureServices(static services =>
{
services.UseMassTransit(static massTransit =>
massTransit.AddConsumer<SubmitOrderConsumer>());
services.UseMassTransit(static massTransit =>
{
massTransit.AddConsumer<CancelOrderConsumer>();
massTransit.UseTransport("default", "Messaging:Orders");
massTransit.UseTransport("audit", "Messaging:Audit");
});
});
}
}
最终行为:
- 包默认调用先进入聚合构建器。
- 应用显式调用按源码顺序追加。
- 重复使用方由构建器去重。
- 应用的
default传输配置覆盖包默认值。 - 只创建一个
MassTransitManifestBuilder。 - 全部终结方法执行后,只调用一次
Apply。
集成契约要求
ManifestServiceIntegrationAttribute 标注的入口必须满足:
- 位于有效公开的静态类中。
- 是公共静态扩展方法。
- 返回
void。 - 第一个参数是
IServiceManifestBuilder。 - 第二个参数是
Action<TBuilder>。 - 不包含方法泛型参数。
TBuilder是有效公开、非抽象、非泛型类型。TBuilder有公共无参构造函数。bootstrapMethod指向同一静态类中的公共静态Apply(IServiceCollection, TBuilder)。
终结方法必须满足:
- 是构建器上的公共实例普通方法。
- 返回
void。 - 不使用
ref、out、in或params参数。 - 有唯一、稳定、区分大小写的
terminalId。 - 参数只能是编译器可表达的常量、
typeof(...)、枚举成员或受支持的泛型类型参数。
包清单片段引用的泛型类型、typeof(...) 类型和枚举必须对应用程序集公开。应用自己的 Manifest 可以引用本程序集内部类型。
版本演进
integrationId和terminalId是协议键,不是显示名称,发布后不要修改。- 兼容的方法重命名不要求修改
terminalId。 - 终结方法主版本必须匹配。
- 新契约可以读取较旧的次版本调用。
- 删除终结方法、改变参数含义、改变泛型数量或做不兼容签名变更时,必须提升主版本。
- 新增可选能力时优先新增终结方法或提升次版本。
领域合并规则
框架只保证顺序、单构建器和单次 Apply,不会猜测第三方库的业务合并语义。每个构建器都应明确:
- 集合配置是否累加、去重或报重复错误。
- 单值配置是首次获胜、末次覆盖还是冲突失败。
- 多实例配置使用哪个稳定键。
- 包默认值与应用显式值如何合并。
- 部分配置无效时是整体失败还是按配置档隔离。
Apply 中不要调用 BuildServiceProvider(),不要进行程序集扫描,也不要把反射作为默认注册路径。
产物与排障
包项目产物
包项目编译和打包时会生成或携带:
SparkdoFragment.g.cs:嵌入程序集的清单片段载体。obj/.../sparkdo/fragments/:中间清单片段。- NuGet 包中的
buildTransitive/sparkdo/fragments/<TFM>/sparkdo.manifest-fragment.v1.json。 - NuGet 包中的传递构建目标,用于把片段交给应用编译。
应用项目产物
应用项目会生成:
Blueprint.g.cs:应用有效蓝图载体。ManifestRuntime.g.cs:Manifest 运行时配置入口。SparkdoApplication.g.cs:应用启动入口。SparkdoHostBuilderExtensions.g.cs:通用宿主集成入口。obj/.../sparkdo/blueprint/runtime-blueprint.v1.json:可检查的运行时蓝图。obj/.../sparkdo/reports/sparkdo-manifest-report.v1.json:Manifest 报告。
发布后,运行时蓝图和报告会复制到发布目录的 sparkdo 子目录。
常见诊断
| 诊断 | 常见原因 | 处理方式 |
|---|---|---|
SPARKDO1001 |
IManifest 形状无效 |
检查具体类型、Define 签名、继承和重载 |
SPARKDO1002 |
存在多个具体 IManifest |
每个项目保留一个 Manifest 根入口 |
SPARKDO1003 |
使用了编译器不支持的 Manifest 终结调用或参数形态 | 改为受支持的直接调用和编译期参数 |
SPARKDO1004 |
第三方服务集成契约无效 | 检查公开性、构建器、终结标识和 Apply 签名 |
SPARKDO2001 |
缺少 Manifest 角色 | 应用引用 Sparkdo.App,包引用 Sparkdo.Package |
SPARKDO2002 |
Manifest 角色冲突 | 不要同时引入互斥元包或手工设置冲突角色 |
SPARKDO5001 |
生成源码或哈希规范化失败 | 检查不可序列化输入和构建环境差异 |
排障顺序
- 先检查项目引用的是
Sparkdo.App还是Sparkdo.Package。 - 确认项目只有一个具体
IManifest。 - 检查
Define中是否存在帮助方法、运行期变量、分支或不受支持的 Lambda。 - 检查包导出类型是否有效公开。
- 查看
sparkdo-manifest-report.v1.json中的来源、操作和诊断。 - 检查
runtime-blueprint.v1.json是否包含预期有效事实。 - 第三方集成再检查
integrationId、terminalId和契约版本。
安全、AOT 与可维护性
- Manifest 只保存声明事实,不保存密码、令牌、连接字符串或私钥。
- 包扩展类型应有效公开,避免生成代码无法访问。
- 使用强类型泛型和
typeof(...),不要保存任意程序集限定字符串并在运行时反射解析。 - 不要在
Apply或扩展引导中创建临时IServiceProvider。 - 不要通过程序集扫描寻找服务、终结方法或配置类型。
- 稳定协议使用
integrationId、terminalId、能力标识和资源标识,不使用方法显示名称代替协议键。 - 默认值和扩展负载应采用确定性序列化,避免时间、随机数和机器路径进入产物。
- 修改公共 API 或线协议时,按只扩展原则新增重载、终结方法或次版本;不兼容变更提升主版本。
测试建议
仅修改本包 API 或文档时,至少运行:
dotnet test src/polaris/test/Sparkdo.Manifest.Tests/Sparkdo.Manifest.Tests.csproj
git diff --check
修改编译器识别或清单片段协议时,再运行:
dotnet test src/polaris/test/Sparkdo.Compiler.Tests/Sparkdo.Compiler.Tests.csproj
dotnet test src/polaris/test/Sparkdo.Compilation.Tests/Sparkdo.Compilation.Tests.csproj
编写生态包时还应验证:
dotnet pack能生成包含清单片段的真实 NuGet 包。- 一个干净使用方只引用生成的
.nupkg也能构建和运行。 - 包默认配置和应用显式配置的合并结果正确。
- 禁用、替换、移除和冲突路径都有测试。
- 裁剪分析器和原生 AOT 发布没有新增警告。
公共 API 速查
根块
| 根方法 | 构建器 | 领域 |
|---|---|---|
ConfigureCapabilities |
ICapabilityManifestBuilder |
能力覆盖与禁用;引用依赖自动推导 |
ConfigureServices |
IServiceManifestBuilder |
服务图与第三方服务集成 |
ConfigureOptions<TOptions> |
IOptionsManifestBuilder<TOptions> |
默认值、绑定和校验 |
ConfigurePublish |
IPublishManifestBuilder |
发布可见性 |
ConfigureLifecycle |
ILifecycleManifestBuilder |
生命周期任务与顺序 |
ConfigureConventions |
IConventionManifestBuilder |
选择器驱动的批量约定 |
ConfigureInvocation |
IInvocationManifestBuilder |
调用中间件 |
ConfigureInstrumentation |
IInstrumentationManifestBuilder |
可观测性 |
ConfigureResources |
IResourceManifestBuilder |
资源拓扑 |
ConfigureExtensions |
IExtensionManifestBuilder |
底层扩展协议 |
枚举
| 类型 | 值 |
|---|---|
SparkdoManifestRole |
Application、Package |
SparkdoServiceLifetime |
Singleton、Scoped、Transient |
SparkdoResourceSourceKind |
Embedded、PackageAsset、Generated、Physical |
SparkdoResourceRequirement |
PackageAssets、GeneratedResources |
相关模块
Sparkdo.App:应用元包、编译与宿主入口。Sparkdo.Package:生态包编写与清单片段打包。Sparkdo.Compiler:Manifest 静态识别和生成。Sparkdo.Compilation:合并、选择器求值和有效计划解析。Sparkdo.Hosting:依赖注入、生命周期和宿主执行。
| 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 was computed. 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 | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- Sparkdo.Abstractions (>= 2.0.0-preview.2)
- System.Threading.Tasks.Extensions (>= 4.6.3)
NuGet packages (50)
Showing the top 5 NuGet packages that depend on Sparkdo.Manifest:
| Package | Downloads |
|---|---|
|
Sparkdo.Localization
Sparkdo Localization 的 VFS JSON runtime、文化回退与 package manifest。 |
|
|
Sparkdo.Security
Sparkdo Security 的默认安全上下文运行时。 |
|
|
Sparkdo.Settings
Sparkdo Settings 的内存运行时、Provider 管线和设置目录。 |
|
|
Sparkdo.VirtualFileSystem
用于物化 Sparkdo 应用与包资产的虚拟文件系统运行时。 |
|
|
Sparkdo.VirtualFileSystem.Abstractions
Sparkdo 虚拟文件系统的 Manifest 声明与宿主读取契约。 |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0-preview.2 | 106 | 7/20/2026 |
| 2.0.0-preview.1 | 137 | 7/18/2026 |