Skip to content

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 — with Eff<RT, T> effect methods for each service method
  • IHasSmtpConfigRoot — 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:

Generated runtime (simplified)
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:

Generated effect method (with tracing)
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:

Generated bootstrapper (tracing registration)
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