Sparkdo.Manifest 2.0.0-preview.2

This is a prerelease version of Sparkdo.Manifest.
dotnet add package Sparkdo.Manifest --version 2.0.0-preview.2
                    
NuGet\Install-Package Sparkdo.Manifest -Version 2.0.0-preview.2
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Sparkdo.Manifest" Version="2.0.0-preview.2" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Sparkdo.Manifest" Version="2.0.0-preview.2" />
                    
Directory.Packages.props
<PackageReference Include="Sparkdo.Manifest" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Sparkdo.Manifest --version 2.0.0-preview.2
                    
#r "nuget: Sparkdo.Manifest, 2.0.0-preview.2"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Sparkdo.Manifest@2.0.0-preview.2
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Sparkdo.Manifest&version=2.0.0-preview.2&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Sparkdo.Manifest&version=2.0.0-preview.2&prerelease
                    
Install as a Cake Tool

Sparkdo.Manifest

Sparkdo.Manifest 是 Sparkdo 应用与生态包的强类型声明层。它让应用或包通过普通 C# API 描述能力开关、依赖注入、选项、生命周期、调用管道、可观测性、资源和扩展协议,再由编译器把这些声明转换为确定、可合并、可校验的 Manifest 事实。

本文既面向使用 Sparkdo 的应用开发者,也面向编写 Sparkdo 生态包和第三方库集成的维护者。

阅读路线

目标 建议阅读
在应用中编写 Manifest “应用快速开始”及各功能章节
编写可复用生态包 “生态包快速开始”“能力覆盖与引用关系”
集成 MassTransit 等第三方库 “第三方服务集成协议”
排查编译或合并失败 “编译期约束”“产物与排障”
查找某个 API 的用途 “公共 API 速查”

模块定位

负责什么

Sparkdo.Manifest 负责提供以下声明契约:

  • IManifestIManifestDefinition:Manifest 根入口。
  • ICapabilityManifestBuilder:能力覆盖与禁用;包依赖默认由项目引用图自动推导。
  • IServiceManifestBuilder:服务注册、替换、移除、开放泛型和装饰。
  • IOptionsManifestBuilder<TOptions>:选项声明、默认值、绑定、校验和应用覆盖。
  • IPublishManifestBuilder:发布可见性配置。
  • ILifecycleManifestBuilder:启动、就绪、关闭任务及其顺序。
  • IConventionManifestBuilder:基于强类型选择器的批量服务注册与属性注入。
  • IInvocationManifestBuilder:调用中间件、适用条件和顺序。
  • IInstrumentationManifestBuilder:观测源、包含、排除和元数据抑制。
  • IResourceManifestBuilder:资源来源、挂载、暴露、禁用和能力要求。
  • IExtensionManifestBuilder:底层扩展点和命名空间委托。
  • ManifestServiceIntegrationAttribute:第三方服务集成入口契约。
  • ManifestIntegrationTerminalAttribute:第三方集成终结方法契约。
  • TypesMethodsPropertiesFieldsEventsConstructors:强类型选择器工厂。

不负责什么

本包只提供声明 API,不负责:

  • 执行 Define 中的普通运行时代码。
  • 访问 Roslyn 符号或解析选择器。
  • 生成 Manifest 清单片段或应用蓝图。
  • 合并包默认配置与应用显式配置。
  • 创建 IServiceProvider、启动宿主或执行生命周期任务。
  • 读取密码、令牌、连接字符串等运行期机密。

这些职责分别由 Sparkdo.CompilerSparkdo.CompilationSparkdo.BuildSparkdo.RuntimeSparkdo.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);
        });
    }
}

生态包对其他包的依赖通过项目文件中的 PackageReferenceProjectReference 表达。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 是编译期领域专用语言,不是运行期配置回调。编写时必须遵守:

  1. 每个项目只能有一个可发现的具体 IManifest
  2. Define 必须是同步、无返回值、签名精确的实例方法。
  3. 不要给 Define 增加重载,也不要改为 async
  4. 配置调用必须直接位于真实的 IManifestDefinition 路径下。
  5. 不要用普通帮助方法包装 Manifest 终结调用。
  6. 不要在 Define 中依赖运行期分支、循环、服务解析或外部状态。
  7. 不捕获外部变量时,显式使用 static Lambda。
  8. 参数应使用编译器可证明的常量、类型、枚举或受支持的选择器表达式。
  9. 包清单片段中导出的类型必须对应用程序集可访问。
  10. 任何不能静态理解的调用都会产生闭锁诊断,而不是在运行时静默忽略。

推荐:

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 仍是公开的底层显式依赖入口,只应在引用图无法表达且扩展协议明确要求时使用。普通应用和生态包不应调用它,也不应把它作为补充 PackageReferenceProjectReference 的手段。

合并优先级

Manifest 合并遵循以下基本方向:

  1. 包清单片段提供默认事实。
  2. 应用 Manifest 提供显式事实。
  3. 能力禁用先过滤失效的包默认事实。
  4. 各领域解析器再处理替换、移除、去重、冲突和顺序。

不要依赖项目引用或 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) 为服务添加装饰器,可选覆盖装饰器生命周期

支持的生命周期为 SingletonScopedTransient

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:非负超时时间。
  • cancellationPolicycooperativenone
  • failurePolicyfail-fastcontinue-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) 方法对应的有效已注册实现满足给定类型选择器;同时检查实现基类

ContractTypeImplementationType 面向解析后的有效服务图,而不是全部可见类型。若同一基础接口方法同时通过两个兄弟服务契约暴露,契约类型选择只作用于实际命中的服务契约,不会把中间件复制到另一个兄弟契约。方法自身或基础契约上的元数据仍会展开到所有实际暴露该 slot 的已注册服务契约。

其他成员选择器

  • Properties.HasAttribute<TAttribute>()
  • Fields.HasAttribute<TAttribute>()
  • Events.HasAttribute<TAttribute>()
  • Constructors.HasAttribute<TAttribute>()

组合

所有 Selector 都可以使用 AndOrNot 组合:

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) 抑制指定源的某个元数据键

IncludeExcludeSuppress 引用的源应先通过 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 安全。服务集成协议把它拆成四个部分:

  1. UseXxx(Action<TBuilder>):应用和包使用的强类型入口。
  2. TBuilder:聚合多次调用的领域构建器。
  3. ManifestIntegrationTerminalAttribute 的终结方法:可序列化的配置步骤。
  4. 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> 采用累加并去重。
  • UseTransportprofileKey 为稳定键,后一次配置覆盖同键值。
  • 多总线或多配置档由稳定键表达,而不是靠多次 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");
            });
        });
    }
}

最终行为:

  1. 包默认调用先进入聚合构建器。
  2. 应用显式调用按源码顺序追加。
  3. 重复使用方由构建器去重。
  4. 应用的 default 传输配置覆盖包默认值。
  5. 只创建一个 MassTransitManifestBuilder
  6. 全部终结方法执行后,只调用一次 Apply

集成契约要求

ManifestServiceIntegrationAttribute 标注的入口必须满足:

  • 位于有效公开的静态类中。
  • 是公共静态扩展方法。
  • 返回 void
  • 第一个参数是 IServiceManifestBuilder
  • 第二个参数是 Action<TBuilder>
  • 不包含方法泛型参数。
  • TBuilder 是有效公开、非抽象、非泛型类型。
  • TBuilder 有公共无参构造函数。
  • bootstrapMethod 指向同一静态类中的公共静态 Apply(IServiceCollection, TBuilder)

终结方法必须满足:

  • 是构建器上的公共实例普通方法。
  • 返回 void
  • 不使用 refoutinparams 参数。
  • 有唯一、稳定、区分大小写的 terminalId
  • 参数只能是编译器可表达的常量、typeof(...)、枚举成员或受支持的泛型类型参数。

包清单片段引用的泛型类型、typeof(...) 类型和枚举必须对应用程序集公开。应用自己的 Manifest 可以引用本程序集内部类型。

版本演进

  • integrationIdterminalId 是协议键,不是显示名称,发布后不要修改。
  • 兼容的方法重命名不要求修改 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 生成源码或哈希规范化失败 检查不可序列化输入和构建环境差异

排障顺序

  1. 先检查项目引用的是 Sparkdo.App 还是 Sparkdo.Package
  2. 确认项目只有一个具体 IManifest
  3. 检查 Define 中是否存在帮助方法、运行期变量、分支或不受支持的 Lambda。
  4. 检查包导出类型是否有效公开。
  5. 查看 sparkdo-manifest-report.v1.json 中的来源、操作和诊断。
  6. 检查 runtime-blueprint.v1.json 是否包含预期有效事实。
  7. 第三方集成再检查 integrationIdterminalId 和契约版本。

安全、AOT 与可维护性

  • Manifest 只保存声明事实,不保存密码、令牌、连接字符串或私钥。
  • 包扩展类型应有效公开,避免生成代码无法访问。
  • 使用强类型泛型和 typeof(...),不要保存任意程序集限定字符串并在运行时反射解析。
  • 不要在 Apply 或扩展引导中创建临时 IServiceProvider
  • 不要通过程序集扫描寻找服务、终结方法或配置类型。
  • 稳定协议使用 integrationIdterminalId、能力标识和资源标识,不使用方法显示名称代替协议键。
  • 默认值和扩展负载应采用确定性序列化,避免时间、随机数和机器路径进入产物。
  • 修改公共 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

编写生态包时还应验证:

  1. dotnet pack 能生成包含清单片段的真实 NuGet 包。
  2. 一个干净使用方只引用生成的 .nupkg 也能构建和运行。
  3. 包默认配置和应用显式配置的合并结果正确。
  4. 禁用、替换、移除和冲突路径都有测试。
  5. 裁剪分析器和原生 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 ApplicationPackage
SparkdoServiceLifetime SingletonScopedTransient
SparkdoResourceSourceKind EmbeddedPackageAssetGeneratedPhysical
SparkdoResourceRequirement PackageAssetsGeneratedResources

相关模块

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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