Elarion

Concepts

The three responsibilities Elarion separates, and why auto-detection is the default.

Elarion separates an application into three responsibilities and uses compile-time generation to connect them. Understanding this split makes every other concept fall into place.

The three responsibilities

  1. Application module composition lives beside the feature code. A module decides which handlers, JSON metadata, and module-owned services it exposes.
  2. Compile-time discovery replaces reflection-heavy startup scanning. Generators emit deterministic registration methods from attributes and conventions.
  3. Platform wiring stays in the host. The host owns WebApplication, authentication, middleware, capability providers, database setup, telemetry exporters, and endpoint publication.

This keeps modules independent of the final host while still allowing real platform abstractions where they matter. For example, modules may declare Minimal API endpoints with IEndpointRouteBuilder directly, so ASP.NET Core endpoint conventions and source generation still work.

Why auto-detection is the default

Elarion prefers declaring application intent near the type over maintaining a second registration list elsewhere. A handler already says "I handle this request"; an annotated request DTO already says "this is what a valid command looks like"; a [Service] class already says "this is a module service". Repeating those facts in Program.cs or hand-written AddXyz() methods creates a parallel model that drifts from the code it describes.

This buys practical advantages:

  • The module owns its application surface. Adding a handler, JSON context, or service under a module namespace is enough to publish it through generated Add{Module}…() methods.
  • Registration drift becomes a compile-time problem. Missing trigger attributes, invalid service contracts, wrong hosted-service lifetimes, and unsupported generic services produce generator diagnostics instead of startup surprises.
  • The host stops knowing feature internals. It wires platform capabilities and transports; it does not remember every application handler or module service.
  • Generated code stays inspectable. The result is ordinary DI registration code, emitted deterministically rather than hidden behind runtime reflection.
  • Refactoring follows structure. Moving a type between modules changes ownership through namespace containment; deleting a type deletes its registration on the next build.
  • AOT and startup behavior are predictable. Compile-time discovery avoids broad runtime scans and keeps the dependency graph visible to the compiler and linker.

Explicit registration still has a place when explicitness is the point: database contexts, external clients, provider-specific decorators, authentication, telemetry, and concrete capability providers should be wired by the platform. The bias is auto-detect application patterns, explicitly wire platform capabilities.

Developers coming from Spring Boot will find the model familiar — components under a namespace boundary, annotations that declare their role, defaults for the common path — implemented with .NET source generation rather than runtime classpath scanning.

The trade-off

You accept conventions. Handler names, the IHandler<,> request/response shape, module namespace containment, and pipeline attributes matter because generators use them. In exchange you get less host boilerplate, inherent modularity, fewer runtime scans, and a clear separation between application policy and host mechanics. The full comparison with idiomatic ASP.NET Core is in How Elarion differs.

The concepts

The pages in this section cover each building block:

Source generation

How generators turn attributes and conventions into inspectable registration code.

Handlers

The primary use-case unit.

Results & errors

Result<T> and transport-agnostic AppError.

Modules

Application boundaries and their lifecycle.

Services

[Service] registration and hosted services.

Validation

Two-tier: DataAnnotations exported to every schema, business rules in the handler.

Decorator pipelines

Ordered cross-cutting behavior around handlers.

Authorization

Declarative, transport-neutral handler authorization.

Resource authorization

Per-resource point checks and data-level list filtering.

Feature flags

Gate handlers with [FeatureGate] over any OpenFeature provider.

Client capabilities

One bootstrap snapshot of modules, flags, and grants for UX-only frontend adaptation.

Frontend modules

Typed extension points and capability-gated contributions for the web app.

Idempotency

Exactly-once command replay with [Idempotent] and a single-transaction, unique-constrained key store.

Audit trail

Who did what: [Auditable] records with field-level change capture, committed with the transaction.

Settings

Runtime-changeable, DB-backed key/value settings.

Persistence & transactions

How handlers, decorators, and events share the DbContext transaction.

Polymorphic references

Notes and files on records of many kinds: exclusive arcs, a table per kind, or a shared identity, instead of a (kind, id) pair.

Variable substitution

Spring-style ${key:-default} placeholders, change-observable.

Cross-module communication

Published [ModuleContract] surfaces between modules.

Actors

In-memory state machines with mailbox-serialized, source-generated typed facades.

Caching

Declarative handler result caching and invalidation.

Current user

Transport-neutral access to the authenticated user.

Solution structure

How application, infrastructure, and host projects fit together.

On this page