Skip to content

Effects Composition

Dispatch handlers return Eff<RT, T> — LanguageExt's lazy async effect type. This page covers how handlers compose effects, how runtime constraints work, and how dispatch integrates with the [Runtime] and [Uses] system.

Handler Signatures

Every [CommandHandler] and [QueryHandler] method must return Eff<RT, T> where RT is a concrete runtime type:

[CommandHandler]
public static Eff<AppRuntime, OrderCreated> Handle(CreateOrder cmd) =>
    from _ in OrderStore.SaveAsync<AppRuntime>(new Order(cmd.Name, cmd.Quantity))
    select new OrderCreated(OrderId.New());

The runtime type parameter is concrete (e.g., AppRuntime), not generic. This is intentional — dispatch methods are generated for a specific runtime, giving the C# compiler full visibility into which capabilities are available.

Composing Effects

Inside a handler, use LINQ from/select syntax to chain multiple effects. Each effect call is constrained to the handler's runtime type:

public static class ArticleCommands
{
    [CommandHandler(Audited = true, ValidatorType = typeof(CreateArticleValidator))]
    [Authorize(nameof(Policies.CanCreateArticles))]
    public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd)
    {
        var id = ArticleId.New();

        return
            from now in Clock.GetUtcNow<ScribeRuntime>()
            let article = new Article(id, cmd.Title, cmd.Body, cmd.Author, now, false)
            from created in ArticleStore.SaveAsync<ScribeRuntime>(article)
                            >> SearchIndex.IndexAsync<ScribeRuntime>(id: id.ToString(), article)
                            >> SuccessEff(new ArticleCreated(article.Id))
            select created;
    }
}

Each from line invokes an effect method (e.g., Clock.GetUtcNow, ArticleStore.SaveAsync) that is generated by [EffectsModule]. These methods are generic with a capability constraint:

// Generated by [EffectsModule] for ClockModule
public static Eff<RT, DateTimeOffset> GetUtcNow<RT>()
    where RT : IHasClock => ...

The constraint where RT : IHasClock is satisfied because ScribeRuntime implements IHasClock (via [Runtime] + [Uses]).

Runtime Type Constraints

The generated dispatch method uses the same concrete runtime as the handler:

// Generated on AppDispatch
public static Eff<AppRuntime, OrderCreated> Dispatch(CreateOrder command) =>
    (from result in OrderCommands.Handle(command)
     from _commit in liftEff<AppRuntime, Unit>(async rt =>
     {
         if (rt is IAutoCommittable c)
             await c.CommitAsync(default);
         return unit;
     })
     select result)
    .WithActivity("Dispatch CreateOrder", ActivitySource,
        destination: "OrderCommands");

This means the runtime passed to .RunAsync() must match the handler's runtime type. The compiler enforces this — there's no runtime type checking.

Runtime Integration

The [Runtime] attribute generates a runtime class that aggregates all capability interfaces. Local modules are auto-discovered:

[Runtime]
public partial class ScribeRuntime;
// ContentModule is auto-discovered

Where ContentModule declares its effect dependencies:

[EffectsModule(typeof(AuditModule))]
[EffectsModule(typeof(StorageModule))]
[EffectsModule(typeof(ClockModule))]
[EffectsModule(typeof(SearchModule<Article>))]
[EffectsModule(typeof(IArticleStore))]
public partial class ContentModule;

The generator produces:

  1. Capability interfacesIHasAuditStore, IHasClock, IHasSearchIndex<Article>, IHasArticleStore, etc.
  2. Composite interface — combines all IHas* interfaces from ContentModule
  3. Runtime classScribeRuntime implementing all capability interfaces, accepting dependencies via primary constructor

This is the bridge between generic effect methods and concrete dispatch:

[EffectsModule] → IHas* capability interface
[Runtime][Uses(...)] aggregates all IHas*
ScribeRuntime implements IHasClock, IHasArticleStore, ...
Effect methods constrained to where RT : IHas*
Handlers use ScribeRuntime as RT — all constraints satisfied

Effect Pipeline Example

A complete example showing dispatch with effects, validation, authorization, and audit:

// Command and result types
public record CreateArticle(string Title, string Body, string Author) : ICommand;
public record ArticleCreated(ArticleId Id);

// Handler composing multiple effects
public static class ArticleCommands
{
    [CommandHandler(Audited = true, ValidatorType = typeof(CreateArticleValidator))]
    [Authorize(nameof(Policies.CanCreateArticles))]
    public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd)
    {
        var id = ArticleId.New();

        return
            from now in Clock.GetUtcNow<ScribeRuntime>()
            let article = new Article(id, cmd.Title, cmd.Body, cmd.Author, now, false)
            from created in ArticleStore.SaveAsync<ScribeRuntime>(article)
                            >> SearchIndex.IndexAsync<ScribeRuntime>(id: id.ToString(), article)
                            >> SuccessEff(new ArticleCreated(article.Id))
            select created;
    }
}

// Dispatch module
[DispatchModule]
public static partial class ScribeDispatch;

// Runtime — auto-discovers all local modules
[Runtime]
public partial class ScribeRuntime;

The generated ScribeDispatch.Dispatch(CreateArticle) composes the full pipeline:

Authorize (CanCreateArticles)
  → Validate (CreateArticleValidator)
    → Handler (Clock + ArticleStore + SearchIndex effects)
      → Audit (record AuditEntry)
        → Auto-commit (IAutoCommittable)

Each step is an Eff expression chained with from/select — the entire pipeline is a single lazy computation that runs when you call .RunAsync(runtime).

Running the Pipeline

var runtime = new ScribeRuntime(clock, articleStore, searchIndex, auditStore);

var result = await ScribeDispatch.Dispatch(new CreateArticle("Hello", "World", "chris"))
    .RunAsync(runtime);

result.Match(
    Succ: created => Console.WriteLine($"Article created: {created.Id}"),
    Fail: error => Console.WriteLine($"Failed: {error.Message}"));