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

Overview of the Dapr State Management building block for .NET

Dapr.StateManagement provides a dedicated client for interacting with the Dapr State Management API from a .NET application. With DaprStateManagementClient you can 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 through dependency injection using [StateStore] and [State] attributes.

The client is registered with dependency injection via AddDaprStateManagement() and can be configured from IConfiguration, environment variables, or a DaprStateManagementClientBuilder for advanced scenarios.

Core concepts

  • How to: Manage state with the Dapr State Management .NET SDK: end-to-end walkthrough covering save, retrieve, delete, and query operations, ETags, bulk operations, state transactions, the source generator for typed state store access, and testing.
  • DaprStateManagementClient usage: lifetime management, builder configuration, environment variables, gRPC channel options, dependency injection overloads, JSON serialization defaults, API response shapes, StateOptions for consistency and concurrency, error handling, and migration from DaprClient.

Next steps

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

These examples target Dapr.StateManagement 1.19.0 or later. Import Dapr.StateManagement, Dapr.StateManagement.Extensions, and System.Text.Json for the client, registration, and serialization. State option and operation models are in Dapr.StateManagement.

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),
});

Upserts use serialized bytes in StateTransactionRequest, rather than the generic save method’s automatic object serialization. Use matching serializer settings:

var data = JsonSerializer.SerializeToUtf8Bytes(
    new Widget("small", "blue"), new JsonSerializerOptions(JsonSerializerDefaults.Web));

await client.ExecuteStateTransactionAsync("statestore", new List<StateTransactionRequest>
{
    new("widget-c", data, StateOperationType.Upsert),
    new("widget-d", null, StateOperationType.Delete)
});

StateTransactionRequest can carry per-operation ETags, options, and metadata. Request-level metadata is passed separately to ExecuteStateTransactionAsync. A bulk save is not a substitute for an atomic transaction.

Query state

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

var query = """
{
    "filter": {
        "EQ": { "color": "blue" }
    },
    "sort": [
        { "key": "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 query syntax and limitations. Follow response.Token in the next query’s pagination fields when paging; do not assume all results fit in one response.

Query fields refer to the stored JSON document. The example uses the client’s default web JSON settings, which serialize Widget.Color and Widget.Size as color and size; adapt the query to your serializer and component’s indexing requirements.

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);

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 bool — true if the operation succeeded, false on an ETag conflict. In 1.19.0, caller cancellation can also return false; check cancellation before treating it as a concurrency conflict.

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

Although StateQueryItem<TValue> has an Error property, the 1.19.0 client throws DaprException when the runtime returns per-item query errors, rather than returning a successful response containing those failed items. Do not rely on partial results being returned in that case.

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 throw DaprException for runtime failures. Invalid argument values raise standard exceptions such as ArgumentNullException or ArgumentException. Caller cancellation has the release-specific behavior described above.

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.

APIs that remain on DaprClient

The dedicated client’s public surface is typed state access. It does not expose these DaprClient helpers:

Legacy methodPurpose
SaveByteStateAsync, TrySaveByteStateAsyncSave already-encoded bytes, optionally conditional on an ETag.
GetByteStateAsync, GetByteStateAndETagAsyncRetrieve raw state bytes, optionally with an ETag.
Non-generic GetBulkStateAsyncRetrieve bulk entries without deserializing values to an application type.
GetStateEntryAsync<T>Return a StateEntry<T> tracking value, key/store, and ETag for save/delete helper operations.

Use DaprClient where these APIs are needed; do not assume substituting byte[] into a JSON generic method preserves raw bytes. JSON serialization encodes a byte array rather than bypassing serialization. For large files and chunked streaming, use the separate binary store building block instead of buffering file content in state.

await legacyClient.SaveByteStateAsync("statestore", "encoded-record", encodedBytes,
    cancellationToken: cancellationToken);
var (bytes, etag) = await legacyClient.GetByteStateAndETagAsync(
    "statestore", "encoded-record", cancellationToken: cancellationToken);

The two client families also use types in different namespaces. Dedicated state operations use Dapr.StateManagement option/operation models; older APIs use their Dapr.Client counterparts. Check types as well as method names when migrating, and qualify overlapping names when using both.