ForgeTrust.AppSurface.Flow 0.2.0-preview.1

This is a prerelease version of ForgeTrust.AppSurface.Flow.
There is a newer prerelease version of this package available.
See the version list below for details.
dotnet add package ForgeTrust.AppSurface.Flow --version 0.2.0-preview.1
                    
NuGet\Install-Package ForgeTrust.AppSurface.Flow -Version 0.2.0-preview.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="ForgeTrust.AppSurface.Flow" Version="0.2.0-preview.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="ForgeTrust.AppSurface.Flow" Version="0.2.0-preview.1" />
                    
Directory.Packages.props
<PackageReference Include="ForgeTrust.AppSurface.Flow" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add ForgeTrust.AppSurface.Flow --version 0.2.0-preview.1
                    
#r "nuget: ForgeTrust.AppSurface.Flow, 0.2.0-preview.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package ForgeTrust.AppSurface.Flow@0.2.0-preview.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=ForgeTrust.AppSurface.Flow&version=0.2.0-preview.1&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=ForgeTrust.AppSurface.Flow&version=0.2.0-preview.1&prerelease
                    
Install as a Cake Tool

ForgeTrust.AppSurface.Flow

ForgeTrust.AppSurface.Flow provides stable contracts for typed long-running app processes.

Release Guidance

AppSurface ships as a coordinated package family. Before installing this package from a prerelease feed, check the package chooser and release hub for current release risk, migration guidance, and readiness.

What It Includes

  • FlowNodeOutcome<TContext> and the sealed outcome records FlowNext<TContext>, FlowWait<TContext>, FlowTimedOut<TContext>, FlowComplete<TContext>, and FlowFaultOutcome<TContext>.
  • IFlowNode<TContext>, FlowExecutionContext<TContext>, and FlowResumeEvent.
  • FlowGraphBuilder<TContext> and immutable FlowDefinition<TContext> graph validation.
  • IFlowDefinitionRegistry for host and adapter lookup by context type, flow id, and version.
  • IFlowRunner<TContext> and InMemoryFlowRunner<TContext> for local tests and examples.
  • Generated-case authoring attributes: FlowAuthoringAttribute, FlowNodeAttribute, FlowStartAttribute, FlowOutcomeAttribute, FlowGraphMappingAttribute, and FlowOutcomeKind.
  • IFlowTransformerNode<TInput, TOutcome> and FlowTransformerContext<TInput> for generated authoring nodes.
  • Passive AppSurfaceFlowModule service registration.

What It Does Not Include

  • Durable persistence, external event queues, timers, replay, or storage.
  • ASP.NET Core endpoints, middleware, authorization handlers, or UI.
  • Semantic Kernel. Keep agentic or LLM-assisted authoring in samples or future packages until the core process contract has settled.
  • Required preview C# union syntax.

Basic Shape

Generated authoring

Generated authoring is the package-first path when transition coverage should fail at build time. Think of context types as typed ports: a node declares the input port it accepts, and each outcome declares the output port it emits. The generator connects a Next outcome to the node whose input port has the same nominal type.

[FlowAuthoring("approval")]
public partial class ApprovalFlow
{
    [FlowStart]
    [FlowNode("intake", typeof(ApprovalOpened))]
    [FlowOutcome("ready-for-review", FlowOutcomeKind.Next, typeof(ReviewRequested))]
    [FlowOutcome("approval-submitted", FlowOutcomeKind.Wait, typeof(ApprovalOpened))]
    public partial class IntakeNode : IFlowTransformerNode<ApprovalOpened, IntakeNodeOutcomes>
    {
        public ValueTask<IntakeNodeOutcomes> ExecuteAsync(
            FlowTransformerContext<ApprovalOpened> context,
            CancellationToken cancellationToken = default)
        {
            if (context.ResumeEvent is null)
            {
                return ValueTask.FromResult<IntakeNodeOutcomes>(
                    IntakeNodeOutcomes.ApprovalSubmitted(context.State));
            }

            return ValueTask.FromResult<IntakeNodeOutcomes>(
                IntakeNodeOutcomes.ReadyForReview(new ReviewRequested(context.State.RequestId)));
        }
    }

    [FlowNode("review", typeof(ReviewRequested))]
    [FlowOutcome("approved", FlowOutcomeKind.Complete, typeof(ApprovalCompleted))]
    public partial class ReviewNode : IFlowTransformerNode<ReviewRequested, ReviewNodeOutcomes>
    {
        public ValueTask<ReviewNodeOutcomes> ExecuteAsync(
            FlowTransformerContext<ReviewRequested> context,
            CancellationToken cancellationToken = default) =>
            ValueTask.FromResult<ReviewNodeOutcomes>(
                ReviewNodeOutcomes.Approved(new ApprovalCompleted(context.State.RequestId)));
    }
}

public sealed record ApprovalOpened(string RequestId);
public sealed record ReviewRequested(string RequestId);
public sealed record ApprovalCompleted(string RequestId);

Then build a definition from node instances:

var definition = ApprovalFlow.BuildDefinition(
    new ApprovalFlow.IntakeNode(),
    new ApprovalFlow.ReviewNode());
var result = await runner.RunAsync(definition, ApprovalFlow.CreateStartContext(new ApprovalOpened("APR-1001")));

The compact BuildDefinition(nodeInstances...) overload applies the generated default graph configuration. This works when each Next outcome output port has exactly one compatible node input port. In the example, ready-for-review emits ReviewRequested, and only ReviewNode accepts ReviewRequested, so the generator can infer that transition.

Use the explicit overload when you want the graph mapping visible at the call site:

var definition = ApprovalFlow.BuildDefinition(
    graph => graph
        .MapIntakeNodeReadyForReviewToReviewNode()
        .MarkIntakeNodeApprovalSubmittedTerminal()
        .MarkReviewNodeApprovedTerminal(),
    new ApprovalFlow.IntakeNode(),
    new ApprovalFlow.ReviewNode());

The explicit overload must receive an inline lambda. Delegate variables and method groups are rejected so analyzer coverage works even when the flow spec is compiled in a reusable library and the host project consumes the generated public builder.

The generator emits outcome records such as IntakeNodeOutcomes.ReadyForReviewOutcome, one serializable envelope context such as ApprovalFlowContext, adapter nodes that implement IFlowNode<ApprovalFlowContext>, a GraphBuilder helper, and BuildDefinition(...) helpers that lower the authored graph into the existing runtime contract.

Use generated authoring for application workflows, package samples, and public APIs where missing transitions should break the build. Use the low-level runtime shape below for tiny tests, custom graph construction, or hand-authored nodes that intentionally own all runtime behavior.

Low-level runtime contract

var definition = FlowGraphBuilder<ApprovalState>
    .Create("approval-request")
    .AddNode("review", new ApprovalReviewNode())
    .StartAt("review")
    .Build();

var result = await runner.RunAsync(definition, new ApprovalState("created"));

Nodes return explicit discriminated outcomes:

public sealed class ApprovalReviewNode : IFlowNode<ApprovalState>
{
    public ValueTask<FlowNodeOutcome<ApprovalState>> ExecuteAsync(
        FlowExecutionContext<ApprovalState> context,
        CancellationToken cancellationToken = default)
    {
        if (context.ResumeEvent is null)
        {
            return ValueTask.FromResult<FlowNodeOutcome<ApprovalState>>(
                FlowNodeOutcome<ApprovalState>.Wait("approval-submitted", context.State));
        }

        return ValueTask.FromResult<FlowNodeOutcome<ApprovalState>>(
            FlowNodeOutcome<ApprovalState>.Complete(context.State with { Status = "approved" }));
    }
}

Decisions And Pitfalls

  • Flow ids, versions, and node ids are durable identifiers. Treat them like persisted schema once real instances exist.
  • Generated authoring uses nominal context types as typed ports. Two record types with the same properties are different ports; one record type reused in multiple places is the same port.
  • Generated authoring resolves Next targets by matching an outcome output context type to exactly one declared node input context, then exposes that transition through a generated GraphBuilder.Map...To... method. If no node or multiple nodes match, the generator reports an error.
  • If two nodes can accept the same kind of work, give the branches distinct nominal port types unless the graph really should be ambiguous. The generator does not guess between two nodes with the same input context type.
  • Wait and TimedOut outcomes resume the same node, so their output port must be the node input port. Fault outcomes must carry FlowFault.
  • The compact BuildDefinition(nodeInstances...) overload calls the generated default graph mapping. Prefer it when the typed ports make every Next transition unambiguous.
  • The explicit BuildDefinition(graph => ..., nodeInstances...) overload requires every declared outcome to be mapped or marked terminal. Use it when graph visibility matters. Complete, Fault, Wait, and TimedOut outcomes use generated Mark...Terminal() methods because they do not declare FlowNext<TContext> targets in the low-level graph.
  • Generated envelopes include concrete nullable context slots and a public serializer constructor so Durable Task JSON round-trip validation can inspect them.
  • FlowExecutionContext<TContext> is an immutable value-type snapshot. Runners populate it for each node call so tight synchronous flows avoid allocating a reference context per step. Do not treat default(FlowExecutionContext<TContext>) as a valid execution context; structs can be default-created without flow ids, node ids, or state.
  • Declare every Next target in AddNode. The builder validates missing targets, and runners reject undeclared runtime targets.
  • The in-memory runner stops at waits. It does not persist state, deliver events, or create timers.
  • The in-memory runner uses prevalidated internal routing metadata from FlowDefinition<TContext> so local execution does not repeat graph-existence checks on every Next transition. This preserves the public Nodes graph view and the undeclared target diagnostic from FlowDefinition<TContext> construction.
  • Flow benchmarks isolate runner orchestration overhead. Use Flow for graph safety, long-running process contracts, and durable-host alignment; use a direct loop when all you need is a pure in-process tight state machine.
  • Use FlowWait<TContext>.Timeout as metadata for durable hosts. The core runner reports the timeout request but does not race timers.
  • Keep context types serializer-friendly if you plan to use Durable Task. The Durable Task adapter validates JSON round-trips before evaluating nodes.
  • Native C# discriminated unions can become authoring sugar later, but this package intentionally ships stable sealed records today.

Generated Authoring Diagnostics

Diagnostic Default Meaning Fix
ASFLOWA001 Error A declared outcome is missing generated graph mapping. Add the matching Map...To... call for Next outcomes, add the matching Mark...Terminal() call for terminal outcomes, or use the compact generated BuildDefinition(nodeInstances...) overload.
ASFLOWA002 Error A declared Next outcome targets a missing context. Add the missing generated node or use the correct output context type.
ASFLOWA003 Error A declared Next outcome matches more than one node input context. Use distinct context types so the generated graph is unambiguous.
ASFLOWA004 Error The authored flow has zero or multiple [FlowStart] nodes. Mark exactly one generated node with [FlowStart].
ASFLOWA005 Error The generator cannot produce the envelope or graph because declarations conflict. Make the flow and generated nodes partial, unique, and fully declared.
ASFLOWA006 Warning Generated authoring is being mixed with low-level registration in a confusing way. Keep generated definitions and hand-built definitions as separate entry points.
Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on ForgeTrust.AppSurface.Flow:

Package Downloads
ForgeTrust.AppSurface.Flow.DurableTask

ForgeTrust.AppSurface.Flow.DurableTask package for AppSurface application composition.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.0-preview.2 367 7/3/2026
0.2.0-preview.1 294 6/28/2026
0.1.0 106 7/2/2026
0.1.0-rc.4 266 6/16/2026
0.1.0-rc.3 129 6/8/2026
0.1.0-rc.2 58 6/3/2026