View on GitHub

Smart Application Framework (SAF)

Open-source, cross-platform framework for distributed applications across cloud and edge.

SAF Host

The SAF Host is the top-level composition root for a SAF application. It wraps .NET’s IHostApplicationBuilder (i.e. Host.CreateApplicationBuilder) and wires together the plugin system, service host info, and optional diagnostics. Messaging and storage infrastructure are not registered by the host directly — they are supplied by dedicated plug-ins and selected through configuration (see below).

How It Fits Together

graph LR
    AB[IHostApplicationBuilder] -->|AddSafHost| SHB[ISafHostBuilder]
    SHB -->|uses| PSB[IPluginSystemHostBuilder]
    PSB -->|registers| SPI[ServicePluginHost\nIHostedService]
    SPI -->|calls| PM[IPluginManifest.ConfigureServices\nper plug-in]
    SPI -->|starts/stops| SP[IServicePlugin\nper plug-in]

Minimal Setup

var builder = Host.CreateApplicationBuilder(args);

builder.AddSafHost()
    .ConfigurePluginSystem(ps => ps.AddPluginAssemblyFolderContainer(options =>
    {
        options.SearchRootPath = AppContext.BaseDirectory;
        // Include your plug-ins AND the messaging/storage implementation plug-ins.
        options.IncludePatterns =
            "MyApp.Plugin.*.dll;SAF.Messaging.InProcess.dll;SAF.Storage.LiteDb.dll";
        options.Recursive = false;
    }));

var host = builder.Build();
await host.RunAsync();

Messaging and storage are provided by plug-ins, selected via configuration:

{
  "Messaging": { "PrimaryKey": "InProcess" },
  "LiteDb":    { "ConnectionString": "Filename=app.db" }
}

AddSafHost() reads plugin system configuration from the "PluginSystem" section and service host info from the "ServiceHost" section of your configuration (e.g. appsettings.json). It also registers SAF’s built-in plugin assemblies from the application base directory using an explicit allow-list.

At the moment the built-in list contains:


Configuration

PluginSystem section

Controls how plug-ins are discovered and configured:

{
  "PluginSystem": {
    "PluginSettingsRootPath": "./config",
    "PluginSettingsFilePath": "./pluginsettings.json",
    "PluginContractsSearchPattern": "MyApp.Contracts.dll"
  }
}
Key Default Description
PluginSettingsRootPath ./config Root directory for per-plugin pluginsettings.json files
PluginSettingsFilePath ./pluginsettings.json Path relative to PluginSettingsRootPath for the plugin’s settings file
PluginContractsSearchPattern SAF.Common.dll;SAF.Messaging.Contracts.dll via AddSafHost() Semicolon-separated glob patterns for additional assemblies exposing public plugin service types

AddSafHost() always includes SAF’s built-in contract assemblies and appends any PluginContractsSearchPattern entries from configuration, so applications only need to specify additional contracts such as MyApp.Contracts.dll.

ServiceHost section

Controls host identity and file system paths:

{
  "ServiceHost": {
    "Id": "my-unique-node-id",
    "ServiceHostType": "MyApp",
    "FileSystemUserBasePath": "tempfs",
    "FileSystemInstallationPath": ".",
    "EnableDiagnostics": false
  }
}
Key Default Description
Id (auto-generated GUID) Unique identifier of this host instance
ServiceHostType "SAF" Logical type label for the host
FileSystemUserBasePath "tempfs" Base directory for application-specific runtime data
FileSystemInstallationPath AppContext.BaseDirectory Installation root directory
EnableDiagnostics false Write diagnostic node-info to disk on startup

Messaging section

Required by SAF.Messaging.Runtime to select which messaging factory to expose as the primary IMessagingInfrastructure:

{
  "Messaging": {
    "PrimaryKey": "InProcess"
  }
}

The value must match one of the well-known keys: InProcess, Redis, Nats, Cde, Routing.


Programmatic Configuration

You can also configure the host entirely in code, without appsettings.json:

builder.AddSafHost(pluginSystemOptions =>
{
    pluginSystemOptions.PluginSettingsRootPath = "./config";
    pluginSystemOptions.PluginContractsSearchPattern = "MyContracts.dll";
})
.ConfigureHostInfo(hostOptions =>
{
    hostOptions.Id = "node-1";
    hostOptions.ServiceHostType = "Demo";
    hostOptions.FileSystemUserBasePath = Path.Combine(AppContext.BaseDirectory, "data");
})
.ConfigurePluginSystem(ps =>
{
    ps.AddPluginConfigurationSource(source =>
    {
        var extension = ".myapp";
        var overlayFileName = $"{Path.GetFileNameWithoutExtension(source.SettingsFileName)}.{source.EnvironmentName}" +
            Path.GetExtension(source.SettingsFileName);

        source.Builder.AddXmlFile(xml =>
        {
            xml.FileProvider = source.SettingsFileProvider;
            xml.Path = Path.ChangeExtension(source.SettingsFileName, extension);
            xml.Optional = true;
            xml.ReloadOnChange = true;
            xml.OnLoadException = source.OnLoadException;
        });

        source.Builder.AddXmlFile(xml =>
        {
            xml.FileProvider = source.SettingsFileProvider;
            xml.Path = Path.ChangeExtension(overlayFileName, extension);
            xml.Optional = true;
            xml.ReloadOnChange = true;
            xml.OnLoadException = source.OnLoadException;
        });
    });

    ps.AddPluginAssemblyFolderContainer(options =>
    {
        options.SearchRootPath = AppContext.BaseDirectory;
        options.IncludePatterns = "MyApp.Plugin.*.dll";
        options.Recursive = false;
    });
})
.AddHostDiagnostics();

If you use AddXmlFile(...), add the package Microsoft.Extensions.Configuration.Xml to the host project.


Plugin Assembly Discovery

AddSafHost() already registers SAF’s built-in plugin assembly (SAF.Messaging.Runtime.dll) from AppContext.BaseDirectory.

Additional AddPluginAssemblyFolderContainer calls should therefore be used for application-specific or externally deployed plugins.

The AddPluginAssemblyFolderContainer call controls which DLL files are scanned for IPluginManifest implementations.

ps.AddPluginAssemblyFolderContainer(options =>
{
    // Root directory to search in
    options.SearchRootPath = AppContext.BaseDirectory;

    // Whether to recurse into subdirectories
    options.Recursive = false;

    // Semicolon-separated file glob patterns to include
    options.IncludePatterns = "MyApp.Plugin.*.dll";

    // Semicolon-separated patterns to exclude
    options.ExcludePatterns = "Microsoft.*;System.*;SAF.PluginSystem.*";
});

You can call AddPluginAssemblyFolderContainer multiple times to add assemblies from different directories.


Diagnostics

Enable diagnostics to write host info (version, paths, environment) to disk at startup:

builder.AddSafHost().AddHostDiagnostics();

Or via configuration:

{ "ServiceHost": { "EnableDiagnostics": true } }

IServiceHostInfo

Every plug-in can inject IServiceHostInfo to read host identity and path information at runtime:

public class MyPlugin(IServiceHostInfo hostInfo)
{
    public void DoWork()
    {
        var dataPath = hostInfo.FileSystemUserBasePath;
        var id       = hostInfo.Id;
    }
}

IServiceHostInfo is registered once in the host container by AddSafHost() and forwarded into every plugin container automatically via IHostServiceForwarder. Plugins receive the same singleton instance that the host uses, including any programmatic overrides applied via ConfigureHostInfo.


Forwarding Host Services into Plugin Containers

The plugin system calls all IHostServiceForwarder registrations before each plugin manifest’s ConfigureServices runs. SAF uses this to bridge IServiceHostInfo into the isolated plugin containers without re-creating it.

You can forward additional host-level services the same way using the built-in HostServiceForwarder<T>:

// Anywhere in host setup — e.g. your own ServiceCollectionExtensions
services.AddSingleton<MySharedSingleton>();
services.AddSingleton<IHostServiceForwarder, HostServiceForwarder<MySharedSingleton>>();

HostServiceForwarder<T> receives the already-resolved host singleton via constructor injection and registers the same instance in each plugin container — no factory, no service locator.

Keep it that way: forward the resolved instance, not a factory delegate that resolves from the host provider. A plugin container disposes only the singletons it created itself, so an instance registration stays owned by the host and survives the disposal of a plugin container — for example when the plugin system is reloaded.


DI Container Layout

The host container holds services that AddSafHost() forwards into every plugin container (IServiceHostInfo, loggers, configuration, IPluginServiceProvider, IFileSystem, IPluginSystemHostEnvironment).

IMessagingInfrastructure and IStorageInfrastructure are not in the host container. They are registered inside the messaging/storage plugin containers and shared with other plugin containers as public services (because their contract assemblies — SAF.Messaging.Contracts.dll, SAF.Common.dll — are in PluginContractsSearchPattern).

graph TB
    subgraph "Main (Host) Container"
        direction TB
        SHI[IServiceHostInfo]
        LOG[ILogger / ILoggerFactory]
        CFG[IConfiguration]
        FS[IFileSystem]
        PSP[IPluginServiceProvider]
    end

    subgraph "Messaging Plugin Container"
        MSF["IMessagingInfrastructureFactory\n(keyed, e.g. 'InProcess')"]
    end

    subgraph "Runtime Plugin Container"
        MSI["IMessagingInfrastructure\n(resolves primary factory)"]
        DISP[IServiceMessageDispatcher]
    end

    subgraph "Storage Plugin Container"
        SSI[IStorageInfrastructure]
    end

    subgraph "Plugin A Container"
        MSI_A["IMessagingInfrastructure\n(imported public service)"]
        SSI_A["IStorageInfrastructure\n(imported public service)"]
        SHI_A["IServiceHostInfo\n(forwarded)"]
        PA_PRIV["PrivateServiceA\n(isolated)"]
    end

    SHI -->|forwarded| SHI_A
    MSF -->|imported| MSI
    MSI -->|imported public| MSI_A
    SSI -->|imported public| SSI_A

Two distinct sharing mechanisms:

Private plugin services (registered against non-contract types) remain invisible to other plugins.

For a full explanation of the DI model, see Plugin System — DI Containers.