Skip to content

Runtime

The runtime is the composition root of your application. It auto-discovers every module in your assembly and wires them together at compile time.

[Runtime]
public sealed partial class AppRuntime;

This one-line declaration:

  • Auto-discovers all local modules[EffectsModule], [DataStore], [ConfigModule], [IntegrationEvents], [Capability]
  • Generates DI registration — one AddAppRuntime() call registers everything
  • Wires observability — OpenTelemetry tracing, metrics, and logging for every effect method
  • Validates at startup[DevelopmentOnly] services in production and missing config are caught before the first request

The runtime is your RT type parameter. When you write Eff<AppRuntime, T>, the compiler guarantees every needed capability is provided.

Bootstrapper

builder.AddAppRuntime(options => options
    .EnableTracing()
    .EnableMetrics()
    .AddAppStorePostgres(connectionString));

Or enable globally via configuration:

appsettings.json
{ "Deepstaging": { "Telemetry": { "Enabled": true } } }

The bootstrapper registers the runtime and all dependencies. DataStore uses TryAdd so production implementations registered in the configure callback take priority.

Auto-Discovery

The runtime generator discovers all module containers in the same assembly. [Uses] is only needed for modules from external assemblies (NuGet packages or referenced projects).

// Local — auto-discovered
[EffectsModule(typeof(IEmailService))]
public sealed partial class EmailEffects;

[DataStore]
public static partial class AppStore;

// Runtime — auto-discovers local modules, [Uses] for external
[Runtime]
[Uses(typeof(JobsModule))]   // from Deepstaging.Jobs package
public sealed partial class AppRuntime;

Capability Aggregation

The generator collects all IHas* interfaces and creates a composite:

public interface IAppRuntimeCapabilities
    : IHasEmailService, IHasAppStore, IHasJobScheduler { }

Effect methods are decoupled from the concrete runtime — they only depend on the capabilities they need.

Attributes

Attribute Description
[Runtime] Marks a partial class as the runtime aggregator. Must be partial (DSRT01).
[Uses(typeof(T))] Includes an external [EffectsModule] in the runtime. Requires [Runtime] (DSRT02). Target must be an [EffectsModule] (DSRT03).
[Capability] Raw service dependency without effect wrapping — for config providers or services that don't need Eff lifting.
[ServiceRegistration("MethodName")] Declares a DI registration method that the runtime generator aggregates into Add{Runtime}(). The method must be a static extension on IServiceCollection, WebApplicationBuilder, or IHostApplicationBuilder.
[DevelopmentOnly] Marks a service implementation as development-only. The bootstrapper refuses to start in production if any [DevelopmentOnly] service is still registered.
[TestImplementation] Marks a class as a test double for a capability interface. The TestRuntime generator auto-wires these in CreateConfigured(). Implicitly development-only.

Diagnostics

ID Severity Description
DSRT01 Error Runtime class must be partial
DSRT02 Error [Uses] requires [Runtime] on the same class
DSRT03 Error [Uses] target must be an [EffectsModule]
DSRT04 Info Effects module available but not referenced (external modules only)

Module Architecture

Every runtime feature module follows the same three-part pattern:

[EffectsModule(typeof(IFeatureService))]
[ServiceRegistration(nameof(AddDefaultFeature))]
public static partial class FeatureModule
{
    public static void AddDefaultFeature(this IServiceCollection services)
    {
        services.TryAddSingleton<IFeatureService, InMemoryFeatureService>();
    }
}

This pattern gives you three guarantees:

1. Production wins via TryAdd ordering

TryAddSingleton only registers if no implementation exists yet. Because production providers register first (in the IRuntimeOptions.AddXxx() callback), the InMemory fallback only activates when no real implementation is configured. This means a fresh dotnet new project works immediately with in-memory implementations, and swapping to production is a one-line change.

2. Three-tier implementation strategy

Every module provides three implementations:

Tier Class Attribute Purpose
Production Provider-specific (e.g., SesEmailService) Real external service integration
Development InMemory{Feature} [DevelopmentOnly] Works out of the box, no external dependencies
Test Test{Feature} [DevelopmentOnly] Rich test double with call recording, seeding, callbacks

3. Startup validation catches misconfigurations

DevelopmentServiceCheck runs at startup and throws if any [DevelopmentOnly] service is registered in a production environment. This prevents InMemory implementations from silently running in production. See Startup Validation for details.

Modules following this pattern

Module Interface InMemory Test
EmailModule IEmailService InMemoryEmailService TestEmailService
SmsModule ISmsService InMemorySmsService TestSmsService
IdempotencyModule IIdempotencyStore InMemoryIdempotencyStore TestIdempotencyStore
JobsModule IJobScheduler, IJobStore InMemoryJobScheduler TestJobScheduler, TestJobStore
AuditModule IAuditStore InMemoryAuditStore TestAuditStore
NotifyModule INotificationChannel, INotificationStore InMemoryNotificationChannel TestNotificationChannel, TestNotificationStore
SearchModule ISearchIndex InMemorySearchIndex TestSearchIndex
VectorModule IVectorIndex InMemoryVectorIndex TestVectorIndex
StorageModule IFileStore InMemoryFileStore TestFileStore
IdentityModule ICurrentUser, IIdentityStore InMemoryIdentityStore TestCurrentUser
ClockModule IClock SystemClock TestClock

For testing patterns with these modules, see Module Patterns.

Built-In Modules

The runtime ships with several built-in modules that provide common infrastructure. Each registers via TryAddSingleton so production implementations (registered first) take priority.

Email (Deepstaging.Email)

public interface IEmailService
{
    Task<EmailResult> SendAsync(EmailMessage message, CancellationToken ct = default);
    Task<IReadOnlyList<EmailResult>> SendBatchAsync(
        IReadOnlyList<EmailMessage> messages, CancellationToken ct = default);
}

EmailModule is an [EffectsModule(typeof(IEmailService))]. The default fallback is InMemoryEmailService ([DevelopmentOnly]).

from _ in EmailModule.EmailService.SendAsync<AppRuntime>(
    new EmailMessage(To: "user@example.com", Subject: "Welcome", Body: "Hello!"))
select unit;

SMS (Deepstaging.Sms)

public interface ISmsService
{
    Task<SmsResult> SendAsync(SmsMessage message, CancellationToken ct = default);
    Task<SmsResult> GetStatusAsync(SmsMessageId messageId, CancellationToken ct = default);
}

SmsModule is an [EffectsModule(typeof(ISmsService))]. The default fallback is InMemorySmsService ([DevelopmentOnly]).

Identity (Deepstaging.Identity)

public interface ICurrentUser
{
    string UserId { get; }
    string Email { get; }
    string Name { get; }
    IReadOnlySet<string> Roles { get; }
    IReadOnlySet<int> Permissions { get; }
    bool HasPermission<TPermission>(TPermission permission) where TPermission : Enum;
    bool HasRole(string roleName);
    bool IsAuthenticated { get; }
}

The identity system integrates with the RBAC attributes:

  • IPermissionResolver — maps role names → permission sets (generated by PermissionsGenerator)
  • IIdentityStore — stores user identities and role assignments

Tenancy (Deepstaging.Tenancy)

TenantMiddleware extracts the tenant ID from JWT claims or an HTTP header and propagates it via CorrelationContext.TenantId.

Resolution order: JWT claim → HTTP header → reject (if RequireTenant is configured).

Webhooks (Deepstaging.Webhooks)

public interface IWebhookValidator
{
    bool Validate(WebhookValidationContext context);
}

Used with [Webhook<TValidator>] on handler methods. The WebhookValidationContext carries the raw body, signature, timestamp, and headers. HmacSignature provides HMAC-SHA256/SHA1 helpers with timing-safe comparison.

Idempotency (Deepstaging.Idempotency)

public interface IIdempotencyStore
{
    Task<bool> TryClaimAsync(string key, TimeSpan? expiry = null, CancellationToken ct = default);
    Task<CachedResponse?> TryGetCachedResponseAsync(string key, CancellationToken ct = default);
    Task StoreCachedResponseAsync(string key, CachedResponse response,
        TimeSpan? expiry = null, CancellationToken ct = default);
    Task ReleaseAsync(string key, CancellationToken ct = default);
}

Configured via IdempotencyOptions (Deepstaging:Idempotency config section). Default header: Idempotency-Key, default TTL: 24 hours. Used with [Idempotent] on handler methods.

Correlation Context (Deepstaging)

public sealed record CorrelationContext
{
    public string? CorrelationId { get; set; }
    public string? UserId { get; set; }
    public string? TenantId { get; set; }
    public string? CausationId { get; set; }
    public IReadOnlyDictionary<string, string>? Metadata { get; set; }
    public static CorrelationContext? Current { get; set; } // AsyncLocal<T>
}

Propagated automatically through effect pipelines. TenantMiddleware sets TenantId; the dispatch pipeline sets CausationId.

Other Runtime Internals

Feature Description
SoftDelete ISoftDeletable, ISoftDeleteService, IDeletedEntityStore — soft-delete interceptor for EF Core
RateLimiting DeepstagingRateLimitPolicies, RateLimitingOptions — pre-configured rate limit policies (per-tenant, per-user)
Telemetry DeepstagingTelemetryOptions, OtlpExporterSetup — OpenTelemetry configuration
RequestResponse IRequestResponseTransport — in-process request/response channel (internal)
Collections EnumerableExtensions — utility collection extensions (internal)

Next