SaveApis.Framework

Documentation for SaveApis.Framework

View on GitHub

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.

  1. All installers are registered via builder.AddInstaller<T>().
  2. On Build(), InstallerGraph.Build() computes the topological order.
  3. For each installer (in sorted order): ShouldInstall(builder, context) → if trueConfigureBuilder.
  4. On RunAsync(), the same sorted order is recomputed and each installer’s ShouldInstall(application, context) → if trueConfigureApplication is 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:

  1. If context.Enabled(InstallerKey) returns a bool → that value is used.
  2. Otherwise → DefaultEnabled is 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

  1. All registered installers are collected.
  2. For each installer, its Dependencies are inspected.
  3. If a required dependency’s type is not registered → MissingInstallerDependencyException is thrown immediately (before any installer runs).
  4. Optional dependencies whose type is not registered are skipped.
  5. A topological sort is performed on the resulting graph.
  6. If a cycle is detected → CircularInstallerDependencyException is 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. Use Before<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


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

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