Build a fullstack app
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, FluentValidation, 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 IAppDbContext | Model the domain |
| Rejecting bad input | Validators | 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, handlers, services, validators, jobs, policies ← Elarion + generators
│ ├─ Billing.Infrastructure/ # BillingDbContext (PostgreSQL), SMTP email sender
│ └─ Billing.Api/ # ASP.NET Core host ← Elarion.JsonRpc + Elarion.AspNetCore (+ MCP)
└─ web/ # Vite + React + TanStack Query frontendThe 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 a local PostgreSQL) for the database.
- 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 sln add src/Billing.Application src/Billing.Infrastructure src/Billing.ApiWire 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.ApplicationAdd the Elarion packages
The application project holds your modules and needs the runtime plus the generators. It also uses EF Core for data access and FluentValidation for input rules.
cd src/Billing.Application
dotnet add package Elarion
dotnet add package Elarion.Generators
dotnet add package Elarion.EntityFrameworkCore
dotnet add package Elarion.EntityFrameworkCore.Generators
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package FluentValidation
cd ../..Mark the two generator packages as analyzer-only so they stay out of published output. Edit
src/Billing.Application/Billing.Application.csproj:
<ItemGroup>
<PackageReference Include="Elarion.Generators" PrivateAssets="all" />
<PackageReference Include="Elarion.EntityFrameworkCore.Generators" PrivateAssets="all" />
</ItemGroup>The infrastructure project owns the concrete DbContext and the PostgreSQL provider, plus the EF
Core integration-event outbox:
cd src/Billing.Infrastructure
dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL
dotnet add package Elarion.Messaging.Outbox
cd ../..The host project references the JSON-RPC transport, the ASP.NET Core integration, the MCP server, and the generators (it emits the RPC map and module bootstrapper). It also registers the scheduler, the Microsoft resilience runtime, and EF Core design-time tooling.
cd src/Billing.Api
dotnet add package Elarion.JsonRpc
dotnet add package Elarion.AspNetCore
dotnet add package Elarion.AspNetCore.Mcp
dotnet add package Elarion.Generators
dotnet add package Microsoft.Extensions.Resilience
dotnet add package Microsoft.EntityFrameworkCore.Design
cd ../..Mark Elarion.Generators as analyzer-only in the host too:
<ItemGroup>
<PackageReference Include="Elarion.Generators" PrivateAssets="all" />
</ItemGroup>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, validators, scheduled
jobs, and resilience policies across the assembly.
Start PostgreSQL
docker run --name billing-db -e POSTGRES_PASSWORD=postgres \
-e POSTGRES_DB=billing -p 5432:5432 -d postgres:17Add the connection string to the host configuration:
{
"ConnectionStrings": {
"Billing": "Host=localhost;Database=billing;Username=postgres;Password=postgres"
}
}The skeleton is in place. Next, model the domain and define the modules that own it.