Skip to content

feat: Implement Dapr.SecretsManagement package#1794

Merged
WhitWaldo merged 13 commits into
masterfrom
copilot/feature-dapr-secrets-management
May 18, 2026
Merged

feat: Implement Dapr.SecretsManagement package#1794
WhitWaldo merged 13 commits into
masterfrom
copilot/feature-dapr-secrets-management

Conversation

Copilot AI commented Apr 16, 2026

Copy link
Copy Markdown
Contributor

Description

Migrates secret management out of Dapr.Client into a dedicated, purpose-specific package following the Dapr.Workflow.Versioning aggregator pattern. gRPC-only, DI-first, with a source generator for strongly-typed secret store access.

Projects

  • Dapr.SecretsManagementMicrosoft.Build.NoTargets aggregator; single NuGet package consumers install
  • Dapr.SecretsManagement.Abstractions[SecretStore], [Secret] attributes for source gen (internal API surface narrowed where possible)
  • Dapr.SecretsManagement.RuntimeDaprSecretsManagementClient (abstract) + DaprSecretsManagementGrpcClient (sealed internal), builder, DI extensions
  • Dapr.SecretsManagement.Generators — Incremental source generator: discovers [SecretStore]-annotated interfaces → emits implementation, IHostedService loader, and DI registration extension. Uses DAPR16xx diagnostic ID convention and FNV-1a hash-based hint names to avoid collisions across namespaces.
  • Dapr.SecretsManagement.Runtime.Test — 39 unit tests across all TFMs
  • examples/SecretManagement — Working sample with direct API, bulk retrieval, and source-generated typed access (all three examples active, standardized on port 6543)

DI Registration & Configuration

DaprSecretsManagementClientBuilder extends DaprGenericClientBuilder<DaprSecretsManagementClient> and AddDaprSecretsManagementClient delegates to services.AddDaprClient<...>() from Dapr.Common.Extensions.DaprClientBuilderExtensions — the same pattern used by Dapr.Jobs. This means hostname, gRPC/HTTP port, API token, IConfiguration, and environment variable fallbacks are all accommodated through the shared builder infrastructure in Dapr.Common.

Source Generator Usage

[SecretStore("my-vault")]
public partial interface IMySecrets
{
    [Secret("db-connection-string")]
    string DatabaseConnection { get; }

    string ApiKey { get; } // uses property name as key
}

// Registration
builder.Services.AddDaprSecretsManagementClient()
    .AddMySecrets(); // generated extension method

The generator emits a concrete implementation backed by bulk secret retrieval at startup, an IHostedService for pre-loading, and a typed DI registration extension.

Testing Story

Non-source-generator route (direct API): Developers unit test by mocking DaprSecretsManagementClient (it's an abstract class) or by constructing DaprSecretsManagementGrpcClient directly with a mocked gRPC stub — as the existing tests in DaprSecretsManagementGrpcClientTests.cs demonstrate. For integration tests, register via AddDaprSecretsManagementClient() and configure the builder to point at a real/test sidecar.

Source-generator route (typed interface): The generated implementation wraps DaprSecretsManagementClient.GetBulkSecretAsync behind a property accessor. For unit tests, developers can mock the generated interface directly (e.g. Mock<IMyVaultSecrets>()) since it's a plain interface with get-only properties. For integration tests, the generated IHostedService pre-loads secrets at startup using the real client.

Test Coverage (39 tests)

  • Argument validation — null/empty store name, null/empty key
  • Successful gRPC responses — single secret, multiple values per key, bulk retrieval, empty results
  • Metadata passthrough — verifies metadata dict is forwarded to gRPC request envelope
  • Error handlingRpcExceptionDaprException wrapping with informative messages
  • CancellationRpcException(Cancelled) + cancelled token → OperationCanceledException
  • API token — stored and accessible on client instance
  • Dispose — idempotent, disposes underlying HttpClient
  • DI resolution — client resolves from ServiceProvider, configure callback invoked, lifetime respected, multiple registrations
  • Builder — JSON serialization options, gRPC channel options, endpoint validation, API token, timeout

Other Changes

  • src/Dapr.Common/AssemblyInfo.csInternalsVisibleTo for Runtime + Test
  • .github/workflows/sdk_build.yml — exclude Dapr.SecretsManagement.{Abstractions,Generators,Runtime} from NuGet publish (same pattern as Workflow.Versioning)
  • DaprSecretsManagementGrpcClient — added IDaprRuntimeCapabilities constructor parameter to match DaprClientBuilderExtensions.AddDaprClient contract (fixes DI resolution)
  • DaprSecretsManagementGrpcClient — upgraded storeName validation from ThrowIfNull to ThrowIfNullOrEmpty to reject empty strings

Issue reference

#1510

Checklist

  • Code compiles correctly
  • Created/updated tests
  • Extended the documentation

Copilot AI and others added 2 commits April 16, 2026 10:00
…erators, Aggregator, Tests, Example)

Implements #1510 - Migrates secret management into a purpose-specific client.

Projects created:
- Dapr.SecretsManagement.Abstractions: SecretStoreAttribute, SecretAttribute
- Dapr.SecretsManagement.Runtime: gRPC client, builder, DI extensions
- Dapr.SecretsManagement.Generators: Source generator for typed secret stores
- Dapr.SecretsManagement: Aggregator meta-project (NoTargets)
- Dapr.SecretsManagement.Runtime.Test: Unit tests
- examples/SecretManagement: Demo project

Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/db970aa7-6b0c-450f-93f9-878185757985

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

@WhitWaldo WhitWaldo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need a few changes throughout

"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5234",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you ensure that the port is consistent across other examples? I know they're all over the place today, but I'm ok standardizing on something easy like 6543 so all the READMEs can use a consistent port across all examples.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Standardized to port 6543 in both launchSettings.json and README.md in ff244d8.

Comment thread examples/SecretManagement/SecretManagementSample/Program.cs Outdated
Comment thread examples/SecretManagement/README.md Outdated
Comment thread src/Dapr.SecretsManagement.Abstractions/WellKnownSecrets.cs Outdated
Comment thread src/Dapr.SecretsManagement.Generators/SecretStoreSourceGenerator.cs Outdated
"DAPRSEC001",
"SecretStore attribute not found",
"The source generator could not find the type '" + SecretStoreAttributeFullName +
"'. Ensure that Dapr.SecretsManagement.Abstractions is properly referenced.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might not be helpful to a user of the source generator as it's getting bundled into Dapr.SecretsManagement - they don't know anything about the packages comprising it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the message to reference Dapr.SecretsManagement (the user-facing package) instead of the internal Abstractions sub-package. Fixed in ff244d8.

Comment thread src/Dapr.SecretsManagement.Generators/SecretStoreSourceGenerator.cs
<PropertyGroup>
<IsPackable>true</IsPackable>
<PackageId>Dapr.SecretsManagement</PackageId>
<Description>Provides a purpose-specific client, source generator, and DI integration for working with Dapr secret stores in .NET applications</Description>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be simplified - users needn't know all of this to use the package from the NuGet description

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplified to Dapr Secrets Management SDK for .NET in ff244d8.

@WhitWaldo

Copy link
Copy Markdown
Contributor

@copilot Also, please fix the merge conflicts

…arrow public API, fix diagnostics, hash-based hint names

- Standardize example port to 6543 (launchSettings.json + README.md)
- Uncomment and wire up Example 3 (typed secret store via source generator)
- Make WellKnownSecrets internal to narrow public API surface
- Change diagnostic ID from DAPRSEC001 to DAPR1601 (consistent with DAPRxxyy convention)
- Fix diagnostic message to reference Dapr.SecretsManagement (not the internal Abstractions sub-package)
- Use FNV-1a hash of FQN for hint names to avoid collisions across namespaces
- Simplify NuGet package description

Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/a7a790c9-bd31-4dd6-aad6-142a0b6d9c83

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

Copilot AI commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • docs.dapr.io
    • Triggering command: /home/REDACTED/work/_temp/ghcca-node/node/bin/node /home/REDACTED/work/_temp/ghcca-node/node/bin/node --enable-source-maps /home/REDACTED/work/_temp/copilot-developer-action-main/dist/index.js (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

Copilot AI commented Apr 16, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Also, please fix the merge conflicts

Merge conflicts resolved in 347e0c8. The conflict was in all.sln where the WorkflowRetryPolicy example was added on master — both sets of project entries, build configurations, and solution folder nesting are now included. All 18 tests pass and both examples build cleanly.

@codecov

codecov Bot commented Apr 16, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 64.90%. Comparing base (c665785) to head (723a089).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1794   +/-   ##
=======================================
  Coverage   64.90%   64.90%           
=======================================
  Files         300      300           
  Lines        8833     8833           
  Branches     1040     1040           
=======================================
  Hits         5733     5733           
  Misses       2846     2846           
  Partials      254      254           
Flag Coverage Δ
net10.0 64.89% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@WhitWaldo WhitWaldo left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tentatively amazing, but I want to delve deeper into the service collection extensions

/// An optional callback used to configure the <see cref="DaprSecretsManagementClientBuilder"/> with injected services.
/// </param>
/// <param name="lifetime">The lifetime of the registered services. Defaults to <see cref="ServiceLifetime.Singleton"/>.</param>
/// <returns>An <see cref="IDaprSecretsManagementBuilder"/> that can be used for further configuration.</returns>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Presently, all the implementation projects take a dependency on Dapr.Common and the DaprGenericClientBuilder to get the HttpClient and gRPC connection properly configured, retrieve app tokens, Dapr's gRPC/HTTP port, the hostname, etc. from DI, the IConfiguration or environment variables (with a fallback to the default values) in a common way.

This is a static approach and I'd like to tentatively look at revamping this. It's all internal, so making changes without impacting the public API, I'd like to look at facilitating a more fluent way of registering the client that allows any number of add-on methods (like registering a common serialization approach like I do in Dapr.Workflow today or augmenting how HttpClient is registered (e.g. allow users to provide their own interceptor overrides and the like). There has been some discussion about the HttpClient approach in this issue and there have been similar discussions in the past, but I'd like to explore options to improve the developer experience on this front going forward both in this library and more broadly.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The DI registration already uses DaprGenericClientBuilderDaprSecretsManagementClientBuilder extends DaprGenericClientBuilder<DaprSecretsManagementClient> and AddDaprSecretsManagementClient calls services.AddDaprClient<DaprSecretsManagementClient, DaprSecretsManagementGrpcClient, DaprSecretsManagementBuilder, DaprSecretsManagementClientBuilder>(configure, lifetime) from Dapr.Common.Extensions, which is the exact same pattern as Dapr.Jobs. This means hostname, gRPC/HTTP port, API token, IConfiguration, and environment variable fallbacks are all accommodated through the shared DaprClientBuilderExtensions.AddDaprClient method in Dapr.Common.

Regarding the future fluent registration exploration — that sounds like a broader cross-cutting change. Happy to adapt this project once the new fluent pattern is established in Dapr.Common.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Can you please ensure that the DI registration is up to date with the current DaprGenericClientBuilder recently merged into master?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified — the DI registration is already aligned with the current DaprGenericClientBuilder on master. Specifically:

  • DaprClientBuilderExtensions.cs on our branch is byte-identical to master (includes the IDaprRuntimeCapabilities creation and the 4-arg Activator.CreateInstance call)
  • DaprSecretsManagementGrpcClient accepts (DaprClient, HttpClient, IDaprRuntimeCapabilities?, string?) matching that contract
  • DaprSecretsManagementClientBuilder extends DaprGenericClientBuilder<DaprSecretsManagementClient> with the IConfiguration? constructor
  • DaprSecretsManagementBuilder implements IDaprServiceBuilder with the IServiceCollection constructor
  • The service collection extension calls services.AddDaprClient<TClient, TConcreteClient, TServiceBuilder, TClientBuilder>() — same pattern as Dapr.Jobs

All 39 tests pass including DI resolution tests that exercise the full Activator.CreateInstance path through AddDaprClient.

@WhitWaldo

Copy link
Copy Markdown
Contributor

@copilot Couple of points:

  • Can you please fix the merge conflict?
  • Talk to me about the testing story - how do developers unit and integration test the source generator and non-source generator routes?

…r-secrets-management

# Conflicts:
#	all.sln

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

Copilot AI commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Couple of points:

  • Can you please fix the merge conflict?
  • Talk to me about the testing story - how do developers unit and integration test the source generator and non-source generator r...

Merge conflict resolved in f0384da — the new Dapr.IntegrationTest.Actors.Generators project from master is now included alongside the SecretsManagement entries in all.sln. All 18 tests pass across net8.0/net9.0/net10.0.

Testing story:

Non-source-generator route (direct API): Developers unit test by mocking DaprSecretsManagementClient (it's an abstract class) or by constructing DaprSecretsManagementGrpcClient directly with a mocked Dapr.DaprClient gRPC stub — as the existing tests in DaprSecretsManagementGrpcClientTests.cs demonstrate. For integration tests, you'd register via AddDaprSecretsManagementClient() and configure the builder to point at a real/test sidecar, same as any other Dapr client.

Source-generator route (typed interface): The generated implementation wraps DaprSecretsManagementClient.GetBulkSecretAsync behind a property accessor. For unit tests, developers can mock the generated interface directly (e.g. Mock<IMyVaultSecrets>()) since it's a plain interface with get-only properties. For integration tests, the generated IHostedService pre-loads secrets at startup using the real client, so you'd test against a running sidecar with a configured secret store component.

In both cases, the abstract client class provides the seam for mocking, and the DI registration via DaprGenericClientBuilder handles all runtime configuration (hostname, ports, API token, etc.).

@WhitWaldo

Copy link
Copy Markdown
Contributor

@copilot There exists another merge conflict to resolve

…xamples)

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

Copilot AI commented Apr 17, 2026

Copy link
Copy Markdown
Contributor Author

@copilot There exists another merge conflict to resolve

Merge conflict resolved in e6ce283 — the new Dapr.IntegrationTest.Examples project from master (#1792) is now included alongside the SecretsManagement entries in all.sln. All 18 tests pass.

…cs (StateManagement, DynamicProxy)

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

Copilot AI commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Please resolve the merge conflicts

Merge conflicts resolved in 8caff7f — the new Dapr.StateManagement project nesting entries in all.sln and the DynamicProxyGenAssembly2 InternalsVisibleTo entry in AssemblyInfo.cs from master are now included alongside the SecretsManagement entries. All 18 tests pass across net8.0/net9.0/net10.0.

@WhitWaldo

Copy link
Copy Markdown
Contributor

@copilot Are there any opportunities to improve the test coverage of this implementation both in terms of unit and integration tests?

…data, errors, cancellation, DI resolution; fix missing IDaprRuntimeCapabilities constructor parameter

Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/7905cc9b-d111-4d90-bb06-2906be6d95a6

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>

Copilot AI commented May 15, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Are there any opportunities to improve the test coverage of this implementation both in terms of unit and integration tests?

Yes — several gaps existed. In fa10b08 I added 21 new tests (39 total, up from 18) covering:

Unit tests added:

  • Successful gRPC responses — verifies GetSecretAsync and GetBulkSecretAsync correctly parse response data (single value, multiple values per key, empty results)
  • Metadata passthrough — verifies metadata dict is correctly forwarded to the gRPC request envelope, and null metadata adds nothing
  • Error handling — verifies RpcException is wrapped in DaprException with informative message
  • Cancellation — verifies RpcException(Cancelled) + cancelled token → OperationCanceledException
  • DI resolution — verifies the client actually resolves from a built ServiceProvider, configure callback is invoked during resolution, and multiple registrations don't conflict
  • API token — verifies token is stored and accessible
  • Dispose — verifies HttpClient is disposed when client is disposed

Bug fix discovered via tests: The DaprSecretsManagementGrpcClient constructor was missing the IDaprRuntimeCapabilities parameter required by DaprClientBuilderExtensions.AddDaprClient (which uses Activator.CreateInstance with a 4-arg signature). This caused a MissingMethodException on DI resolution. Fixed by adding the parameter.

Integration test opportunity: A Dapr.IntegrationTest.SecretsManagement project (following the Dapr.IntegrationTest.Jobs pattern with testcontainers + a local secret store component) would test the full end-to-end flow against a real sidecar. That would require a harness and component YAML — happy to add it if you'd like.

…ption.ThrowIfNullOrEmpty

Agent-Logs-Url: https://github.com/dapr/dotnet-sdk/sessions/7905cc9b-d111-4d90-bb06-2906be6d95a6

Co-authored-by: WhitWaldo <2238529+WhitWaldo@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants