Stateflows.Testing
0.21.0-beta
Prefix Reserved
See the version list below for details.
dotnet add package Stateflows.Testing --version 0.21.0-beta
NuGet\Install-Package Stateflows.Testing -Version 0.21.0-beta
<PackageReference Include="Stateflows.Testing" Version="0.21.0-beta" />
<PackageVersion Include="Stateflows.Testing" Version="0.21.0-beta" />
<PackageReference Include="Stateflows.Testing" />
paket add Stateflows.Testing --version 0.21.0-beta
#r "nuget: Stateflows.Testing, 0.21.0-beta"
#:package Stateflows.Testing@0.21.0-beta
#addin nuget:?package=Stateflows.Testing&version=0.21.0-beta&prerelease
#tool nuget:?package=Stateflows.Testing&version=0.21.0-beta&prerelease
Introduction
Stateflows is a .NET-based framework for building executable State Machines. It targets .NET 8.0+ and uses actor model to achieve scalability and robustness. It comes with minimum dependencies and enables developers to write state-oriented business logic in a testable, object-oriented, enterprise-class code.
Getting Started
Go to Stateflows homepage to learn more.
Execution failures
State machines, activities, actions, and entities execute against a detached copy
of their stored context. Their shared persistence flow commits once, only after
execution and executor cleanup succeed. A failed result or any recorded execution
exception (including one handled by the behavior) returns EventStatus.Failed
without saving context changes, initialization, configuration, or scheduling metadata.
Stateless actions continue to discard their context on every execution.
This rollback covers the behavior's stored context, not notifications, external side effects, or separate events sent to other behaviors or to itself. Contexts must support the framework's polymorphic JSON serialization used to isolate the working copy, including nested values.
Embedded activities, like embedded state machines, do not bubble unconsumed
events marked with NoBubblingAttribute. Ordinary unconsumed events still bubble
to the parent with forwarding disabled. State exit continues to send Finalize
to embedded behaviors, even when immediate finalization has already taken effect.
Entity observers and interceptors
Entities support asynchronous, filter-style hooks, globally or for a single entity:
builder.AddEntities(entities => entities
.AddObserver<TelemetryObserver>()
.AddInterceptor<AuthorizationInterceptor>()
.AddEntity<ICounter>("counter", entity => entity
.AddObserver<CounterObserver>()
.AddInterceptor<CounterInterceptor>()
)
);
Generic registrations activate hooks through dependency injection, including constructor
dependencies. Both registration levels also accept synchronous or asynchronous factories
with (IServiceProvider services, IBehaviorActionContext context) parameters:
entities.AddObserver((services, context) => new TelemetryObserver());
entities.AddInterceptor(async (services, context) =>
{
var policy = await services.GetRequiredService<PolicyProvider>().GetAsync(context.Behavior.Id);
return new AuthorizationInterceptor(policy);
});
Hooks are resolved once per incoming event and reused throughout that event's execution. Global hooks wrap per-entity hooks; within each group, registration order is outermost first. Asynchronous factories, continuations, and hook cleanup are awaited.
Observing entity operations
Implement Stateflows.Entities.IEntityObserver with these filter-style methods:
Task OnMutationAsync<TTemplate, TMutationEvent>(
IMutationObservationContext<TTemplate, TMutationEvent> context, Func<Task> next);
Task OnComputedFieldAsync<TTemplate, TField>(
IComputedFieldObservationContext<TTemplate> context, Func<Task<TField>> next);
Task OnProjectionAsync<TTemplate, TProjection>(
IProjectionObservationContext<TTemplate> context, Func<Task<TProjection>> next);
Each context exposes the typed Entity and IBehaviorActionContext capabilities,
including Behavior. Mutation contexts also expose MutationEvent, computed-field
contexts expose FieldName, and projection contexts expose their configured PublishScope.
Awaiting next() exposes computed-field and projection results for inspection:
public async Task OnProjectionAsync<TTemplate, TProjection>(
IProjectionObservationContext<TTemplate> context, Func<Task<TProjection>> next)
{
try
{
var projection = await next();
// Record telemetry about the result without changing it or the entity.
}
finally
{
// Cleanup/telemetry also runs if projection evaluation throws.
}
}
Observers are observational: call and await next exactly once, propagate its
exceptions, and do not change entity state or results. Missing or repeated continuation
calls fail execution; leaving entity state unchanged and propagating exceptions remain
the observer implementation's responsibility. Continuation results are preserved by the
pipeline, not replaced by observers. Use an interceptor, not an observer, to reject events.
Computed-field and projection hooks run during initialization and dependency-driven recalculation, including cascades after mutations or external field writes. Mutation hooks wrap registered mutations, not field writes or initialization. Projection requests evaluate the projection again and invoke its hooks; ordinary field reads use cached values and do not invoke observer hooks. There are no entity lifecycle observer hooks. Field reads performed by observers do not become computed-field or projection dependencies.
Handling entity exceptions
Implement Stateflows.Entities.IEntityExceptionHandler to observe failures from entity
initialization, registered mutations, computed fields, or projections. Register handlers
globally or for an individual entity using DI activation or synchronous/asynchronous
factories that receive (IServiceProvider services, IBehaviorActionContext context):
builder.AddEntities(entities => entities
.AddExceptionHandler<TelemetryExceptionHandler>()
.AddEntity<ICounter>("counter", entity => entity
.AddExceptionHandler<CounterExceptionHandler>()
)
);
IEntityInitializationExceptionContext exposes ImplicitInitialization and inherits
the execution trigger metadata. Initialization handlers run for explicit initialization,
custom initialization, and implicit initialization; failures in computed fields or
projections during initialization also invoke their operation-specific handler.
Mutation handlers wrap only the registered mutation delegate. Computed-field and
projection handlers also run for dependency recalculations and projection requests.
Handler contexts expose isolated entity proxies, so reads made by a handler do not add
reactive dependencies.
Each asynchronous handler returns true to classify its exception as handled or false
otherwise. This classification does not suppress the exception or fabricate a result:
the original failure still propagates through normal execution, producing
EventStatus.Failed and rolling back the entity's partial state. Global handlers run
before per-entity handlers in registration order, and all handlers are invoked even after
one returns true. A handler that throws is logged and recorded; remaining handlers
still run and the original operation exception remains the failure.
Intercepting events
Implement Stateflows.Entities.IEntityInterceptor:
public async Task<EventStatus> ProcessEventAsync<TEvent>(
Stateflows.Common.Context.Interfaces.IEventContext<TEvent> context,
Func<Task<EventStatus>> next)
{
if (!IsAllowed(context.Event))
return EventStatus.Rejected; // No continuation: no implicit initialization or mutation.
var status = await next();
return status; // An interceptor may instead return a replacement status.
}
Interceptors wrap incoming event processing, including requests and explicit or implicit
initialization. They may short-circuit without calling next, or call and await it at
most once and replace its returned status. Repeated continuation calls fail execution.
Replacing a status cannot undo external side
effects already performed; reject before invoking the continuation to prevent execution.
These hooks belong to the shared entity runtime, not a particular hosting integration. When hosted in Orleans, the grain awaits the complete asynchronous hook pipeline before completing event execution. Without hooks, entity behavior is unchanged. Unhandled hook or operation exceptions follow the normal execution-failure and rollback rules above.
Entity OpenTelemetry tracing
The existing Stateflows.Extensions.OpenTelemetry package automatically instruments
entities when registered with AddOpenTelemetry(); no separate entity package or
per-entity tracing registration is needed:
builder.AddOpenTelemetry()
.AddEntities(entities => entities.AddEntity<ICounter>("counter"));
The Stateflows activity source emits an internal event span named
Entity 'counter:instance' processing 'EventName', tagged with EntityId, Event,
and EventStatus. Its internal child spans describe Mutation 'EventName',
Computed field 'FieldName', and Projection 'ProjectionType', with EntityId
and the corresponding Mutation, Field, or Projection tag. Incoming serialized
W3C trace context is honored, falling back to the ambient activity; parent-based
sampling applies to the event and its operations.
Mark an event type with [NoTracing] or send a NoTracing event header to suppress
all entity spans and tracer error logs for that event. Operation exceptions are
recorded as exception events with error status, and failed event spans receive error
status without marking the caller's span. Tracing observes failures without handling
or suppressing them: normal EventStatus.Failed and entity rollback semantics remain
unchanged, including initialization failures.
Orleans hosting
Stateflows.Runtimes.Orleans hosts registered state machines, activities, and actions
through one shared Orleans grain contract and implementation. Each tenant and
BehaviorId identifies a separate grain; no per-definition grain generation is needed.
Orleans hosting does not automatically dispatch Startup events or maintain startup
indexes. Default instances are still initialized for configured tenants when their
hosting silo starts. The shared framework's Startup event remains available for
explicit sends and other runtimes. Previously persisted startup indexes are unused
and can be removed from storage.
Register each silo's definitions using ISiloBuilder.AddStateflows(...). Definitions
must remain fixed after startup, but different silos can register different sets.
Every silo pulls the behavior classes and definition versions of each Active silo
directly from that silo (through a small grain pinned to it) once per second.
Capabilities are cached for the silo's lifetime (a restarted silo has a new address);
failed or timed-out pulls are retried on every refresh, so recovery does not wait for a
membership change, and silos that leave or die are dropped. Orleans silo metadata is
not used. A silo always treats its own definitions as available, even before
discovery completes.
Placement is filtered by the behavior class in the grain key. Among silos hosting the class, only those registering every version of it registered by any candidate silo are eligible. This keeps rolling upgrades safe when new silos keep the old versions (v1 → v1+v2): any persisted version can be processed, and new instances initialize with the newest version wherever they land. Existing activations keep running where they are until deactivated. If no silo hosts every version (an incompatible deployment, e.g. v1-only and v2-only silos), silos hosting the newest version are used and a warning is logged. An activation whose persisted version is not registered locally fails with an explicit error and never runs under another version; the instance becomes reachable again once a silo hosting all versions joins. Orleans selects among eligible silos using its configured placement strategy. If no silo hosting the class is known (e.g. the last one was lost), sends fail fast with "No eligible silo hosts Stateflows behavior"; persisted state is left intact and the instance resumes once a hosting silo rejoins. Reminders firing meanwhile are retried on later ticks. After abrupt termination, an instance reactivates on another eligible silo with its last committed state once Orleans declares the silo dead.
Locators discover remote definitions asynchronously. Discovery is eventually consistent: allow for membership propagation and the one-second refresh interval before locating newly available remote definitions. The same definition name and version on multiple silos must have compatible code, dependencies, and event/response types; this is not verified. Runtime registration does not distribute that code.
Resource budgets configured with AddResource(...).SetMaxConcurrentBehaviorExecutions(...)
and behavior SetResourceName(...) are enforced independently on each silo, across
all instances and behavior classes assigned to that resource (including tenants).
Normal events, initialization, and reminder-triggered events acquire
the same local budget. Other Orleans infrastructure grains do not consume it.
Unconfigured limits remain unlimited; configured values of zero or less use the
local processor count, as in the legacy runtime.
Budgets are strict: a behavior awaiting another behavior retains its permit. Awaited dependency chains must use separate resources or have sufficient capacity; otherwise they can deadlock when all permits are occupied.
Admission and overload
A concurrency limit bounds running executions, not queued work. A grain processes one event at a time, so each behavior instance waits for at most one permit; further messages for it queue in its Orleans mailbox. Two optional per-resource limits shed load before it accumulates (both require a concurrency limit):
.AddResource("database", r => r
.SetMaxConcurrentBehaviorExecutions(8)
.SetMaxQueuedBehaviorExecutions(100) // permit waiters per silo; default unlimited
.SetMaxQueueWaitTime(TimeSpan.FromSeconds(5))) // default unlimited
An event rejected by either limit, or arriving while the silo is stopping, returns
EventStatus.Overloaded (HTTP 503 in Minimal APIs). It is not executed, committed,
or recorded as received, so retrying the same event ID executes it once. Requests
(including status requests) are admitted the same way. Reminder-triggered time events
and durable notification deliveries treat Overloaded as retryable and retry on
their next reminder tick. Default-instance initialization only logs it.
Without a wait limit, a waiting caller is still bounded by the Orleans request
timeout. Orleans' own load shedding (SiloMessagingOptions.LoadSheddingEnabled)
protects the silo as a whole and is configured separately.
Graceful shutdown
When a silo stops gracefully, permit waiters are released immediately with
Overloaded, and new executions are rejected. Executions that already hold a permit
run to completion, publish and commit before the silo starts deactivating grains.
The drain is bounded by the host shutdown timeout. An abrupt stop or crash does not
drain; uncommitted executions are retried by their senders, reminders, or deliveries.
Metrics and diagnostics
IStateflowsResources is independent of the legacy execution engine and reports
running executions (BehaviorExecutionsCount) and executions waiting for a local
resource permit (WaitingExecutionsCount). Waiters are not Orleans mailbox depth:
messages queued inside Orleans for a busy grain are not counted. Use Orleans'
own metrics for mailbox and messaging load.
AddOpenTelemetry() publishes, per resource and silo, the execution counter, the
duration histogram, the running gauge and the waiting gauge
(stateflows.resource.{name}.execution.waiting). queueLength is deprecated; it
equals waiting under Orleans. Execution metrics are tagged with behavior.class,
event.name and event.status only. They do not use behavior instance IDs, so
cardinality is bounded by the definitions. Instruments are owned by each silo's
service provider. AddOpenTelemetry() also subscribes to the Stateflows.Orleans
meter, which counts operational failures. These counters use low-cardinality tags
only:
| Counter | Tags |
|---|---|
stateflows.orleans.persistence.failures |
state, behavior.class (for behavior state) |
stateflows.orleans.reminder.failures |
operation (register, unregister, restore, notifications.*), behavior.class |
stateflows.orleans.index.failures |
operation (add, remove), behavior.class |
stateflows.orleans.publication.failures |
operation (publish, signal, delivery.*) |
stateflows.orleans.admission.rejections |
resource, reason (QueueFull, WaitTimeout, ShuttingDown), behavior.class |
stateflows.orleans.discovery.failures |
none |
Failures are also logged. Startup logs the number of hosted classes, the progress and outcome of default-instance initialization, and changes in discovered silos and classes.
Orleans hosting does not register StateflowsEngine or StateflowsService;
the legacy in-process registration still provides them.
HTTP hosts should also configure WithTracing(tracing => tracing.AddAspNetCoreInstrumentation())
(from OpenTelemetry.Instrumentation.AspNetCore). Stateflows propagates the incoming
trace's sampling decision; without HTTP instrumentation, an unrecorded ASP.NET Core
request activity can suppress behavior spans under the default parent-based sampler.
Storage contract
Under Orleans, behavior grains own behavior state. No separate Stateflows storage
(IStateflowsStorage implementation) is needed or used for it. Configure:
- Grain storage named
stateflows. It holds all Stateflows grain state:stateflows-context: each behavior's context, including its values, pending time events and event receipts, or a receipts-only tombstone after deletion;notifications,retainedNotificationsState,subscriptionsState: each publisher's notification history, subscriptions and pending deliveries;instances: the instance index of each tenant and behavior class.
- A durable reminder table, used for time events and notification delivery retries.
For production, use a durable grain storage provider that enforces ETag concurrency
(for example Azure Table/Blob, ADO.NET or Cosmos DB). ETags make failover safe by
rejecting commits from a lost activation. AddMemoryGrainStorage("stateflows") and
UseInMemoryReminderService() are for development only: state and reminders are
lost with the silo.
Stateflows grains use string keys made of /-separated, URI-escaped segments:
tenant/Type/Name/Instance for behaviors (also the key of their subscriptions and
notifications grains), tenant/Type/Name for instance indexes and
publisherKey|subscriberKey for notification deliveries. A null tenant or instance
is encoded as %. Keys never contain {, } or ". Orleans ADO.NET storage
(10.4.0) passes grain keys to string.Format, so braces in a key would make every
write fail.
The Orleans integration tests include SQL Server tests (TestCategory=SqlServer) that
run against ADO.NET grain storage and reminders. They use
Server=localhost;Database=Stateflows;Integrated Security=True;TrustServerCertificate=True,
or the STATEFLOWS_SQLSERVER environment variable when set. They are inconclusive when
the server is unreachable. The official Orleans scripts in
Tests/Orleans.IntegrationTests/SqlServer are installed if missing. Each test uses
its own ServiceId and deletes its rows.
Behavior contexts are persisted as a Stateflows JSON payload inside a
[GenerateSerializer] wrapper (BehaviorContextState), so application-defined
context values do not need to be added to the Orleans storage serializer type
allow-list.
State is accessible only inside the behavior's grain. Under Orleans,
IStateflowsStorage.HydrateAsync/DehydrateAsync throw InvalidOperationException
when called from anywhere else. GetAllContextsAsync and the time- and
startup-triggered scans throw NotSupportedException. To read or change state,
send events or requests to the behavior. A failed behavior write/clear restores the
activation's previously committed context and requests deactivation, so the next
activation reloads authoritative storage. A provider error after an actual commit
is an ambiguous outcome, not a guarantee that the durable write was rolled back.
Orleans stages dehydration until its AfterExecute interceptors have also succeeded;
an interceptor failure therefore does not commit the staged context.
Standalone Orleans client registration and live definition updates are not provided
by this integration.
Instance listing
IStateflowsStorage.GetAllContextIdsAsync and the Minimal APIs listing endpoints
(GET /stateflows, /stateflows/stateMachines/{name}, ...) are served by an
instance index. The index is one StateflowsInstances grain per tenant and behavior
class, persisted in the stateflows storage. Listing uses the tenant returned by
IStateflowsTenantProvider, which defaults to host.
- An instance is listed once it has committed state of a live behavior: one that is
initialized or holds runtime data (time events, subscriptions, ...). A context that
is deleted, or still not initialized, is persisted at most as a receipts-only
tombstone and is never listed. A status request with the
NoImplicitInitializationheader commits nothing, so it neither creates an entry nor resurrects a reset instance. - The entry is added before the first commit. If adding fails, the event fails with nothing committed and can be retried. A committed instance is therefore never missing from listings.
- The entry is removed (best effort) after deletion, for example after a full
Reset. If removal fails or the silo crashes in between, a stale entry remains until that instance is next activated and deactivated without state. Pruning is skipped on silo shutdown and after an ambiguous commit failure. - Each index grain serializes all first commits and deletions of its class and keeps the whole instance set in one state record. A class with very many instances, or a high rate of instance creation/deletion, makes it a hot grain with a large record. Listing returns the full set without paging.
Minimal API endpoint contexts
Custom endpoints declared with AddEndpoints(...) never hydrate or save behavior
state. The endpoint filter reads the behavior's status with a
NoImplicitInitialization request, so it still passes resource admission. It returns
404 unless the instance is initialized, or unless the endpoint's state or node is
active. Endpoint contexts (IBehaviorEndpointContext, IStateMachineEndpointContext,
IStateEndpointContext, IActivityEndpointContext, IActivityNodeEndpointContext,
IActionEndpointContext) expose:
- behavior information: ID, current states or active nodes, expected events, links;
- messaging:
Send, subscribe/unsubscribe, and entity operations.
They expose no Values and no Publish. Sends are completed after the handler
returns. To change state, send an event to the behavior.
The Orleans integration tests also start two silo hosts as separate operating-system processes and verify cross-silo discovery and execution for state machines, activities, and actions.
Orleans watches reuse one observer reference per behavior handle while watches are
active and renew every four minutes. Removing the last watch stops renewal and
deletes the observer reference. Dispose handles before stopping their silo/client:
Dispose() disables local delivery immediately and starts remote cleanup;
GrainBehavior.DisposeAsync() also waits for that cleanup and propagates failures.
Already-started handlers may finish after unwatch/disposal; pending callbacks are
discarded. Handlers execute independently on the thread pool, may overlap, and are
awaited only inside their background dispatch, never by the publisher or subscription
grain. Handler, renewal, and cleanup failures are logged. Watches support timestamp
replay and replay on renewal after subscription-grain reactivation. Reconnection can
repeat notifications, and observer acknowledgment does not confirm that a user
handler has completed. Watches are not durable behavior subscriptions.
Orleans notification publication waits for durable notification and pending-delivery
storage, not subscriber execution. Synchronous behavior Publish calls stage
publication within the current execution; the behavior turn asynchronously waits
for publication persistence before committing behavior state or returning success.
Publication and behavior-state writes are not one transaction: a behavior-state
write can fail after publication has succeeded.
The publisher's StateflowsSubscriptions grain stores history, subscriptions, and
pending deliveries together. Separate publisher/subscriber-pair delivery grains
await behavior processing and persist delivery confirmations without holding the
subscription grain open. Persistent reminders retry unconfirmed deliveries and
periodically remove expired history and pending work. Initialized, Deferred,
Consumed, Forwarded, NotConsumed, Invalid, and Duplicated confirm delivery; other results
and exceptions remain retryable until expiration or unsubscription.
Each publisher/subscriber pair processes sequentially; a failed notification blocks
later notifications for that pair until it succeeds, expires, or is unsubscribed.
Other pairs run independently. A permanently failing notification with no TTL can
therefore block its pair indefinitely. Long-running processing is subject to the
application's configured Orleans request timeout, not an additional delivery timeout.
TTL applies to retained notifications too; zero means no expiration. Retain selects the latest retained value of a notification name for immediate delivery upon subscription/watch registration; it does not prevent expiration. Timestamp replay uses unexpired history. Confirmation removes pending work, not replay history. Subscription registration does not await retained or replay delivery.
Delivery is at-least-once while eligible. Orleans stateful behaviors deduplicate
all incoming events, including notifications, commands, and requests, by
event ID at the behavior grain boundary. Confirmed event IDs persist atomically
with their behavior state, returning
Duplicated on retries without executing again. Receipts survive reactivation,
reset, and logical deletion. Positive-TTL receipts expire at SentAt + TTL, and
expired incoming events never execute. Zero-TTL receipts expire one hour after
successful processing; a later retry or replay may execute again. Expired receipts
are pruned on subsequent state writes. Failed/retryable outcomes do not record
receipts. Stateless actions are deliberately not deduplicated. Internal
deferred-event processing still executes normally inside the processor.
Duplicate requests return Duplicated with no response; previous responses are
not cached. Calls without an EventId header create new IDs and are independent
events.
Deduplication can be disabled for idempotent events. Event types marked with
[NoDeduplication] are never deduplicated: a redelivery executes again and no
receipt is recorded. The built-in read-only requests are marked: BehaviorInfoRequest,
StateMachineInfoRequest, ActivityInfoRequest, EntityInfoRequest,
NotificationsRequest, TokensOutputRequest, TokensOutputRequest<T>,
ProjectionRequest<T>, and FieldStateRequest<T>. A retried status request is
therefore answered again instead of returning Duplicated. For a single event,
add a NoDeduplication header instead.
An execution that changes nothing is not persisted. Before committing, the grain
compares the new context with the committed one, ignoring LastExecutedAt; if
they are equal and no receipt was recorded, no storage write, reminder change, or
index update happens. Status requests on an existing behavior therefore never write
state, while a status request that implicitly initializes a behavior does. Under
Orleans, LastExecutedAt therefore records the last persisted execution, not the
last delivered event.
External channel adapters should supply the same EventId header on every
redelivery. Both SendAsync(payload, headers) and RequestAsync(request, headers)
use its Guid Id when constructing the event holder, including through client
proxies and the Orleans transport. The header's dictionary key is arbitrary;
the header type identifies it. Supply at most one EventId header per call.
For example:
var headers = new Dictionary<string, EventHeader>
{
[nameof(EventId)] = new EventId(messageId) // stable Guid from the external channel
};
var result = await behavior.SendAsync(command, headers);
// Reusing messageId during redelivery returns Duplicated after a committed execution.
Existing envelopes retain their original EventHolder.Id during retries and
transport deserialization; an EventId header does not replace an existing
envelope's ID. The header is applied only when constructing a new holder.
This is not exactly-once execution of external side effects: an uncommitted execution can be retried, and publication is not transactional with behavior state. Expiration cannot recall a notification already processing. Unsubscribe cancels pending work but cannot recall an in-flight call. Publisher reset/deletion currently does not remove the separate subscription grain's history or registrations. Existing retained values and subscription registrations are imported into the new combined notification state on activation. Historical ordinary notifications were not previously persisted by this Orleans adapter and cannot be reconstructed.
Time events are scheduled in UTC in every runtime. TimeEvent.TriggerTime and
StateflowsContext.TriggerTime convert local values to UTC and treat unspecified
values as UTC; registrations use DateTime.UtcNow. Calendar-aligned built-ins
(AfterOneHour, EveryFifteenMinutes, AtEvery9AM, ...) align to UTC boundaries,
so AtEvery9AM means 09:00 UTC. DelayEvent applies its delay once.
Orleans schedules each behavior's earliest pending time event with one Orleans
reminder (Stateflows.TimeTrigger). The reminder is registered or updated before
the behavior state that needs it is committed; if registration fails, the event
fails and nothing is committed. It is unregistered after a commit leaves no pending
time events; an unregistration failure does not fail the event, and the leftover
reminder removes itself on its next tick. Events that do not change the earliest
trigger time do not access the reminder table, so traffic cannot postpone a due
time event. The reminder period is ReminderOptions.MinimumReminderPeriod (Orleans
default: one minute); it is the retry interval for failed or interrupted callbacks.
Use a durable reminder table and grain storage with ETag concurrency checks in
production: UseInMemoryReminderService() loses reminders with the silo that holds
its table, and failover relies on ETags to reject commits from a lost activation.
A time event never executes before its trigger time, as measured by the clock of
the silo hosting the behavior; keep silo clocks synchronized. While the silo is
running it normally executes within about a second after the trigger time. After
downtime, failover, or a failed callback it executes on a later tick: Orleans keeps
ticks on the reminder's original StartAt + N × Period schedule, so allow up to one
period after recovery (plus silo-failure detection time). Each overdue occurrence
executes once. Due occurrences run in trigger-time order, each against the state
committed by the previous one, so an earlier occurrence can clear or reschedule a
later one. Recurring events re-arm from their processing time; missed intervals are
skipped, not replayed. Reminder ticks are at-least-once. A tick uses the occurrence's
ID as the event ID, and an occurrence is only accepted while still pending, so it
commits at most once. Side effects of an interrupted, uncommitted execution can
run again.
This adapter is still under development: immediate-finalization signals contain unfinished implementations. Capability-aware placement does not complete them.
Beehaviors runtime bridge
Stateflows.Runtimes.Beehaviors keeps Stateflows runtime-agnostic while executing
all registered state machines, activities, actions, and entities through the
in-process Beehaviors actor runtime.
Usage:
services.AddBeehaviors(b => b
.AddStateflows(sf => sf
.AddStateMachines(...)
.AddActivities(...)
.AddActions(...)
.AddEntities(...)));
| 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 (>= 10.0.13)
- Stateflows (>= 0.21.0-beta)
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.21.1-beta | 0 | 10/20/2026 |
| 0.21.0-beta | 45 | 10/19/2026 |
| 0.20.13-beta | 71 | 9/25/2026 |
| 0.20.12-beta | 78 | 9/8/2026 |
| 0.20.11-beta | 72 | 9/7/2026 |
| 0.20.10-beta | 76 | 9/2/2026 |
| 0.20.9-beta | 78 | 9/1/2026 |
| 0.20.8-beta | 95 | 8/17/2026 |
| 0.20.7-beta | 84 | 8/6/2026 |
| 0.20.6-beta | 86 | 8/5/2026 |
| 0.20.5-beta | 87 | 7/23/2026 |
| 0.20.4-beta | 87 | 6/24/2026 |
| 0.20.3-beta | 97 | 6/24/2026 |
| 0.20.2-beta | 87 | 6/23/2026 |
| 0.20.1-beta | 81 | 6/23/2026 |
| 0.20.0-beta | 94 | 6/23/2026 |
| 0.19.6-beta | 86 | 6/1/2026 |
| 0.19.5-beta | 132 | 5/11/2026 |
| 0.19.4-beta | 91 | 4/20/2026 |
| 0.19.3-beta | 93 | 4/16/2026 |