Workflow exposes executors and a serializable projection of the graph's shape, but the routing behaviour (edge conditions and fan-out assigners) is unreachable outside the assembly. Anything that wants to run, analyze or visualize a workflow (alternative execution hosts, diagram tools, static validators) has to live in this repo or be granted InternalsVisibleTo.
API change proposal
Three declarations, plus the overrides they force:
// Workflow.cs:26
internal Dictionary<string, HashSet<Edge>> Edges { get; init; } = [];
// -> public IReadOnlyDictionary<string, IReadOnlyCollection<Edge>> Edges { get; internal init; }
// EdgeData.cs:15 (EdgeData is already public)
internal abstract EdgeConnection Connection { get; }
// -> public abstract EdgeConnection Connection { get; }
// FanOutEdgeData.cs:14 (SourceId / SinkIds / EdgeAssigner are already public)
internal sealed class FanOutEdgeData : EdgeData
// -> public sealed class FanOutEdgeData : EdgeData
// DirectEdgeData.cs:39, FanInEdgeData.cs:31, FanOutEdgeData.cs:41
// required by the EdgeData.Connection change: an override can't reduce accessibility (else CS0507)
internal override EdgeConnection Connection { get; }
// -> public override EdgeConnection Connection { get; }
Not a breaking change. All four are internal -> public widening, so nothing public becomes less accessible and no existing consumer breaks. The new public abstract EdgeData.Connection can't break external subclasses either, since EdgeData's only constructor is internal. There are no PublicAPI.*.txt baselines to update.
Edges is the only one that touches existing code, and only code that can already see it. IReadOnlyDictionary isn't covariant in TValue (its TryGetValue takes an out TValue), so this isn't a plain re-type. WorkflowBuilder projects once where it builds the workflow,
Edges = this._edges.ToDictionary(
keySelector: kvp => kvp.Key,
elementSelector: kvp => (IReadOnlyCollection<Edge>)kvp.Value),
and InProcessRunnerContext changes one out HashSet<Edge>? to out IReadOnlyCollection<Edge>?. Every other reader binds unchanged, since they only enumerate, index or count. We never mutate these, so a read-only view is all a consumer needs.
No new construction surface. Every EdgeData subclass constructor, and EdgeData's own, is already internal, so nobody outside can derive from it or construct an edge. WorkflowBuilder stays the only way to build a graph.
Why ReflectEdges() isn't enough
DirectEdgeInfo/FanOutEdgeInfo expose bool HasCondition/HasAssigner, not the delegates, and EdgeInfo.IsMatch(Edge) is internal, so results can't be correlated back to edges. Hanging Func<> on EdgeInfo would conflate serializable checkpoint metadata with live behaviour. A conditions-only accessor keyed by (SourceId, SinkId) is lossy: WorkflowBuilder.cs:378 only guards duplicates when condition is null, so two conditional edges between the same pair are legal today and would collapse.
Separately, FanOutEdgeData being internal while the identically shaped DirectEdgeData is public means direct edges are inspectable today and fan-out edges are not.
Workflowexposes executors and a serializable projection of the graph's shape, but the routing behaviour (edge conditions and fan-out assigners) is unreachable outside the assembly. Anything that wants to run, analyze or visualize a workflow (alternative execution hosts, diagram tools, static validators) has to live in this repo or be grantedInternalsVisibleTo.API change proposal
Three declarations, plus the overrides they force:
Not a breaking change. All four are
internal->publicwidening, so nothing public becomes less accessible and no existing consumer breaks. The new public abstractEdgeData.Connectioncan't break external subclasses either, sinceEdgeData's only constructor is internal. There are noPublicAPI.*.txtbaselines to update.Edgesis the only one that touches existing code, and only code that can already see it.IReadOnlyDictionaryisn't covariant inTValue(itsTryGetValuetakes anout TValue), so this isn't a plain re-type.WorkflowBuilderprojects once where it builds the workflow,and
InProcessRunnerContextchanges oneout HashSet<Edge>?toout IReadOnlyCollection<Edge>?. Every other reader binds unchanged, since they only enumerate, index or count. We never mutate these, so a read-only view is all a consumer needs.No new construction surface. Every
EdgeDatasubclass constructor, andEdgeData's own, is alreadyinternal, so nobody outside can derive from it or construct an edge.WorkflowBuilderstays the only way to build a graph.Why
ReflectEdges()isn't enoughDirectEdgeInfo/FanOutEdgeInfoexposebool HasCondition/HasAssigner, not the delegates, andEdgeInfo.IsMatch(Edge)is internal, so results can't be correlated back to edges. HangingFunc<>onEdgeInfowould conflate serializable checkpoint metadata with live behaviour. A conditions-only accessor keyed by(SourceId, SinkId)is lossy:WorkflowBuilder.cs:378only guards duplicates whencondition is null, so two conditional edges between the same pair are legal today and would collapse.Separately,
FanOutEdgeDatabeing internal while the identically shapedDirectEdgeDatais public means direct edges are inspectable today and fan-out edges are not.