Repository navigation
5.7.2. Mutations
A mutation changes an Entity in response to a typed event. Register mutation events deliberately: they are the Entity's domain operations and the normal way to enforce invariants around stored fields.
sequenceDiagram
participant Caller
participant Entity
participant Fields as Fields and computations
participant Views as Dependent projections
Caller->>Entity: Send mutation event
Entity->>Entity: Run matching mutation
Entity->>Fields: Detect changed fields and recompute
Fields->>Views: Refresh dependent projections
Entity-->>Caller: Consumed / Failed
In an attribute-based interface template, mark a default interface method with [Mutation]. It must accept exactly one event argument, return void, and provide a default implementation:
using System;
using Stateflows.Entities.Attributes;
public sealed record SetQuantity(int Quantity);
public interface ICart
{
[Field]
int Quantity { get; set; }
[Mutation]
void ChangeQuantity(SetQuantity mutation)
{
if (mutation.Quantity < 0)
{
throw new ArgumentOutOfRangeException(nameof(mutation.Quantity));
}
Quantity = mutation.Quantity;
}
}The event type identifies the mutation handler. Register at most one mutation for a given event type in an Entity definition. A method marked [Mutation] is invoked by the Entity engine, not by calling it directly on the template proxy.
An equivalent builder-based registration is:
builder.AddMutation<SetQuantity>(context =>
{
if (context.MutationEvent.Quantity < 0)
{
throw new ArgumentOutOfRangeException(nameof(context.MutationEvent.Quantity));
}
context.Entity.Quantity = context.MutationEvent.Quantity;
});Send the event through the Entity behavior handle:
var result = await cart.SendAsync(new SetQuantity(3));
if (result.Status != EventStatus.Consumed)
{
// Handle rejection, failure, or an undelivered mutation.
}For a boolean result, TryMutateAsync<TEvent> returns true only when the event was consumed:
var consumed = await cart.TryMutateAsync(new SetQuantity(3));Mutation events are exact typed event registrations. If no handler matches, it is not consumed as a mutation; use the full SendResult when the distinction between event statuses matters.
Behavior contexts provide TryMutateAsync and TrySetFieldAsync for updating
related entities. Direct field updates require the field's external write permission.
These operations commit independently of the calling behavior by default; a later
failure of the caller does not roll back an entity update. In the Orleans runtime,
pass a JoinTransaction header to opt into the sender transaction. The same opt-in
applies to TryGetProjectionAsync and TryGetFieldAsync: joined reads see tentative
writes from that transaction. Staged entity mutations/field writes are no longer
provided; use an explicit joined operation when atomicity is required.
See staged sends for the separate post-commit self-send operation.
The Entity runtime snapshots field values around a mutation. If stored fields changed, it recalculates dependent computed fields and refreshes projections that depend on the changed values. See Fields and Projections.
If mutation logic or a dependent computation/projection fails, the event returns EventStatus.Failed and the Entity operation is rolled back; a partially applied field update is not committed. Register an Entity exception handler to observe failures without treating them as successful mutations. Acknowledging an exception in a handler does not suppress the failure or commit partial state.
TrySetAsync is a separate, permission-controlled direct field operation. It recalculates dependent values, but bypasses mutation handlers and their domain rules. Use mutations for business commands and direct field writes only when the exposed field contract intentionally allows an external caller to set that value. See Direct field access.
Home page Support Code licensed under an MIT-style License. Documentation licensed under CC BY 4.0. © by Mikołaj Milewski, 2026