Introduction
Elarion is a .NET application framework that turns modules, handlers, and attributes into deterministic, compile-time wiring — no runtime reflection scanning.
Elarion is a .NET application framework for module-based handler pipelines and deterministic, compile-time wiring. Its focused packages add HTTP, JSON-RPC and MCP transports; background work; events and realtime delivery; PostgreSQL-backed coordination and persistence; device connections; and optional EF Core or NativeAOT-friendly SQL data access.
The central idea is simple: your application assemblies define modules and handlers; your host assembly only wires infrastructure, transport, and deployment concerns. Everything that can be discovered from your code — handler pipelines, module registrations, transport maps, serializers, jobs, event consumers, policy catalogs, actor facades, and data-access metadata — is emitted by source generators at compile time instead of scanned by reflection at startup.
[Handler("clients.get")]
public sealed class GetClient(AppDbContext db)
: IHandler<GetClient.Query, Result<GetClient.Response>> {
public sealed record Query(Guid Id);
public sealed record Response(Guid Id, string Name);
public async ValueTask<Result<Response>> HandleAsync(Query query, CancellationToken ct) {
var client = await db.Clients
.Where(c => c.Id == query.Id)
.Select(c => new Response(c.Id, c.Name))
.FirstOrDefaultAsync(ct);
return client is null
? AppError.NotFound($"Client {query.Id} was not found.")
: client;
}
}That single class is a use case, a registered service, a JSON-RPC method, an MCP tool for AI agents,
and (optionally) a schema-exported TypeScript contract — with no entry added to any Program.cs
registration list.
Why Elarion
Compile-time, not reflection
Handlers, services, modules, RPC maps, validation metadata, and scheduled jobs are generated as ordinary DI code. Startup is deterministic and AOT-friendly; missing wiring is a build error, not a runtime surprise.
Modules own their surface
A module is a namespace plus an [AppModule] marker. Add a handler under it and the module
publishes it automatically through generated Add{Module}…() methods.
Transport-neutral results
Handlers return Result<T> with a transport-agnostic AppError. The host maps failures to
JSON-RPC, HTTP, or any other protocol.
End-to-end JSON-RPC
Mark a handler with [Handler], export a schema at build time, and generate a typed
TypeScript + Zod client — without hand-writing DTOs.
AI-native, no extra code
Expose the same [Handler] handlers to AI agents as an MCP server. Tool names, descriptions,
and input schemas are generated from your handlers and [Description] attributes at compile
time — no separate tool layer, no duplicated schemas.
In-process scheduling
Source-generated scheduled jobs share one scheduler with explicit overlap, misfire, and resilience policies — and full OpenTelemetry instrumentation.
Declarative feature flags
Gate any handler behind a flag with [FeatureGate] — runtime rollouts, kill switches, and
per-variant service injection over OpenFeature. Transport-neutral and provider-agnostic, a closed
gate returns 404 so the feature name never leaks.
Declarative authorization
Gate any handler with [RequirePermission]/[RequireRole]/[RequireClaim]/[RequirePolicy] — the
generator auto-attaches the decorator, so the same rules apply under JSON-RPC, MCP, and HTTP.
Opt-in deny-by-default, and no ASP.NET coupling.
Resource & data-level access control
Point-check a resource with [RequireResource], or filter whole result sets at the database with
[ResourceFilter<TEntity>] + WhereAuthorized(spec, user) — owner/tenant/role sharing compiled into
EF Core predicates, no in-memory post-filter.
Blob storage
Depend on provider-neutral blob contracts in application code and choose PostgreSQL-backed storage in the host.
Observable by default
JSON-RPC, scheduling, caching, and resilience emit OpenTelemetry-compatible traces and metrics
through System.Diagnostics — no SDK dependency forced on you.
Choose the right runtime shape
The framework now covers several kinds of work, but they are not interchangeable. Start with the simplest shape whose correctness contract fits:
Request or command
Use a handler for bounded request/reply work. It is the transport-facing gate for authorization, validation, transactions, idempotency, resilience, caching, and audit policy.
Deferred response to one request
Use a cold stream when one request returns lazy output such as an export, token output, or tail query.
Browser needs to refresh
Publish a client event when a lost message merely leaves the UI stale and re-querying converges.
Sequence is the data
Use an ordered stream when a missing item makes the consumer wrong and one live producer owns the key, sequence, replay window, and resume behavior.
Conversation is stateful
Use a connection for interactive-rate client input, device links, or server-to-client request/reply.
Live in-memory consistency
Default to the database. Reach for an actor only when a live resource, ephemeral hot state, or a decide-once coordinator is genuinely the consistency unit.
One instance owns a role
Use a PostgreSQL role lease for a handful of coarse leader roles—not as a per-record distributed lock.
Data access
Choose EF Core for the full application data platform, or generated SQL mapping and migrations for EF-free NativeAOT hosts.
High-rate data
Batch loss-tolerant writes and conflate latest-wins values before adding a reactive or streaming stack.
The philosophy in one line
Auto-detect application patterns, explicitly wire platform capabilities.
Repeating what your code already states — "I handle this request", "this is what a valid command looks like", "this is a module service" — in a separate registration list creates a parallel model that drifts. Elarion declares intent next to the type and generates the wiring. The host stays a thin composition and transport shell that owns authentication, middleware, database providers, telemetry exporters, and deployment.
This is the same broad approach used by mature annotation-driven frameworks such as Spring Boot — components live below a namespace boundary, attributes declare their role, sensible defaults cover the common path — implemented with .NET source generation instead of runtime classpath scanning.
For the full reasoning behind each design decision — compile-time generation, handlers as the use-case boundary, result-based errors, JSON-RPC for internal APIs, and a thin host — see Design & philosophy.
When Elarion is a good fit
- You are building a modular application or service and want feature modules to be self-contained.
- You value deterministic startup, AOT compatibility, and inspectable generated code over runtime reflection.
- You want a typed RPC contract shared between a .NET backend and a TypeScript frontend.
- You want to expose your application to AI agents as MCP tools without hand-writing a separate tool layer.
- You want in-process recurring/background work without standing up external job infrastructure.
- You need realtime, device, or coordination building blocks sized for roughly 1–10 nodes on the PostgreSQL your application already runs.
It is intentionally not a thin wrapper around default ASP.NET Core. You trade some convention freedom for less host boilerplate, inherent modularity, and a clear line between application policy and host mechanics. See How Elarion differs from ASP.NET Core for the full trade-off table.
Next steps
Installation
Add the packages and generators to an application and host project.
Quickstart
Build your first module, handler, and JSON-RPC endpoint end to end.
Core concepts
Handlers, results, modules, services, validation, pipelines, and caching.
Package reference
What each Elarion package contains and when you need it.
Elarion is pre-1.0. The public surface described here is stable enough to build on, but minor releases may introduce breaking changes until 1.0. See the changelog for details.