Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .agents/skills/project-placement-defaults/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Align identities with existing repository conventions:
- Test project names should clearly indicate scope/type with recognized test suffixes.
- `NamespacePrefix` should remain the root identity source for the repo.
- `RootNamespace` usually flows from the logical project identity generated by the SDK; avoid custom namespace overrides unless required.
- `AssemblyName` and `PackageId` default to the fully evaluated `RootNamespace`, so a project's package/assembly identity follows its namespace unless the repo explicitly overrides `AssemblyName`/`PackageId` or opts out via `EnableAssemblyNameGeneration=false`.
- `AssemblyName` and `PackageId` default to the fully evaluated `RootNamespace` — or to the full logical project name when suffix-stripping removed a segment (e.g. `Shared`, `ServiceDefaults`) — so a project's package/assembly identity follows its namespace and stays distinct, unless the repo explicitly overrides `AssemblyName`/`PackageId`/`RootNamespace` or opts out via `EnableAssemblyNameGeneration=false`.
- When moving files between projects, update namespaces so they match the destination project's conventions.

Do not invent a new naming scheme when an existing one is already in use.
Expand Down
2 changes: 1 addition & 1 deletion .config/lefthook.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ pre-commit:
commit-msg:
jobs:
- name: conventional commits
run: npx commitlint --edit {1}
run: bunx commitlint --edit {1}
52 changes: 30 additions & 22 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
<Project>
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
<TUnitVersion>1.65.63</TUnitVersion>
<PurviewSourceGeneratorFrameworkVersion>1.0.0-prerelease.37</PurviewSourceGeneratorFrameworkVersion>
<RoslynVersion>5.9.0</RoslynVersion>
<TUnitVersion>1.67.0</TUnitVersion>
<PurviewSourceGeneratorFrameworkVersion>1.0.0-prerelease.39</PurviewSourceGeneratorFrameworkVersion>
<DotNetRuntimeVersion>10.0.12</DotNetRuntimeVersion>
<MSExtensionVersion>10.10.0</MSExtensionVersion>
<OTelVersion>1.18.0</OTelVersion>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="5.9.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.Analyzers" Version="$(RoslynVersion)" />
<PackageVersion Include="Aspire.Azure.Npgsql" Version="13.4.6" />
<PackageVersion Include="Aspire.Azure.Security.KeyVault" Version="13.4.6" />
<PackageVersion Include="Aspire.Azure.Storage.Blobs" Version="13.4.6" />
Expand All @@ -18,18 +22,22 @@
<PackageVersion Include="Aspire.Hosting.Azure.Storage" Version="13.4.6" />
<PackageVersion Include="Aspire.Microsoft.Azure.StackExchangeRedis" Version="13.4.6" />
<PackageVersion Include="Bogus" Version="35.6.5" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="10.0.10" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="10.0.10" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.400" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="10.0.10" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="5.9.0" />
<!-- Keep Roslyn pinned only for netstandard2.0 because the generator must not pull System.Collections.Immutable 10.x into net8+ consumer and test builds. -->
<!--<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" Condition="'$(TargetFramework)' == 'netstandard2.0'" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.13.0" Condition="'$(TargetFramework)' == 'net8.0' Or '$(TargetFramework)' == 'net9.0' Or '$(TargetFramework)' == 'net10.0'" />-->
<PackageVersion Include="Microsoft.Extensions.Telemetry.Abstractions" Version="10.8.0" />
<PackageVersion Include="Purview.Telemetry.SourceGenerator" Version="5.0.0-prerelease.6" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="$(DotNetRuntimeVersion)" />
<PackageVersion
Include="Microsoft.Extensions.DependencyInjection.Abstractions"
Version="$(DotNetRuntimeVersion)"
/>
<PackageVersion Include="Microsoft.Extensions.Options" Version="$(DotNetRuntimeVersion)" />
<PackageVersion
Include="Microsoft.Extensions.Options.ConfigurationExtensions"
Version="$(DotNetRuntimeVersion)"
/>
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Abstractions" Version="$(DotNetRuntimeVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp.Workspaces" Version="$(RoslynVersion)" />
<PackageVersion Include="Microsoft.Extensions.Telemetry.Abstractions" Version="$(MSExtensionVersion)" />
<PackageVersion Include="Purview.Telemetry.SourceGenerator" Version="5.0.0-prerelease.7" />
<PackageVersion
Include="Purview.SourceGeneratorFramework"
Version="$(PurviewSourceGeneratorFrameworkVersion)"
Expand All @@ -42,12 +50,12 @@
<PackageVersion Include="TUnit.Core" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Aspire" Version="$(TUnitVersion)" />
<PackageVersion Include="TUnit.Mocks" Version="$(TUnitVersion)" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="10.8.0" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="10.8.0" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="1.17.0" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="1.17.0" />
<PackageVersion Include="Microsoft.Extensions.Http.Resilience" Version="$(MSExtensionVersion)" />
<PackageVersion Include="Microsoft.Extensions.ServiceDiscovery" Version="$(MSExtensionVersion)" />
<PackageVersion Include="OpenTelemetry.Exporter.OpenTelemetryProtocol" Version="$(OTelVersion)" />
<PackageVersion Include="OpenTelemetry.Extensions.Hosting" Version="$(OTelVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.AspNetCore" Version="$(OTelVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Http" Version="$(OTelVersion)" />
<PackageVersion Include="OpenTelemetry.Instrumentation.Runtime" Version="$(OTelVersion)" />
</ItemGroup>
</Project>
1 change: 1 addition & 0 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ pipeline-release *args:
[group('Pipeline')]
pipeline-local-release *args:
just ensure-pipeline-tool
just lint-fix
echo "Running local release pipeline..."
"{{ pipeline_tool }}" --Release:Mode=LocalNuGet {{ args }}

Expand Down
15 changes: 9 additions & 6 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,16 +99,19 @@ Use this hook to react to runtime state, for example environment-specific availa
Use `OptionsHelper` to generate command-line configuration arguments from strongly typed assignments.

```csharp
var args = OptionsHelper.ForSet<ShopHostKitOptions>(
var args = OptionsHelper.Assign<ShopHostKitOptions>(
c => c.API.IsEnabled = false,
c => c.API.Name = "api-test"
).Build();
```

For a single argument, use `ForOne` with a member selector and access the first element:
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.

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 arg = OptionsHelper.ForOne<ShopHostKit.ShopHostKitOptions>(f => f.API.Name).Build()[0];
var path = OptionsHelper.PathFor<ShopHostKit.ShopHostKitOptions>(f => f.API.Name);
// "API.Name"
```

Resulting args are in this form:
Expand All @@ -119,7 +122,7 @@ Resulting args are in this form:
To produce environment variables instead, call `AsEnvironmentVariables()` before `Build()`:

```csharp
var envVars = OptionsHelper.ForSet<ShopHostKitOptions>(
var envVars = OptionsHelper.Assign<ShopHostKitOptions>(
c => c.API.IsEnabled = false,
c => c.API.Name = "api-test"
).AsEnvironmentVariables().Build();
Expand All @@ -134,11 +137,11 @@ This returns a dictionary such as `{"ShopHostKit__API__IsEnabled": "false", "Sho
> protected override string[] Args =>
> [
> .. base.Args,
> .. OptionsHelper.ForSet<ShopHostKit.ShopHostKitOptions>(
> .. OptionsHelper.Assign<ShopHostKit.ShopHostKitOptions>(
> c => c.API.IsEnabled = false,
> c => c.API.Name = "api-test"
> ).Build(),
>];
> ];
> ```

## Section-name resolution order
Expand Down
39 changes: 37 additions & 2 deletions docs/diagnostics.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,43 @@ exiting the constructor" warning does not apply.
| SG0015 | Error | Generic `ResourceDefinition<TResource>` cannot declare explicit base type |
| SG0016 | Error | No Aspire resource type could be inferred/found |
| SG0017 | Warning | An `IResourceBuilder<T>` property is never assigned in `BuildResource` or `ConfigureResource` |
| SG0018 | Error | A project resource kit does not add the declared project via `AddProject<T>()` |
| SG0019 | Error | A project resource kit declares an explicit base class that does not use `ProjectResource` |
| SG0018 | Warning | A project resource kit does not add the declared project via `AddProject<T>()` |
| 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 |

## 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<T>()` 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<TOptions>(...)` action must set exactly one property path. A block-bodied lambda
such as the following is rejected at compile time:

```csharp
OptionsHelper.Assign<ShopHostKitOptions>(o =>
{
o.API.IsEnabled = false;
o.API.Name = "api-test";
});
```

Split each property into its own assignment argument instead:

```csharp
OptionsHelper.Assign<ShopHostKitOptions>(
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.

## Fast troubleshooting checklist

Expand Down
4 changes: 1 addition & 3 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
{
"sdk": {
"version": "10.0.400",
"rollForward": "latestMajor",
"allowPrerelease": false
},
"msbuild-sdks": {
"Purview.DotNetProjectSdk": "1.0.0-prerelease.47"
"Purview.DotNetProjectSdk": "1.0.0-prerelease.51"
},
"test": {
"runner": "Microsoft.Testing.Platform"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

{
"name": "purview-aspire-appresources",
"version": "1.0.0-prerelease.24",
"version": "1.0.0-prerelease.25",
"keywords": [],
"homepage": "https://github.com/purview-dev/aspire-appresources#readme",
"bugs": {
Expand Down
1 change: 1 addition & 0 deletions src/ResourceKit.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
<Folder Name="/src/">
<Project Path="src/ResourceKit/ResourceKit.csproj" />
<Project Path="src/SourceGeneration/SourceGeneration.csproj" />
<Project Path="src/SourceGeneration.CodeFixes/SourceGeneration.CodeFixes.csproj" />
</Folder>
<Folder Name="/src/examples/">
<Project Path="src/Example.AppHost/Example.AppHost.csproj" />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel;
using System.ComponentModel;
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;

Expand All @@ -21,7 +21,7 @@ public WebApplication MapDefaultEndpoints()
// Only health checks tagged with the "live" tag must pass for app to be considered alive
app.MapHealthChecks(
Platform.EndpointsDefinitions.Aliveness,
new HealthCheckOptions { Predicate = r => r.Tags.Contains("live") }
new HealthCheckOptions { Predicate = static r => r.Tags.Contains("live") }
);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System.ComponentModel;
using System.ComponentModel;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Logging;
Expand All @@ -22,7 +22,7 @@ public TBuilder AddServiceDefaults()

builder.Services.AddServiceDiscovery();

builder.Services.ConfigureHttpClientDefaults(http =>
builder.Services.ConfigureHttpClientDefaults(static http =>
{
// Turn on resilience by default
http.AddStandardResilienceHandler();
Expand Down Expand Up @@ -102,7 +102,7 @@ public TBuilder AddDefaultHealthChecks()
builder
.Services.AddHealthChecks()
// Add a default liveness check to ensure app is responsive
.AddCheck("self", () => HealthCheckResult.Healthy(), ["live"]);
.AddCheck("self", static () => HealthCheckResult.Healthy(), ["live"]);

return builder;
}
Expand Down
87 changes: 2 additions & 85 deletions src/src/ResourceKit/IOptionsBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
using System.Runtime.CompilerServices;

namespace Purview.Aspire.ResourceKit;

/// <summary>
Expand All @@ -15,7 +11,7 @@ public interface IOptionsBuilder
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="assignments">One or more property assignment actions.</param>
/// <returns>The same builder.</returns>
IOptionsBuilder ForSet<TOptions>(params Action<TOptions>[] assignments);
IOptionsBuilder Assign<TOptions>(params Action<TOptions>[] assignments);

/// <summary>
/// Adds entries from assignment expressions for the specified options type with an explicit root section name.
Expand All @@ -24,86 +20,7 @@ public interface IOptionsBuilder
/// <param name="sectionName">The root section name override.</param>
/// <param name="assignments">One or more property assignment actions.</param>
/// <returns>The same builder.</returns>
IOptionsBuilder ForSet<TOptions>(string sectionName, params Action<TOptions>[] assignments);

/// <summary>
/// Adds a single entry from an assignment expression for the specified options type.
/// </summary>
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="assignment">A property assignment expression.</param>
/// <param name="assignmentExpression">Captured source text for <paramref name="assignment"/>.</param>
/// <returns>The same builder.</returns>
IOptionsBuilder ForSetOne<TOptions>(
Action<TOptions> assignment,
[CallerArgumentExpression(nameof(assignment))] string assignmentExpression = ""
);

/// <summary>
/// Adds a single entry from an assignment expression for the specified options type with an explicit root section name.
/// </summary>
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="sectionName">The root section name override.</param>
/// <param name="assignment">A property assignment expression.</param>
/// <param name="assignmentExpression">Captured source text for <paramref name="assignment"/>.</param>
/// <returns>The same builder.</returns>
IOptionsBuilder ForSetOne<TOptions>(
string sectionName,
Action<TOptions> assignment,
[CallerArgumentExpression(nameof(assignment))] string assignmentExpression = ""
);

/// <summary>
/// Adds a single entry from a member selector expression for the specified options type.
/// </summary>
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="selector">A member selector expression.</param>
/// <returns>The same builder.</returns>
IOptionsBuilder ForOne<TOptions>(Expression<Func<TOptions, object?>> selector);

/// <summary>
/// Adds a single entry from a member selector expression for the specified options type with an explicit root section name.
/// </summary>
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="sectionName">The root section name override.</param>
/// <param name="selector">A member selector expression.</param>
/// <returns>The same builder.</returns>
IOptionsBuilder ForOne<TOptions>(string sectionName, Expression<Func<TOptions, object?>> selector);

/// <summary>
/// Adds entries from multiple member selector expressions for the specified options type.
/// </summary>
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="selector">The first member selector expression.</param>
/// <param name="selectors">Additional member selector expressions.</param>
/// <returns>The same builder.</returns>
[SuppressMessage(
"Naming",
"CA1716:Identifiers should not match keywords",
Justification = "For is the established OptionsHelper entry point name; it is not a C# keyword and is used only in this context."
)]
IOptionsBuilder For<TOptions>(
Expression<Func<TOptions, object?>> selector,
params Expression<Func<TOptions, object?>>[] selectors
);

/// <summary>
/// Adds entries from multiple member selector expressions for the specified options type with an explicit root section name.
/// </summary>
/// <typeparam name="TOptions">The root options type.</typeparam>
/// <param name="sectionName">The root section name override.</param>
/// <param name="selector">The first member selector expression.</param>
/// <param name="selectors">Additional member selector expressions.</param>
/// <returns>The same builder.</returns>
[SuppressMessage(
"Naming",
"CA1716:Identifiers should not match keywords",
Justification = "For is the established OptionsHelper entry point name; it is not a C# keyword and is used only in this context."
)]
IOptionsBuilder For<TOptions>(
string sectionName,
Expression<Func<TOptions, object?>> selector,
params Expression<Func<TOptions, object?>>[] selectors
);
IOptionsBuilder Assign<TOptions>(string sectionName, params Action<TOptions>[] assignments);

/// <summary>
/// Builds the collected entries as command-line arguments (default mode).
Expand Down
Loading