Skip to content

.NET: Suggestions and questions on Workflows APIs #1880

@jozkee

Description

@jozkee

I was reviewing documentation of Workflows Core Concepts following the samples and noticed several concerns with the current APIs shown there.

The following list is composed of suggestions and questions intended to foster discussion, and there's no particular order in the items described.

  1. Generic edge methods like AddCase<T>, AddFanOutEdge<T>, AddEdge<T> box values, silently convert type mismatches to default instead of throwing exceptions and lack type safety. I also don't see a way to ensure static type safety between executors. These APIs should be honest about their runtime behavior and use object? instead.

  2. FanOutEdges "partitioner" parameter should be "discriminator" or "selector". Contrast it with Partitioner.Create which actually partitions data.

  3. AddEdge idempotent parameter only works when condition is null. What's the use case for bypassing the duplicate edge exception?

    WorkflowBuilder builder = new WorkflowBuilder(sourceExecutor)
         .AddEdge<string>(sourceExecutor, targetExecutor, condition: a => true, idempotent: false)
         .AddEdge<string>(sourceExecutor, targetExecutor, condition: a => true, idempotent: false); // duplicate
    builder.Build(); // no error
  4. StreamAsync has overloads without input parameters but RunAsync doesn't. This also causes ambiguity as described in .NET: Unintuitive behaviour with StreamAsync overloads binding runId more tightly than input; #1773.

  5. IMessageHandler is in MAAI.Workflows.Reflection namespace but is used beyond ReflectingExecutor. Should be in a more general namespace.

  6. Workflow Validation documentation claims don't match actual behavior:

    • Ensures message types are compatible between connected executors

      WorkflowBuilder builder = new WorkflowBuilder(sourceExecutor)
         .AddEdge(sourceExecutor, new FloatExecutor());
      var workflow = builder.Build(); // no error
      RunAsync<T> with mismatched type also silently completes:
      Func<string, IWorkflowContext, CancellationToken, ValueTask<string>> reverseFunc = 
          (text, ctx, ct) => ValueTask.FromResult(string.Concat(text.Reverse()));
      var executor = reverseFunc.AsExecutor("ReverseExecutor");
      Workflow workflow = new WorkflowBuilder(executor).Build();
      await using Run run = await InProcessExecution.RunAsync(workflow, 42); // no error with int
    • Graph Connectivity: Verifies all executors are reachable from the start executor

      WorkflowBuilder builder = new WorkflowBuilder(first)
         .AddEdge(second, third); // second & third unreachable from first
      builder.Build(); // no error
    • Executor Binding: Confirms all executors are properly bound and instantiated

      WorkflowBuilder builder = new WorkflowBuilder(new UnboundExecutor());
      builder.Build(); // no error
      
      internal class UnboundExecutor() : Executor("UnboundExecutor"), IMessageHandler<string, string>
      {
          public ValueTask<string> HandleAsync(string message, IWorkflowContext context, CancellationToken cancellationToken = default) => ValueTask.FromResult(message);
          protected override RouteBuilder ConfigureRoutes(RouteBuilder routeBuilder) => routeBuilder; // Empty - handler never bound!
      }
    • Edge Validation: Checks for duplicate edges and invalid connections

    (See number 3 for duplicate edge example)

  7. Is ReflectingExecutor<TExecutor> : Executor where TExecutor : ReflectingExecutor<TExecutor> only generic for trimming? Is binary size a primary goal?

  8. FanInEdge - how to detect when all sources have completed without an ahead-of-time count?

cc @lokitoth @stephentoub @jeffhandley

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions