Elarion

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 concernElarion featureWhere it appears
Feature boundaries (Clients, Invoicing) and a shared foundationModules (feature + core)Model the domain
Reads and writes as named use casesHandlers + ResultsWrite the features
Data access without a repository layerEF Core BillingDbContextModel the domain
Rejecting bad inputValidationWrite the features
Transactions + logging around every commandDecorator pipelinesWrite the features
"Who is calling?" for scoping and auditCurrent userWrite the features
Fast, per-user reads that stay fresh on writesCachingWrite the features
Flaky SMTP that must not drop an invoiceResilienceBackground work
Send-in-the-background + nightly remindersSchedulingBackground work
After-commit reactions that must not be lostEvents (durable outbox)Background work
A number generator, an email portServicesBackground work
A typed, optional transportJSON-RPCHost the API
The same handlers as AI toolsMCP serverHost the API
Traces and metrics, no SDK lock-inTelemetryHost the API
A frontend that calls handlers like functionsTypeScript clientBuild 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 frontend

Entities 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.AppHost

Wire 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.Api

Add 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.

src/Billing.Application/ElarionAssembly.cs
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.PostgreSQL
src/Billing.AppHost/AppHost.cs
var 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.

On this page