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.
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:
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:
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 byPermissionsGenerator)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)¶
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¶
- Observability — OpenTelemetry tracing, metrics, and Aspire integration
- Startup Validation —
[DevelopmentOnly], config validation, secret safety