Base Framework
The Base Framework (SaveApis.Framework) is the foundation of every application built with SaveApis. It defines the Installer abstraction, the Dependency Graph, the InstallerContext, and the ApplicationContext. All higher-level frameworks (Web, Console, CLI) and utilities (CritterStack) build on top of it.
Table of Contents
Installer
Concept & Purpose
An Installer is the primary extension point of SaveApis. Every feature — logging, authentication, database access, your own services — is registered through an Installer.
Installers replace the typical inline service registration in Program.cs. Instead of scattered builder.Services.Add... calls, each concern lives in its own class with a well-defined lifecycle, optional enabling/disabling, and declarative ordering via dependencies.
The base interface is:
public interface IInstaller<TBuilder, TApplication, TContext>
{
string InstallerKey { get; }
bool DefaultEnabled { get; }
IReadOnlyCollection<InstallerDependency> Dependencies { get; }
bool ShouldInstall(TBuilder builder, TContext context);
bool ShouldInstall(TApplication application, TContext context);
void ConfigureBuilder(TBuilder builder, TContext context);
void ConfigureApplication(TApplication application, TContext context);
}
In practice, you always inherit from a framework-specific base class (e.g. BaseWebInstaller, BaseConsoleInstaller, BaseCliInstaller) instead of implementing IInstaller directly.
Lifecycle
The lifecycle of an installer spans two phases:
| Phase | Method | When |
|---|---|---|
| Builder phase | ConfigureBuilder(builder, context) |
During Build() – registers services, middleware, options |
| Application phase | ConfigureApplication(application, context) |
During RunAsync() – configures the running application (e.g. middleware pipeline) |
Both phases are guarded by ShouldInstall – an installer whose ShouldInstall returns false is skipped entirely in that phase.
Execution Order
Installers are not executed in registration order. Instead, the framework builds a dependency graph and applies a topological sort before either phase. The sorted order is the same for both ConfigureBuilder and ConfigureApplication.
- All installers are registered via
builder.AddInstaller<T>(). - On
Build(),InstallerGraph.Build()computes the topological order. - For each installer (in sorted order):
ShouldInstall(builder, context)→ iftrue→ConfigureBuilder. - On
RunAsync(), the same sorted order is recomputed and each installer’sShouldInstall(application, context)→ iftrue→ConfigureApplicationis called.
Note: Installers that are registered but whose type is already present in the graph are silently deduplicated — each type can only appear once.
Reading Options
BaseInstaller provides a GetOption<TOption> helper that binds configuration from IConfiguration using the installer’s key as the default section name:
// Reads configuration from the section named by InstallerKey (e.g. "Logging")
var options = GetOption<LoggerOptions>(builder);
// Or with an explicit section key
var options = GetOption<ZitadelOptions>(builder, ZitadelInstaller.Key);
The option type must have a parameterless constructor. If the configuration section is absent or empty, a default-constructed instance is returned.
Configuring the Builder
Override ConfigureBuilder to register services or configure the DI container:
public override void ConfigureBuilder(SaveApisWebApplicationBuilder builder, WebInstallerContext context)
{
builder.Services.AddScoped<IMyService, MyService>();
}
Configuring the Application
Override ConfigureApplication to configure the application after the container is built (e.g. middleware pipeline, route registration):
public override void ConfigureApplication(SaveApisWebApplication application, WebInstallerContext context)
{
application.Use(app => app.UseMyMiddleware());
}
Both builder and application expose:
| Property | Type | Description |
|---|---|---|
Configuration |
IConfiguration |
Access to appsettings.json, environment variables, etc. |
Environment |
IHostEnvironment |
Current environment (Development, Production, …) |
Services |
IServiceCollection / IServiceProvider |
DI container (builder: registration, application: resolution) |
ShouldInstall
ShouldInstall controls whether an installer’s phase is executed. The default implementation in BaseInstaller reads the feature flag from the context:
private bool DefaultShouldInstall(TContext context)
{
var enabled = context.Enabled(InstallerKey);
return enabled ?? DefaultEnabled;
}
Priority:
- If
context.Enabled(InstallerKey)returns abool→ that value is used. - Otherwise →
DefaultEnabledis used (default:true).
This means an installer is enabled by default unless explicitly disabled.
Typical scenarios:
| Scenario | How |
|---|---|
| Disable a built-in installer | context.SetEnabled("Logging", false) in the configure callback |
| Disable via extension method | context.WithLogging(false) (framework-provided helpers) |
| Conditionally enable a custom installer | Override ShouldInstall with custom logic |
| Installer off by default | Set public override bool DefaultEnabled => false; and enable explicitly |
Example — disabling a default installer:
await SaveApisApplication
.AsWeb(args, (app, installer) =>
{
app.Name = "My.Service";
app.Namespace = "My.Service";
installer.SetSoftwareAssembly();
installer.WithZitadel(false); // disable Zitadel authentication
})
.Build()
.RunAsync();
Dependencies
Dependencies express ordering constraints between installers. They are declared via the Dependencies property:
public override IReadOnlyCollection<InstallerDependency> Dependencies =>
[
InstallerDependency.After<AuthenticationInstaller>(),
InstallerDependency.Before<OpenApiInstaller>(DependencyRequirement.Optional),
];
Definition
| Factory method | Meaning |
|---|---|
InstallerDependency.After<T>() |
T must run before this installer |
InstallerDependency.Before<T>() |
T must run after this installer |
Both methods accept an optional DependencyRequirement:
| Requirement | Behaviour when T is not registered |
|---|---|
DependencyRequirement.Required (default) |
Throws MissingInstallerDependencyException |
DependencyRequirement.Optional |
Constraint is silently ignored |
Sorting via Dependencies
The framework resolves all Dependencies and builds a directed acyclic graph (DAG). It then runs a depth-first topological sort to produce a stable execution order that satisfies all constraints.
Example — the Web Framework’s built-in chain:
LoggingInstaller
OpenTelemetryInstaller
ValidationInstaller
AuthenticationInstaller
└─ AuthorizationInstaller
└─ ZitadelInstaller
└─ ControllerInstaller
└─ OpenApiInstaller
└─ ScalarInstaller
Resolution Process
- All registered installers are collected.
- For each installer, its
Dependenciesare inspected. - If a required dependency’s type is not registered →
MissingInstallerDependencyExceptionis thrown immediately (before any installer runs). - Optional dependencies whose type is not registered are skipped.
- A topological sort is performed on the resulting graph.
- If a cycle is detected →
CircularInstallerDependencyExceptionis thrown, including the full cycle path in the message.
Missing Dependencies
MissingInstallerDependencyException: Installer 'MyInstaller' has a required dependency on 'OtherInstaller', but 'OtherInstaller' is not registered.
Fix: Either register the missing installer, or change the dependency requirement to Optional.
Circular Dependencies
CircularInstallerDependencyException: A circular dependency was detected: InstallerA → InstallerB → InstallerA
Fix: Review the dependency chain and remove or redirect one of the conflicting edges.
Tip: Prefer
After<T>dependencies. UseBefore<T>only when you need to enforce ordering between two installers that have no direct knowledge of each other.
InstallerContext
The InstallerContext is created once before Build() and passed to all installers throughout both lifecycle phases. It carries cross-cutting state that installers share.
Structure
Each framework provides a concrete context class that extends BaseInstallerContext<TBuilder, TApplication, TContext>:
| Framework | Context class |
|---|---|
| Web | WebInstallerContext |
| Console | ConsoleInstallerContext |
| CLI | CliInstallerContext |
Available Properties
| Property | Type | Description |
|---|---|---|
SoftwareAssembly |
Assembly |
The entry assembly of the application. Required for assembly-scanning features (controllers, validators). Throws if not set. |
Assemblies |
IReadOnlyList<Assembly> |
Additional assemblies to scan (e.g. for controllers or validators from external packages). |
Available Methods
| Method | Description |
|---|---|
SetSoftwareAssembly() |
Sets SoftwareAssembly to the calling assembly. Call this from the configure callback in Program.cs. |
AddAssembly(Type) |
Adds the assembly containing the given type to Assemblies. |
AddAssembly(params Type[]) |
Adds assemblies for multiple types at once. |
AddAssembly(Assembly) |
Adds an assembly directly. |
AddAssembly(params Assembly[]) |
Adds multiple assemblies directly. |
Enabled(string featureKey) |
Returns bool? — true/false if explicitly set, null if not configured. |
SetEnabled(string featureKey, bool enabled) |
Explicitly enables or disables an installer by key. |
Feature Flags
The context acts as the central feature-flag store for installers. The ShouldInstall mechanism reads from it:
// In your configure callback:
installer.SetEnabled("Logging", false); // low-level
installer.WithLogging(false); // via extension method (where available)
Lifecycle
The context is created in the SaveApisApplication.AsXxx() extension method, configured via the callback, and then passed to SaveApisXxxApplicationBuilder. It is never replaced or recreated — the same instance is shared across the entire application lifetime.
Typical Usage
await SaveApisApplication
.AsWeb(args, (app, installer) =>
{
app.Name = "My.Service";
app.Namespace = "My.Service";
installer.SetSoftwareAssembly(); // required for controller scanning
installer.AddAssembly(typeof(SomeExternalType)); // include external assembly
installer.WithZitadel(false); // disable Zitadel
})
.Build()
.RunAsync();
Best Practices
- Always call
SetSoftwareAssembly()fromProgram.cswhen using frameworks that scan assemblies (Web, Console). - Use the typed extension methods (
WithLogging,WithZitadel, etc.) instead of rawSetEnabledfor readability. - Do not modify the context from within installers — it is intended for configuration, not runtime mutation.
ApplicationContext
The ApplicationContext configures global application metadata that is accessible via the static SaveApisApplication class throughout the application’s lifetime.
Structure
| Framework | Context class |
|---|---|
| Web | WebApplicationContext (extends ApplicationContext) |
| Console | ConsoleApplicationContext (extends ApplicationContext) |
| CLI | CliApplicationContext (extends ApplicationContext) |
Available Properties
| Property | Type | Description |
|---|---|---|
Name |
string (set-only) |
The application name, used in OpenTelemetry resource attributes and Spectre CLI configuration. |
Namespace |
string (set-only) |
The application namespace, used in OpenTelemetry resource attributes. |
After setting, values are available globally via:
SaveApisApplication.Name // e.g. "My.Service"
SaveApisApplication.Namespace // e.g. "My.Org"
Lifecycle
The ApplicationContext is instantiated inside SaveApisApplication.AsXxx() and passed to the configure callback before any installer runs. Its values are stored in static fields on SaveApisApplication and are therefore available immediately after the callback executes.
Typical Usage
await SaveApisApplication
.AsWeb(args, (app, installer) =>
{
app.Name = "My.Service";
app.Namespace = "My.Org";
// ...
})
.Build()
.RunAsync();
Differences from InstallerContext
ApplicationContext |
InstallerContext |
|
|---|---|---|
| Purpose | Global app metadata (name, namespace) | Feature flags, assembly scanning |
| Used by | OpenTelemetry resource, Spectre CLI | Installers (ShouldInstall, controllers, validators) |
| Access | Static SaveApisApplication.Name / .Namespace |
Passed as parameter to every installer method |
| Mutable after setup? | No (set once in callback) | No |
Related Frameworks & Utilities
| Package | Entry Point | Description |
|---|---|---|
| Web Framework | SaveApisApplication.AsWeb() |
ASP.NET Core web applications with controllers, auth, OpenAPI |
| Console Framework | SaveApisApplication.AsConsole() |
Long-running background / hosted services |
| CLI Framework | SaveApisApplication.AsCli() |
Command-line tools built with Spectre.Console |
| CritterStack Utility | .AddCritterStack() |
Marten event store + Wolverine messaging |