Skip to content

Using Generated Code

Every Deepstaging module generates standard .NET interfaces, implementations, and DI registration. There are two ways to use them: effects composition — the intended path — and direct injection for teams that prefer plain async/await.


Effects Composition

Effects composition is the primary way to use Deepstaging. Each [EffectsModule] generates Eff<RT, A> wrappers that let you compose service calls, constrain capabilities at compile time, and get automatic OpenTelemetry tracing.

Why Effects?

  • Compile-time capability constraintswhere RT : IHasArticleStore means your handler can only access what the runtime declares. Undeclared dependencies are compiler errors, not runtime surprises.
  • Composable pipelines — chain operations with from/select syntax. Errors short-circuit automatically. No try/catch pyramids.
  • Testable by construction[TestRuntime] generates stubs and builders for every capability. No mocking frameworks, no DI containers in tests.
  • Automatic observability — every effect invocation creates an OpenTelemetry span when Instrumented = true.

Example: DataStore with Effects

[DataStore]
public static partial class ScribeStore
{
    [StoredEntity]
    public static partial class Articles;
}

[Runtime]
public sealed partial class AppRuntime;
// ScribeStore and NotifyModule are auto-discovered
public static Eff<AppRuntime, ArticleId> Handle(CreateArticle cmd) =>
    let id = ArticleId.New()
    let article = new Article(id, cmd.Title, cmd.Body, cmd.Author,
        DateTimeOffset.UtcNow, false)
    from _ in Articles.Save<AppRuntime>(article)
              >> NotifyModule.NotificationChannel.SendAsync<AppRuntime>(
                  new Notification("article-created", $"Article {id} published", cmd.Author))
    select id;

The runtime enforces that AppRuntime provides both ScribeStore and NotifyModule. Add a [Uses] declaration and the capability is available. Remove it and the compiler catches every broken handler.

Example: CQRS Dispatch

The Dispatch module builds on effects to generate typed command/query routing with built-in validation, authorization, and audit:

[CommandHandler(ValidatorType = typeof(CreateArticleValidator), Audited = true)]
[Authorize("CanCreateArticles")]
public static Eff<ScribeRuntime, ArticleCreated> Handle(CreateArticle cmd) =>
    let id = ArticleId.New()
    let article = new Article(id, cmd.Title, cmd.Body, cmd.Author,
        DateTimeOffset.UtcNow, false)
    from _ in Articles.Save<ScribeRuntime>(article)
    select new ArticleCreated(id);

Method overload resolution IS the dispatch — no runtime routing, no reflection, no service locator.


Direct Injection

Every module also generates standard .NET interfaces that you can inject and call with async/await. No LanguageExt dependency in your code. This is useful for teams adopting Deepstaging incrementally, or for parts of your codebase where functional composition isn't needed.

Example: DataStore

[DataStore]
public static partial class ScribeStore
{
    [StoredEntity]
    public static partial class Articles;
}

Inject the generated interface directly:

public class ArticleService(IArticleStore store)
{
    public async Task<Article> CreateAsync(string title, string body, string author)
    {
        var article = new Article(
            ArticleId.New(), title, body, author,
            DateTimeOffset.UtcNow, false);

        await store.SaveAsync(article);
        return article;
    }

    public Task<QueryResult<Article>> ListAsync(int page, int pageSize) =>
        store.Query().ToPageAsync(page, pageSize);
}

Example: Event Store

public class OrderService(IOrderEventStore orderStore)
{
    public async Task<Order?> GetAsync(OrderId id) =>
        await orderStore.AggregateAsync(id);

    public async Task CreateAsync(OrderId id, string customerName)
    {
        await orderStore.StartStreamAsync(id, [new OrderCreated(id, customerName)]);
        await orderStore.CommitAsync();
    }
}

Example: HTTP Client

[HttpClient<ApiConfig>(BaseAddress = "https://api.example.com")]
[BearerAuth]
public partial class UsersClient
{
    [Get("/users/{id}")]
    private partial User GetUser(int id);

    [Post("/users")]
    private partial User CreateUser([Body] CreateUserRequest request);
}
public class UserService(IUsersClient client)
{
    public User GetUser(int id) => client.GetUser(id);
}

The Runtime Still Works

Even without writing a single Eff<RT, A>, the runtime generates one-call DI setup, OpenTelemetry wiring, and compile-time dependency validation:

[Runtime]
[Uses(typeof(StorageModule))]  // external — from Deepstaging.Storage package
[Uses(typeof(JobsModule))]     // external — from Deepstaging.Jobs package
public sealed partial class ScribeRuntime;
// ScribeStore is auto-discovered
builder.Services.AddScribeRuntime(options =>
{
    options.EnableTracing();
    options.EnableMetrics();
});

Inject your interfaces via standard DI alongside the runtime — they coexist naturally.

All Injectable Interfaces

Module Injectable Interface
Data Store IArticleStore — async CRUD with in-memory default
Event Store IOrderEventStore — async event stream with in-memory default
Configuration ISmtpConfigRoot — typed config binding from appsettings.json
HTTP Client IUsersClient — typed HTTP client with bearer auth, retries
Typed IDs ArticleId — type-safe ID struct with JSON/EF converters
Event Queue EventQueueChannel<T> — in-process Channel<T> with background workers
File Storage IFileStore — upload, download, presigned URLs
Search ISearchIndex<T> — full-text search with filtering, facets
Background Jobs IJobScheduler — enqueue, schedule, retry, monitor
Notifications INotificationChannel — send via pluggable channels
Audit Trail IAuditStore — record and query audit entries
Cache ICacheStore — typed get/set with expiration
Clock IClock — testable time abstraction
Auth IUserContext — current user from JWT/OAuth/Supabase
Vector Search IVectorIndex<T> — similarity search over embeddings

Testing

Both approaches are testable by design. Every module ships in-memory implementations, and [TestRuntime] generates stubs and builders for every declared capability — no mocking frameworks or DI containers needed.

[TestRuntime]
public sealed partial class TestRuntime;

This generates a class with With*() builder methods and in-memory implementations for every capability your runtime declares:

var runtime = TestRuntime.Create();

var program =
    from _ in Articles.Save<ScribeRuntime>(article)
    from loaded in Articles.GetById<ScribeRuntime>(id)
    select loaded;

var result = await program.RunAsync(runtime);

For direct injection, the same in-memory implementations work as standard test doubles — register them in a test ServiceCollection or pass them directly.

See Testing for the full testing story.


Incremental Adoption

You can mix both approaches in the same application. Start with direct injection, and adopt effects where composition and compile-time constraints add value:

  1. Start — inject generated interfaces, use async/await
  2. Add effects — wrap frequently-composed services with [EffectsModule] for pipeline composition
  3. Add dispatch — use [DispatchModule] for CQRS handlers with validation, authorization, and audit

The generated interfaces don't change — effects are an additional layer on top. See the Effects module and the Build an eShop walkthrough for the full effects story.