Elarion

Why Elarion

The reasoning behind Elarion's opinions, how it differs from idiomatic ASP.NET Core, and whether the trade-offs fit your project.

Elarion is opinionated on purpose. This page collects the reasoning behind those opinions in one place, so you can decide whether the trade-offs fit your project before adopting the framework. Each principle links to the page where it is applied.

The guiding principle

Auto-detect application patterns, explicitly wire platform capabilities.

Your code already states its intent: a handler says "I handle this request", a validator says "I validate this command", a [Service] class says "this is a module service". Repeating those facts in a separate registration list (Program.cs, hand-written AddXyz() methods) creates a parallel model that drifts from the code it describes. Elarion declares intent next to the type and generates the wiring. The host stays a thin composition and transport shell.

Everything below follows from that one idea.

Compile-time generation over runtime reflection

Discovery happens at build time through Roslyn generators, not at startup through assembly scanning.

Why: startup is deterministic and AOT-friendly; the generated code is ordinary DI registration you can read and step through; and mistakes — a missing trigger, an invalid contract, a scoped hosted service — become build errors instead of runtime surprises. Registration drift is caught by the compiler.

Source generation · Why auto-detection is the default

Handlers are the use-case boundary

The unit of application logic is an IHandler<TRequest, TResponse>, not a controller, a route lambda, or a service method.

Why: a handler is a single named use case with one entry point. It is transport-agnostic — it does not know whether it was called over JSON-RPC, HTTP, a scheduled job, or a test — which makes it trivial to test and to reuse across transports. Cross-cutting behavior lives in decorators wrapped around it in a deterministic order, so the handler body stays focused on business orchestration.

Handlers

Results over exceptions for expected failures

Handlers return Result<T> carrying either a value or a transport-agnostic AppError, rather than throwing for expected outcomes like "not found" or "validation failed".

Why: expected failures are normal return values, so they are explicit in the type and cheap to produce. AppError.Kind describes what kind of failure occurred, not how a protocol should report it — the host maps it to a JSON-RPC code or an HTTP status once, centrally. The same handler behaves correctly behind any transport.

Results & errors

Modules own their surface

A module is a namespace plus an [AppModule] marker. Adding a handler, validator, service, or JSON context under that namespace is enough to publish it.

Why: ownership follows structure. Moving a type between modules changes its ownership through namespace containment; deleting a type deletes its registration on the next build. The host never needs to know a module's internals — it composes modules and maps transports. Feature flags decide which modules are exposed.

Modules · Project structure

The context interface is the data abstraction

Handlers query through a generated IAppDbContext interface and its DbSets — there is no per-entity repository layer.

Why: the generated interface already provides full LINQ, projections, and EF Core change tracking, while keeping handlers in the application layer (they never reference the infrastructure project that owns the concrete DbContext). A hand-written IClientRepository per table usually adds indirection without value. This is the reason the EF Core package exists.

Entity Framework Core · Handlers → Accessing data

JSON-RPC for internal APIs

When you need a network transport, JSON-RPC is offered as a first-class (but optional) one because it exposes operations, not resources — a method maps one-to-one to a handler.

Why: it removes an entire category of REST design work (paths, verbs, status-code mapping, resource modeling). The generated, typed client makes a frontend call feel like invoking the handler directly, so the iteration loop is just add handler → regenerate client → call it. That fits internal, first-party APIs where one team controls both ends. It is deliberately optional and outside the core package, and modules can expose Minimal API endpoints instead when REST semantics are what you need.

Why JSON-RPC?

Observability without lock-in

The runtime emits OpenTelemetry-compatible traces and metrics through System.Diagnostics, but depends on no OpenTelemetry SDK.

Why: the framework instruments its own boundaries (JSON-RPC, scheduling, caching, resilience) while letting the host choose exporters and which sources to collect. You get observability for free without the framework dictating your telemetry stack.

Telemetry & observability

A small reusable contract; the platform stays in the host

The framework packages stay domain-neutral and small. Authentication, middleware, database providers, telemetry exporters, and concrete capability implementations are the host's job.

Why: keeping platform mechanics out of the reusable packages is what lets modules move between hosts unchanged and keeps the framework's public surface stable. Explicit registration is preserved exactly where explicitness is the point.

Packages

The trade-off

The cost of all this is that you accept conventions: handler names, the IHandler<,> request/response shape, namespace containment, and pipeline attributes matter because the generators use them. In exchange you get less host boilerplate, inherent modularity, fewer runtime scans and faster, more predictable startup, and a clear line between application policy and host mechanics.

How Elarion differs from ASP.NET Core

Elarion is intentionally not a thin wrapper around the default ASP.NET Core style. It chooses a more application-centric architecture where modules and handlers are the source of truth, while the ASP.NET Core host becomes a composition and transport shell. The differences below are the same principles above, viewed against idiomatic ASP.NET Core.

TopicIdiomatic ASP.NET CoreElarion
RegistrationExplicit services.AddScoped<…>(), endpoint maps, and feature wiring in Program.cs.Source-generated auto-registration from handler, validator, module, and RPC attributes. Explicit registration remains for infrastructure.
Application shapeControllers, Minimal API lambdas, or endpoint classes are the use-case boundary.IHandler<TRequest, TResponse> is the use-case boundary; transport adapters call handlers.
ModularityFolder/project organization plus manually maintained AddXyz() methods.First-class modules with [AppModule]: feature flags, ordering, handler/validator aggregation, endpoint hooks, JSON metadata hooks.
Cross-cutting behaviorMiddleware, endpoint filters, MVC filters, MediatR behaviors, or nested services.Generated decorator pipelines wrap handlers in a deterministic order chosen by assembly, module, or handler.
DiscoveryRuntime reflection scanning for validators, handlers, controllers, endpoint modules.Compile-time source generation for deterministic startup, AOT friendliness, and inspectable code.
Host responsibilitiesThe host often knows every feature and registers most feature services.The host owns platform capabilities, middleware, auth, telemetry, database setup, and transports; modules own composition.
Serialization metadataGlobal JSON options plus reflection fallback.Modules contribute JsonSerializerContext resolvers following the module boundary.
EF Core model wiringHand-written DbSet<T> and ApplyConfigurationsFromAssembly(…).Optional generator with interface-first [GenerateDbSets], explicit [DbEntity], optional scopes, and direct configuration calls.
Background workIndividual BackgroundService loops own their timers, overlap, logging, and cancellation.Source-generated scheduled jobs share one scheduler with typed invocation, explicit overlap, TimeProvider, and telemetry.
Error modelExceptions, ProblemDetails, action results, or ad-hoc DTOs.Handlers return Result<T> with transport-agnostic AppError; the host maps errors per transport.
TransportHTTP REST is the default API.HTTP, JSON-RPC, and MCP are parallel first-class optional transports over the same handlers; JSON-RPC and MCP live outside the core package.
Client contractsFrontends hand-write DTOs or ad-hoc REST helpers.JSON-RPC clients consume generated TypeScript/Zod artifacts and a portable fetch client from the schema.

Is Elarion for me?

Elarion pays for its conventions when your application's shape is the thing you want to keep stable, and it gets in the way when each endpoint needs to be bespoke.

A good fit if you are building:

Probably not a good fit if you need:

  • maximum freedom in how each endpoint is shaped — bespoke routes, verbs, content negotiation, and status-code mapping per operation;
  • a public or third-party REST API where resource modeling and stable URLs are part of the contract;
  • to stay on the idiomatic ASP.NET Core path with controllers/Minimal APIs as the use-case boundary and explicit registration everywhere.

In those cases idiomatic ASP.NET Core fits better. Elarion does not try to be a universal web framework — it is opinionated where the opinions buy you modularity and a thin host.

Influences

The model will feel familiar if you have used annotation-driven frameworks such as Spring Boot — components live below a namespace boundary, attributes declare their role, defaults cover the common path, and explicit configuration remains for the exceptional case. Elarion implements that philosophy with .NET source generation rather than runtime classpath scanning, which is what makes it deterministic and AOT-friendly.

On this page