# Aspire ResourceKit > Strongly typed, source-generated resource kits for .NET Aspire. A source-generator-powered framework for structuring .NET Aspire AppHost resource composition as strongly typed, test-friendly classes. Generated host wiring, typed options, and enablement toggles keep large AppHosts maintainable and discoverable. - Repository: https://github.com/purview-dev/aspire-resourcekit - Package: https://www.nuget.org/packages/Purview.Aspire.ResourceKit - Project page: https://purview.dev/projects/aspire-resourcekit/ - Documentation: https://purview.dev/docs/aspire-resourcekit/ - Full machine-readable content: https://purview.dev/projects/aspire-resourcekit/llms-full.txt # Getting started This guide walks through a minimal host + resource setup using source generation. It assumes you have an Aspire AppHost project and a service project ready to compose. :::tip For a quick jump across lifecycle concepts, see [Lifecycle: Build vs Configure](lifecycle-build-configure/) and [Enablement](enablement/). ::: ## Install the package Install the `Purview.Aspire.ResourceKit` package in your AppHost project. ```bash dotnet add package Purview.Aspire.ResourceKit ``` :::tip This package can include one or more bundled [Agent Skills](https://agentskills.io/). On build, `skills/**/SKILL.md` entries are copied to `.agents/skills/**` in the consuming repository (for example: `skills/aspire-apphost-to-resourcekit/SKILL.md` → `.agents/skills/aspire-apphost-to-resourcekit/SKILL.md`), along with a local `.gitignore` in each generated skill folder to keep updates out of source control noise. To opt out, set `false` in your project (or `Directory.Build.props`). See [Bundled Agent Skills](agent-skills/). ::: ## Define a host kit Create a partial class and annotate it with `[HostKit]`. ```csharp using Purview.Aspire.ResourceKit; [HostKit] partial class ShopHostKit; ``` ## Define one or more resources Create a partial resource class per resource and annotate it with `[ResourceDefinition]`. ```csharp using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using Purview.Aspire.ResourceKit; [ResourceDefinition("api")] partial class ApiResourceKit { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); } ``` :::note For a project resource you can use the Aspire-generated project reference type (`Projects.Example_Service`, the type accepted by `AddProject()`) instead of the concrete `ProjectResource`. The generator maps it to `ProjectResource` for the generated base class, and the analyzer validates that `BuildResource` adds the same project via `AddProject()` (SG0018) and that an explicit base, when used, resolves to `ProjectResource` (SG0019). See [Project Resources](project-resources/). ::: ### Choose an attribute style You have two valid styles: - **Generic style**: `[ResourceDefinition(...)]` - Preferred for most cases. - Do not declare an explicit base type. - **Non-generic style**: `[ResourceDefinition(...)]` - Requires an explicit compatible base type that provides the resource type. Example non-generic style: ```csharp [ResourceDefinition("api")] partial class ApiResourceKit : ResourceKitBase { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); } ``` Avoid mixing both styles on the same class. See [Attributes Reference](attributes-reference/) for the full rules. ## Register generated wiring in AppHost Call the generated extension method from your AppHost entry point. ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddAspireResourceKit(); ``` The method name can be customized with `HostKitAttribute.ExtensionMethodName`. ## Extend generated typed options ResourceKit generates typed options as nested `sealed partial` classes, so you can extend them with your own settings. Host options are extended on the host kit; resource options are extended on each resource kit. ```csharp [ResourceDefinition("api")] sealed partial class ApiResourceKit { partial class ApiResourceKitOptions { public string PublishEnvironmentVariableName { get; set; } = "PUBLISH_MARKER"; } } ``` See [Configuration and Options](configuration-and-options/) for the full pattern. ## Understand Build vs Configure ResourceKit has two lifecycle pairs: - `Build` / `BuildResource(...)` builds the resource itself. - `Configure` / `ConfigureResource()` wires resources together after build. Think of it as: - **Build phase**: create each resource. - **Configure phase**: connect the created resources. See [Lifecycle: Build vs Configure](lifecycle-build-configure/) and [Enablement](enablement/) before adding cross-resource dependencies. ## Expand incrementally A common progression: - Start with one resource kit class. - Add more resource kit classes as the AppHost grows. - Use generated options to toggle resources for local/dev/test scenarios. For configuration details, continue with [Configuration and Options](configuration-and-options/). --- # Attributes reference ResourceKit ships two public attributes in the `Purview.Aspire.ResourceKit` namespace. Both are generated into the consuming compilation by the bundled source generator, so you do not reference a separate attribute package. ## `HostKitAttribute` (`[HostKit]`) Marks the single host kit class per compilation. Exactly one host kit may exist per compilation (SG0004). ```csharp [HostKit] sealed partial class ShopHostKit; ``` Optional named arguments: | Argument | Type | Default | Purpose | | --- | --- | --- | --- | | `Name` | `string?` | `null` | Controls generated naming. | | `ExtensionMethodName` | `string?` | Derived from host kit name | Overrides the generated builder extension name (for example `AddAspireResourceKit()`). | | `GenerateOptions` | `bool` | `true` | Enables/disables generated host options. | ## `ResourceDefinitionAttribute` (`[ResourceDefinition]`) Marks a resource kit class that participates in generation. ```csharp [ResourceDefinition("api", PropertyName = "API")] partial class APIResourceKit; ``` Options: | Argument | Type | Default | Purpose | | --- | --- | --- | --- | | `Name` | `string?` | Derived from class name (suffix trimmed) | Logical Aspire resource name. | | `PropertyName` | `string?` | Derived from class name | Generated host property name. Must be a valid C# identifier (SG0008). | | `AspireResourceType` | generic type argument | `null` | The Aspire `IResource` type (generic style only). | ## `ResourceDefinition` vs `ResourceDefinition` ResourceKit supports two declaration styles with different base-type behavior. ### Generic attribute (recommended) Use `[ResourceDefinition]` when you want the resource type declared directly on the attribute. ```csharp [ResourceDefinition("api")] partial class APIResourceKit { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); } ``` - Do **not** declare an explicit base type on the class (SG0015). - The generator supplies the host-specific base in generated partial code. The type argument may be: - an Aspire resource type implementing `IResource` (used as-is), or - an `IProjectMetadata` type (the type accepted by `AddProject()`), which is mapped to `ProjectResource` for the generated base class. Anything else is rejected (SG0016). ### Non-generic attribute Use `[ResourceDefinition]` when you prefer (or need) to specify the resource type through an explicit base type. ```csharp [ResourceDefinition("api")] partial class APIResourceKit : ResourceKitBase { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); } ``` - You **must** declare an explicit valid base type (SG0014). - The base must derive from the generated `ResourceKitBase` or the runtime `ResourceKitBase` (SG0006). ### Rules summary | Situation | Diagnostic | Severity | | --- | --- | --- | | Both styles on one class | SG0013 | Error | | Generic style with explicit base | SG0015 | Error | | Non-generic style without explicit base | SG0014 | Error | | Explicit base not derived from a valid Resource Kit base | SG0006 | Error | | Resource type cannot be inferred/found | SG0016 | Error | ## Constructors Attributed classes must not declare constructors with parameters or executable constructor bodies (SG0012). The generator supplies the constructor wiring in generated partial code; the primary constructor of a resource kit receives the host kit and options. In the manual (non-generated) pattern you declare it yourself — see [Examples](../examples/). ## See also - [Generated Output](../generated-output/) — what the generator emits from these attributes. - [Diagnostics](../diagnostics/) — full diagnostic reference. --- # Generated output From your `[HostKit]` and `[ResourceDefinition]` declarations, the generator emits all of the boilerplate that wires your resource kits together. The generated code is marked with `[CompilerGenerated]` and `[GeneratedCode("HostKitGenerator", ...)]` and is excluded from code coverage. ## What gets generated - a host resource base class (the concrete host kit deriving from `HostKitBase`), - resource properties on the host (one per resource definition), - host + per-resource options (when `GenerateOptions` is enabled), - a typed `ResourceKitBase` base per host kit, - each resource kit's `sealed partial` skeleton with a constructor and `Options` property, - an AppHost extension method to build/configure/register the host kit. ## Host kit members For every `[ResourceDefinition]`, the host kit gets a lazily initialized property. Accessing a property before `Build` throws, and setting it twice throws: ```csharp public Purview.Aspire.ResourceKit.Example.AppHost.AppModels.Resources.ExampleAPIKit ExampleAPI { get { return field ?? throw new global::System.InvalidOperationException( "The 'ExampleAPI' resource has not been initialized. Call Build first."); } private set { ... } } ``` ## Generated host Build/Configure `Build` instantiates each resource kit from its options, registers it with the base class, invokes the optional `onBuilt` callback, then calls `base.Build(builder)`: ```csharp public override void Build(global::Aspire.Hosting.IDistributedApplicationBuilder builder) { // Creating ExampleAPIKit Resource Kit. ExampleAPI = new(this, Options.ExampleAPI); // Register the discovered app resources with the base class. AddResource(ExampleAPI); // Now the additional post-build func builder onBuilt?.Invoke(this, builder); base.Build(builder); } public override void Configure() { base.Configure(); onConfigured?.Invoke(this); } ``` ## Generated resource kit skeleton Each resource kit derives from the generated `ResourceKitBase` and gains a constructor that accepts the host kit and its options: ```csharp internal sealed partial class ExampleAPIKit : global::Purview.Aspire.ResourceKit.ResourceKitBase { public ExampleAPIKit(ExampleHostKit hostKit, ExampleAPIKitOptions options) : base(hostKit, (options ?? throw new global::System.ArgumentNullException(nameof(options))).Name) { Options = options; IsEnabled = options.IsEnabled; } public ExampleAPIKitOptions Options { get; } } ``` Your `partial class` supplies `BuildResource(...)` and optionally `ConfigureResource()` and `IsResourceEnabled(builder)`. ## Generated options Host options are nested under the host kit and expose a `SectionName` constant plus one nested options object per resource: ```csharp public sealed partial class ExampleHostKitOptions { public const string SectionName = "ExampleHostKit"; public ExampleAPIKitOptions ExampleAPI { get; init; } = new(); } ``` Each resource options type carries `Name` (defaulting to the logical resource name) and `IsEnabled`: ```csharp public sealed partial class ExampleAPIKitOptions { [global::System.ComponentModel.DataAnnotations.Required(AllowEmptyStrings = false)] public string Name { get; set; } = "api"; public bool IsEnabled { get; set; } = true; } ``` `Name` is decorated with `[Required(AllowEmptyStrings = false)]`; the generated extension method calls `ValidateOnStart()`. ## Generated extension method The extension method binds host options from configuration, creates the host kit, and runs the full lifecycle: ```csharp public static global::Aspire.Hosting.IDistributedApplicationBuilder AddAspireResourceKit( this global::Aspire.Hosting.IDistributedApplicationBuilder builder, global::System.Action? onBuilt = null, global::System.Action? onConfigured = null, global::System.Action>? configureOptions = null ) { var optionsBuilder = builder.Services .AddOptions() .BindConfiguration(ExampleHostKitOptions.SectionName); configureOptions?.Invoke(optionsBuilder); optionsBuilder.ValidateOnStart(); var hostKitOptions = builder.Configuration .GetSection(ExampleHostKitOptions.SectionName) .Get() ?? new(); ExampleHostKit hostKit = new(onBuilt, onConfigured, hostKitOptions); hostKit.Build(builder); hostKit.Configure(); builder.Services.AddSingleton(hostKit); return builder; } ``` The extension method name defaults to `Add()` (for example `AddAspireResourceKit()` for `ExampleHostKit`) and can be overridden with `HostKitAttribute.ExtensionMethodName`. ## See also - [Attributes Reference](../attributes-reference/) - [Lifecycle: Build vs Configure](../lifecycle-build-configure/) - [Configuration and Options](../configuration-and-options/) --- # Lifecycle: Build vs Configure ResourceKit executes resources in a predictable sequence so dependency flow stays explicit and easy to reason about. ## Runtime order When the generated extension method is invoked, ResourceKit performs: 1. Instantiate resource kits from options. 2. `Build` each enabled resource. 3. `Configure` each enabled resource. This happens before `DistributedApplication.Build()` completes. ## The two lifecycle pairs - `Build` / `BuildResource(...)`: **construct this resource** (`AddProject`, `AddRedis`, `AddAzureStorage`, and so on). - `Configure` / `ConfigureResource()`: **attach resources to each other** after construction (references, bindings, cross-resource wiring). The separation keeps creation and cross-resource wiring explicit and deterministic. ## How the generated host kit drives the lifecycle The generated host kit overrides `Build` and `Configure`: - `Build` creates each discovered resource kit from its options, registers all of them with the base class via `AddResource`, invokes an optional `onBuilt` callback, and then calls `base.Build(builder)`, which runs `BuildResource(...)` for each resource. - `Configure` calls `base.Configure()` first (running each resource's `ConfigureResource()`), then invokes an optional `onConfigured` callback. The extension method signature gives you hooks for extra wiring: ```csharp builder.AddAspireResourceKit( onBuilt: (hostKit, builder) => { /* after resources are created, before Configure */ }, onConfigured: hostKit => { /* after all resources are configured */ }, configureOptions: optionsBuilder => { /* additional options configuration */ } ); ``` `HostKitBase` (the runtime base of the generated host kit) enforces that resources cannot be added after `Build` seals the resource list, keeping the lifecycle single-run and deterministic. ## Build vs Configure per resource Each resource kit implements `IResourceKit`: - `Build(IDistributedApplicationBuilder builder)` calls your `BuildResource(...)` override to **construct** the resource and store the resulting `IResourceBuilder` in `ResourceBuilder`. - `Configure()` calls your `ConfigureResource()` override to **attach** resources to each other after construction. A common Configure example that connects an API to its dependencies: ```csharp protected override void ConfigureResource() { ResourceBuilder.WithReference(HostKit.Postgres.Database).WaitFor(HostKit.Postgres.Database); ResourceBuilder.WithReference(HostKit.AzureStorage.Blobs).WaitFor(HostKit.AzureStorage.Blobs); if (HostKit.Redis.IsEnabled) ResourceBuilder.WithReference(HostKit.Redis).WaitFor(HostKit.Redis); } ``` Because `ConfigureResource()` runs after every resource is built, you can safely reach other resources through the host kit's generated properties. ## Enablement gates both phases Before `BuildResource(...)` runs, ResourceKit checks whether the resource should participate: - If `IsEnabled` is `false`, both `BuildResource(...)` and `ConfigureResource()` are skipped. - During `Build`, `IsResourceEnabled(builder)` is evaluated (only when `IsEnabled` is already `true`) and its result is assigned back to `IsEnabled`. See [Enablement](../enablement/) for the full model. ## See also - [Generated Output](../generated-output/) - [Enablement](../enablement/) --- # Enablement: IsEnabled vs IsResourceEnabled ResourceKit supports flexible runtime enablement through two related members. ## The two toggles - `IsEnabled` — the current enablement flag, usually sourced from generated options. This is the persisted/configured toggle. - `IsResourceEnabled(builder)` — a runtime decision hook. Its default implementation returns `IsEnabled`, and you override it when enablement should react to runtime conditions. ## How they interact At runtime, `Build` evaluates enablement *before* resource construction: 1. If `IsEnabled` is `true`, ResourceKit calls `IsResourceEnabled(builder)` and assigns the result back to `IsEnabled`. 2. If the resulting `IsEnabled` is `false`, both `BuildResource(...)` and `ConfigureResource()` are skipped for that resource. Two important consequences: - The hook is only invoked when `IsEnabled` is already `true`. A kit disabled via `IsEnabled=false` (for example from options) is **never** re-enabled by the hook. - `ResourceBuilder` cannot be accessed while the resource is disabled — doing so throws an `InvalidOperationException` (the property guards itself). ## When to override `IsResourceEnabled` Use the hook when enablement depends on runtime state rather than only static options. Common examples are environment-specific availability, publish mode, or dynamic configuration checks. ```csharp protected override bool IsResourceEnabled(IDistributedApplicationBuilder builder) { // Example: allow config + environment based behavior. var isEnabledInConfig = IsEnabled; var isProd = builder.Configuration["ASPNETCORE_ENVIRONMENT"] == "Production"; return isEnabledInConfig && isProd; } ``` Publish-only resources are a typical pattern. The example hosts run Key Vault and a publish marker parameter only when publishing: ```csharp protected override bool IsResourceEnabled(IDistributedApplicationBuilder builder) => builder.ExecutionContext.IsPublishMode; ``` ## Disabling a resource from options Because `IsEnabled` is bound from generated options, a resource can be disabled without code changes by setting the options section in configuration: ```json { "ShopHostKit": { "Redis": { "IsEnabled": false } } } ``` See [Configuration and Options](../configuration-and-options/) for the generated options shape. ## See also - [Lifecycle: Build vs Configure](../lifecycle-build-configure/) - [Configuration and Options](../configuration-and-options/) --- # Configuration and options `Purview.Aspire.ResourceKit` can generate host and resource options types so you can control names and enablement without changing code. ## Generated options shape A host options type contains one nested options object per resource. ```csharp public class ShopHostKitOptions { public APIKitOptions Api { get; set; } = new(); public RedisKitOptions Redis { get; set; } = new(); } public class APIKitOptions { public string Name { get; set; } = "api"; public bool IsEnabled { get; set; } = true; } ... ``` The generated host options type exposes a `SectionName` constant that is used to bind configuration: ```csharp public const string SectionName = "ShopHostKit"; ``` Generated options are emitted as nested `sealed partial` classes. You can extend both host and resource option types with custom properties. ## Extend host and resource typed options ### Extend host options ```csharp [HostKit] partial class ShopHostKit { public sealed partial class ShopHostKitOptions { public bool EnablePreviewResources { get; set; } } } ``` ### Extend resource options ```csharp [ResourceDefinition("api")] sealed partial class APIKit { partial class APIKitOptions { public string PublishEnvironmentVariableName { get; set; } = "PUBLISH_MARKER"; } } ``` > Tip: keep custom option members `public` with `get; set;` so configuration binding can populate them. > Data annotations such as `[Required]` are honored by the generated `ValidateOnStart()` registration. ### Use extended options at runtime - Access host-level values through `HostKit.Options`. - Access resource-level values through `Options` in each resource kit. Example: ```csharp protected override void ConfigureResource() { if (HostKit.Options.EnablePreviewResources) { ResourceBuilder.WithEnvironment(Options.PublishEnvironmentVariableName, "true"); } } ``` ## Common configuration keys Typical keys (example): - `ShopHostKit:API:Name` - `ShopHostKit:API:IsEnabled` Configuration is bound by the generated extension method: - Host options bind to `ShopHostKitOptions.SectionName` (for example `ShopHostKit`) and `ValidateOnStart()` is called. - Resource options are nested by generated resource property name. ## Disable a resource Set `IsEnabled=false` for that resource's options section. ```json { "ShopHostKit": { "Redis": { "IsEnabled": false } } } ``` ## `IsEnabled` vs `IsResourceEnabled(...)` Use both together for flexible control: - `IsEnabled`: static/configured toggle (usually generated options). - `IsResourceEnabled(builder)`: runtime decision hook. At runtime, `Build` only calls `IsResourceEnabled(builder)` when `IsEnabled` is already `true`. If the hook returns `false`, ResourceKit skips both `BuildResource(...)` and `ConfigureResource()` for that resource. A kit disabled via `IsEnabled=false` is never re-enabled by the hook. See [Enablement](../enablement/) for details. ## OptionsHelper for tests and overrides For tests and scenario toggles, `OptionsHelper` converts typed assignments into command-line arguments or environment variables. It can also flatten a populated options object into ASP.NET Core binder-style environment variables: ```csharp var args = OptionsHelper.Assign( c => c.API.IsEnabled = false, c => c.API.Name = "api-test" ).Build(); ``` See [OptionsHelper](../optionshelper/) for the complete API, including `PathFor`, `SectionNameFor`, and the `SG0020` single-property-path rule. For resource wiring, `IResourceBuilder.WithEnvironment(options)` now accepts a populated options object and emits environment variables using the same binder-style key convention. ## Section-name resolution Generated options types carry a `SectionName` constant (for example `"ShopHostKit"`). When you do not pass a section name explicitly, `OptionsHelper` resolves it as follows: 1. `const string SectionName` on the options type 2. Type name trimmed by one suffix: `Options`, `Settings`, `Configuration`, `Config` 3. Original type name You can look up the resolved section name with [`OptionsHelper.SectionNameFor`](../optionshelper/#sectionnamefor). ## Tip Prefer resource-level toggles over conditional host code. It keeps the composition model declarative and testable. --- # OptionsHelper `OptionsHelper` builds configuration arguments or environment variables for options objects by using strongly typed assignment expressions or by flattening a populated options object. It is useful for integration-test fixtures, scenario toggles, and CLI overrides. ## Assign `Assign(...)` starts building entries from assignment expressions. ```csharp var args = OptionsHelper.Assign( c => c.API.IsEnabled = false, c => c.API.Name = "api-test" ).Build(); ``` Resulting args are in this form: - `--ShopHostKit:API:IsEnabled=false` - `--ShopHostKit:API:Name=api-test` Each assignment action must set exactly one property path. To set multiple properties, pass one assignment per property (as above). The compiler reports `SG0020` if an action assigns more than one property path, and offers a code fix that splits it into separate assignments. You can also pass an explicit root section name: ```csharp OptionsHelper.Assign("MySection", c => c.API.IsEnabled = false); ``` ## AsEnvironmentVariables Call `AsEnvironmentVariables()` before `Build()` to produce environment variables instead: ```csharp var envVars = OptionsHelper.Assign( c => c.API.IsEnabled = false, c => c.API.Name = "api-test" ).AsEnvironmentVariables().Build(); ``` This returns a dictionary such as `{"ShopHostKit__API__IsEnabled": "false", "ShopHostKit__API__Name": "api-test"}`. ## Environment Call `Environment(...)` to flatten a populated options object into ASP.NET Core binder-style environment variables: ```csharp var envVars = OptionsHelper.Environment(new ShopOptions { Api = new ApiOptions { Name = "api-test", Enabled = true, }, ReplicaNames = ["a", "b"], Labels = { ["region"] = "west", }, }) .Override(static o => o.Api.Name = "api-prod") .Ignore(static o => o.Labels) .Build(); ``` The output uses binder-style keys such as: - `Shop__Api__Name` - `Shop__Api__Enabled` - `Shop__ReplicaNames__0` - `Shop__ReplicaNames__1` - `Shop__Labels__region` Use `Override(...)` to replace selected values before emission, and `Ignore(...)` to drop selected properties from the flattened output. The dictionary returned by `Build()` can be passed straight to a resource: ```csharp ResourceBuilder.WithEnvironment(OptionsHelper.Environment(Options).Build()); ``` `IResourceBuilder.WithEnvironment(IReadOnlyDictionary)` applies the entries directly, so the keys are preserved verbatim (for example `Services__ServiceName`). ## PathFor If you need a property path as a plain string (for example, to build keys or log config), use `PathFor` with a member selector: ```csharp var path = OptionsHelper.PathFor(f => f.API.Name); // "API.Name" ``` ## SectionNameFor Look up the resolved section name for any options type directly: ```csharp var sectionName = OptionsHelper.SectionNameFor(); // "ShopHostKit" var sectionNameFromType = OptionsHelper.SectionNameFor(typeof(ShopHostKit.ShopHostKitOptions)); // "ShopHostKit" ``` `SectionNameFor` accepts both a generic type argument and a `Type`, and applies the same resolution rules as `Assign`: 1. `const string SectionName` on the options type 2. Type name trimmed by one suffix: `Options`, `Settings`, `Configuration`, `Config` 3. Original type name Suffix trimming works for generic types by ignoring type arguments. ## TUnit.Aspire integration `OptionsHelper` pairs well with [TUnit.Aspire](https://www.nuget.org/packages/TUnit.Aspire/) when passing args to an AppHost under test: ```csharp protected override string[] Args => [ .. base.Args, .. OptionsHelper.Assign( c => c.API.IsEnabled = false, c => c.API.Name = "api-test" ).Build(), ]; ``` See [Testing with ResourceKit](../testing-with-resourcekit/). ## The SG0020 rule An `OptionsHelper.Assign(...)` action must set exactly one property path. A block-bodied lambda such as the following is rejected at compile time: ```csharp OptionsHelper.Assign(o => { o.API.IsEnabled = false; o.API.Name = "api-test"; }); ``` Split each property into its own assignment argument instead: ```csharp OptionsHelper.Assign( o => o.API.IsEnabled = false, o => o.API.Name = "api-test" ); ``` Visual Studio offers a **"Split into separate assignments"** code fix that performs this conversion for you. See [Source Generator Behaviors](../source-generator-behaviors/) for how the fix works. ## See also - [Configuration and Options](../configuration-and-options/) - [Testing with ResourceKit](../testing-with-resourcekit/) --- # Project resources ResourceKit has first-class support for Aspire project resources (`AddProject()`) with generator and analyzer validation to catch wiring mistakes. ## Declaring a project resource kit Use the Aspire-generated project reference type directly in the attribute. The generator maps it to `ProjectResource` for the generated base class: ```csharp using Aspire.Hosting; using Aspire.Hosting.ApplicationModel; using Purview.Aspire.ResourceKit; [ResourceDefinition("api")] partial class ApiResourceKit { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); } ``` `Projects.Example_Service` is the type generated by the Aspire SDK into the AppHost. It implements `IProjectMetadata` (the type accepted by `AddProject()`). When the type argument implements `IProjectMetadata`, ResourceKit: - validates the resource type (rather than rejecting it), - maps it to `ProjectResource` for the generated base class, - enables the two project-specific execution-only rules below. ## The declared project must be added (SG0018) `BuildResource` must register the same project it declares via `AddProject()`. ```csharp // OK protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); ``` ```csharp // SG0018 (warning): the declared project is never registered at runtime. [ResourceDefinition("api")] partial class ApiResourceKit { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddContainer("api", "my-image"); } ``` SG0018 is an **execution-only** warning: generation still proceeds so you can complete the override without losing the host kit output. At runtime the declared project is not registered, so the resource fails. ## Explicit base must use ProjectResource (SG0019) When a project resource kit declares an explicit base class (non-generic attribute style), the base must resolve to `ProjectResource`. ```csharp [ResourceDefinition("api")] partial class ApiResourceKit : ResourceKitBase { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); } ``` If the explicit base resolves to a different resource type, SG0019 is reported (execution-only warning) because that base cannot build the declared project. ## Generic vs non-generic styles - **Generic style** (`[ResourceDefinition(...)]`) is recommended for project resources: the type is declared on the attribute and the base is supplied by the generator. - **Non-generic style** (`[ResourceDefinition(...)]` + explicit base) requires the base to use `ProjectResource` (SG0019). See [Attributes Reference](../attributes-reference/) for the base-type rules shared by all resources. ## See also - [Attributes Reference](../attributes-reference/) - [Diagnostics](../diagnostics/) --- # Diagnostics ResourceKit reports diagnostics with `SGxxxx` IDs to help you fix model issues quickly. The rules are evaluated by the bundled `ResourceKitDiagnosticAnalyzer` (and the source generator uses the same shared rule set to decide what can be generated). ResourceKit also ships `ResourceKitDiagnosticSuppressor`, which automatically suppresses `CS8618` for non-nullable `IResourceBuilder` properties declared on resource kits — these are populated at runtime during the `BuildResource`/`ConfigureResource` lifecycle, so the "must contain a non-null value when exiting the constructor" warning does not apply. ## Diagnostic reference | ID | Severity | What it means | | - | - | - | | SG0001 | Error | A participating class must be `partial` | | SG0002 | Info | Host kit exists but no resources were defined | | SG0003 | Warning | Resources exist but no host kit was defined | | SG0004 | Error | More than one `[HostKit]` class was found | | SG0005 | Error | Two resources map to the same generated property name | | SG0006 | Error | A resource does not derive from the expected generated base | | SG0007 | Error | Resource name could not be inferred and `Name` was not set | | SG0008 | Error | Explicit `PropertyName` is not a valid C# identifier | | SG0009 | Error | Missing `IServiceCollection` dependency | | SG0010 | Error | Missing configuration binder dependency | | SG0011 | Error | Missing options configuration extensions dependency | | SG0012 | Error | Non-empty constructors are not supported on attributed classes | | SG0013 | Error | Mixed `ResourceDefinition` and `ResourceDefinition` usage on one class | | SG0014 | Error | Non-generic `ResourceDefinition` requires explicit compatible base type | | SG0015 | Error | Generic `ResourceDefinition` cannot declare explicit base type | | SG0016 | Error | No Aspire resource type could be inferred/found | | SG0017 | Warning | An `IResourceBuilder` property is never assigned in `BuildResource` or `ConfigureResource` | | SG0018 | Warning | A project resource kit does not add the declared project via `AddProject()` | | SG0019 | Warning | A project resource kit declares an explicit base class that does not use `ProjectResource` | | SG0020 | Error | An `OptionsHelper.Assign` action sets more than one property path | See [Attributes Reference](../attributes-reference/) for the rules tied to attribute styles, and [Project Resources](../project-resources/) for SG0018/SG0019 specifics. ## Execution-only vs generation-blocking rules SG0017, SG0018, and SG0019 are **execution-only** rules. They report problems that break the resource at runtime (an unset builder property, a project that is never registered, or a base class that cannot build a project) but they do **not** prevent source generation. They are reported as warnings so generation always proceeds — for example, a resource kit whose `BuildResource` does not yet register its declared project via `AddProject()` is still generated (and the host kit is still emitted) so the user can complete the override instead of losing the whole output. Only Error-severity rules (SG0001–SG0016) block generation. ## `OptionsHelper.Assign` action with multiple property paths (SG0020) Each `OptionsHelper.Assign(...)` action must set exactly one property path. A block-bodied lambda such as the following is rejected at compile time: ```csharp OptionsHelper.Assign(o => { o.API.IsEnabled = false; o.API.Name = "api-test"; }); ``` Split each property into its own assignment argument instead: ```csharp OptionsHelper.Assign( o => o.API.IsEnabled = false, o => o.API.Name = "api-test" ); ``` Visual Studio offers a **"Split into separate assignments"** code fix that performs this conversion for you. See [OptionsHelper](../optionshelper/) and [Source Generator Behaviors](../source-generator-behaviors/). ## Fast troubleshooting checklist 1. Ensure host/resource classes are marked `partial`. 2. Ensure exactly one `[HostKit]` class exists in the compilation. 3. Ensure resource property names are unique (explicit `PropertyName` can help). 4. Ensure each resource uses a compatible base and definition attribute style. 5. Set explicit `Name` when inference cannot determine resource name. ## Common fixes ### Duplicate generated property names (SG0005) Use unique `PropertyName` values: ```csharp [ResourceDefinition("api", PropertyName = "Api")] [ResourceDefinition("admin", PropertyName = "AdminApi")] ``` ### Invalid `PropertyName` (SG0008) Use valid C# identifiers only (`Api`, `RedisCache`, `OrderDb`, etc.). ### Multiple host kits (SG0004) Keep one `[HostKit]` per compilation; split scenarios into separate projects if needed. ### Mixed attribute styles (SG0013) Use exactly one style per class: - `[ResourceDefinition("name")]` - or `[ResourceDefinition("name")]` Do not apply both to the same class. ### Base-type mismatch by style (SG0014 / SG0015) - If you use non-generic `[ResourceDefinition("name")]`, declare an explicit compatible base. - If you use generic `[ResourceDefinition("name")]`, do not declare an explicit base. ### Unassigned `IResourceBuilder` property (SG0017) Every `IResourceBuilder` property on a resource kit (nullable or not) must be assigned in either `BuildResource` or `ConfigureResource`. `CS8618` is automatically suppressed for these properties because they are populated at runtime, so SG0017 is the signal that a property is never set: ```csharp [ResourceDefinition("sql")] sealed partial class SqlServerKit { public IResourceBuilder Database { get; private set; } protected override void ConfigureResource() { Database = ResourceBuilder.AddDatabase("changeops-db", "ChangeOps"); base.ConfigureResource(); } } ``` --- # Testing with ResourceKit ResourceKit is designed to be test-friendly: resource composition is split into focused classes, and generated options give you a typed way to override names and enablement from tests. ## Test project layout The repository uses [TUnit](https://thomhurst.github.io/TUnit/) with the [TUnit.Aspire](https://www.nuget.org/packages/TUnit.Aspire/) integration. Test projects live under `src/tests`: - `ResourceKit.UnitTests` — runtime behavior of the kit base types and `OptionsHelper`. - `ResourceKit.IntegrationTests` — starts the example AppHosts and asserts against real resources. - `SourceGeneration.UnitTests` / `SourceGeneration.IntegrationTests` — generator output, diagnostics, suppression, code fixes, and caching. Reports are written to `TestResults/`. ## Integration-testing an AppHost Create an `AspireFixture` for the AppHost under test: ```csharp using TUnit.Aspire; namespace Purview.Aspire.ResourceKit.Fixtures; public sealed class ExampleAppHostFixture : AspireFixture where TAppHost : class; ``` Then drive it from a TUnit test class: ```csharp using Projects; [ClassDataSource>(Shared = SharedType.PerTestSession)] public sealed class ExampleAppHostIntegrationTests(ExampleAppHostFixture fixture) { [Test] public async Task AppHost_WhenServicesStarted_APIIsHealthy(CancellationToken cancellationToken) { var client = fixture.CreateHttpClient("api"); var response = await client.GetAsync(new Uri("/health", UriKind.Relative), cancellationToken); await Assert.That(response.StatusCode).IsEqualTo(HttpStatusCode.OK); } } ``` `TUnit.Aspire` wires the AppHost lifecycle (start/stop) around the fixture. The example API exposes a `/health` endpoint for this purpose. ## Overriding options from tests The generated extension method binds host options from configuration, so you can pass command-line arguments through the fixture to toggle resources. `OptionsHelper` generates these arguments from strongly typed assignments: ```csharp using TUnit.Aspire; public sealed class CustomOptionsExampleAppHostFixture : AspireFixture { public const string AzureStorageName = "custom-options-azure-storage-example"; protected override string[] Args => [ "--ExampleHostKit:Redis:IsEnabled=false", $"--ExampleHostKit:AzureStorage:Name={AzureStorageName}", ]; } ``` The test then asserts the options took effect — Redis is disabled (no connection string), and Azure Storage is registered under the custom name: ```csharp [Test] public async Task AppHost_WithCustomOptions_IsPassedToTheHostKit(CancellationToken cancellationToken) { await Helpers.ConnectionStringIsUnavailableAsync(fixture, "redis", cancellationToken); await Assert .That(fixture.GetResourceSnapshot(CustomOptionsExampleAppHostFixture.AzureStorageName)) .IsNotNull() .Because("The custom Azure Storage name should be passed to the host kit."); } ``` Prefer `OptionsHelper.Assign(...)` for typed, refactor-safe args: ```csharp protected override string[] Args => [ .. base.Args, .. OptionsHelper.Assign( c => c.Redis.IsEnabled = false, c => c.AzureStorage.Name = "custom-options-azure-storage-example" ).Build(), ]; ``` See [OptionsHelper](../optionshelper/) for `Assign`, `AsEnvironmentVariables`, and the `SG0020` single-property-path rule. If you already have a populated options object, use `OptionsHelper.Environment(...)` to flatten it into configuration-style environment variables and selectively override or ignore members before emission. ## Unit-testing the manual pattern The manual host pattern (see [Examples](../examples/)) composes `ResourceKitBase` without generation, which makes the lifecycle directly unit-testable: instantiate a kit, call `Build(builder)`/`Configure()`, and assert on `ResourceBuilder` and option-driven behavior. ## Running tests Unit tests are filtered with the TUnit tree-node filter: ```bash just test "/*/*/*/*[Category=Unit]" ``` Integration tests (which require Docker/Testcontainers) run locally: ```bash just test ``` See [Contributing](../contributing/) and [Release Flow](../release-flow/) for the CI filter and local workflow. --- # Examples: Generated vs Manual The repository ships two example AppHosts that compose the same set of resources in two different styles: - `src/src/Example.AppHost` — the **source-generated** pattern (attributes, no hand-written wiring). - `src/src/Example.ManualAppHost` — the **manual** pattern (hand-written `HostKitBase` and `ResourceKitBase` composition, no generator). Both hosts build the same logical resources through `Purview.Aspire.ResourceKit.Example` constants in `Example.ServiceDefaults/Platform.cs`: | Resource | Name | Aspire type | | --- | --- | --- | | Postgres | `postgres` (+ `db` database) | `AzurePostgresFlexibleServerResource` | | Azure Storage | `azure-storage` (+ `blob`) | `AzureStorageResource` | | Redis | `redis` | `AzureManagedRedisResource` | | Key Vault | `kv` | `AzureKeyVaultResource` | | API | `api` | `ProjectResource` (`Projects.Example_Service`) | | Publish marker | `publish-marker` | `ParameterResource` | Key Vault and the publish marker are publish-only (`IsResourceEnabled` returns `builder.ExecutionContext.IsPublishMode`). ## Generated pattern (`Example.AppHost`) `AppHost.cs` registers everything with a single generated call: ```csharp var builder = DistributedApplication.CreateBuilder(args); builder.AddAspireResourceKit(); var app = builder.Build(); await app.RunAsync(); ``` The host kit is a one-line attribute declaration: ```csharp [HostKit] sealed partial class ExampleHostKit; ``` Each resource is a `[ResourceDefinition]` partial class that supplies the overrides. For example, the API kit also wires dependencies in `ConfigureResource()` and can now forward a populated options object to a destination resource: ```csharp [ResourceDefinition(Platform.ResourceKits.API)] sealed partial class ExampleAPIKit { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name).WithUrl("/health", "Health"); protected override void ConfigureResource() { if (HostKit.PublishMarker.IsEnabled) ResourceBuilder.WithEnvironment(Options.PublishEnvironmentVariableName, HostKit.PublishMarker); ResourceBuilder.WithEnvironment( OptionsHelper.Environment( new DemoServiceEnvironmentOptions { Service = new() { Name = Name, Enabled = true, Labels = { ["region"] = "west", }, }, Replicas = [1, 2, 3], Routes = { ["health"] = "/health", }, } ) .Override(static o => o.Service.Name = "api") .Ignore(static o => o.Service.Labels) .Build() ); ResourceBuilder.WithReference(HostKit.Postgres.Database).WaitFor(HostKit.Postgres.Database); ResourceBuilder.WithReference(HostKit.AzureStorage.Blobs).WaitFor(HostKit.AzureStorage.Blobs); if (HostKit.KeyVault.IsEnabled) ResourceBuilder.WithReference(HostKit.KeyVault).WaitFor(HostKit.KeyVault); if (HostKit.Redis.IsEnabled) ResourceBuilder.WithReference(HostKit.Redis).WaitFor(HostKit.Redis); } partial class ExampleAPIKitOptions { [Required(AllowEmptyStrings = false)] public string PublishEnvironmentVariableName { get; set; } = "PUBLISH_MARKER"; } sealed class DemoServiceEnvironmentOptions { public DemoServiceOptions Service { get; set; } = new(); public List Replicas { get; set; } = []; public Dictionary Routes { get; set; } = []; } sealed class DemoServiceOptions { public string Name { get; set; } = string.Empty; public bool Enabled { get; set; } public Dictionary Labels { get; set; } = []; } } ``` The generator emits the host kit members, options, `ResourceKitBase` base classes, and the extension method. See [Generated Output](../generated-output/). ## Manual pattern (`Example.ManualAppHost`) `AppHost.cs` registers a host kit instance directly via the generic extension: ```csharp builder.AddAspireResourceKit(); ``` The host kit is hand-written and composes the resource kits: ```csharp sealed class ExampleHostKit : HostKitBase { public AzureStorageKit AzureStorage { get; init; } public ExampleAPIKit ExampleAPI { get; init; } // ... public ExampleHostKit() { AzureStorage = new(this); ExampleAPI = new(this); // ... AddResource(AzureStorage); AddResource(ExampleAPI); // ... } } ``` Each resource kit is a hand-written partial class deriving from the runtime base, with a primary constructor that takes the host kit: ```csharp sealed partial class ExampleAPIKit(ExampleHostKit hostKit) : ResourceKitBase(hostKit, Platform.ResourceKits.API) { protected override IResourceBuilder BuildResource(IDistributedApplicationBuilder builder) => builder.AddProject(Name); protected override void ConfigureResource() { if (HostKit.PublishMarker.IsEnabled) ResourceBuilder.WithEnvironment("PUBLISH_MARKER", HostKit.PublishMarker); ResourceBuilder.WithReference(HostKit.Postgres.Database).WaitFor(HostKit.Postgres.Database); // ... } } ``` ## Choosing between the two | Consideration | Generated | Manual | | --- | --- | --- | | Boilerplate | Minimal (attributes + overrides) | Explicit (base classes, constructors, `AddResource`) | | Options | Generated, typed, bindable | You manage options yourself | | Diagnostics | Analyzer checks model rules | No generator diagnostics | | Testability | Both test well | Lifecycle is directly unit-testable | | Best for | Most AppHosts | Learning the model, or when you want full control | Both patterns produce the same runtime behavior: instantiate resources, `Build` enabled resources, `Configure` enabled resources. ## See also - [Getting Started](../) - [Attributes Reference](../attributes-reference/) - [Generated Output](../generated-output/) - [Lifecycle: Build vs Configure](../lifecycle-build-configure/) --- # Bundled agent skills `Purview.Aspire.ResourceKit` ships one or more bundled [Agent Skills](https://agentskills.io/) that are auto-installed into consuming repositories so supported coding agents can discover ResourceKit-specific guidance automatically. ## How auto-install works When a consuming project builds, any `skills/**/SKILL.md` files in the package are copied to `.agents/skills/**` in the consuming repository. Example mapping: - `skills/aspire-apphost-to-resourcekit/SKILL.md` → `.agents/skills/aspire-apphost-to-resourcekit/SKILL.md` A local `.gitignore` is written into each generated skill folder to keep updates out of source control noise, so the skills stay fresh on rebuild without polluting the repository history. ## Opting out To disable this behavior, set the shared opt-out property in your project (or `Directory.Build.props`): ```xml false ``` ## Bundled skill - `aspire-apphost-to-resourcekit` — guides migrating an inline AppHost into ResourceKit composition, covering attributes, lifecycle, options, and configuration. --- # Source generator behaviors The source generator ships alongside the runtime package and is exposed to consuming projects as an analyzer. This page covers how the generator, analyzers, and code fixes are organized and how they behave. ## Assembly layout | Assembly | Contents | | --- | --- | | `Purview.Aspire.ResourceKit` | Runtime abstractions (`HostKitBase`, `ResourceKitBase`, `OptionsHelper`, interfaces) | | `Purview.Aspire.ResourceKit.SourceGeneration` | The `HostKitGenerator` incremental generator, `ResourceKitDiagnosticAnalyzer`, `OptionsHelperAssignAnalyzer`, `ResourceKitDiagnosticSuppressor`, and the shared rule set | | `Purview.Aspire.ResourceKit.SourceGeneration.CodeFixes` | The `OptionsHelperAssignCodeFixProvider` (separate assembly so the main generator assembly never needs `Microsoft.CodeAnalysis.Workspaces`) | The generator and code-fix assemblies are packed under `analyzers/dotnet/cs` of the `Purview.Aspire.ResourceKit` package, so consumers get generation, diagnostics, and IDE code fixes from a single package reference. ## Incremental generation `HostKitGenerator` is an `IIncrementalGenerator`. It: 1. Registers the embedded attributes (`HostKitAttribute`, `ResourceDefinitionAttribute`) as post-initialization output. 2. Runs a set of incremental value providers over the compilation. 3. Emits a single hint-name file per host kit that contains the host kit, resource kit skeletons, options, the typed `ResourceKitBase`, and the AppHost extension method. Generation is skipped when the generator is disabled via settings (`IsSourceGeneratorDisabled`). ## Shared rule set `ResourceKitRules` is the single source of truth for model rules. Both the `ResourceKitDiagnosticAnalyzer` (which reports diagnostics in the IDE/build) and the generator (which uses the same rules to decide whether a kit should be generated) evaluate through this shared helper so the two never drift apart. - **Analyzer-owned rules** are reported only by the analyzer; the generator computes them for its `ShouldProcess` gating but does not re-report them (avoiding duplicate diagnostics). - **Generation-blocking rules** (Error severity, SG0001–SG0016) halt generation via the `GeneratorResult.ShouldProcess` gate. - **Execution-only rules** (SG0017–SG0019, warnings) never block generation, so a resource kit with incomplete `BuildResource`/`ConfigureResource` wiring is still generated and the host kit output is still emitted. See [Diagnostics](../diagnostics/) for the full rule reference. ## `OptionsHelperAssignAnalyzer` (SG0020) A dedicated analyzer reports SG0020 when an `OptionsHelper.Assign` (or chained `IOptionsBuilder.Assign`) action is a block-bodied lambda that assigns more than one property path. It matches `Assign` calls by method name, containing namespace, and the `params Action[]` parameter shape — including the `sectionName` overload. See [OptionsHelper](../optionshelper/). ## IDE code fix: "Split into separate assignments" The `OptionsHelperAssignCodeFixProvider` fixes SG0020 by rewriting a block-bodied lambda that assigns several properties into one assignment argument per property: ```csharp // Before (SG0020) OptionsHelper.Assign(o => { o.API.IsEnabled = false; o.API.Name = "api-test"; }); // After OptionsHelper.Assign( o => o.API.IsEnabled = false, o => o.API.Name = "api-test" ); ``` The fix lives in a separate code-fix assembly and uses a stable equivalence key. The generator assembly never acquires a `Microsoft.CodeAnalysis.Workspaces` dependency; the compiler loads the code-fix assembly without instantiating its Workspaces-dependent types, and the IDE activates them for fixes. ## `ResourceKitDiagnosticSuppressor` (SGSUP0001) The suppressor removes `CS8618` ("Non-nullable property must contain a non-null value when exiting constructor") for **non-nullable** `IResourceBuilder` properties declared on resource kits. These properties are populated at runtime during `BuildResource`/`ConfigureResource`, so the warning does not apply. Nullable `IResourceBuilder?` properties are left untouched. A type qualifies as a resource kit when it carries `[ResourceDefinition]`/`[ResourceDefinition]` or derives from `Purview.Aspire.ResourceKit.ResourceKitBase<,>` (directly or via the generated `ResourceKitBase`). ## Incremental caching The generator uses incremental value providers and equatable models so that unrelated edits do not re-trigger generation. The `GeneratorCachingTests` integration suite verifies that generation is cached across incremental runs and only recomputed when inputs change. ## See also - [Generated Output](../generated-output/) - [Diagnostics](../diagnostics/) - [Contributing](../contributing/) --- # Contributing This page covers the repository layout and the day-to-day commands for working on `Purview.Aspire.ResourceKit`. It is aimed at contributors, not consumers. ## Repository layout - `src/src/ResourceKit` — runtime APIs consumed by AppHost projects (the packable project). - `src/src/SourceGeneration` — the Roslyn incremental generator, analyzers, suppressor, and attributes. - `src/src/SourceGeneration.CodeFixes` — the SG0020 IDE code fix provider. - `src/src/Example.*` — sample Aspire applications (`Example.AppHost` generated pattern, `Example.ManualAppHost` manual pattern, `Example.Service`, `Example.ServiceDefaults`). - `src/tests/*` — unit and integration tests for the runtime and the generator. - `docs/wiki` — this documentation suite. The NuGet package is produced from `src/src/ResourceKit/ResourceKit.csproj` and includes the runtime APIs plus the analyzer/code-fix assemblies. ## Building and testing The repository uses [Just](https://just.systems/) recipes (see `Justfile`). All recipes run from the repository root. ```bash just build # build the solution (Debug) just test # run all tests just test-unit # run unit tests only (TUnit [Category=Unit] filter) just restore # restore dependencies just pack # produce NuGet packages into ./artifacts just lint-check # CSharpier formatting check just lint-fix # CSharpier format just scrub # remove bin/obj, clean, restore, stop build server just version # show the current version from package.json ``` ## Testing conventions - Tests use **TUnit** (and TUnit.Mocks); do not introduce NUnit, xUnit, or MSTest patterns. - All tests follow AAA with explicit `// Arrange`, `// Act`, `// Assert` comments. - Test names mirror production structure: `{ClassUnderTest}Tests` and `{SubjectOrMethodUnderTest}_{Scenario}_{Expectation}`. - When a method under test accepts a `CancellationToken`, pass one and make it the final argument. - Unit tests are tagged `[Category=Unit]` and are the only tests CI runs. Test projects: - `src/tests/ResourceKit.UnitTests` — runtime behavior (host app resources, `OptionsHelper`). - `src/tests/ResourceKit.IntegrationTests` — starts the example AppHosts via TUnit.Aspire. - `src/tests/SourceGeneration.UnitTests` — generator diagnostics/severity and attributes. - `src/tests/SourceGeneration.IntegrationTests` — generated source content, caching, diagnostics, suppression, and the SG0020 code fix. Reports are written to `TestResults/`. ## Agent skills and workflows The repository carries agent skills and prompt workflows under `.agents/`. Consult them when working on source generators, tests, the project SDK, or conventional commits. AGENTS.md is the canonical agent guidance and references `.agents/` for reusable workflows. ## Releases Releases are produced by the shared pipeline when a version bump is merged to `main`; see [Release Flow](../release-flow/). When preparing a release: 1. Update `package.json` (the authoritative version) and align `global.json` and `Directory.Packages.props` with stable dependency versions. 2. Move new diagnostics from `src/src/SourceGeneration/AnalyzerReleases.Unshipped.md` into `src/src/SourceGeneration/AnalyzerReleases.Shipped.md` under the released version, leaving the unshipped file empty. 3. Add a [`CHANGELOG.md`](https://github.com/purview-dev/aspire-resourcekit/blob/main/CHANGELOG.md) entry and update the wiki [release notes](../release-notes/). 4. Validate with `just lint-check` and `just pipeline-pack-validate` before merging. ## Pull requests Pull requests target `main` and are validated by `.github/workflows/pr.yml`, which delegates to the shared [Purview.Build](https://github.com/purview-dev/build) pipeline (restore, build, CSharpier lint, unit tests, pack, package-content validation). See [Release Flow](../release-flow/). Commits follow [Conventional Commits](https://www.conventionalcommits.org/), enforced by commitlint and Lefthook hooks. --- # Release flow This repository uses the shared [Purview.Build](https://github.com/purview-dev/build) pipeline for both PR validation and releases. Consuming repositories own configuration (through `purview-build.json`) but not pipeline source code. - `.github/workflows/pr.yml` — PR validation - `.github/workflows/release.yml` — release on push to `main` - `purview-build.json` — pipeline configuration ## PR validation `pr.yml` runs on pull requests targeting `main` and delegates to the shared `purview-build.yml` workflow. It runs: 1. `dotnet restore` of `src/ResourceKit.slnx` 2. `dotnet build --no-restore --configuration Release` 3. CSharpier lint across the repository 4. Unit tests (discovered under `src/tests` matching `*Tests.csproj`, run with the `/*/*/*/*[Category=Unit]` TUnit tree-node filter) 5. `dotnet pack` and package-content validation Integration tests are never discovered in CI: `purview-build.json` sets `Build:TestPatterns` to `*Tests.csproj` and `Build:TestFilter` to `/*/*/*/*[Category=Unit]`, so only unit-test projects (tagged `[Category=Unit]` by the `Purview.BuildSdk`) are executed; integration tests (which require Docker/Testcontainers) run locally via `just test`. The PR workflow does not tag, release, or publish packages. ## Versioning model `package.json` is the authoritative release version source. The release workflow reads: ```bash bun -p "require('./package.json').version" ``` This flow assumes version prep already happened before release (for example with `@changesets/cli` versioning and changelog updates merged to `main`). The release pipeline does not invent or auto-bump versions. ## Release on push to main `release.yml` triggers on push to `main` and delegates to the shared `purview-release.yml` workflow with `release-mode: NuGet`. The shared workflow: 1. Reads `package.json` `version` and computes the `v` tag. 2. Skips the entire release if `v` already exists (so re-merging to `main`, or merging `main` into a `release` branch, releases exactly once). 3. Restores, builds, lints, runs unit tests, packs, and validates packages. 4. Pushes every `.nupkg` to nuget.org (`--skip-duplicate`). 5. Creates the `v` GitHub release with generated release notes and attaches the package artifacts. A release is therefore produced simply by bumping `package.json` (via changesets) and merging to `main`. Do not create release tags manually. ## Prerelease support Prerelease versions (any SemVer containing a hyphen, for example `1.0.0-prerelease.28`) release through the same push-to-`main` flow. The `v` tag and GitHub release are still created and packages published; the shared pipeline does not mark the GitHub release with the prerelease flag. ## Stable release preparation A stable release (for example `1.0.0`) follows the same push-to-`main` flow, but requires version preparation before the merge: 1. Set `package.json` `version` to the stable version (for example `1.0.0`). 2. Align the build SDK in `global.json` and the centrally-managed package versions in `Directory.Packages.props` with their stable releases. 3. Move every entry from `src/src/SourceGeneration/AnalyzerReleases.Unshipped.md` into `src/src/SourceGeneration/AnalyzerReleases.Shipped.md` under the released version, and leave the unshipped file empty (see below). 4. Add the release to `CHANGELOG.md` and update the [release notes](../release-notes/) page. 5. Validate locally (`just lint-check` and `just pipeline-pack-validate`) before merging to `main`. The GitHub release is created with automatically generated notes, grouped by the categories in `.github/release.yml`. ## Analyzer release tracking `src/src/SourceGeneration` ships public diagnostics (`SG0001`–`SG0020`), so it maintains the Roslyn release tracking files: - `AnalyzerReleases.Shipped.md` — rules that have shipped in a released version. - `AnalyzerReleases.Unshipped.md` — rules added or changed since the last release. `Microsoft.CodeAnalysis.Analyzers` automatically adds these files as analyzer additional files when they exist in the project directory, so they are validated during the build (Roslyn components build with `TreatWarningsAsErrors`). Add new rules to the *unshipped* file while developing, and move them into the *shipped* file as part of a release. See [Contributing](../contributing/) for the day-to-day workflow. ## Build-time dependencies Some centrally-managed packages are build/analysis-time only (consumed with `PrivateAssets`), for example `Purview.Telemetry.SourceGenerator`, which `Purview.BuildSdk` injects into every non-test C# project, and `Purview.SourceGeneratorFramework`. They never flow to consumers of the NuGet package, but the repository cannot restore unless the pinned version exists on a configured feed. A stable release therefore requires the pinned versions to be resolvable before merging to `main`. ## NuGet publishing NuGet publishing uses the shared workflow's API-key path with the organization `NUGET__APIKEY` secret (available through `secrets: inherit`). The pipeline also accepts `NUGET_APIKEY`. No long-lived repository-level API key secrets are required. To use NuGet Trusted Publishing (OIDC) instead, the consuming repository would need to mint the federated credential before the shared pipeline runs; the shared workflow itself does not perform the `NuGet/login` step. ## Shared pipeline configuration `purview-build.json` at the repository root drives the pipeline: | Key | Value | Purpose | | --- | --- | --- | | `Build:Solution` | `src/ResourceKit.slnx` | Solution passed to restore/build/pack | | `Build:TestRoot` | `src/tests` | Test project discovery root | | `Build:TestPatterns` | `*Tests.csproj` | Test projects discovered for the test step | | `Build:TestFilter` | `/*/*/*/*[Category=Unit]` | TUnit tree-node filter (unit-only) | | `PackValidation:RequireSymbolPackage` | `true` | Every `.nupkg` needs a matching `.snupkg` | | `PackValidation:RequireSymbolFiles` | `true` | Every `.snupkg` must contain PDBs | | `PackValidation:RequiredContent` | Expected package contents | Asserts the package ships its expected output — `lib/net8.0|net9.0|net10.0` runtime assemblies + XML docs, the analyzer assembly, `buildTransitive/Purview.Aspire.ResourceKit.props`, `README.md`, and `purview-logo-light.png` | | `Release:Mode` | `None` | Publishing is enabled only by the release workflow | Configuration precedence is command line, environment variables, `purview-build.json`, then the tool's built-in defaults. Nested environment keys use `__`, for example `Release__Mode=NuGet`. --- # Release notes Stable release history for `Purview.Aspire.ResourceKit`, with the highlights that matter when you upgrade from the `1.0.0-prerelease.*` line. The full, machine-readable history lives in [`CHANGELOG.md`](https://github.com/purview-dev/aspire-resourcekit/blob/main/CHANGELOG.md) and on the [GitHub releases page](https://github.com/purview-dev/aspire-resourcekit/releases). ## 1.0.0 The first stable release. The public surface that shipped during the prerelease line is now frozen under [semantic versioning](../versioning/). ### Highlights - **Attributes drive generation** — `[HostKit]` and `[ResourceDefinition]` / `[ResourceDefinition]` produce the host kit, resource kit skeletons, typed options, and the AppHost extension method. See [Attributes Reference](../attributes-reference/) and [Generated Output](../generated-output/). - **Deterministic lifecycle** — `Build`/`BuildResource(...)` constructs resources, then `Configure`/`ConfigureResource()` wires them together. See [Lifecycle: Build vs Configure](../lifecycle-build-configure/). - **Declarative enablement** — `IsEnabled` and `IsResourceEnabled(builder)`. See [Enablement](../enablement/). - **Typed, extendable options** — generated `sealed partial` options bound from configuration. See [Configuration and Options](../configuration-and-options/). - **Test-friendly overrides** — `OptionsHelper` for args, environment variables, and flattening. See [OptionsHelper](../optionshelper/) and [Testing with ResourceKit](../testing-with-resourcekit/). - **Project resources** — first-class `AddProject()` support with SG0018/SG0019 validation. See [Project Resources](../project-resources/). - **Diagnostics SG0001–SG0020** with an IDE code fix for SG0020. See [Diagnostics](../diagnostics/). - **Bundled agent skill** — `aspire-apphost-to-resourcekit` auto-installs into `.agents/skills/**`. See [Bundled Agent Skills](../agent-skills/). ### Upgrade notes - No public API changes are required when moving from `1.0.0-prerelease.*` to `1.0.0`. - Pin `Purview.Aspire.ResourceKit` to `1.0.0` (or a later compatible `1.x`). ## See also - [Versioning and compatibility](../versioning/) - [Release flow](../release-flow/) - [Getting Started](../) --- # Versioning and compatibility `Purview.Aspire.ResourceKit` follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html) (`MAJOR.MINOR.PATCH`). The release version is authoritative in `package.json`; the release pipeline tags the commit `v` and publishes matching NuGet packages. ## What the version numbers mean - **MAJOR** — a breaking change to the public surface (see below). - **MINOR** — additive, backwards-compatible functionality. - **PATCH** — backwards-compatible fixes and documentation updates. ## Prereleases Prereleases use a hyphenated suffix (for example `1.0.0-prerelease.41`) and ship through the same push-to-`main` pipeline. They are published so consumers can validate upcoming changes early; they carry no stability guarantee. `1.0.0` is the first stable release. ## What counts as a breaking change Once a version is stable, the following are treated as public contracts: - The attributes `[HostKit]`, `[ResourceDefinition]`, and `[ResourceDefinition]`, including their named arguments (`Name`, `PropertyName`, `ExtensionMethodName`, `GenerateOptions`). - The runtime types and members consumed by AppHost code (`HostKitBase`, `ResourceKitBase`, `OptionsHelper`, and the `IResourceBuilder`/`WithEnvironment` extensions). - The names and shapes of generated members (`BuildResource`, `ConfigureResource`, `IsResourceEnabled`, generated host properties, generated options types, and the generated `Add()` extension method). - The diagnostic IDs `SG0001`–`SG0020`, their meanings, and their severities. Renaming or removing any of these requires a MAJOR version and a documented migration path. ### Not covered - The exact text of generated code and diagnostic messages. - Files generated into a consuming repository (for example the copied `.agents/skills/**` folder). - Build-time-only dependencies consumed through `PrivateAssets` (they never flow to consumers). ## Diagnostics as contracts Diagnostic IDs are stable identifiers. New rules are introduced in the *unshipped* analyzer release tracking file and only become part of a released version when they move into the *shipped* file as part of a release. See [Contributing](../contributing/) and [Release flow](../release-flow/) for the release-time procedure. ## See also - [Release notes](../release-notes/) - [Release flow](../release-flow/)