Skip to content

Generated Code

This page covers everything the Configuration generator produces — the interface, partial class, DI extension, configuration source method, and configuration files. All generated code is plain .NET with no Deepstaging runtime dependency.

ConfigRoot Generated Code

Interface

For each [ConfigRoot], the generator emits an interface named I{ClassName} with a property for each [Exposes<T>] type:

Generated interface
public interface ISlackConfigRoot
{
    /// <summary>
    /// Gets the <see cref="SlackConfig"/> configuration.
    /// </summary>
    public SlackConfig SlackConfig { get; }
}

Partial Class

The generator completes the partial class with a primary constructor accepting IConfiguration and property implementations that read from the appropriate section:

Generated implementation
public partial class SlackConfigRoot(IConfiguration configuration)
    : ISlackConfigRoot
{
    private readonly IConfigurationSection _section
        = configuration.GetSection("Slack");

    public SlackConfig SlackConfig
        => _section.GetSection("SlackConfig").Get<SlackConfig>()
           ?? throw new InvalidOperationException(
               "Configuration section 'Slack:SlackConfig' is not configured.");
}

DI Extension

A static extension method registers the config root as a singleton and binds each exposed type via IOptions<T> with ValidateOnStart:

Generated DI registration
public static class SlackConfigRootExtensions
{
    public static IServiceCollection AddSlackConfigRoot(
        this IServiceCollection services,
        IConfiguration configuration)
    {
        var provider = new SlackConfigRoot(configuration);
        services.AddSingleton<ISlackConfigRoot>(provider);
        services.AddOptions<SlackConfig>()
            .BindConfiguration("Slack:SlackConfig")
            .ValidateOnStart();
        return services;
    }
}

Required properties

Properties with the required modifier get .Validate(...) calls added to the options chain, so missing required values fail at startup rather than at first access.

Configuration Source Method

The generated source method loads settings files and, when [Secret] properties exist, adds User Secrets:

Generated source configuration
public static IConfigurationBuilder ConfigureSlackConfigRootSources(
    this IConfigurationBuilder builder,
    string? environmentName = null,
    string? settingsPath = null)
{
    // Loads deepstaging.settings.json (required)
    // Loads deepstaging.settings.{Environment}.json (optional)
    // If secrets exist: builder.AddUserSecrets(...)
    return builder;
}

Injecting and Using

Inject the generated interface anywhere via constructor injection:

public class NotificationService(ISlackConfigRoot config)
{
    public void Notify(string message)
    {
        var webhook = config.SlackConfig.WebhookUrl;
        var channel = config.SlackConfig.Channel;
        // ...
    }
}

Or resolve IOptions<T> directly if you prefer the options pattern:

public class NotificationService(IOptions<SlackConfig> options)
{
    public void Notify(string message)
    {
        var config = options.Value;
        // ...
    }
}

Connection String Bridge

When your [ConfigRoot] references packages that define [ConfigSection] types with [Secret] ConnectionString properties (e.g., Deepstaging.Data.Postgres, Deepstaging.Cloud.Azure), the generator discovers these from referenced assemblies and adds optional resource name parameters to the DI extension:

Generated DI registration (with bridge parameters)
public static class EShopConfigRootExtensions
{
    public static IServiceCollection AddEShopConfigRoot(
        this IServiceCollection services,
        IConfiguration configuration,
        string? postgresResource = null,
        string? serviceBusResource = null,
        string? blobsResource = null)
    {
        // Bridge Aspire connection strings to Deepstaging config sections
        if (postgresResource is not null
            && configuration.GetConnectionString(postgresResource)
                is { Length: > 0 } postgresCs)
            configuration["Deepstaging:Postgres:ConnectionString"] = postgresCs;

        if (serviceBusResource is not null
            && configuration.GetConnectionString(serviceBusResource)
                is { Length: > 0 } serviceBusCs)
            configuration["Deepstaging:Azure:ServiceBus:ConnectionString"]
                = serviceBusCs;

        // ... normal config root registration follows
    }
}

How It Works

  1. The generator scans all Compilation.References for types marked with [ConfigSection]
  2. For each section type containing a [Secret] ConnectionString property, it derives a parameter name from the section path (last segment, camelCased + Resource)
  3. The generated bridge code copies ConnectionStrings:{resourceName} → the section's ConnectionString key
Config Section Path Generated Parameter
Deepstaging:Postgres postgresResource
Deepstaging:Azure:ServiceBus serviceBusResource
Deepstaging:Azure:Storage:Blobs blobsResource

Usage

// Aspire injects ConnectionStrings:eshopdb and ConnectionStrings:messaging
builder.Services.AddEShopConfigRoot(builder.Configuration,
    postgresResource: "eshopdb",
    serviceBusResource: "messaging");

Parameters are optional and default to null. Omitting them preserves backward compatibility — infrastructure packages read from their own config sections as usual.

For each [ConfigModule], the generator produces a provider, capability interface, effect accessors, and a DI extension method.

Given:

public sealed class OrderingConfig { public int GracePeriodMinutes { get; init; } = 1; }

[ConfigModule]
[Exposes<OrderingConfig>]
public static partial class OrderingConfigModule;

Provider

A provider class wrapping IOptions<T> for each exposed type:

Generated provider
public sealed class OrderingConfigModuleProvider(IOptions<OrderingConfig> orderingConfigOptions)
{
    public OrderingConfig OrderingConfig => orderingConfigOptions.Value;
}

Capability Interface

An IHas* interface for use as a runtime constraint:

Generated capability interface
public interface IHasOrderingConfigModule
{
    OrderingConfigModuleProvider OrderingConfigModuleProvider { get; }
}

Effect Accessors

Static effect methods on the module class for each exposed type:

Generated effect accessor
public static partial class OrderingConfigModule
{
    public static Eff<RT, OrderingConfig> OrderingConfig<RT>()
        where RT : IHasOrderingConfigModule =>
        liftEff<RT, OrderingConfig>(rt => rt.OrderingConfigModuleProvider.OrderingConfig);
}

DI Extension

A registration method for IServiceCollection:

Generated DI extension
public static IServiceCollection AddOrderingConfigModule(
    this IServiceCollection services)
{
    services.AddSingleton<OrderingConfigModuleProvider>();
    return services;
}

Using in a Runtime

The config module is auto-discovered by the runtime in the same assembly:

[Runtime]
public partial class OrderingRuntime;
// OrderingConfigModule is auto-discovered

Access configuration values in effect compositions:

from config in OrderingConfigModule.OrderingConfig<OrderingRuntime>()
let minutes = config.GracePeriodMinutes

Configuration Files

The deepstaging sync command (run automatically after build via MSBuild targets, or continuously via deepstaging watch) generates the following files at your project root:

File Purpose
deepstaging.schema.json JSON Schema Draft-7 for non-secret configuration
deepstaging.settings.json Settings template (required at runtime)
deepstaging.settings.Development.json Development environment overrides
deepstaging.settings.Staging.json Staging environment overrides
deepstaging.settings.Production.json Production environment overrides
deepstaging.secrets.schema.json JSON Schema for secret properties (if any)
deepstaging.secrets.json Local secrets file (added to .gitignore)

File Nesting

The schema and environment files nest under deepstaging.settings.json in Solution Explorer. File nesting is handled automatically by the NuGet package targets.

JSON Schema

The generated schema follows JSON Schema Draft-7. Each exposed type becomes a nested object under the config root's section:

deepstaging.schema.json
{
  "$schema": "https://json-schema.org/draft-07/schema#",
  "$id": "deepstaging.schema.json",
  "type": "object",
  "properties": {
    "Smtp": {
      "type": "object",
      "properties": {
        "SmtpSettings": {
          "type": "object",
          "properties": {
            "Host": { "type": "string" },
            "Port": { "type": "integer" }
          },
          "required": ["Host", "Port"],
          "additionalProperties": false
        }
      }
    }
  }
}

Secret properties are excluded from this schema and placed in deepstaging.secrets.schema.json instead. See Secrets for details on schema separation.

Automatic Sync

Config files are kept current automatically by the deepstaging sync command, which runs after every build via the MSBuild target. When you add, remove, or rename [ConfigSection] properties, the schema regenerates and settings files are intelligently merged — new keys are added with defaults, removed keys are pruned, and your custom values are preserved. No manual steps needed.

For continuous sync during development, run deepstaging watch — it holds the compilation warm and updates config files within milliseconds of saving a source file.