TemporalCommunity.DurableObjects
0.1.1-preview.13
Prefix Reserved
See the version list below for details.
dotnet add package TemporalCommunity.DurableObjects --version 0.1.1-preview.13
NuGet\Install-Package TemporalCommunity.DurableObjects -Version 0.1.1-preview.13
<PackageReference Include="TemporalCommunity.DurableObjects" Version="0.1.1-preview.13" />
<PackageVersion Include="TemporalCommunity.DurableObjects" Version="0.1.1-preview.13" />
<PackageReference Include="TemporalCommunity.DurableObjects" />
paket add TemporalCommunity.DurableObjects --version 0.1.1-preview.13
#r "nuget: TemporalCommunity.DurableObjects, 0.1.1-preview.13"
#:package TemporalCommunity.DurableObjects@0.1.1-preview.13
#addin nuget:?package=TemporalCommunity.DurableObjects&version=0.1.1-preview.13&prerelease
#tool nuget:?package=TemporalCommunity.DurableObjects&version=0.1.1-preview.13&prerelease
TemporalCommunity.DurableObjects
An object-based programming model built on top of the Temporal .NET SDK. Write stateful, durable actors backed by Temporal workflows — without managing workflow plumbing directly.
Requires Temporal Server v1.28.0 or later (Update-with-Start GA). Requires .NET 10.0. NativeAOT is not supported in v1 — see ADR 002.
Installation
dotnet add package TemporalCommunity.DurableObjects
Quick Start
Five steps from zero to a working DurableObject. No raw Temporal concepts in calling code.
Step 1: Define the contract
Every DurableObject starts with an interface extending IDurableObject. Decorate it with
[Workflow] (the Temporal SDK uses this to derive the workflow type name). Updates are
fire-and-confirm ([WorkflowUpdate]); queries are synchronous reads ([WorkflowQuery]).
using Temporalio.Workflows;
using TemporalCommunity.DurableObjects;
[Workflow]
public interface ICounter : IDurableObject
{
[WorkflowUpdate] Task IncrementAsync();
[WorkflowQuery] int GetCount();
}
Step 2: Implement it
Extend DurableObjectBase and implement your interface. The one mandatory boilerplate is
[WorkflowRun] public Task RunAsync() => DurableObjectRunAsync(); — the SDK does not inherit
[WorkflowRun] from the base class, so every concrete DurableObject must declare it.
See BOILERPLATE.md for the full explanation.
[Workflow]
public class Counter : DurableObjectBase, ICounter
{
private int _count;
// Required boilerplate — see docs/BOILERPLATE.md
[WorkflowRun] public Task RunAsync() => DurableObjectRunAsync();
[WorkflowUpdate]
public Task IncrementAsync()
{
_count++;
return Task.CompletedTask;
}
[WorkflowQuery]
public int GetCount() => _count;
}
Step 3: Register on the worker
AddDurableObjects registers IDurableObjectFactory in DI (client-side).
AddDurableObjectWorkflows scans the assembly and registers the workflow types on the worker
(worker-side). Both go in the same service configuration; they serve different roles.
// Using Temporalio.Extensions.Hosting
services.AddTemporalClient(opts => opts.TargetHost = "localhost:7233");
// Client-side: registers IDurableObjectFactory with DefaultTaskQueue = "my-task-queue"
services.AddDurableObjects("my-task-queue");
// Worker-side: scans the assembly, registers DurableObject workflow types
services.AddHostedTemporalWorker("my-task-queue")
.AddDurableObjectWorkflows(typeof(Counter).Assembly);
Step 4: Call it
Inject IDurableObjectFactory, get a typed proxy, and call methods. No task queue, no workflow
ID management, no SDK ceremony.
public class MyService(IDurableObjectFactory factory)
{
public async Task RunAsync()
{
ICounter counter = factory.Get<ICounter>("my-counter");
await counter.IncrementAsync(); // starts the object if not running; delivers the update
int count = counter.GetCount(); // 1
}
}
Step 5: Verify
dotnet run
The counter object starts on first call and persists across restarts. State survives worker crashes, ContinueAsNew history compaction, and re-deployments.
Key Concepts
Tier model
Objects have three lifecycle tiers. V1 ships Tier 1 and Tier 3. See TIER_MODEL.md.
| Tier | Name | Behavior |
|---|---|---|
| 1 | Resident | Stays open indefinitely; Temporal sticky-cache handles idle periods. Default. |
| 2 | Cold Passivation | Deferred to v1.1 — not in this release. |
| 3 | Explicit Deactivation | Caller or object itself calls DeactivateAsync() / Deactivate() to close. |
DurableObject vs plain Temporal workflow
Use a DurableObject when you want an always-accessible stateful actor by stable ID — the mental model is a grain or entity, not a process. Use a plain Temporal workflow when the execution has a defined start and end with complex branching logic, or when you need child workflows, signals, or other SDK primitives not exposed by the DurableObject API surface.
Signals are banned — use updates
All methods on a DurableObject interface must be [WorkflowUpdate] or [WorkflowQuery].
[WorkflowSignal] is banned in v1, including on framework-provided methods. Signals bypass
the authorization hook, give callers no confirmation, and offer no rollback on partial state
mutation. See ADR 005.
NativeAOT limitation
DispatchProxy (used internally for the typed proxy) uses Reflection.Emit. NativeAOT strips
this at publish time. Publishing a DurableObject worker with PublishAot=true will fail at
runtime. A source-generator-based proxy is planned for v1.1. See ADR 002.
API Reference
IDurableObjectFactory methods
| Method | Description |
|---|---|
Get<T>(objectId) |
Returns a typed proxy. No RPC issued — creation is local. Uses DefaultTaskQueue. |
Get<T>(objectId, taskQueue) |
Same, with explicit task queue override. |
GetOrCreateAsync<T>(objectId) |
Guarantees the object exists (starts if not running) then returns a proxy. Issues one RPC. |
GetOrCreateAsync<T>(objectId, taskQueue) |
Same, with explicit task queue override. |
QueryDurableObjectAsync<TResult>(objectId, queryName, args) |
Non-blocking async query. Use on hot paths where thread-parking is unacceptable. |
QueryOrDefaultAsync<TResult>(objectId, queryName, args) |
Same, but returns default instead of throwing when the object is absent. |
ListDurableObjectsAsync<T>(runningOnly) |
Async stream of object IDs from Temporal visibility. |
CreateDurableObjectScheduleAsync<T>(...) |
Temporal Schedule that activates a fresh execution per tick. |
CreateDurableObjectReminderAsync<T>(...) |
Temporal Schedule that delivers recurring reminders to a canonical object. |
DurableObjectBase lifecycle hooks
Override any of these in your class. All have no-op defaults.
| Hook | When called | Exception behavior |
|---|---|---|
OnActivateAsync() |
Once per execution start (including post-ContinueAsNew). | Non-ApplicationFailureException → workflow terminates cleanly. |
OnDeactivateAsync() |
After drain completes on deactivation. | Swallowed and logged — deactivation completes regardless. |
OnTimerAsync(name) |
When a durable timer registered with ScheduleTimer fires. |
Non-ApplicationFailureException → workflow terminates cleanly. |
OnBeforeContinueAsNewAsync() |
Just before ContinueAsNew; return carry-forward constructor args. | Non-ApplicationFailureException → workflow terminates cleanly. |
DurableObjectWorkerInterceptor options
Pass via AddDurableObjectWorkflows(assembly, options):
| Option | Type | Default | Description |
|---|---|---|---|
Serialize |
bool |
true |
Non-reentrant by default — handlers execute turn-based. |
Authorize |
Func<HandleUpdateInput, bool>? |
null (allow all) |
Return false to reject an update with errorType: "Unauthorized". |
The interceptor is installed automatically by AddDurableObjectWorkflows. It also wraps
arbitrary update handler exceptions into clean ApplicationFailureException rejections so the
object does not wedge permanently. See FAILURE_HANDLING.md.
Failure Handling
Exception mapping, update handler failure taxonomy, lifecycle hook failure behavior, authorization patterns, and reminder idempotency — see FAILURE_HANDLING.md.
Versioning
Long-lived DurableObjects accumulate workflow history. Changes to handler names, signatures, and
ContinueAsNew constructor schemas require care. See ADR 004
for the full versioning strategy: what is safe, what requires Workflow.Patched, and how to
deploy new versions safely.
Object-to-Object Communication
Direct DurableObject-to-DurableObject messaging is deferred from v1. The v1 workaround is an
Activity that calls the target object via ITemporalClient:
[Activity]
public class CallCounterActivity(IDurableObjectFactory factory)
{
public async Task<int> GetRemoteCountAsync(string targetId)
{
var counter = factory.Get<ICounter>(targetId);
return counter.GetCount();
}
}
Execute it from inside an update handler:
[WorkflowUpdate]
public async Task SyncFromRemoteAsync(string remoteId)
{
_count = await Workflow.ExecuteActivityAsync(
(CallCounterActivity a) => a.GetRemoteCountAsync(remoteId),
new ActivityOptions { StartToCloseTimeout = TimeSpan.FromSeconds(30) });
}
Three candidate designs (Activity-mediated, child workflows, Nexus) are under evaluation for v1.1. See ADR 003.
Building from Source
Prerequisites: .NET 10 SDK, just.
dotnet tool restore # install minver-cli and reportgenerator
just build # restore + compile (Release)
just test # unit + integration tests
just pack # produces artifacts/packages/*.nupkg
just doctor # verify all prerequisites
Run just with no arguments to list all available recipes.
Common recipes
| Recipe | What it does |
|---|---|
just build |
Restore and compile in Release mode. |
just test-unit |
Unit tests only — no Temporal server needed. |
just test-integration |
15 integration scenarios (uses embedded WorkflowEnvironment). |
just test-filter "FullyQualifiedName~ScenarioA" |
Run a specific scenario. |
just pack |
Pack the library; MinVer reads the git tag for the version. |
just run-sample |
Run the sample app (requires a live Temporal server at localhost:7233). |
just ci |
Full CI pipeline: clean → build → unit tests → pack. |
Contributing
- Fork and clone.
dotnet tool restoreonce after cloning.- Run
just cibefore opening a PR — this is what CI runs. - For integration tests:
just test-integration(no external Temporal server needed; it usesWorkflowEnvironment.StartLocalAsync()). - Add an ADR in
docs/adr/for any architectural decision, API surface change, or significant constraint. Follow the format in the existing ADRs.
License
MIT — see LICENSE.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Temporalio (>= 1.16.0)
- Temporalio.Extensions.Hosting (>= 1.16.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.3.4 | 110 | 7/15/2026 |
| 0.3.2 | 96 | 7/14/2026 |
| 0.2.0 | 111 | 7/13/2026 |
| 0.1.1-preview.13 | 52 | 7/8/2026 |