Configuration
A configuration model is a partial class of private fields. The generator turns it into an interface, a property implementation, and the code that reads the environment variables it names.
using Hardened.Shared.Runtime.Attributes;
[ConfigurationModel]
public partial class DynamoDbOptions {
[FromEnvironmentVariable("DYNAMODB_SERVICE_URL")]
private string _serviceUrl = "";
[FromEnvironmentVariable("AWS_REGION")]
private string _region = "";
private int _retentionDays = 180;
}What that becomes
An interface, named I + the class name. DynamoDbOptions produces IDynamoDbOptions, and that is what everything else depends on.
A property per field, named from the field with its leading underscore removed and its first letter capitalised. _serviceUrl becomes ServiceUrl, _retentionDays becomes RetentionDays. The field's initialiser is the default.
A registration, so that IOptions<IDynamoDbOptions> resolves:
public sealed class DynamoDbClientProvider(
IOptions<IDynamoDbOptions> options,
IServiceProvider serviceProvider) : IDynamoDbClientProvider {
// options.Value.ServiceUrl, options.Value.Region, …
}IConfigurationManager.GetConfiguration<T>() is the direct route when you need one:
var options = provider.GetRequiredService<IConfigurationManager>()
.GetConfiguration<IDynamoDbOptions>();An unregistered model throws when it is first asked for
GetConfiguration<T> throws "<T> is not a registered configuration type" if no module in the application contributed that model. The usual cause is that the model lives in an assembly whose module was never imported.
Environment variables
[FromEnvironmentVariable("NAME")] reads the variable when the model is first constructed, falling back to the field's initialiser when the variable is unset or empty. The value is converted to the field's type, so an int field backed by RETENTION_DAYS=90 arrives as 90.
Reading happens once. Configuration models are cached per type for the life of the application, so a variable changed after startup is not picked up.
To keep a field out of the generated interface — a factory, a secret, something with no sensible property — mark it [HideConfigurationField].
Fields that are not simple values
A field can hold a delegate, for a default that is computed and an application that may replace it wholesale:
[ConfigurationModel]
public partial class DynamoDbOptions {
private Dictionary<string, Func<IServiceProvider, IAmazonDynamoDB>> _clients = new();
private Func<IServiceProvider, IAmazonDynamoDB>? _defaultClient;
}Amending configuration
An amender changes a model a library defined — add a response header, register a named client, raise a timeout — without editing the library. AppConfig implements both IAppConfig and IConfigurationPackage, so an application contributes one from its module:
using DependencyModules.Runtime.Interfaces;
using Hardened.Shared.Runtime.Configuration;
[HardenedModule]
public partial class Application : IServiceCollectionConfiguration {
public void ConfigureServices(IServiceCollection services) {
var config = new AppConfig();
config.Amend((ResponseHeaderConfiguration response) =>
response.Add("Access-Control-Allow-Origin", "*"));
config.Amend((DynamoDbOptions options) =>
options.Clients["audit"] = provider =>
new AmazonDynamoDBClient(assumedRoleCredentials, new AmazonDynamoDBConfig()));
services.AddSingleton<IConfigurationPackage>(config);
}
}Amenders run against the concrete model — DynamoDbOptions, not IDynamoDbOptions. Every registered IConfigurationPackage contributes its amenders, and all of them run, in registration order, the first time the model is resolved.
Amending only in one environment
Amend takes an environment name:
config.Amend((DynamoDbOptions options) => options.ServiceUrl = "http://localhost:8000", "development");The overload taking a function receives the environment, for when the value itself depends on it:
config.Amend((IHardenedEnvironment env, RetryConfiguration retry) => {
retry.MaxAttempts = env.Matches("production") ? 5 : 1;
return retry;
});Replacing a model outright
ProvideValue supplies the implementation rather than amending the default:
config.ProvideValue<IRateTableConfiguration, RateTableConfiguration>(
env => new RateTableConfiguration(env.Value("RATE_TABLE", "rates")));Where models come from
The generator collects every [ConfigurationModel] in an assembly into the module's generated ConfigurationProvider. Import the module and its models come with it, which is why [DynamoDbModule] is enough to make IDynamoDbOptions resolvable.