Skip to content

feat: Implement Dapr.SecretsManagement as a purpose-specific client#1794

Draft
Copilot wants to merge 4 commits intomasterfrom
copilot/feature-dapr-secrets-management
Draft

feat: Implement Dapr.SecretsManagement as a purpose-specific client#1794
Copilot wants to merge 4 commits intomasterfrom
copilot/feature-dapr-secrets-management

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 16, 2026

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 — 18 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)

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.

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)

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>
Copy link
Copy Markdown
Contributor

@WhitWaldo WhitWaldo left a comment

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.

return Results.Ok(secrets);
});

// --- Example 3: Using typed secret store (source-generated) ---
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.

Why is example 3 commented out?

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.

Uncommented and fully wired up — the example now registers AddMyVaultSecrets() and maps a /typed-secrets endpoint that injects the source-generated IMyVaultSecrets. Fixed in ff244d8.

</PropertyGroup>

<!--
NOTE: When consuming from NuGet, use the single 'Dapr.SecretsManagement' package instead of
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.

Perfect

Comment thread examples/SecretManagement/README.md Outdated
## Running the Sample

```bash
dapr run --app-id secret-sample --app-port 5234 -- dotnet run
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.

Remember to use the standardized port here

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 to 6543 in ff244d8.

/// <summary>
/// Well-known names and constants related to Dapr Secrets Management.
/// </summary>
public static class WellKnownSecrets
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.

We should mark as many things internal and use InternalsVisibleTo as possible to narrow the public API

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.

Made WellKnownSecrets internal in ff244d8. The generator already uses its own string constants for attribute lookup, so no InternalsVisibleTo is needed.

{
spc.ReportDiagnostic(Diagnostic.Create(
new DiagnosticDescriptor(
"DAPRSEC001",
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.

We only use DAPR as a prefix and instead use the first two numbers of the identifier as a type identifier and refer to the documentation at https://docs.dapr.io/developing-applications/sdks/dotnet/dotnet-guidance/dotnet-guidance-source-generators/ for more details beyond the diagnostic message itself. We've generally reserved numbers for analyzers and codefixes - not sure how to approach diagnostics. Should this be handled in the same way?

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.

Changed to DAPR1601 following the DAPRxxyy convention (16 = SecretsManagement, matching how 13 = Workflow, 14 = Actors, 15 = Jobs). Fixed in ff244d8.

"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.


// Implementation class name: strip leading 'I' from interface name, append "SecretStoreClient".
var implName = interfaceName.StartsWith("I", StringComparison.Ordinal) && interfaceName.Length > 1 && char.IsUpper(interfaceName[1])
? interfaceName.Substring(1) + "SecretStoreClient"
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.

How well does this work if there are multiple secrets being registered and they share the same type name across different namespaces? Seems like this should instead do some sort of hash of the full type name instead and then append something more readable.

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.

Now uses implName + "_" + FNV-1a-hash(fullyQualifiedName).ToString("X8") + ".g.cs" for the hint name, e.g. MyVaultSecretsSecretStoreClient_2D89517D.g.cs. This ensures unique hint names even when different namespaces contain interfaces with the same simple name. Fixed in ff244d8.

<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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

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>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented Apr 16, 2026

@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
Copy link
Copy Markdown

codecov bot commented Apr 16, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 43.87%. Comparing base (5169561) to head (347e0c8).

❗ There is a different number of reports uploaded between BASE (5169561) and HEAD (347e0c8). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (5169561) HEAD (347e0c8)
net10.0 11 10
Additional details and impacted files
@@             Coverage Diff             @@
##           master    #1794       +/-   ##
===========================================
- Coverage   62.37%   43.87%   -18.51%     
===========================================
  Files         285      285               
  Lines        8421     8421               
  Branches      980      980               
===========================================
- Hits         5253     3695     -1558     
- Misses       2935     4508     +1573     
+ Partials      233      218       -15     
Flag Coverage Δ
net10.0 43.87% <ø> (-18.49%) ⬇️

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.

Copy link
Copy Markdown
Contributor

@WhitWaldo WhitWaldo left a comment

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.

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.

2 participants