Skip to content

Dependency Injection

Mikołaj Milewski edited this page Oct 27, 2026 · 1 revision

Dependency Injection

Stateflows runtime elements can receive application services and the context of their current execution through constructor injection. This applies to State Machine states, guards and effects, Activity nodes and flows, Action implementations, and supported typed middleware. A context parameter does not need a special attribute:

public sealed class Working(
    IStateContext state,
    IBehaviorContext behavior,
    ILogger<Working> logger) : IStateEntry
{
    public Task OnEntryAsync()
    {
        logger.LogInformation("Entering {State} in {Behavior}", state.Name, behavior.Id);
        return Task.CompletedTask;
    }
}

This example uses System.Threading.Tasks, Microsoft.Extensions.Logging, Stateflows.Common, and Stateflows.StateMachines. Register the state through a typed builder overload such as AddState<Working>(). Application services must be registered with the host's IServiceCollection; Stateflows supplies the execution contexts when it activates the element.

Runtime activation versus definition

Stateflows activates typed runtime elements through StateflowsActivator. It selects the public constructor with the most parameters, resolves its arguments, and constructs the element. Prefer one public constructor. Ordinary arguments are resolved as required DI services; missing registrations fail activation.

Model-element types referenced by typed builders do not need a separate AddTransient<TElement>() registration. Their application dependencies do. Stateflows-specific parameter attributes are processed on the model element's constructor, not recursively on constructors of arbitrary application services resolved by Microsoft DI.

The static Build methods of IStateMachine, IActivity, IStateDefinition, and other definition contracts run while definitions are registered. They are not per-instance execution callbacks and have no current behavior context to inject. An Entity's IEntity<TTemplate>.Build similarly configures a definition, not an executing Entity.

Do not treat runtime element objects as persistent behavior instances. Keep durable data in Behavior Values or Entities, not in element instance fields.

Shared execution contexts

The common interfaces are in Stateflows.Common:

Injectable service Meaning
IBehaviorContext The currently executing Behavior: identity, Values, messaging, subscriptions, and associated Entity operations.
IExecutionContext The current trigger, trigger ID, event headers, and cancellation token.
IParentBehaviorContext Messaging and Entity operations directed at the immediate containing Behavior, when one exists.
IOwnerBehaviorContext Messaging and Entity operations directed at the behavioral-group owner, when one exists.

These are execution-dependent services, not arbitrary services that can be resolved at startup or from an unrelated HTTP request. The runtime keeps their current values in AsyncLocal holders and exposes them through transient DI factories. Transient registration does not create a new Behavior or extend a context's lifetime.

If a context is unavailable, its factory throws InvalidOperationException with a message such as No service for type '...' is available in this context. Registering all behavior families does not make all their contexts available everywhere. A nullable constructor parameter does not bypass this throwing factory.

Inject parent/owner contexts directly when embedding is a requirement of the element. If the element must also work as a standalone Behavior, use the execution callback's TryGetParentBehaviorContext / TryGetOwnerBehaviorContext methods and handle absence deliberately. See Behavioral Groups.

State Machine contexts

In addition to the common contexts, typed State Machine runtime elements can inject IStateMachineContext and Stateflows.StateMachines.IExecutionContext. The latter extends the common execution context with recorded ExecutionSteps. Use fully qualified names or aliases when importing both namespaces.

Activation site Element-specific context Not available at this site
State entry/exit, composite/orthogonal initialization/finalization IStateContext ITransitionContext
State deferral guard IStateContext ITransitionContext
Transition guard/effect, including default transitions ITransitionContext IStateContext
Machine initializer/finalizer Machine and execution contexts only IStateContext, ITransitionContext

For transition endpoints, use ITransitionContext.Source and .Target; the target is absent for an internal transition. Do not inject a current IStateContext into a transition to represent either endpoint. The registered service is the non-generic ITransitionContext; receive the typed event through GuardAsync(TEvent) / EffectAsync(TEvent), not by assuming the callback's generic context interface is a DI service.

IStateMachineInspection is also registered for runtime inspection. Use Observability for observation hooks rather than retaining the inspection model beyond execution.

Share state lifecycle logic through a base class

An inherited IStateEntry implementation can use the registered state identity, eliminating repeated entry lambdas and duplicated name literals:

using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Stateflows.Common;
using Stateflows.StateMachines;

public abstract class LoggedState(
    IStateContext state,
    IBehaviorContext behavior,
    ILogger<LoggedState> logger) : IStateEntry
{
    public Task OnEntryAsync()
    {
        logger.LogInformation("Entering {State} in {Behavior}", state.Name, behavior.Id);
        return Task.CompletedTask;
    }
}

public sealed class Waiting(
    IStateContext state,
    IBehaviorContext behavior,
    ILogger<LoggedState> logger) : LoggedState(state, behavior, logger);

public sealed class Processing(
    IStateContext state,
    IBehaviorContext behavior,
    ILogger<LoggedState> logger) : LoggedState(state, behavior, logger);

Definition fragment:

.AddInitialState<Waiting>(state => state
    .AddTransition<StartProcessing, Processing>())
.AddState<Processing>()

Supply the application's StartProcessing event. The base can instead await a domain operation, such as updating the group's Entity with the current stage. Only states that should perform that operation should inherit it. Composite and orthogonal implementations must additionally implement their appropriate marker interfaces.

Use typed registration and target overloads consistently. Where an API requires a string, use State<TState>.Name rather than a hardcoded copy of a type-derived name. An explicitly overridden registration name must also be used consistently; State<TState>.Name resolves naming metadata/settings, not an arbitrary custom registration alias. See State identity.

Reuse guards and effects through event contracts

ITransitionGuard<in TEvent> and ITransitionEffect<in TEvent> are contravariant. One handler can consume an interface implemented by several concrete event types:

using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Stateflows.Common;
using Stateflows.StateMachines;

public interface IReasonable
{
    string Reason { get; }
}

public sealed record Cancel(string Reason) : IReasonable;
public sealed record Reject(string Reason) : IReasonable;

public sealed class RecordReason(
    IBehaviorContext behavior,
    ILogger<RecordReason> logger) : ITransitionGuard<IReasonable>, ITransitionEffect<IReasonable>
{
    public Task<bool> GuardAsync(IReasonable @event) =>
        Task.FromResult(!string.IsNullOrWhiteSpace(@event.Reason));

    public Task EffectAsync(IReasonable @event)
    {
        logger.LogInformation("Reason for {Behavior}: {Reason}", behavior.Id, @event.Reason);
        return Task.CompletedTask;
    }
}

One class can implement both contracts. The typed transition overload automatically attaches its guard and effect, so there is no need to repeat AddGuard and AddEffect callbacks at each use:

.AddTransition<Cancel, RecordReason, Stopped>()
.AddTransition<Reject, RecordReason, Stopped>()

Stopped must be a registered IState type. The transitions still declare concrete trigger types; sharing an interface-based handler does not itself register an interface-triggered transition. Do not infer event-dispatch behavior from handler contravariance.

The type-parameter order is trigger, transition, target, omitting the transition type when only a target is needed, and omitting parameters that do not apply:

Transition kind Typed form
Regular AddTransition<TEvent, TTarget>() or AddTransition<TEvent, TTransition, TTarget>()
Internal AddInternalTransition<TEvent, TTransition>() (no target)
Default AddDefaultTransition<TTarget>() or AddDefaultTransition<TTransition, TTarget>() (no trigger)
Choice/junction completion branch AddTransition<TTarget>() or AddTransition<TTransition, TTarget>() (no trigger)
Else regular AddElseTransition<TEvent, TEffect, TTarget>() (effect only)
Else internal AddElseInternalTransition<TEvent, TEffect>() (no target, effect only)
Else default AddElseDefaultTransition<TEffect, TTarget>() (no trigger, effect only)
Else choice/junction branch AddElseTransition<TEffect, TTarget>() (no trigger, effect only)

Use the corresponding contracts: event-triggered transitions implement ITransitionGuard<TEvent> and/or ITransitionEffect<TEvent>; default transitions use IDefaultTransitionGuard and/or IDefaultTransitionEffect. Else paths have no guard. The optional builder callback can still add site-specific behavior, such as an embedded invoice Activity after the transition's typed effect.

Separate guard/effect classes remain useful when they are independently reusable. When the same combination always belongs together, use one transition class, optionally inheriting shared logic and fixing operation-specific settings in small derived types. Guard and effect hooks are activated separately; do not use instance fields to pass data from GuardAsync to EffectAsync.

For per-use settings, implement IConfigurable<TConfiguration> (Stateflows.Common) and supply element => element.AddConfiguration(value) in the typed registration callback. The AddConfiguration extension requires Stateflows.Common.Interfaces. Configuration is applied after construction, before the handler method runs; do not read it in the constructor. This allows one effect to distinguish cancellation from failure without duplicating mutation logic, or one guard to match a configured operation.

Event-independent logic can implement ITransitionGuard / ITransitionEffect; completion-only logic can use IDefaultTransitionGuard / IDefaultTransitionEffect.

Activity contexts and tokens

Typed Activity nodes and flows can inject IActivityContext, common behavior/execution contexts, and contexts appropriate to their activation site:

Activation site Element-specific contexts
Executable node, accept-event node, or structured node lifecycle INodeContext
Flow guard/transformation IFlowContext, not INodeContext
Graph exception-handler node INodeContext and IExceptionContext
Activity initializer/finalizer Activity and execution contexts, not node/flow contexts

IFlowContext.SourceNode and .TargetNode expose the flow endpoints. A flow does not have one current node to inject. IExceptionContext describes the protected node and node of origin; it is supplied for graph exception-handler node activation, not as a general context for every callback or middleware exception handler. IActivityInspection (in Stateflows.Activities.Inspection.Interfaces) is registered for runtime inspection.

Token interfaces, in Stateflows.Activities, are also injectable into runtime workers:

Interface family Purpose
IInputTokens<T>, IInputToken<T> Read inputs of the requested type as a collection or a single token.
IOptionalInputTokens<T>, IOptionalInputToken<T> Declare optional inputs; the single-token form exposes IsAvailable and TokenOrDefault.
IInputOutputTokens<T>, IInputOutputToken<T> and optional counterparts Read and pass through tokens using the combined input/output contracts.
IOutputTokens<T> Add typed output tokens for the current execution.

For example:

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Stateflows.Activities;

public sealed class SumPrices(
    INodeContext node,
    IInputTokens<decimal> prices,
    IOutputTokens<decimal> totals) : IActionNode
{
    public Task ExecuteAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        totals.Add(prices.Sum());
        return Task.CompletedTask;
    }
}

Register it with AddAction<SumPrices>(...) and route incoming/outgoing decimal flows. The injected node.Name identifies this node's registered identity when shared code needs it. IInputToken<T>.Token selects the first matching token and fails if none exists; it is not an implicit exactly-one validation. Activity activation requirements and token routing still apply. See Tokens.

Flow logic can similarly implement IFlowGuard<TToken> or IFlowTransformation<TToken, TResult>, inject IFlowContext and application services, and receive the token in GuardAsync / TransformAsync. These contracts are not State Machine transition contracts.

Action contexts and tokens

A class implementing Stateflows.Actions.IAction can inject Stateflows.Actions.IActionContext, common behavior/execution contexts, and the same token-interface families listed above. The token interfaces remain in the Stateflows.Activities namespace even when consumed by an Action:

using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Stateflows.Actions;
using Stateflows.Activities;

public sealed class CalculateTotal(
    Stateflows.Actions.IActionContext action,
    IInputTokens<decimal> prices,
    IOutputTokens<decimal> totals) : IAction
{
    public Task ExecuteAsync(CancellationToken cancellationToken)
    {
        cancellationToken.ThrowIfCancellationRequested();
        totals.Add(prices.Sum());
        return Task.CompletedTask;
    }
}

Register with AddAction<CalculateTotal>() under AddActions. action.Id supplies the current Action identity. An Action has no Activity node/flow or State Machine state/transition context. Its parent/owner contexts address related Behaviors; they do not inject the parent's current state or node into the worker. ExecuteAsync receives cancellation directly; alternatively inject Stateflows.Common.IExecutionContext to inspect cancellation and event headers.

Entity templates and middleware

An Entity template is an interface implemented by a runtime proxy, not a constructor-injected model-element class. Its default-interface initializers, mutations, computations, and projections use the template's fields and their declared arguments. Do not add arbitrary service parameters to these methods expecting automatic DI, or try to inject the template as a live Entity service. Use events, projections, and associated Entity operations.

Typed Entity observers, interceptors, and exception handlers are activated separately. That activation supplies common IBehaviorContext / IExecutionContext and available parent/owner contexts, alongside registered application services. It does not provide an IEntityContext DI service or State Machine/Activity-specific element contexts. Use each hook's supplied context for its Entity template, mutation, field, or projection information. See Entities and Observability.

Do not generalize this middleware activation to every hook in every behavior family. Observers/interceptors may be constructed before a particular state, node, or transition is selected, and some registration paths use ordinary DI activation. Prefer the hook's context argument for operation-specific information.

Values and Entity snapshots

Special constructor-parameter attributes provide another kind of injection:

Mechanism Resolves
[GlobalValue] A Behavior-wide Value or IValue<T> accessor.
[StateValue], [ParentStateValue] State Machine state-scoped Values at a compatible activation site.
[SourceStateValue], [TargetStateValue] Transition endpoint Values; an internal transition has no target.
[FromField<TTemplate>] A directly readable field of an associated Entity.
[FromProjection<TTemplate>] A projection of an associated Entity.

Value and Entity attributes are in Stateflows.Common.Attributes, except state-specific Value attributes, which are in Stateflows.StateMachines.Attributes. Entity attributes select EntityScope.Self (default), Parent, or Owner; see Behavioral Groups.

A plain injected Value, field, or projection is read during element activation. It is a snapshot, not a live property binding or a subscription. An IValue<T> accessor instead performs Value operations when called. For current Entity data, use explicit checked Entity reads through the appropriate behavior/parent/owner context. Field injection requires direct Get access; missing required data fails activation. Making a data value optional does not create a missing parent/owner relationship.

For observers, interceptors, and exception handlers activated through the specialized model-element resolver, plain Value injection is explicitly rejected; use an IValue<T> accessor and read it in an appropriate hook instead. Registration paths using ordinary DI activation do not process these Stateflows parameter attributes.

See Behavior Values, Entity Fields, and Entity Projections for their detailed contracts.

Lifetime and delegate boundaries

Keep injected contexts and token accessors inside the execution that supplies them. Do not capture them in singletons, static fields, caches, or detached background tasks. They are not persistent state or a cross-Behavior shared container. Normal DI lifetimes still govern application services; a service that captures a current context must not outlive that execution.

An asynchronous child Activity/Action is a separate Behavior with its own execution context, even when it shares a behavioral-group owner. A typed inline transition effect is awaited; replacing it with AddEffectAction / AddEffectActivity changes it into embedded behavior dispatch, not merely a different injection syntax. See Behavioral Groups.

Lambda callbacks already receive their appropriate execution context. Some APIs also expose a Delegate overload that resolves parameters through Stateflows; use that only where the overload explicitly supports it. Constructor-activation features are not a promise that every lambda, delegate, middleware registration, or Entity method supports the same parameter attributes.

Implementation references

The registration factories and their holders establish which service types exist; the activation sites establish which contexts have meaningful values:

Overview
Installation
Runtimes
   Orleans runtime
Concepts
   Events and Structural Events
   Messaging Patterns
   Validation
   Scheduling
   Observability
Behaviors
   Behavior Values
   Dependency Injection
   Behavioral Groups
   State Machines
       Concepts
           State Identity
           In-State Event Deferrals
           Evaluation of Transitions
           Exception Handling
       Building blocks
           Regions
           States
               State
               Composite State
               Orthogonal State
               Final State
           Pseudostates
               Choice
               Junction
               Fork
               Join
               History
           Transitions
               Transition
               Default Transition
               Internal Transition
   Activities
       Concepts
           Exception Handling
           Implicit Fork and Join
           Tokens
       Building blocks
           Nodes
               Action Node
               Decision Node
               Merge Node
               Initial Node
               Final Node
               Input Node
               Output Node
               Fork Node
               Join Node
               Accept Event Action Node
               Send Event Action Node
               Publish Event Action Node
               Time Event Action Node
               Data Store Node
               Structured Activity Node
               Iterative Activity Node
               Parallel Activity Node
           Flows
               Data Flow
               Control Flow
   Actions
   Entities
       Fields
       Mutations
       Projections
Extensions
   ClearScript
   FluentValidation
   JSON definitions
   Minimal APIs
   OneOf
   OpenTelemetry
   PlantUML
   Scheduling

Clone this wiki locally