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