Architecture
SchulyBackend is a clean-architecture solution with CQRS. Requests flow into thin controllers, which dispatch commands/queries through Mediator to handlers in the application layer; persistence and external integrations live in infrastructure.
Projects
The solution (Schuly.API.slnx) is split into the following projects:
| Project | Role |
|---|---|
Schuly.API | Entry point. Controllers, OIDC wiring, OpenAPI/Scalar, startup migrations, plugin host registration. Owns the Dockerfile. |
Schuly.Application | CQRS commands/queries + Mediator handlers, DTOs, mappers, authorization and pipeline behaviors. Must not reference Infrastructure. |
Schuly.Domain | Pure entities (School, Class, Exam, Grade, Absence, AgendaEntry, ApplicationUser, SchoolUser, Teacher, SchoolSystem, SemesterReport, StudentDocument, …). Each inherits Base (Id, CreatedAt, UpdatedAt). |
Schuly.Infrastructure | SchulyDbContext, OIDC/user services, storage and vault, repositories, plugin runtime (PluginBackgroundTaskHost). |
Schuly.Tests / Schuly.Tests.Plugin | Test projects (TUnit). |
Schuly.Plugin.Abstractions is consumed as a NuGet PackageReference, not a
project reference. The abstractions and the plugin implementations live in separate
repositories.
Layering rules
- Dependencies point inward:
API → Application → Domain, andInfrastructure → Application/Domain. Schuly.Applicationmust not referenceSchuly.Infrastructure. Handlers depend on abstractions; the API project composes the concrete infrastructure services into the DI container at startup (Program.cs).Schuly.Domainhas no project dependencies — entities stay pure.
Request pipeline
Controllers are thin and delegate to Mediator. Two pipeline behaviors are registered
explicitly in Program.cs and run in registration order:
AuthorizationBehavior— enforces role gates before the handler runs.PluginEventBehavior— dispatches backend commands to plugin event handlers.
Mediator handlers are registered automatically via source generation, so a new command/query and its handler are wired up just by adding the classes.
Adding an entity + endpoint
- Entity in
Schuly.Domain(inheritsBase). - DbSet + configuration in
Schuly.Infrastructure/SchulyDbContext.cs. - Migration — see Migrations.
- Command/Query in
Schuly.Application/Commands/<Entity>/orQueries/<Entity>/. - Handler alongside the command/query (auto-registered via Mediator source-gen).
- Controller in
Schuly.API/Controllers/— thin, delegates to Mediator.
Plugin host
The backend hosts plugins implementing ISchulyPlugin from
Schuly.Plugin.Abstractions. Plugins are downloaded at runtime from a registry into
/app/plugins, each loaded into its own collectible AssemblyLoadContext with a
child DI container, and can register controllers, minimal-API endpoints, and
recurring background tasks (run by PluginBackgroundTaskHost). Plugin requests
execute inside the owning plugin's DI scope via PluginScopeMiddleware. See
Plugin management for the registry, hot-swap, and admin
endpoints.