How to: Manage state with 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.cscontains both usage patterns (typed store and direct client). - The
IWidgetStore.csfile demonstrates the typed state store interface.
Prerequisites
- Dapr CLI
- Initialized Dapr environment
- .NET 8, .NET 9, .NET 10, or .NET 11 installed
- Dapr.StateManagement NuGet package installed to your project
Set up the environment
.NET framework support
Be advised that .NET 8 and .NET 9 reach end of support in November 2026. Support for both will be dropped in Dapr v1.20; only .NET 10 and .NET 11 will be supported through the end of 2027.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:
| Key | Description |
|---|---|
DAPR_GRPC_ENDPOINT | The gRPC endpoint for the Dapr sidecar |
DAPR_HTTP_ENDPOINT | The HTTP endpoint for the Dapr sidecar |
DAPR_API_TOKEN | The 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.Tokenin 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:
- A sealed internal implementation class that forwards all
IDaprStateStoreClientcalls toDaprStateManagementClientwith the store name"statestore"pre-filled. - A DI registration extension method on
IDaprStateManagementBuildernamedWithWidgetStore()(the leadingIis 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);