Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Each concept has its own page:
- [State machine actors]({{< ref dotnet-actorsnext-statemachine.md >}}): the `StateMachineActor` model, when to choose it over a workflow, and how to test it.
- [Subscription streams]({{< ref dotnet-actorsnext-streams.md >}}): driving actors from pub/sub topics with `[Subscribe]` metadata and explicit stream registration.
- [Dynamic invocation]({{< ref dotnet-actorsnext-dynamic.md >}}): calling actors by type, id, and method with no compile-time contract, for gateways, tooling, and cross-language callers.
- [Testing]({{< ref dotnet-actorsnext-testing.md >}}): the in-memory test runtime, xUnit usage, and optional Coyote integration for deep concurrency testing.
- [Testing]({{< ref dotnet-actorsnext-testing.md >}}): the in-memory test runtime, remote-call simulation, paused state writes and deactivation races, virtual timers/reminders, callback-stream fault injection, and optional Coyote integration.
- Per-type runtime options are supported through `options.Actors.RegisterActor<TActor>(...)`, including idle timeout, reentrancy, draining, and state-migration overrides.

## Learn by example
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -213,10 +213,14 @@ builder.Services.AddDaprActors(options =>

The idle timeout, draining settings, and reentrancy settings are advertised to the Dapr sidecar for the registered actor type. `DisableStateMigration` is applied locally by the SDK when that actor's state is persisted and is not advertised to the sidecar. The complete runnable version is in `examples/Actor.Next/07-PerTypeOptions`.

The SDK also enforces `EnableReentrancy` and `MaxReentrantDepth` when admitting a matching call chain locally, using per-type values before application-wide values. Configure these explicitly to match your application's call graph.

{{% alert title="There is no lifetime parameter" color="primary" %}}
`AddDaprActors` does not accept a `ServiceLifetime`. An actor's lifetime is its activation, which the runtime owns: it is keyed by id, spans many turns, and ends on deactivation. That is not a DI lifetime, so it is not configurable. The actor instance is constructed per activation in a dedicated DI scope, cached across turns, and disposed together with its scope on deactivation. Your actor's dependencies keep whatever lifetimes you registered them with.
{{% /alert %}}

Deactivation is serialized with actor work: an external request waits behind active work, while one requested by the active call chain is deferred until the turn and its admitted reentrant children finish. The activation and its scope are not disposed underneath an in-flight turn.

### Controlling automatic registration

By default, `AddDaprActors()` discovers every `[DaprActor]` in scope and registers it for hosting: it wires the dispatcher and activation factory and advertises the type to the runtime so the runtime delivers callbacks for it. It also discovers and registers every state upcaster. Two options let you turn those automatic behaviors off independently:
Expand Down Expand Up @@ -451,7 +455,7 @@ await reminders.CancelAsync("Cart", Id, nameof(AbandonCart), ct);

Timer and reminder callbacks arrive over the same actor event stream as normal invokes. The SDK acks the callback only after the actor turn commits and pending state has been saved. For one-shot timers, a successful ack includes `cancel: true` so the runtime does not fire the timer again. Periodic timers and reminders continue according to their registration until canceled, expired by TTL, or replaced. If the handler throws or the stream drops before the ack reaches the runtime, the runtime can redeliver the callback. Keep handlers idempotent, especially when they perform external side effects.

See [Testing]({{< ref dotnet-actorsnext-testing.md >}}) for advancing virtual time to fire timers and reminders in tests. In the in-memory test runtime, `IActorTimerScheduler` and `IActorReminderScheduler` are backed by the virtual time provider, so actor code that schedules either through DI can be tested without a sidecar.
Timer and reminder callbacks are independent actor turns, not reentrant continuations of the registering turn. They wait for an in-flight turn on the same actor to finish. Canceling a registration does not undo a callback that has already been admitted for execution, so a callback already in progress may still finish.

## Saving actor state

Expand Down Expand Up @@ -486,14 +490,16 @@ After `SaveStateAsync` succeeds, the saved cache entries are marked clean. The e

Actor callbacks are at-least-once, and this shapes how you write actor methods. It is worth understanding before you build anything non-trivial, because it is the assumption most likely to be wrong if you carry a mental model of a method that runs exactly once.

An invoke can be redelivered. If the runtime delivered a call but did not receive the response (for example the stream dropped mid-turn), it treats the call as failed and retries it. Because pending actor state is saved at the end of the turn and the response is sent after that save, a retry can re-execute a turn against state the previous attempt already committed. Your actor methods must therefore be safe to run more than once. A method that only reads and mutates actor `State` is naturally safe, because a re-run lands on the same state. A method that also performs an external side effect (publishing an event, calling another actor, charging a card) is exposed, and that side effect must be made idempotent, for example by keying it on a stable request identity or by making the downstream operation naturally idempotent.
An invoke can be redelivered. If the runtime delivered a call but did not receive the response (for example the stream dropped after commit), the caller can observe a failure even though the state change succeeded. Depending on the configured retry policy, the invocation may run again. Pending actor state is saved before the response is sent, and those steps are not atomic.

Your actor methods must therefore be safe to run more than once, including their state mutations. Incrementing a counter or appending an item is not automatically idempotent merely because it uses actor `State`; a retry against committed state can apply the mutation again. Use naturally idempotent updates or persist a stable request identity and its result with the state change so duplicates can be recognized. External side effects (publishing an event, calling another actor, charging a card) also require an idempotency strategy. A lost response does not imply rollback.

A reminder re-fires until it is acked. A durable reminder that comes due is redelivered every second, indefinitely, until the turn that handles it commits. Two conclusions follow from this:
1) Reminder handlers must be idempotent, because you will see the same reminder more than once under any failure or delay.
2) A reminder handler that reliably throws produces a one-second retry loop for that actor, so treat a consistently failing reminder as something to surface and fix, not to swallow.

{{% alert title="The practical rule" color="primary" %}}
Design every actor method, reminder, and subscription handler to be safe to run more than once. Turn-local state changes are safe automatically; external side effects are not, and are your responsibility to make idempotent. The [test runtime]({{< ref "dotnet-actorsnext-testing.md#fault-injection" >}}) can inject the stream drops and redeliveries that trigger these paths, so you can prove your handlers are re-run-safe in a unit test rather than discovering it in production.
Design every actor method, reminder, and subscription handler to be safe to run more than once. Both state changes and external side effects need idempotency. Treat a missing response as an uncertain outcome, not evidence that the operation never ran or that its state changes were rolled back.
{{% /alert %}}

## Call an actor
Expand Down Expand Up @@ -557,10 +563,12 @@ Three things are true of a call made from inside a turn, and two are easy to mis
- The call is at-least-once, like any invoke. If your turn is redelivered (see [delivery guarantees](#delivery-guarantees-you-must-design-for)), the outbound call runs again, so the target must be idempotent or the call must be safe to repeat.
- Reentrancy is handled for you when it is enabled. Actor authors do not manually supply reentrancy headers. When reentrancy is enabled for the actor runtime, `Dapr.Actors.Next` automatically propagates the call-chain metadata across actor calls made during an actor turn, so a chain that re-enters an actor already in the chain (A calls B, B calls back into A) is recognized as reentrant rather than blocking on the lock the first turn still holds.

{{% alert title="Reentrancy is automatic, but must be enabled" color="primary" %}}
The SDK propagates the call-chain metadata for you, but it does not enable reentrancy on its own: the runtime has to allow reentrant delivery in the first place. Turn it on in options with `DaprActorsOptions.EnableReentrancy`, and bound the depth with `DaprActorsOptions.MaxReentrantDepth` so a chain that would exceed the configured depth is stopped rather than recursing without limit. With reentrancy disabled, a call that loops back into an actor already in the chain is not treated as reentrant and will block on the lock the first turn still holds, so enable it deliberately when your call graphs are re-entrant, and leave it off when they are acyclic.
{{% alert title="Configure reentrancy explicitly" color="primary" %}}
Set `DaprActorsOptions.EnableReentrancy` and bound the depth with `DaprActorsOptions.MaxReentrantDepth`. Per-type settings override application settings. The SDK admits a reentrant invocation only when it matches the occupied actor's active call chain; it rejects such admission with `ActorInvocationException` when reentrancy is explicitly disabled or the depth limit would be exceeded. Dapr separately controls reentrant delivery according to the advertised actor configuration, so do not rely on a cyclic call graph progressing when it is disabled.

The canonical propagated header is `Dapr-Reentrancy-Id`. The SDK accepts it case-insensitively, also recognizes `dapr-reentrant-id` and `x-dapr-reentrant-id`, and prefers the canonical name if multiple aliases are present. For custom invocation infrastructure, `ActorHeaders.WithCurrentReentrancy(headers)` captures the active chain without mutating the supplied collection; explicit call-chain headers take precedence over ambient context. Normal actor proxy calls handle this for you.

In the in-memory test runtime this works without any sidecar configuration, because the SDK runtime owns the scheduling decision itself; you can exercise reentrant call chains in a unit test directly.
Await nested actor calls rather than launching parallel or fire-and-forget branches over mutable actor state. Reentrant admission is not a general thread-safety guarantee.
{{% /alert %}}

### Call an actor in another Dapr app
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ Options that no longer exist as such: `UseJsonSerialization` and `JsonSerializer

## Delivery semantics are the same contract — now stated plainly

`Dapr.Actors` actor calls were already at-least-once, and reminders already re-fired until acknowledged; many teams simply did not design for it. `Dapr.Actors.Next` does not change the contract, but it makes it explicit and testable: an invoke can be redelivered, and a reminder re-fires until the handling turn commits, so every actor method, reminder, and subscription handler must be safe to run more than once. Turn-local state changes are safe automatically; external side effects (publishing, calling another actor, charging a card) are your responsibility to make idempotent. Unlike the old world, you can now prove your handlers are re-run-safe in a unit test by injecting the redelivery. See [Delivery guarantees you must design for]({{< ref "dotnet-actorsnext-howto.md#delivery-guarantees-you-must-design-for" >}}).
`Dapr.Actors` actor calls were already at-least-once, and reminders already re-fired until acknowledged; many teams simply did not design for it. `Dapr.Actors.Next` does not change the contract, but it makes failure paths testable: an invoke can be redelivered, and a reminder can re-fire if its acknowledgment is lost, even after the handling turn commits. Every actor method, reminder, and subscription handler must be safe to run more than once. Both state mutations and external side effects (publishing, calling another actor, charging a card) require idempotency or deduplication; using actor state alone does not make an increment or append safe to repeat. Use a decorated callback transport to inject response loss after commit, then explicitly exercise redelivery. See [Delivery guarantees you must design for]({{< ref "dotnet-actorsnext-howto.md#delivery-guarantees-you-must-design-for" >}}).

## Testing

Expand Down Expand Up @@ -522,7 +522,7 @@ See [One interface can back several actor types]({{< ref "dotnet-actorsnext-howt
- **Serializer option drift.** State is JSON on both sides, but a different naming policy, enum handling, or a custom old-side serializer can make existing state unreadable. Verify, and use state migration for real shape changes.
- **Remoting/JSON straddle during coexistence.** A Next proxy (JSON) does not line up with an old remoting caller (DataContract). Move an actor type and its callers together.
- **Reminder method renames.** The reminder name is the routed operation; renaming a method leaves registered reminders pointing at the old name.
- **Assuming exactly-once.** Handlers must be safe to run more than once; make external side effects idempotent.
- **Assuming exactly-once.** Handlers must be safe to run more than once; make state updates and external side effects idempotent or deduplicate requests.
- **Static `ActorProxy` facade.** It works, but its process-wide mutable state is a footgun; inject `IActorProxyFactory` instead.
- **Actors in a referenced project.** Discovery scans only the executing assembly unless you set `DaprActorsScanReferences`.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ public async Task AddItem(CartItem item, CancellationToken ct = default)
You rarely call save explicitly, and multiple mutations in a single method (or across a reentrant call chain within the same turn) collapse into a single write.

{{% alert title="The end-of-turn save interacts with retries" color="primary" %}}
The save is what makes a turn's state changes durable, and the response the SDK sends afterward is the runtime's signal that the turn completed. Those two steps are not a single atomic operation, and invokes are at-least-once, so a turn can be retried after its state was already committed (see [delivery guarantees]({{< ref "dotnet-actorsnext-howto.md#delivery-guarantees-you-must-design-for" >}})). State-only changes are safe under a re-run because they land on the same state; external side effects in the same method are not, and must be made idempotent. Keeping mutations in `State` rather than in side effects is what keeps a turn re-run-safe.
The save is what makes a turn's state changes durable, and the response the SDK sends afterward signals completion. Those two steps are not a single atomic operation, so a turn can be retried after its state was already committed (see [delivery guarantees]({{< ref "dotnet-actorsnext-howto.md#delivery-guarantees-you-must-design-for" >}})). State-only changes are not automatically safe under a re-run: incrementing a counter or appending an item can apply twice. Make both state updates and external side effects idempotent, or deduplicate using a stable request identity persisted with the change. [Callback-stream fault injection]({{< ref "dotnet-actorsnext-testing.md#inject-callback-stream-failures" >}}) can test response loss after commit; it does not roll state back.
{{% /alert %}}

{{% alert title="The serializer is part of your at-rest contract" color="warning" %}}
Expand All @@ -97,7 +97,7 @@ When a state type's shape changes between releases, older persisted data must st
- Register a `JsonSerializerContext` for your actor payloads to stay on the AOT-safe path.
- Keep serializer configuration for state types stable, and treat a change that alters the persisted shape as a versioning decision (see [State migration]({{< ref dotnet-actorsnext-statemigration.md >}})).
- Keep per-actor state reasonably sized, since the whole state saves as one value per turn.
- Keep mutations in `State` rather than in external side effects, so a re-run turn stays safe under at-least-once delivery.
- Make state mutations and external side effects idempotent, or deduplicate requests, so a re-run turn stays safe under at-least-once delivery.

## Next steps

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ Silent rehydration: when an idle actor is reactivated, `CurrentState` and `Data`
Durable deferral: because an incoming event is a method call that must return, `Defer` cannot hold a turn open waiting for a future message, which would deadlock. `Defer<TEvent>()` instead persists the event into the actor's state, returns an accepted or deferred acknowledgment, and re-raises it on the next transition that no longer defers it.

{{% alert title="Effects run inside an at-least-once turn" color="primary" %}}
A method invocation is an event, and like any actor invoke it is at-least-once: a call the runtime could not confirm is retried, which re-runs the turn (see [delivery guarantees]({{< ref "dotnet-actorsnext-howto.md#delivery-guarantees-you-must-design-for" >}})). The transition itself is safe under a re-run, because replaying the same event from the committed state lands in the same place. What is not automatically safe is an effect or entry action that performs an external side effect, such as publishing an event or starting a workflow; a redelivered event can run it twice. Keep effects that mutate `Data` free of external side effects where you can, and make any external effect idempotent, for example by keying a started workflow on a deterministic instance id derived from the actor and the transition rather than a fresh one each time. This is the state-machine form of the same re-run-safety rule that applies to every actor method.
A method invocation is an event, and like any actor invoke it can be redelivered after its state commits if the response is lost (see [delivery guarantees]({{< ref "dotnet-actorsnext-howto.md#delivery-guarantees-you-must-design-for" >}})). A transition is safe to repeat only if the machine is designed to handle that event idempotently from the committed state. A self-transition, a repeated effect that mutates `Data`, or another transition accepting the same event can apply changes again. Test duplicate events explicitly and deduplicate where needed. Effects and entry actions that publish events or start workflows also need idempotency, for example by keying a started workflow on a deterministic instance id derived from the actor and the transition rather than a fresh one each time.
{{% /alert %}}

## Hierarchical states
Expand Down
Loading
Loading