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:
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:
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:
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:
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:
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¶
- The generator scans all
Compilation.Referencesfor types marked with[ConfigSection] - For each section type containing a
[Secret] ConnectionStringproperty, it derives a parameter name from the section path (last segment, camelCased +Resource) - The generated bridge code copies
ConnectionStrings:{resourceName}→ the section'sConnectionStringkey
| 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:
public sealed class OrderingConfigModuleProvider(IOptions<OrderingConfig> orderingConfigOptions)
{
public OrderingConfig OrderingConfig => orderingConfigOptions.Value;
}
Capability Interface¶
An IHas* interface for use as a runtime constraint:
public interface IHasOrderingConfigModule
{
OrderingConfigModuleProvider OrderingConfigModuleProvider { get; }
}
Effect Accessors¶
Static effect methods on the module class for each exposed type:
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:
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:
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:
{
"$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.