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:
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:
- Capability interfaces —
IHasAuditStore,IHasClock,IHasSearchIndex<Article>,IHasArticleStore, etc. - Composite interface — combines all
IHas*interfaces fromContentModule - Runtime class —
ScribeRuntimeimplementing 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}"));