Effects Composition¶
Configuration integrates with the Deepstaging effects system in two ways:
[ConfigModule]— the primary pattern, generates effect accessors for typed config access within effect pipelines.[Capability]— exposes a[ConfigRoot]as a direct runtime dependency without effect wrapping.
[ConfigModule] — Effect-Based Config Access¶
[ConfigModule] generates effect accessors that let you read configuration values directly in from/select compositions:
public sealed class SmtpSettings { public string Host { get; init; } = ""; public int Port { get; init; } = 587; }
[ConfigModule]
[Exposes<SmtpSettings>]
public static partial class SmtpConfigModule;
[EffectsModule(typeof(IEmailService))]
public sealed partial class EmailEffects;
[Runtime]
public sealed partial class AppRuntime;
// SmtpConfigModule and EmailEffects are auto-discovered
Access configuration in effect compositions:
public static class EmailWorkflows
{
public static Eff<RT, Unit> SendWelcome<RT>(string to)
where RT : IHasSmtpConfigModule, IHasEmailService =>
from settings in SmtpConfigModule.SmtpSettings<RT>()
from _ in EmailEffects.EmailService.SendAsync<RT>(to, "Welcome", $"Via {settings.Host}")
select unit;
}
[Capability] — Configuration as a Runtime Dependency¶
Configuration roots expose properties, not methods, so when composed directly into the runtime they use [Capability] instead of [EffectsModule]. This generates the IHas* capability interface and wires the dependency into the runtime, but does not generate effect wrapper methods or OpenTelemetry instrumentation.
[EffectsModule(typeof(IEmailService))]
[Capability(typeof(ISmtpConfigRoot))]
public sealed partial class EmailEffects;
This generates:
IHasEmailService— withEff<RT, T>effect methods for each service methodIHasSmtpConfigRoot— with a direct property for configuration access (no effect wrapping)
Prefer [ConfigModule]
For most use cases, [ConfigModule] is the recommended approach. It generates typed effect accessors that compose naturally in from/select pipelines. Use [Capability] only when you need the full [ConfigRoot] interface as a direct runtime dependency.
Composing with [Runtime] and [Uses]¶
Use [Uses] to compose configuration into the runtime alongside effect modules:
[ConfigRoot(Section = "Database")]
[Exposes<DatabaseConfig>]
public sealed partial class DatabaseConfigRoot;
[ConfigModule]
[Exposes<DatabaseConfig>]
public static partial class DatabaseConfigModule;
[EffectsModule(typeof(IOrderService))]
public sealed partial class OrderEffects;
[Runtime]
public sealed partial class AppRuntime;
// DatabaseConfigModule and OrderEffects are auto-discovered
The generated runtime class receives the configuration module via constructor injection:
public partial class AppRuntime(
IOrderService orderService,
DatabaseConfigModuleProvider configModuleProvider,
CorrelationContext? correlationContext = null,
ILoggerFactory? loggerFactory = null)
: IAppRuntimeCapabilities,
IHasOrderService,
IHasDatabaseConfigModule
{
public IOrderService OrderService => orderService;
public DatabaseConfigModuleProvider DatabaseConfigModuleProvider => configModuleProvider;
// ...
}
Effect-Wrapped Access to Configuration¶
Inside effect compositions, access configuration through [ConfigModule] effect accessors:
public static class OrderWorkflows
{
public static Eff<RT, Order> GetOrder<RT>(OrderId id)
where RT : IHasOrderService, IHasDatabaseConfigModule =>
from config in DatabaseConfigModule.DatabaseConfig<RT>()
from order in OrderEffects.OrderService.GetAsync<RT>(id)
select order;
}
Configuration is read as a lightweight effect — no OpenTelemetry span is created for config reads. This keeps configuration access lightweight while still participating in the runtime's dependency graph.
OpenTelemetry Instrumentation¶
Configuration providers themselves are not instrumented — they have no methods to trace. However, the effect modules that use configuration are fully instrumented:
public static partial class EmailEffects
{
public static partial class EmailService
{
private static readonly ActivitySource ActivitySource
= new("MyApp.EmailService", "1.0.0");
public static Eff<RT, Unit> SendAsync<RT>(
string to, string subject, string body)
where RT : IHasEmailService =>
liftEff<RT, Unit>(async rt =>
{
await rt.EmailService.SendAsync(to, subject, body);
return unit;
}).WithActivity("EmailService.SendAsync", ActivitySource,
destination: "EmailService");
}
}
The generated bootstrapper registers all ActivitySource names with OpenTelemetry:
if (options.Tracing)
{
services.AddOpenTelemetry()
.WithTracing(tracing => tracing
.AddSource("MyApp.EmailService")
.AddSource("MyApp.SlackService"));
}
Configuration providers do not appear as trace sources — they are accessed inline within the traced effect methods.
Summary¶
| Aspect | [EffectsModule] |
[Capability] (Config) |
|---|---|---|
| Purpose | Wrap interface methods as Eff<RT, T> effects |
Expose dependencies without effect wrapping |
| Best for | Services with methods (email, storage, APIs) | Configuration providers, logging, metrics |
| Generated methods | Yes — Eff<RT, T> wrappers |
No — only IHas* interface |
| OpenTelemetry | Yes — ActivitySource per module |
No — not instrumented |
| Usage in effects | Eff<RT, T> composition |
Direct property access via IHas* constraint |