This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr State Management .NET SDK

Get up and running with Dapr State Management .NET SDK

With the Dapr State Management package, you can interact with the Dapr State Management API from a .NET application to save, retrieve, delete, and query key/value state entries in configured state store components. The package supports single-key and bulk operations, optimistic concurrency control via ETags, state transactions, and state queries. It also includes a source generator that provides strongly-typed, store-scoped access via dependency injection.

To get started, walk through the Dapr State Management how-to guide and refer to best practices documentation for additional guidance.

1 - How to: Manage state with the Dapr State Management .NET SDK

Learn how to save, retrieve, delete, and query state using the Dapr State Management .NET SDK

Let’s walk through how to manage state using the Dapr.StateManagement package. We’ll use the sample project provided here for the following demonstration, covering typed state store clients via the source generator, direct client usage for bulk operations and transactions, and optimistic concurrency control with ETags. In this guide, you will:

  • Deploy a .NET application (StateManagementExample)
  • Utilize the Dapr .NET State Management SDK to save, retrieve, and delete state entries
  • Use ETags for optimistic concurrency control
  • Perform bulk operations and state transactions
  • Use the source generator to create a strongly-typed, store-scoped interface

In the .NET example project:

  • The main Program.cs contains both usage patterns (typed store and direct client).
  • The IWidgetStore.cs file demonstrates the typed state store interface.

Prerequisites

Set up the environment

Clone the .NET SDK repo.

git clone https://github.com/dapr/dotnet-sdk.git

From the .NET SDK root directory, navigate to the Dapr State Management example.

cd examples/StateManagement

Run the application locally

To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the StateManagementExample directory.

cd StateManagementExample

We’ll run a command that starts both the Dapr sidecar and the .NET program at the same time.

dapr run --app-id statemanagement-example --dapr-grpc-port 50001 -- dotnet run

Dapr listens for gRPC requests at http://localhost:50001.

Register the client

The DaprStateManagementClient is registered through the AddDaprStateManagementClient() extension method on IServiceCollection. This returns an IDaprStateManagementBuilder that can be used to further register source-generated typed state store clients.

Default registration

Register the client with default settings. The Dapr endpoint and API token will be read from environment variables or IConfiguration automatically.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprStateManagementClient();

Registration with custom configuration

If you need to configure the client (for example, to override the gRPC endpoint or JSON serializer options), pass a callback:

builder.Services.AddDaprStateManagementClient((sp, clientBuilder) =>
{
    clientBuilder.UseGrpcEndpoint("http://localhost:50001");
});

Registration with IServiceProvider

The configuration callback receives the IServiceProvider, allowing you to resolve other services:

builder.Services.AddDaprStateManagementClient((sp, clientBuilder) =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    var endpoint = config["DaprGrpcEndpoint"];
    if (!string.IsNullOrEmpty(endpoint))
    {
        clientBuilder.UseGrpcEndpoint(endpoint);
    }
});

Configure the client from IConfiguration

The client builder inherits from DaprGenericClientBuilder<T>, which supports reading configuration values from IConfiguration. The following configuration keys are recognized:

KeyDescription
DAPR_GRPC_ENDPOINTThe gRPC endpoint for the Dapr sidecar
DAPR_HTTP_ENDPOINTThe HTTP endpoint for the Dapr sidecar
DAPR_API_TOKENThe API token for authenticating with the Dapr sidecar

These values can be provided via environment variables, appsettings.json, or any other IConfiguration source.

Using in-memory configuration:

builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
    ["DAPR_GRPC_ENDPOINT"] = "http://localhost:50001",
});

builder.Services.AddDaprStateManagementClient();

Using prefixed environment variables:

builder.Configuration.AddEnvironmentVariables(prefix: "MYAPP_");

// MYAPP_DAPR_GRPC_ENDPOINT will be read automatically
builder.Services.AddDaprStateManagementClient();

Save and retrieve state

Use GetStateAsync<TValue> to retrieve a single value and SaveStateAsync<TValue> to save one:

var client = app.Services.GetRequiredService<DaprStateManagementClient>();

// Save a widget
var widget = new Widget("medium", "blue");
await client.SaveStateAsync("statestore", "my-widget", widget);

// Retrieve it
var loaded = await client.GetStateAsync<Widget>("statestore", "my-widget");
Console.WriteLine($"Widget: {loaded?.Size} / {loaded?.Color}");

Delete state

await client.DeleteStateAsync("statestore", "my-widget");

Optimistic concurrency with ETags

Use GetStateAndETagAsync to retrieve a value along with its ETag, then use TrySaveStateAsync or TryDeleteStateAsync to conditionally write only if the value hasn’t changed:

// Read the value and its ETag
var (existingValue, etag) = await client.GetStateAndETagAsync<Widget>("statestore", "my-widget");

if (etag is not null)
{
    // Modify the value
    var updated = existingValue! with { Color = "green" };

    // Save only if nobody else has modified it since our read
    bool saved = await client.TrySaveStateAsync("statestore", "my-widget", updated, etag);
    Console.WriteLine(saved ? "ETag save succeeded." : "ETag mismatch — concurrent modification.");
}

And similar for deletes:

var (_, etag) = await client.GetStateAndETagAsync<Widget>("statestore", "my-widget");

if (etag is not null)
{
    bool deleted = await client.TryDeleteStateAsync("statestore", "my-widget", etag);
    Console.WriteLine(deleted ? "Deleted." : "ETag mismatch.");
}

Bulk operations

Save multiple entries

await client.SaveBulkStateAsync("statestore", new List<SaveStateItem<Widget>>
{
    new("widget-a", new Widget("small", "red")),
    new("widget-b", new Widget("large", "white")),
});

Retrieve multiple entries

var bulk = await client.GetBulkStateAsync<Widget>("statestore", new[] { "widget-a", "widget-b" });

foreach (var item in bulk)
{
    Console.WriteLine($"Key={item.Key}, Size={item.Value?.Size}, Color={item.Value?.Color}");
}

Delete multiple entries

await client.DeleteBulkStateAsync("statestore", new List<BulkDeleteStateItem>
{
    new("widget-a"),
    new("widget-b"),
});

State transactions

Execute multiple upsert and delete operations atomically within a single transaction. Not all state stores support transactions — check the Dapr documentation for your specific store.

await client.ExecuteStateTransactionAsync("statestore", new List<StateTransactionRequest>
{
    new("widget-a", null, StateOperationType.Delete),
    new("widget-b", null, StateOperationType.Delete),
});

Query state

If the underlying state store supports queries, you can use QueryStateAsync with a JSON query expression:

var query = """
{
    "filter": {
        "EQ": { "value.Color": "blue" }
    },
    "sort": [
        { "key": "value.Size", "order": "ASC" }
    ]
}
""";

var response = await client.QueryStateAsync<Widget>("statestore", query);

foreach (var item in response.Results)
{
    Console.WriteLine($"Key={item.Key}, Size={item.Data?.Size}, Color={item.Data?.Color}");
}

Query support depends on the state store component. Refer to the Dapr state store query API documentation for details. Do note that this functionality is not expected to receive any additional implementation support and is thus effectively deprecated.

Pass metadata to requests

Most state management operations accept an optional metadata parameter. Metadata is a dictionary of string key-value pairs whose valid keys and values depend on the state store component being used:

var metadata = new Dictionary<string, string>
{
    ["partitionKey"] = "widgets",
};

await client.SaveStateAsync("statestore", "my-widget", widget, metadata: metadata);
var loaded = await client.GetStateAsync<Widget>("statestore", "my-widget", metadata: metadata);

Use the source generator for typed state store access

For a cleaner, more maintainable, and testable approach, use the source generator to create a strongly-typed interface bound to a specific state store. This eliminates repeated store name strings throughout your code.

Step 1: Define the interface

Create a partial interface that extends IDaprStateStoreClient and annotate it with [StateStore("storeName")]:

using Dapr.StateManagement;

[StateStore("statestore")]
public partial interface IWidgetStore : IDaprStateStoreClient;

At compile time, a source generator produces:

  1. A sealed internal implementation class that forwards all IDaprStateStoreClient calls to DaprStateManagementClient with the store name "statestore" pre-filled.
  2. A DI registration extension method on IDaprStateManagementBuilder named WithWidgetStore() (the leading I is stripped from the interface name).

Step 2: Register the typed store

Chain the generated extension method onto AddDaprStateManagementClient():

builder.Services
    .AddDaprStateManagementClient()
    .WithWidgetStore();

Step 3: Inject and use

Inject IWidgetStore into your service. All methods mirror IDaprStateStoreClient but omit the storeName parameter:

public class WidgetService(IWidgetStore store)
{
    public async Task<Widget?> GetWidgetAsync(string id)
    {
        return await store.GetStateAsync<Widget>(id);
    }

    public async Task SaveWidgetAsync(string id, Widget widget)
    {
        await store.SaveStateAsync(id, widget);
    }

    public async Task<bool> UpdateWidgetAsync(string id, Widget widget)
    {
        var (_, etag) = await store.GetStateAndETagAsync<Widget>(id);
        if (etag is null)
            return false;

        return await store.TrySaveStateAsync(id, widget, etag);
    }
}

Registering multiple typed stores

You can define multiple interfaces for different Dapr state store components and register them all:

[StateStore("statestore")]
public partial interface IWidgetStore : IDaprStateStoreClient;

[StateStore("cachestore")]
public partial interface ICacheStore : IDaprStateStoreClient;
builder.Services
    .AddDaprStateManagementClient()
    .WithWidgetStore()
    .WithCacheStore();

Testing

Testing with the direct client

Because DaprStateManagementClient is an abstract class, you can mock it using any mocking framework:

var mockClient = new Mock<DaprStateManagementClient>();
mockClient
    .Setup(c => c.GetStateAsync<Widget>(
        "statestore", "my-widget", null, null, default))
    .ReturnsAsync(new Widget("medium", "blue"));

var service = new WidgetService(mockClient.Object);

Testing with typed store clients

Since typed stores implement IDaprStateStoreClient, mock the interface directly:

var mockStore = new Mock<IWidgetStore>();
mockStore
    .Setup(s => s.GetStateAsync<Widget>("my-widget", null, null, default))
    .ReturnsAsync(new Widget("medium", "blue"));

var service = new WidgetService(mockStore.Object);

2 - DaprStateManagementClient usage

Essential tips and advice for using DaprStateManagementClient

Lifetime management

A DaprStateManagementClient holds long-lived network resources (gRPC channels, HTTP connections). For best performance:

  • Register a single shared instance and reuse it for the lifetime of the application.
  • Avoid creating and disposing a client per operation — this can lead to socket exhaustion.
  • The DI registration via AddDaprStateManagementClient() defaults to ServiceLifetime.Singleton, which is the recommended approach for most applications.

The client is thread-safe and can be shared across concurrent requests.

Client builder configuration

DaprStateManagementClientBuilder inherits from DaprGenericClientBuilder<DaprStateManagementClient> and supports the same configuration surface as the other Dapr .NET client builders.

Builder methods

MethodDescription
UseGrpcEndpoint(string)Sets the gRPC endpoint for the Dapr sidecar.
UseHttpEndpoint(string)Sets the HTTP endpoint for the Dapr sidecar.
UseJsonSerializationOptions(JsonSerializerOptions)Configures custom JSON serializer options.
UseDaprApiToken(string)Sets the API token for authenticating with the Dapr sidecar.
UseGrpcChannelOptions(GrpcChannelOptions)Provides custom gRPC channel options.
UseTimeout(TimeSpan)Configures an HTTP request timeout.

Environment variables

The builder reads the following environment variables automatically:

VariableDescription
DAPR_GRPC_ENDPOINTgRPC endpoint address for the Dapr sidecar
DAPR_HTTP_ENDPOINTHTTP endpoint address for the Dapr sidecar
DAPR_API_TOKENAPI token for authenticating with the Dapr sidecar
DAPR_GRPC_PORTgRPC port (used as fallback if DAPR_GRPC_ENDPOINT is not set)
DAPR_HTTP_PORTHTTP port (used as fallback if DAPR_HTTP_ENDPOINT is not set)

Values explicitly set on the builder take precedence over environment variables.

gRPC channel options

For fine-grained control over gRPC behavior:

builder.Services.AddDaprStateManagementClient((sp, clientBuilder) =>
{
    clientBuilder.UseGrpcChannelOptions(new GrpcChannelOptions
    {
        MaxReceiveMessageSize = 16 * 1024 * 1024, // 16 MB
    });
});

Cancellation tokens

All asynchronous methods on DaprStateManagementClient and IDaprStateStoreClient accept a CancellationToken parameter. Passing a token allows you to cancel long-running operations in response to timeouts or user cancellation:

using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));

var widget = await client.GetStateAsync<Widget>("statestore", "my-widget",
    cancellationToken: cts.Token);

When canceled, an OperationCanceledException is thrown.

Dependency injection registration

Registration overloads

The AddDaprStateManagementClient method has the following signature:

public static IDaprStateManagementBuilder AddDaprStateManagementClient(
    this IServiceCollection services,
    Action<IServiceProvider, DaprStateManagementClientBuilder>? configure = null,
    ServiceLifetime lifetime = ServiceLifetime.Singleton)

The returned IDaprStateManagementBuilder enables chaining typed state store registrations:

builder.Services
    .AddDaprStateManagementClient()
    .WithWidgetStore()
    .WithCacheStore();

Chaining source generator registrations

Each [StateStore]-annotated interface generates an extension method on IDaprStateManagementBuilder. The methods return IDaprStateManagementBuilder, so they chain naturally:

builder.Services
    .AddDaprStateManagementClient((sp, clientBuilder) =>
    {
        clientBuilder.UseGrpcEndpoint("http://localhost:50001");
    })
    .WithWidgetStore()
    .WithCacheStore()
    .WithUserPreferencesStore();

JSON serialization

By default, the client uses System.Text.Json with JsonSerializerDefaults.Web, which provides:

  • camelCase property naming (PropertyNamingPolicy = JsonNamingPolicy.CamelCase)
  • Case-insensitive property name matching (PropertyNameCaseInsensitive = true)
  • Strings read as numbers allowed (NumberHandling = JsonNumberHandling.AllowReadingFromString)

These defaults match the DaprClient package, ensuring serialization compatibility — data written by one client can be read correctly by the other.

To customize:

builder.Services.AddDaprStateManagementClient((sp, clientBuilder) =>
{
    clientBuilder.UseJsonSerializationOptions(new JsonSerializerOptions
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
    });
});

Note: Changing serializer settings can break compatibility with data previously written using default settings. Test thoroughly when customizing.

API response shapes

GetStateAsync

GetStateAsync<TValue> returns TValue?. If the key does not exist in the store, default(T) is returned (i.e., null for reference types).

GetStateAndETagAsync

GetStateAndETagAsync<TValue> returns (TValue? Value, string? ETag). The ETag can be passed to TrySaveStateAsync or TryDeleteStateAsync for optimistic concurrency control.

GetBulkStateAsync

GetBulkStateAsync<TValue> returns IReadOnlyList<BulkStateItem<TValue>>. Each BulkStateItem<TValue> contains:

PropertyTypeDescription
KeystringThe state key
ValueTValue?The deserialized value, or null if the key was not found
ETagstringThe ETag for optimistic concurrency control

TrySaveStateAsync / TryDeleteStateAsync

Both return booltrue if the operation succeeded, false if the ETag did not match (indicating a concurrent modification).

QueryStateAsync

QueryStateAsync<TValue> returns StateQueryResponse<TValue>, which contains:

PropertyTypeDescription
ResultsIReadOnlyList<StateQueryItem<TValue>>The list of matching items
Tokenstring?Pagination token for continuing the query, or null if no more results
MetadataIReadOnlyDictionary<string, string>Additional metadata returned by the state store

Each StateQueryItem<TValue> contains:

PropertyTypeDescription
KeystringThe state key
DataTValue?The deserialized value
ETagstring?The ETag for the item
Errorstring?An error message if the item could not be retrieved, or null on success

StateOptions

The StateOptions class controls consistency and concurrency behavior for individual operations:

var options = new StateOptions
{
    Consistency = ConsistencyMode.Strong,
    Concurrency = ConcurrencyMode.FirstWrite,
};

await client.SaveStateAsync("statestore", "my-widget", widget, stateOptions: options);
PropertyValuesDescription
ConsistencyEventual, StrongControls whether reads reflect the latest write. null uses the store default.
ConcurrencyFirstWrite, LastWriteControls conflict resolution. FirstWrite fails on ETag mismatch; LastWrite always overwrites. null uses the store default.

Error handling

Methods on DaprStateManagementClient 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 state store component not being configured or available
  • ETag mismatches when using optimistic concurrency (for TrySaveStateAsync and TryDeleteStateAsync, these return false rather than throwing)

In any of these cases, you can examine more exception details through the .InnerException property.

Migration from DaprClient

If you are migrating from the DaprClient state management methods to the new Dapr.StateManagement package, note the following:

  • The API surface is very similar. Methods like GetStateAsync, SaveStateAsync, DeleteStateAsync, GetBulkStateAsync, and ExecuteStateTransactionAsync are all present with the same semantics.
  • The default JSON serialization settings (JsonSerializerDefaults.Web) are identical, so data written by DaprClient is fully compatible with DaprStateManagementClient and vice versa.
  • The new package additionally provides IDaprStateStoreClient and the [StateStore] source generator for strongly-typed, store-scoped access. This reduces boilerplate and eliminating store name strings at call sites.
  • DaprStateManagementClient is registered independently from DaprClient. Both can coexist in the same application during a gradual migration.