diff --git a/daprdocs/content/en/developing-applications/building-blocks/workflow/workflow-patterns.md b/daprdocs/content/en/developing-applications/building-blocks/workflow/workflow-patterns.md index 8158ddfdbbc..88b8a3d1034 100644 --- a/daprdocs/content/en/developing-applications/building-blocks/workflow/workflow-patterns.md +++ b/daprdocs/content/en/developing-applications/building-blocks/workflow/workflow-patterns.md @@ -1423,7 +1423,7 @@ Here's an example workflow for an e-commerce process: 1. The payment is processed. 1. The order is shipped. 1. If any of the above actions results in an error, the actions are compensated with another action: - - The shipment is cancelled. + - The shipment is canceled. - The payment is refunded. - The inventory reservation is released. diff --git a/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/_index.md b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/_index.md new file mode 100644 index 00000000000..167068db60f --- /dev/null +++ b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/_index.md @@ -0,0 +1,11 @@ +--- +type: docs +title: "Dapr Secrets Management .NET SDK" +linkTitle: "Secrets Management" +weight: 52000 +description: Get up and running with Dapr Secrets Management .NET SDK +--- + +With the Dapr Secrets Management package, you can interact with the Dapr Secrets API from a .NET application to retrieve individual or bulk secrets from configured secret store components. The package also includes a source generator that provides strongly-typed access to your secrets via dependency injection. + +To get started, walk through the [Dapr Secrets Management]({{% ref dotnet-secrets-howto.md %}}) how-to guide and refer to [best practices documentation]({{% ref dotnet-secretsclient-usage.md %}}) for additional guidance. \ No newline at end of file diff --git a/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/dotnet-secrets-howto.md b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/dotnet-secrets-howto.md new file mode 100644 index 00000000000..98b6e39fdd7 --- /dev/null +++ b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/dotnet-secrets-howto.md @@ -0,0 +1,294 @@ +--- +type: docs +title: "How to: Manage secrets with the Dapr Secrets Management .NET SDK" +linkTitle: "How to: Manage secrets" +weight: 52050 +description: Learn how to retrieve and manage secrets using the Dapr Secrets Management .NET SDK +--- + +Let's walk through how to retrieve secrets from a Dapr secret store using the `Dapr.SecretsManagement` package. We'll +use the [sample project provided here](https://github.com/dapr/dotnet-sdk/tree/master/examples/SecretManagement) for +the following demonstration, covering direct secret retrieval, bulk secret retrieval, and strongly-typed access via the +included source generator. In this guide, you will: + +- Deploy a .NET Web API application ([SecretManagementSample](https://github.com/dapr/dotnet-sdk/tree/master/examples/SecretManagement/SecretManagementSample)) +- Utilize the Dapr .NET Secrets Management SDK to retrieve individual and bulk secrets +- Use the source generator to create a strongly-typed interface for your secret store + +In the .NET example project: +- The main [`Program.cs`](https://github.com/dapr/dotnet-sdk/tree/master/examples/SecretManagement/SecretManagementSample/Program.cs) contains all three usage patterns. +- The [`IMyVaultSecrets.cs`](https://github.com/dapr/dotnet-sdk/tree/master/examples/SecretManagement/SecretManagementSample/IMyVaultSecrets.cs) file demonstrates the typed secret store interface. + +## Prerequisites +- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/) +- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost) +- [.NET 8](https://dotnet.microsoft.com/download/dotnet/8.0), + [.NET 9](https://dotnet.microsoft.com/download/dotnet/9.0), or + [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) installed +- [Dapr.SecretsManagement](https://www.nuget.org/packages/Dapr.SecretsManagement) NuGet package installed to your project + +## Set up the environment +Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk). + +```sh +git clone https://github.com/dapr/dotnet-sdk.git +``` + +From the .NET SDK root directory, navigate to the Dapr Secrets Management example. + +```sh +cd examples/SecretManagement +``` + +## Run the application locally + +To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the `SecretManagementSample` directory. + +```sh +cd SecretManagementSample +``` + +We'll run a command that starts both the Dapr sidecar and the .NET program at the same time. + +```sh +dapr run --app-id secretsapp --app-port 6543 --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run +``` + +> Dapr listens for HTTP requests at `http://localhost:3500` and internal gRPC requests at `http://localhost:4001`. + +## Register the Dapr Secrets Management client with dependency injection +The Dapr Secrets Management SDK provides an extension method to simplify the registration of the client. Before completing the dependency injection registration in `Program.cs`, add the following line: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Add anywhere between these two lines +builder.Services.AddDaprSecretsManagementClient(); + +var app = builder.Build(); +``` + +It's possible that you may want to provide some configuration options to the client that should be present with each call to the sidecar, such as a Dapr API token or a non-standard HTTP or gRPC endpoint. This is possible through use of an overload of the registration method that allows configuration of a `DaprSecretsManagementClientBuilder` instance: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprSecretsManagementClient((_, daprSecretsClientBuilder) => +{ + daprSecretsClientBuilder.UseDaprApiToken("abc123"); + daprSecretsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint +}); + +var app = builder.Build(); +``` + +Still, it's possible that whatever values you wish to inject need to be retrieved from some other source, itself registered as a dependency. There's one more overload you can use to inject an `IServiceProvider` into the configuration action method. In the following example, we register a fictional singleton that can retrieve configuration values from somewhere and pass it into the configuration method for `AddDaprSecretsManagementClient` so we can retrieve our Dapr API token from somewhere else for registration here: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddSingleton(); +builder.Services.AddDaprSecretsManagementClient((serviceProvider, daprSecretsClientBuilder) => +{ + var secretRetriever = serviceProvider.GetRequiredService(); + var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value; + daprSecretsClientBuilder.UseDaprApiToken(daprApiToken); + + daprSecretsClientBuilder.UseHttpEndpoint("http://localhost:8512"); +}); + +var app = builder.Build(); +``` + +## Use the Dapr Secrets Management client using IConfiguration +It's possible to configure the Dapr Secrets Management client using the values in your registered `IConfiguration` as well without explicitly specifying each of the value overrides using the `DaprSecretsManagementClientBuilder` as demonstrated in the previous section. Rather, by populating an `IConfiguration` made available through dependency +injection the `AddDaprSecretsManagementClient()` registration will automatically use these values over their respective defaults. + +Start by populating the values in your configuration. This can be done in several different ways as demonstrated below. + +### Configuration via `ConfigurationBuilder` +Application settings can be configured without using a configuration source and by instead populating the value in-memory using a `ConfigurationBuilder` instance: + +```csharp +var builder = WebApplication.CreateBuilder(); + +//Create the configuration +var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary { + { "DAPR_HTTP_ENDPOINT", "http://localhost:54321" }, + { "DAPR_API_TOKEN", "abc123" } + }) + .Build(); + +builder.Configuration.AddConfiguration(configuration); +builder.Services.AddDaprSecretsManagementClient(); //This will automatically populate the HTTP and gRPC endpoints and API token values from the IConfiguration +``` + +### Configuration via Environment Variables +Application settings can be accessed from environment variables available to your application. + +The following environment variables will be used to populate both the HTTP endpoint and API token used to register the Dapr Secrets Management client. + +| Key | Value | +| --- | --- | +| DAPR_HTTP_ENDPOINT | http://localhost:54321 | +| DAPR_API_TOKEN | abc123 | + +```csharp +var builder = WebApplication.CreateBuilder(); + +builder.Configuration.AddEnvironmentVariables(); +builder.Services.AddDaprSecretsManagementClient(); +``` + +The Dapr Secrets Management client will be configured to use both the HTTP endpoint `http://localhost:54321` and populate +all outbound requests with the API token header `abc123`. + +## Retrieve a single secret + +To retrieve a single secret from a Dapr secret store, use the `GetSecretAsync` method on the +`DaprSecretsManagementClient`. The method accepts the name of the secret store component, the secret key, and an +optional metadata dictionary. + +```cs +app.MapGet("/secrets/{storeName}/{key}", async ( + string storeName, + string key, + DaprSecretsManagementClient secretsClient, + CancellationToken cancellationToken) => +{ + var secret = await secretsClient.GetSecretAsync(storeName, key, cancellationToken: cancellationToken); + return Results.Ok(secret); +}); +``` + +The result is a `IReadOnlyDictionary`. Some secret stores (such as Kubernetes) can store multiple values +per key — each entry in the dictionary represents one such value. + +## Retrieve bulk secrets + +To retrieve all secrets that the application is allowed to access from a secret store, use the `GetBulkSecretAsync` +method: + +```cs +app.MapGet("/secrets/{storeName}", async ( + string storeName, + DaprSecretsManagementClient secretsClient, + CancellationToken cancellationToken) => +{ + var secrets = await secretsClient.GetBulkSecretAsync(storeName, cancellationToken: cancellationToken); + return Results.Ok(secrets); +}); +``` + +The result is a nested `IReadOnlyDictionary>`. The outer key is the secret +name; the inner dictionary contains one or more key-value pairs representing the secret data. + +## Passing metadata + +Both `GetSecretAsync` and `GetBulkSecretAsync` accept an optional `metadata` parameter. This allows you to pass +additional context to the secret store component. The valid metadata keys and values are determined by the type of +secret store in use. + +```cs +var metadata = new Dictionary +{ + { "version", "2" } +}; + +var secret = await secretsClient.GetSecretAsync("my-vault", "db-password", metadata: metadata); +``` + +## Use the source generator for strongly-typed secrets + +The `Dapr.SecretsManagement` package includes an incremental source generator that produces strongly-typed access to +your secret store via a simple interface definition. This eliminates the need to pass store names and key strings +throughout your codebase. + +### Define a typed secret store interface + +Create a `partial interface` decorated with the `[SecretStore]` attribute, specifying the Dapr secret store component +name. Each property maps to a single secret key. + +```cs +using Dapr.SecretsManagement.Abstractions; + +[SecretStore("my-vault")] +public partial interface IMyVaultSecrets +{ + [Secret("db-connection-string")] + string DatabaseConnection { get; } + + string ApiKey { get; } // Uses the property name "ApiKey" as the secret key +} +``` + +The `[Secret]` attribute is optional. When applied, it overrides the secret key to the specified value. When omitted, +the property name is used as the secret key. + +### Register the typed secret store + +The source generator produces a DI registration extension method named after your interface (e.g., `AddMyVaultSecrets()`). +Chain it after `AddDaprSecretsManagementClient()`: + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprSecretsManagementClient() + .AddMyVaultSecrets(); + +var app = builder.Build(); +``` + +### How it works + +The source generator emits: + +1. A concrete implementation of your interface that is backed by `DaprSecretsManagementClient.GetBulkSecretAsync`. +2. An `IHostedService` that pre-loads all mapped secrets at application startup. +3. A typed DI registration extension method on `IDaprSecretsManagementBuilder`. + +Because secrets are loaded in bulk at startup, properties are synchronous and available immediately after host startup +without requiring callers to manage async flows. + +### Consume the typed secrets + +Once registered, inject your interface anywhere via standard dependency injection: + +```cs +app.MapGet("/do-something", (IMyVaultSecrets secrets, IOtherSvc svc) => +{ + var svcInstance = svc.Build(secrets.ApiKey); + return svcInstnce.DoSomething(); +}); +``` + +## Testing your application + +### Unit testing with the direct API + +`DaprSecretsManagementClient` is an abstract class that provides a natural seam for mocking. You can mock it directly +in your unit tests: + +```cs +var mockClient = new Mock(); +mockClient + .Setup(c => c.GetSecretAsync("my-vault", "db-password", null, It.IsAny())) + .ReturnsAsync(new Dictionary { { "db-password", "s3cr3t" } }); +``` + +### Unit testing with the source generator + +The generated implementation wraps a plain interface with get-only properties, so you can mock it directly: + +```cs +var mockSecrets = new Mock(); +mockSecrets.Setup(s => s.DatabaseConnection).Returns("Server=localhost;..."); +mockSecrets.Setup(s => s.ApiKey).Returns("my-api-key"); +``` + +### Integration testing + +For integration tests, register via `AddDaprSecretsManagementClient()` and configure the builder to point at a +real or test sidecar. The generated `IHostedService` will pre-load secrets at startup using the real client, testing +the full end-to-end flow. diff --git a/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/dotnet-secretsclient-usage.md b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/dotnet-secretsclient-usage.md new file mode 100644 index 00000000000..fe0607497df --- /dev/null +++ b/sdkdocs/dotnet/content/en/dotnet-sdk-docs/dotnet-secrets/dotnet-secretsclient-usage.md @@ -0,0 +1,200 @@ +--- +type: docs +title: "DaprSecretsManagementClient usage" +linkTitle: "DaprSecretsManagementClient usage" +weight: 52900 +description: Essential tips and advice for using DaprSecretsManagementClient +--- + +## Lifetime management + +A `DaprSecretsManagementClient` is a subset of the Dapr client that is dedicated to interacting with the Dapr Secrets +API. It can be registered alongside a `DaprClient` and other Dapr clients without issue. + +It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and +implements `IDisposable` to support the eager cleanup of resources. + +For best performance, create a single long-lived instance of `DaprSecretsManagementClient` and provide access to that +shared instance throughout your application. `DaprSecretsManagementClient` instances are thread-safe and intended to be +shared. + +This can be aided by utilizing the dependency injection functionality. The registration method supports registration +as a singleton, a scoped instance or as transient (meaning it's recreated every time it's injected), but also enables +registration to utilize values from an `IConfiguration` or other injected service in a way that's impractical when +creating the client from scratch in each of your classes. + +Avoid creating a `DaprSecretsManagementClient` for each operation and disposing it when the operation is complete. + +## Configuring DaprSecretsManagementClient via the DaprSecretsManagementClientBuilder + +A `DaprSecretsManagementClient` can be configured by invoking methods on the `DaprSecretsManagementClientBuilder` class +before calling `.Build()` to create the client itself. The settings for each `DaprSecretsManagementClient` are separate +and cannot be changed after calling `.Build()`. + +```cs +var daprSecretsClient = new DaprSecretsManagementClientBuilder() + .UseDaprApiToken("abc123") // Specify the API token used to authenticate to other Dapr sidecars + .Build(); +``` + +The `DaprSecretsManagementClientBuilder` contains settings for: + +- The HTTP endpoint of the Dapr sidecar +- The gRPC endpoint of the Dapr sidecar +- The `JsonSerializerOptions` object used to configure JSON serialization +- The `GrpcChannelOptions` object used to configure gRPC +- The API token used to authenticate requests to the sidecar +- The factory method used to create the `HttpClient` instance used by the SDK +- The timeout used for the `HttpClient` instance when making requests to the sidecar + +The SDK will read the following environment variables to configure the default values: + +- `DAPR_HTTP_ENDPOINT`: used to find the HTTP endpoint of the Dapr sidecar, example: `https://dapr-api.mycompany.com` +- `DAPR_GRPC_ENDPOINT`: used to find the gRPC endpoint of the Dapr sidecar, example: `https://dapr-grpc-api.mycompany.com` +- `DAPR_HTTP_PORT`: if `DAPR_HTTP_ENDPOINT` is not set, this is used to find the HTTP local endpoint of the Dapr sidecar +- `DAPR_GRPC_PORT`: if `DAPR_GRPC_ENDPOINT` is not set, this is used to find the gRPC local endpoint of the Dapr sidecar +- `DAPR_API_TOKEN`: used to set the API token + +### Configuring gRPC channel options + +Dapr's use of `CancellationToken` for cancellation relies on the configuration of the gRPC channel options. If you need +to configure these options yourself, make sure to enable the [ThrowOperationCanceledOnCancellation setting](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation). + +```cs +var daprSecretsClient = new DaprSecretsManagementClientBuilder() + .UseGrpcChannelOptions(new GrpcChannelOptions { ... ThrowOperationCanceledOnCancellation = true }) + .Build(); +``` + +## Using cancellation with DaprSecretsManagementClient + +The APIs on `DaprSecretsManagementClient` perform asynchronous operations and accept an optional `CancellationToken` +parameter. This follows a standard .NET practice for cancellable operations. Note that when cancellation occurs, there +is no guarantee that the remote endpoint stops processing the request, only that the client has stopped waiting for +completion. + +When an operation is cancelled, it will throw an `OperationCancelledException`. + +## Configuring DaprSecretsManagementClient via dependency injection + +Using the built-in extension methods for registering the `DaprSecretsManagementClient` in a dependency injection +container can provide the benefit of registering the long-lived service a single time, centralize complex configuration +and improve performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. `HttpClient` +instances). + +There are three overloads available to give the developer the greatest flexibility in configuring the client for their +scenario. Each of these will register the `IHttpClientFactory` on your behalf if not already registered, and configure +the `DaprSecretsManagementClientBuilder` to use it when creating the `HttpClient` instance in order to re-use the same +instance as much as possible and avoid socket exhaustion and other issues. + +In the first approach, there's no configuration done by the developer and the `DaprSecretsManagementClient` is +configured with the default settings. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprSecretsManagementClient(); //Registers the `DaprSecretsManagementClient` to be injected as needed +var app = builder.Build(); +``` + +Sometimes the developer will need to configure the created client using the various configuration options detailed +above. This is done through an overload that passes in the `DaprSecretsManagementClientBuilder` and exposes methods +for configuring the necessary options. + +```cs +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprSecretsManagementClient((_, daprSecretsClientBuilder) => { + //Set the API token + daprSecretsClientBuilder.UseDaprApiToken("abc123"); + //Specify a non-standard HTTP endpoint + daprSecretsClientBuilder.UseHttpEndpoint("http://dapr.my-company.com"); +}); + +var app = builder.Build(); +``` + +Finally, it's possible that the developer may need to retrieve information from another service in order to populate +these configuration values. That value may be provided from a `DaprClient` instance, a vendor-specific SDK or some +local service, but as long as it's also registered in DI, it can be injected into this configuration operation via the +last overload: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Register a fictional service that retrieves secrets from somewhere +builder.Services.AddSingleton(); + +builder.Services.AddDaprSecretsManagementClient((serviceProvider, daprSecretsClientBuilder) => { + //Retrieve an instance of the `SecretService` from the service provider + var secretService = serviceProvider.GetRequiredService(); + var daprApiToken = secretService.GetSecret("DaprApiToken").Value; + + //Configure the `DaprSecretsManagementClientBuilder` + daprSecretsClientBuilder.UseDaprApiToken(daprApiToken); +}); + +var app = builder.Build(); +``` + +## Chaining the source generator registration + +The `AddDaprSecretsManagementClient()` method returns an `IDaprSecretsManagementBuilder` that enables chaining of +source-generator-produced registration methods. First, you need to create an appropriately decorated interface: + +```cs +[SecretStore("my-vault")] +public partial interface IMyVaultSecrets +{ + /// + /// The database connection string, retrieved from the "db-connection-string" secret key. + /// + [Secret("db-connection-string")] + string DatabaseConnection { get; } + + /// + /// The API key. Uses the property name "ApiKey" as the secret name. + /// + string ApiKey { get; } +} +``` + +Because this interface is decorated with the `[SecretStore]` attribute specifying the name of the secret store component it should use, the source generator emits an extension method you can chain directly (replacing the conventional "I" prefix with "Add" from your interface type name): + +```cs +builder.Services.AddDaprSecretsManagementClient() + .AddMyVaultSecrets() // Generated from [SecretStore("my-vault")] on IMyVaultSecrets + .AddPaymentSecrets(); // Generated from another typed interface +``` + +Each call registers the typed interface, its concrete implementation, and an `IHostedService` that pre-loads the secrets at startup. + +From there, simply inject your `IMyVaultSecrets` into a type and the properties will be populated with the values from your secret store via Dapr. + +## Understanding the API response shapes + +### GetSecretAsync + +`GetSecretAsync` returns an `IReadOnlyDictionary`. Most secret stores return a single key-value pair, +but some (like Kubernetes) can store multiple values per secret key. Each entry in the dictionary represents one such +value. + +### GetBulkSecretAsync + +`GetBulkSecretAsync` returns an `IReadOnlyDictionary>`. The outer key is +the secret name; the inner dictionary contains one or more key-value pairs representing the secret data for that key. + +## Error handling + +Methods on `DaprSecretsManagementClient` will throw a `DaprException` if an issue is encountered when communicating +with the Dapr sidecar. In case of illegal argument values, the appropriate standard exception will be thrown +(e.g. `ArgumentNullException` or `ArgumentException`) with the name of the offending argument. When an operation is +canceled via a `CancellationToken`, an `OperationCanceledException` will be thrown. + +The most common cases of failure will be related to: + +- Incorrect argument formatting (e.g. an empty store name or key) +- Transient failures such as a networking problem +- The specified secret store component not being configured or available + +In any of these cases, you can examine more exception details through the `.InnerException` property.