How to: Author, register, and call actors with the .NET SDK

Define actors, let the source generator wire them up, and call them with Dapr.Actors.Next

This article shows how to define an actor, what attributes drive the source generator, the minimum you must register, and how to call actors, including cross-assembly and cross-app scenarios.

Prerequisites

Dapr runtime v1.18.0 or later, a port on your application for gRPC inbound connections, and the Dapr.Actors.Next package. Dapr.Actors.Next is a meta-package that brings the runtime, source generators, and analyzers in one reference.

<ItemGroup>
  <PackageReference Include="Dapr.Actors.Next" Version="..." />
</ItemGroup>

Define an actor

An actor has two parts: a contract interface that derives from IActor and is decorated with [GenerateActorClient], and an implementation that derives from Actor and is decorated with [DaprActor].

using Dapr.Actors.Next;

[GenerateActorClient]
public interface ICartActor : IActor
{
    Task AddItem(CartItem item, CancellationToken ct = default);
    Task<CartSummary> GetSummary(CancellationToken ct = default);
}

[DaprActor]
public sealed class CartActor(ActorActivationContext context, IPricingClient pricing) : Actor, ICartActor
{
    protected override ActorId Id => context.ActorId;
    protected override IActorStateAccessor State => context.State;

    public async Task AddItem(CartItem item, CancellationToken ct = default)
    {
        var cart = await State.GetOrCreateAsync("cart", () => new CartState(), ct);
        cart.Value.Items.Add(item);            // mutate through .Value; saved at end of turn
    }

    public async Task<CartSummary> GetSummary(CancellationToken ct = default)
    {
        var cart = await State.GetOrCreateAsync("cart", () => new CartState(), ct);
        return new(cart.Value.Items.Count, await pricing.Total(cart.Value, ct));
    }
}

On the Actor base, Id and State are abstract, so each actor supplies them. The established pattern is to inject an ActorActivationContext and override the two from it, as shown above. State is accessed by name through IActorStateAccessor: a read returns an IActorState<T> wrapper whose value you reach through .Value, and mutations to .Value are saved once at the end of the turn unless you explicitly call State.SaveStateAsync() earlier.

Both attributes are required, and they work together

The two attributes are not interchangeable, and neither is optional.

[GenerateActorClient] on the interface is what selects it for the generator. The generator builds its set of actor contracts only from interfaces that both derive from IActor and carry [GenerateActorClient]; an interface that derives from IActor alone is invisible to it. The attribute takes no arguments; it is purely the marker that says “generate a client for this contract.”

[DaprActor] on the implementation is discovered separately, but a [DaprActor] class only becomes a generated actor if it implements one of those [GenerateActorClient] interfaces. This is the part that may come as a surprise:

What the source generator produces

You write the decorated interface and the decorated implementation; the source generator produces everything needed to host and call the actor, with no reflection at call time.

You writeThe source generator produces
[GenerateActorClient] on interface ICartActor : IActorA strongly-typed client proxy for calling the actor, and the contract’s entry in the interface manifest
[DaprActor] on an implementation of a decorated interfaceA dispatcher that routes incoming invocations to your methods
[DaprActor] on an implementation of a decorated interfaceA typed activation factory that constructs the actor from DI
[DaprActor] on an implementation of a decorated interfaceAn entry in the generated registration and the actor registry (its type, methods, and signatures)

You do not implement proxies, dispatchers, or factories, and you do not write registration code per actor.

One interface can back several actor types

The actor type name, not the interface, is the identity of a registered actor. More than one [DaprActor] class may implement the same [GenerateActorClient] interface, and each becomes its own distinct actor type with its own name, its own state, its own placement, and its own registration. The shared interface is only the callable shape they have in common.

[GenerateActorClient]
public interface IQueueActor : IActor { Task Enqueue(WorkItem item, CancellationToken ct = default); }

[DaprActor]                                   // actor type "OrdersQueue"
public sealed class OrdersQueueActor : Actor, IQueueActor { /* ... */ }

[DaprActor]                                   // actor type "EmailsQueue", same contract, different actor
public sealed class EmailsQueueActor : Actor, IQueueActor { /* ... */ }

This is why creating a proxy takes the actor type name as well as the interface (see Call an actor): the interface names the contract, and the type name selects which registered actor you mean. When several actor types implement one interface, the type name is what chooses among them; a lookup by interface returns all the actor types that implement it, and you pick the one you intend by its type name.

Because the type name is the identity, the implementations that share an interface must each resolve to a distinct name. An analyzer (DAPR1420) flags the collision when two actors implementing the same contract would register under the same actor type name, and points you to fix it by giving them distinct [DaprActor] names or explicit registration aliases.

One actor can implement several contracts

The relationship also runs the other way: a single [DaprActor] implementation may implement more than one [GenerateActorClient] interface, and all of them are served by that one actor type. This is useful when a single entity has distinct facets you would rather express as separate contracts — a read side and a write side, or a public contract and an administrative one — without splitting the entity into separate actors.

[GenerateActorClient]
public interface IDocumentReader : IActor
{
    Task<DocumentSummary> Read(CancellationToken ct = default);
}

[GenerateActorClient]
public interface IDocumentWriter : IActor
{
    Task Write(DocumentPatch patch, CancellationToken ct = default);
}

[DaprActor("Document")]
public sealed class DocumentActor(ActorActivationContext context) : Actor, IDocumentReader, IDocumentWriter
{
    protected override ActorId Id => context.ActorId;
    protected override IActorStateAccessor State => context.State;

    public Task<DocumentSummary> Read(CancellationToken ct = default) => /* ... */;
    public Task Write(DocumentPatch patch, CancellationToken ct = default) => /* ... */;
}

The generator produces one strongly-typed proxy per interface (IDocumentReader and IDocumentWriter each get their own), but the actor is hosted as a single actor type with one registration and one dispatcher that covers the union of every implemented contract’s methods. Because there is one actor type, there is one identity, one activation, and one set of state; the interfaces are just different callable shapes over the same actor.

Call it by creating whichever proxy you need, each addressed with the same actor type name:

var reader = proxies.Create<IDocumentReader>(ActorId.Create(docId), "Document");
var summary = await reader.Read();

var writer = proxies.Create<IDocumentWriter>(ActorId.Create(docId), "Document");
await writer.Write(patch);

Both proxies route to the same activation, so a write through IDocumentWriter is visible to a subsequent read through IDocumentReader on the same id.

Register at startup, the minimum

The only required call is AddDaprActors. The options delegate is optional.

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddDaprActors();          // discovers and wires every [DaprActor] in this assembly

var app = builder.Build();
app.Run();

That single call starts the actor host (the persistent SubscribeActorEvents streams to the runtime) as a hosted service, registers every discovered actor’s proxy, dispatcher, and factory, registers any state upcasters (see State migration), and populates the actor registry.

Configuring options

builder.Services.AddDaprActors(options =>
{
    options.ActorIdleTimeout = TimeSpan.FromMinutes(30);
    // ...
});

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:

builder.Services.AddDaprActors(options =>
{
    options.EnableAutoActorRegistration = true;           // default; auto-host discovered [DaprActor] types
    options.EnableAutoStateMigrationRegistration = true;  // default; auto-register discovered upcasters
});

The distinction that matters is hosting versus calling. Calling an actor uses a source-generated proxy, obtained from IActorProxyFactory (or the static ActorProxy.Create<T>() facade), and does not depend on registration; it and IDynamicActorClient work as long as the contract is referenced. Registration is what makes this app host a type: advertise it, receive its callbacks, and construct instances. Turning EnableAutoActorRegistration off gives you an app that can still call actors but hosts none automatically, which is the gateway half of a dual-mode app.

A single app that runs as a gateway in one mode and hosts actors in another installs Dapr.Actors.Next once (both modes need the proxy hardware) and chooses per mode:

// Gateway mode: forward calls to actors hosted elsewhere; host nothing here.
builder.Services.AddDaprActors(options =>
{
    options.EnableAutoActorRegistration = false;
    options.EnableAutoStateMigrationRegistration = false;   // hosting nothing, so no state migrations run
});

// Hosted mode: host the discovered actors and receive their callbacks from the runtime.
builder.Services.AddDaprActors();   // defaults: both on

In gateway mode no streams are opened to the runtime for hosting, because no type is advertised; the app holds only the calling infrastructure.

Registering an actor under a custom name

The actor type name is the address the runtime routes on. To choose it explicitly, for example from configuration, register the actor by name in the options:

[DaprActor]                                      // attribute required; the name argument is optional
public sealed class QueueActor : Actor, IQueueActor { /* ... */ }

// ...

var queueActorTypeName = builder.Configuration.GetValue("QUEUE_ACTOR_TYPE_NAME", "QueueActor");

builder.Services.AddDaprActors(options =>
{
    options.Actors.RegisterActor<QueueActor>(queueActorTypeName);   // this is what the type is hosted under
});

The hosted name resolves by precedence: an explicit alias from RegisterActor<T>(name) if you give one, otherwise the name on the [DaprActor] attribute if you set one, otherwise the actor implementation’s type name. An explicit alias always wins over the other two, and it wins whether or not EnableAutoActorRegistration is enabled. With auto-registration off, only the types you register explicitly are hosted; RegisterActor<T>() with no name registers the type under its resolved default, which is the attribute name or, failing that, the type-name fallback.

Cross-assembly discovery

By default the generator scans only the executing assembly. If your actors or their state upcasters live in a referenced project, opt in to reference scanning by adding the following to the host application’s .csproj:

<ItemGroup>
  <CompilerVisibleProperty Include="DaprActorsScanReferences" />
</ItemGroup>

When enabled, the generator discovers and registers [DaprActor] types and IActorStateUpcaster<,> implementations from referenced assemblies as well, not just from the executing assembly. It is off by default because scanning references increases build time. Note that upcaster discovery is also subject to EnableAutoStateMigrationRegistration: if you have turned automatic migration registration off, the scan will not auto-register upcasters, whether they are local or in a referenced project.

Inject dependencies

Registering the actor infrastructure requires only AddDaprActors(), optionally with the options overload. Nothing else is needed to host and run actors:

builder.Services.AddDaprActors();

Constructor injection into an actor is supported, and any injected service resolves from the actor’s per-activation scope and is disposed with it. Injection does require that you register the service yourself: AddDaprActors() wires up the actor infrastructure only, and does not register any Dapr building-block clients on your behalf.

For example, to use the state management client inside an actor for application state stores (which are separate from the actor’s own State), add the Dapr.StateManagement package, register DaprStateManagementClient, and inject it:

builder.Services.AddDaprActors();
builder.Services.AddDaprStateManagementClient();   // from the Dapr.StateManagement package
[DaprActor]
public sealed class CartActor(ActorActivationContext context, IPricingClient pricing, DaprStateManagementClient stateStores)
    : Actor, ICartActor
{
    protected override ActorId Id => context.ActorId;
    protected override IActorStateAccessor State => context.State;
    // pricing and stateStores resolve per activation, provided you registered them, and are disposed on deactivation
}

Lifecycle hooks

Override the per-actor lifecycle hooks for setup and teardown. These run on the actor’s own turns and are separate from the cross-cutting filter pipeline.

[DaprActor]
public sealed class CartActor(ActorActivationContext context) : Actor, ICartActor
{
    protected override ActorId Id => context.ActorId;
    protected override IActorStateAccessor State => context.State;

    protected override ValueTask OnActivateAsync(CancellationToken ct = default) => /* runs when the instance is activated */;
    protected override ValueTask OnDeactivateAsync(CancellationToken ct = default) => /* runs before deactivation */;
    protected override ValueTask OnPreActorMethodAsync(ActorMethodContext context, CancellationToken ct = default) => /* before each method */;
    protected override ValueTask OnPostActorMethodAsync(ActorMethodContext context, Exception? exception, CancellationToken ct = default) => /* after each method */;
}

For cross-cutting concerns that span many actor types, you can implement an IActorTurnFilter, but it is rarely the right tool; see the next section.

Cross-cutting turn filters

An IActorTurnFilter wraps every actor turn, across every actor type, as a middleware pipeline around invocation. It applies to a narrow category of work, and most applications never need one.

Reserve a turn filter for concerns that are infrastructural rather than behavioral, and that must apply uniformly across many actor types: enforcing that a tenant or auth header is present before any turn runs, or enriching every turn’s trace and metrics. These are things you would otherwise copy into every actor, and that genuinely belong outside any single actor.

Avoid a turn filter for anything else, for several reasons:

  • It runs on every turn of every actor type, so it sits on the hot path globally; logic placed there is paid for everywhere, whether or not it is relevant.
  • It moves behavior away from the actor. You can no longer read a single actor and understand what a turn does, because part of the behavior lives in a filter the actor never mentions.
  • Ordering across multiple filters becomes a subtle, global concern that is easy to get wrong and hard to see.
  • Business logic in a filter is harder to discover and harder to test than the same logic in the actor, which you can drive directly with the test runtime.
  • The turn pipeline also performs framework work such as context restore and the end-of-turn state save. Heavy or failure-prone logic in a filter shares a lifecycle with that work and can put the integrity of the turn at risk.

For concerns specific to one actor, use that actor’s lifecycle hooks (OnPreActorMethodAsync, OnPostActorMethodAsync, OnActivateAsync, OnDeactivateAsync) instead; they are discoverable on the actor and testable in isolation. For behavior shared by several actors, prefer composing it into the actors through injected services rather than intercepting their turns.

An analyzer (DAPR1416, informational) flags when an IActorTurnFilter appears to contain business logic, to keep filters thin and infrastructural.

Timers and reminders

Register timers and reminders from inside the actor by injecting the scheduler you need. Timers use IActorTimerScheduler; durable reminders use IActorReminderScheduler.

using Dapr.Actors.Next;
using Dapr.Actors.Next.Core.Timers;

[DaprActor("Cart")]
public sealed class CartActor(
    ActorActivationContext context,
    IActorTimerScheduler timers,
    IActorReminderScheduler reminders) : Actor, ICartActor
{
    protected override ActorId Id => context.ActorId;
    protected override IActorStateAccessor State => context.State;

    public async Task AddItem(CartItem item, CancellationToken ct = default)
    {
        var cart = await State.GetOrCreateAsync("cart", () => new CartState(), ct);
        cart.Value.Items.Add(item);

        // One-shot activation-local timer. If the activation goes away before
        // this fires, the callback is lost.
        await timers.RescheduleAsync(
            actorType: "Cart",
            actorId: Id,
            name: "refresh-prices-once",
            dueTime: TimeSpan.FromMinutes(1),
            operationName: nameof(RefreshPrices),
            arguments: item.Sku,
            cancellationToken: ct);

        // Periodic activation-local timer, limited by TTL.
        await timers.RescheduleAsync(
            actorType: "Cart",
            actorId: Id,
            name: "refresh-prices-periodic",
            dueTime: TimeSpan.FromMinutes(1),
            operationName: nameof(RefreshPrices),
            arguments: item.Sku,
            period: TimeSpan.FromMinutes(5),
            ttl: TimeSpan.FromMinutes(30),
            cancellationToken: ct);

        await reminders.ScheduleAsync(
            actorType: "Cart",
            actorId: Id,
            name: nameof(AbandonCart),
            dueTime: TimeSpan.FromMinutes(30),
            period: TimeSpan.Zero,
            arguments: Array.Empty<byte>(),
            ttl: TimeSpan.FromDays(1),
            overwrite: true,
            cancellationToken: ct);
    }

    public Task RefreshPrices(string sku, CancellationToken ct = default)
    {
        return Task.CompletedTask;
    }

    public async Task AbandonCart(CancellationToken ct = default)
    {
        var cart = await State.GetOrCreateAsync("cart", () => new CartState(), ct);
        cart.Value.Abandoned = true;
    }
}

The durability difference is the thing to design around. A timer is in-memory and tied to the activation: it does not survive deactivation or a process restart, so treat it as a best-effort convenience within a live activation, not a durable schedule. A reminder is durable and delivered by the Dapr scheduler, so it fires even if the actor was idle and had to be reactivated. Use a reminder whenever the wake-up must happen regardless of activation state, and a timer only for work that is fine to lose when the actor deactivates.

A timer registration has both a timer name and an operationName; the runtime routes the callback to operationName. A reminder registration has no separate callback field. The reminder name is both the durable reminder identity and the actor operation routed on callback, so choose a stable operation name and do not rename it casually. If the actor method is renamed, any already-registered durable reminders using the old name will continue to call the old operation name until they are updated or canceled. It’s recommended to keep the original callback method and have it replace the reminder to directly register the new method.

Timer and reminder payloads can be scheduled as typed values or as raw byte[]. Passing a typed value uses the configured actor wire serializer, so the callback method can declare the matching typed argument. Use the byte[] overload when you have already serialized the payload yourself or when the callback takes no payload and you want to pass Array.Empty<byte>().

For timers, dueTime controls the first firing. If you omit period, or set it to Timeout.InfiniteTimeSpan or TimeSpan.Zero, the SDK treats the timer as one-shot and cancels it after the callback turn commits. Set a positive period to create a periodic timer. ttl is optional and limits how long the timer can live; when supplied, it must be greater than or equal to dueTime. Cancel a timer with the same actor type, actor id, and timer name:

await timers.CancelAsync("Cart", Id, "refresh-prices-periodic", ct);

For reminders, dueTime controls the first firing and period controls repeat firing; use TimeSpan.Zero for a one-shot reminder. ttl is optional and limits how long the reminder can live. overwrite controls whether registering an existing reminder name replaces the previous registration; Dapr defaults this to overwrite, and setting overwrite: false lets duplicate registration fail instead. Cancel a durable reminder with the same actor type, actor id, and name:

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 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.

Saving actor state

Actor state uses an activation-scoped write-behind cache. In the common case, mutate state through .Value or SetAsync, return from the actor method, and let the runtime save pending changes at the end of the turn:

public async Task AddItem(CartItem item, CancellationToken ct = default)
{
    var cart = await State.GetOrCreateAsync("cart", () => new CartState(), ct);
    cart.Value.Items.Add(item);
}

Call State.SaveStateAsync(ct) only when you need the current changes to reach the underlying state store before the method continues, for example before making an external call that must observe durable actor state:

public async Task AddItemAndNotify(CartItem item, CancellationToken ct = default)
{
    var cart = await State.GetOrCreateAsync("cart", () => new CartState(), ct);
    cart.Value.Items.Add(item);

    await State.SaveStateAsync(ct);
    await notifications.CartUpdated(Id.Value, ct);
}

After SaveStateAsync succeeds, the saved cache entries are marked clean. The end-of-turn save does not write them again unless the actor changes them after the explicit save. Multiple changes before a save still collapse into one write.

EvictCacheAsync is different: it evicts cached entries from the current actor activation so the next read goes back to durable state. It does not persist dirty values. By default it rejects dirty cache entries; if you pass new DaprEvictStateOptions { EvictOnDirtyState = true }, those dirty in-memory values are discarded.

Delivery guarantees you must design for

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.

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.

Call an actor

Calling an actor has two independent questions: what you know at compile time (which decides between the strongly-typed proxy and the dynamic client), and where you are calling from (a client, or another actor). The actor type name is the addressing key the runtime uses for placement in every case, and serialization is the same IDaprSerializer path regardless.

Use the strongly-typed proxy when you have the actor’s contract interface available at compile time, which is the common case in application code. It gives you compile-time signature checking and editor completion. Use the dynamic client when the actor type or method exists in another app or not known until run time, for example in a gateway or a tool that calls actors it doesn’t reference.

Get a proxy from IActorProxyFactory

The DI-friendly way to obtain a proxy is to inject IActorProxyFactory and call Create<TActor>(id, actorType):

public sealed class CheckoutService(IActorProxyFactory proxies)
{
    public async Task Add(string userId, CartItem item)
    {
        var cart = proxies.Create<ICartActor>(ActorId.Create(userId), "CartActor");
        await cart.AddItem(item);
    }
}

The generic argument is the contract interface; the string is the actor type name you are addressing. Both are needed because a single interface can back more than one actor type (see One interface can back several actor types), so the interface alone does not identify which registered actor you mean. Pass the type name of the specific actor you intend to call.

There is also a static ActorProxy.Create<TActor>(...), backed by ActorProxy.Configure(...) and ActorProxy.Reset(), but prefer the injected IActorProxyFactory in essentially all application and test code.

Call an actor from inside an actor

An actor calls another actor the same way, by injecting IActorProxyFactory into its constructor. An actor reads its own identity from the protected ActorId Id supplied by activation, which is useful for deriving a target id or passing a correlation:

[DaprActor]
public sealed class CartActor(ActorActivationContext context, IActorProxyFactory proxies, IPricingClient pricing)
    : Actor, ICartActor
{
    protected override ActorId Id => context.ActorId;
    protected override IActorStateAccessor State => context.State;

    public async Task Checkout(CancellationToken ct = default)
    {
        var orders = proxies.Create<IOrderActor>(ActorId.Create($"order-{Id}"), "OrderActor");
        await orders.Place(Id, ct);   // Id is this cart's own actor id
    }
}

Three things are true of a call made from inside a turn, and two are easy to miss:

  • Awaiting the call does not release your turn. The runtime holds this actor’s lock for the whole turn, including while you await a call to another actor. The other actor runs its own turn; yours stays open until your method returns.
  • The call is at-least-once, like any invoke. If your turn is redelivered (see delivery guarantees), 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.

Call an actor in another Dapr app

From the caller’s side, calling an actor hosted by a different app looks identical to calling one in your own app. You create the same proxy with the same type name and await it; placement routes the call to whichever app hosts that type, and there is no special cross-app client, no endpoint, and no app id to manage in the common case.

// Same call, whether "CartActor" is hosted here or in another app:
var cart = proxies.Create<ICartActor>(ActorId.Create(userId), "CartActor");
await cart.AddItem(item);   // placement routes it to wherever "CartActor" lives

What makes this work across app boundaries is only how you share the contract:

  • Put the actor contract interface, with its [GenerateActorClient] attribute, in a shared assembly that both apps reference. Because the attribute travels with the interface, the calling app’s own build generates the client proxy from it, even though that app has no implementation.
  • The hosting app references the implementation and calls AddDaprActors().
  • The calling app references only the contract; it does not host the actor and does not need EnableAutoActorRegistration.

Because serialization is the same IDaprSerializer path on both sides and not tied to the proxy, the strongly-typed proxy works for cross-app calls too, which the old remoting proxy did not.

Dynamic invocation

The less common case is calling without a compile-time contract at all. When the actor type or method is not known until run time, such as in a gateway, in admin tooling, in a cross-language caller, or in an agent, call through IDynamicActorClient and discover what is callable at runtime through IActorRegistry, instead of the strongly-typed proxy. This is a distinct, language-agnostic capability with its own page: see Dynamic invocation.

Analyzers and diagnostics

The package ships analyzers that catch actor-specific mistakes at build time, several with code fixes. They use the DAPR14xx range.

IDFlagsCategorySeverity
DAPR1410A state shape change that breaks deserialization of shipped stateCompatibilityWarning
DAPR1411Work that escapes the actor scheduler, such as Task.RunConcurrencyWarning
DAPR1412A blocking call inside an actor turnConcurrencyWarning
DAPR1413Reading wall-clock time instead of an injected TimeProviderDeterminismWarning
DAPR1414An unseeded nondeterministic source, such as Guid.NewGuid() or RandomDeterminismWarning
DAPR1415A state migration target that is unreachable because no upcaster migrates to itCompatibilityWarning
DAPR1416Business logic inside an IActorTurnFilterDesignInfo
DAPR1417An actor interface method with an unsupported return typeUsageWarning
DAPR1418An in-place change to a shipped actor interface wire contractCompatibilityWarning
DAPR1419Mutable shared state held in an actor instance fieldConcurrencyWarning
DAPR1420Multiple actors that share a contract register the same actor type nameUsageWarning
DAPR1421A [DaprActor] implementation that does not expose a [GenerateActorClient] contractUsageWarning
DAPR1423A state type that looks like part of a migration family but is not connected to itCompatibilityWarning
DAPR1424A gap between ordered fragments in an actor state migration chainCompatibilityWarning
DAPR1425A non-additive state migration step that requires a hand-authored upcasterCompatibilityWarning
DAPR1426An actor state migration family with more than one fold path to a targetCompatibilityWarning
DAPR1427One persisted actor state name used with multiple migration familiesUsageWarning
DAPR1428Actor code that targets an older state type while a later reachable version existsUsageInfo
DAPR1429A scheduled reminder/timer callback that matches no dispatchable actor methodUsageError
DAPR1430A scheduled reminder/timer that targets an actor type not found in this applicationUsageWarning
DAPR1431A scheduled reminder/timer callback method that is not exposed through a generated actor clientUsageError

Best practices

  • Add only AddDaprActors() and let the generator do the rest; register building-block clients such as DaprStateManagementClient separately if you use them.
  • Keep actor state in State, keep fields immutable, and keep injected services stateless.
  • Use the strongly-typed proxy when you have the contract at compile time via IActorProxyFactory, and reserve IDynamicActorClient for gateways and tools that genuinely need late binding.
  • Pick the actor type name once and never change it.
  • Put [DaprActor] on the implementation and derive the contract from IActor; do not hand-write proxies or registration.

Next steps

Last modified July 13, 2026: Fix baseUrl for current (#5242) (d29da20)