Tutorial
A complete, end-to-end walkthrough that builds a small billing application with every opinionated Elarion feature, plus a typed React frontend.
The Quickstart gets one handler onto the wire. This tutorial goes the whole way: a small but realistic billing application with a .NET backend and a typed React frontend, exercising every opinionated Elarion feature on its happy path.
You will build Billing — a single-tenant-per-user app where a signed-in account manages its clients and issues invoices. Each feature below earns its place in that story; nothing is bolted on just to demo an attribute.
What you will build
A typed backend
Two feature modules and a core module, handlers exposed over JSON-RPC, declarative request validation, a transactional decorator pipeline, and per-user result caching.
Reliable background work
Invoice emails sent through a resilient, retrying background job, plus a nightly cron job that chases overdue invoices.
An AI-ready surface
The same handlers exposed to AI agents as MCP tools, with OpenTelemetry traces across RPC, caching, scheduling, and resilience.
A typed React frontend
A Vite + React app using shadcn/ui and TanStack Query, calling a generated, Zod-validated client
with full request cancellation via AbortSignal.
How each feature maps to the app
Every Elarion capability shows up exactly where it pulls its weight in a real billing flow:
| App concern | Elarion feature | Where it appears |
|---|---|---|
Feature boundaries (Clients, Invoicing) and a shared foundation | Modules (feature + core) | Model the domain |
| Reads and writes as named use cases | Handlers + Results | Write the features |
| Data access without a repository layer | EF Core BillingDbContext | Model the domain |
| Rejecting bad input | Validation | Write the features |
| Transactions + logging around every command | Decorator pipelines | Write the features |
| "Who is calling?" for scoping and audit | Current user | Write the features |
| Fast, per-user reads that stay fresh on writes | Caching | Write the features |
| Flaky SMTP that must not drop an invoice | Resilience | Background work |
| Send-in-the-background + nightly reminders | Scheduling | Background work |
| After-commit reactions that must not be lost | Events (durable outbox) | Background work |
| A number generator, an email port | Services | Background work |
| A typed, optional transport | JSON-RPC | Host the API |
| The same handlers as AI tools | MCP server | Host the API |
| Traces and metrics, no SDK lock-in | Telemetry | Host the API |
| A frontend that calls handlers like functions | TypeScript client | Build the frontend |
The shape of the solution
Billing follows the recommended layout: the application project declares intent, infrastructure provides concrete capabilities, and the host wires platform concerns.
billing/
├─ Billing.sln
├─ src/
│ ├─ Billing.Application/ # modules (behavior), handlers, services, jobs, policies ← Elarion + generators
│ │ ├─ Domain/ # shared-kernel entities + enums (plain classes, under no [AppModule])
│ │ └─ Persistence/ # the database is app logic: [EntityConfiguration], BillingDbContext, migrations
│ ├─ Billing.Infrastructure/ # intent-only mechanism adapters: the SMTP email sender
│ ├─ Billing.Api/ # ASP.NET Core host ← Elarion.JsonRpc + Elarion.AspNetCore (+ MCP)
│ └─ Billing.AppHost/ # .NET Aspire orchestration → provisions PostgreSQL, runs the API
└─ web/ # Vite + React + TanStack Query frontendEntities live in a shared-kernel namespace (Billing.Application.Domain), not a separate project,
and their [EntityConfiguration] schema lives in a shared Persistence layer (configuration is part of the
shared data layer, not feature-owned) — see Solution structure for why.
The dependency direction is Api → Infrastructure → Application. The application project never
references the host or concrete infrastructure — that boundary is what lets the generators wire
modules without a central registration list.
Prerequisites
- .NET 10 SDK or later (see Installation).
- Node.js 20+ and npm for the frontend and client generator.
- Docker or Podman — the .NET Aspire app host provisions PostgreSQL in a container.
- The EF Core CLI for migrations:
dotnet tool install --global dotnet-ef.
Scaffold the solution
Create the projects
mkdir billing && cd billing
dotnet new sln -n Billing
dotnet new classlib -n Billing.Application -o src/Billing.Application
dotnet new classlib -n Billing.Infrastructure -o src/Billing.Infrastructure
dotnet new web -n Billing.Api -o src/Billing.Api
dotnet new aspire-apphost -n Billing.AppHost -o src/Billing.AppHost
dotnet sln add src/Billing.Application src/Billing.Infrastructure src/Billing.Api src/Billing.AppHostWire project references
The onion points inward — infrastructure and the host depend on the application, never the reverse.
dotnet add src/Billing.Infrastructure reference src/Billing.Application
dotnet add src/Billing.Api reference src/Billing.Infrastructure src/Billing.Application
dotnet add src/Billing.AppHost reference src/Billing.ApiAdd the Elarion packages
The source generators ship inside their runtime packages — the Elarion generator in Elarion,
the EF Core generator in Elarion.EntityFrameworkCore — so there are no separate analyzer packages to
install or mark PrivateAssets="all". Because NuGet analyzer assets are not transitive, each project
that needs a generator references its package directly.
The application project holds your modules, entities, and the [GenerateDbSets] concrete
BillingDbContext. It uses EF Core for data access and Elarion.Validation so the DataAnnotations
input rules on request DTOs are enforced (and the per-module validation resolvers generated):
cd src/Billing.Application
dotnet add package Elarion
dotnet add package Elarion.Validation
dotnet add package Elarion.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore
cd ../..The application's persistence layer owns the concrete BillingDbContext, the PostgreSQL provider, the
EF Core integration-event outbox, and the migrations — because the database is application logic, not an
infrastructure detail. The generated class-side DbSets and entity configuration are emitted there beside
the configurations (so no cross-assembly manifest is involved):
cd src/Billing.Application
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Elarion.Messaging.Outbox
cd ../..The host project references the JSON-RPC transport, the ASP.NET Core integration, and the MCP
server. It also references Elarion directly so the bundled generator runs in the host (it emits the
RPC map and module bootstrapper), and registers the scheduler, the Microsoft resilience runtime, and
EF Core design-time tooling.
cd src/Billing.Api
dotnet add package Elarion
dotnet add package Elarion.JsonRpc
dotnet add package Elarion.AspNetCore
dotnet add package Elarion.AspNetCore.Mcp
dotnet add package Microsoft.Extensions.Resilience
dotnet add package Microsoft.EntityFrameworkCore.Design
cd ../..Turn on the generators
Opt the application assembly into Elarion generation once. We will add the pipeline attribute to this file in Write the features.
using Elarion.Abstractions;
[assembly: UseElarion][assembly: UseElarion] enables generation for module handlers, services, scheduled
jobs, and resilience policies across the assembly.
Provision PostgreSQL with .NET Aspire
Rather than starting a database by hand, let the Aspire app host provision PostgreSQL and inject its connection string into the API. Add the PostgreSQL hosting integration and orchestrate the resources:
dotnet add src/Billing.AppHost package Aspire.Hosting.PostgreSQLvar builder = DistributedApplication.CreateBuilder(args);
var postgres = builder.AddPostgres("postgres").WithDataVolume();
var billingDb = postgres.AddDatabase("billing");
builder.AddProject<Projects.Billing_Api>("api")
.WithReference(billingDb) // injects ConnectionStrings__billing into the API
.WaitFor(billingDb);
builder.Build().Run();Running the app host (in Host the API) starts the database container and the
API together — no manual connection string, and the API reads it via
builder.Configuration.GetConnectionString("billing"). The host applies EF migrations on startup, so a
fresh database is ready on first run.
Aspire needs a container runtime (Docker or Podman). The Aspire dashboard it opens also collects the OpenTelemetry traces and metrics the host emits — no separate collector to run locally.
The skeleton is in place. Next, model the domain and define the modules that own it.