Skip to content

Commit 4a732b1

Browse files
authored
Merge pull request #50 from tgiachi/feature/onconfigloaded
feat(bootstrap): OnConfigLoaded hook to inspect and override loaded config
2 parents 9782e49 + fc8d98e commit 4a732b1

11 files changed

Lines changed: 344 additions & 3 deletions

File tree

docs/articles/concepts/bootstrap-lifecycle.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,24 @@ bootstrap.ConfigureServices(container =>
3131

3232
See [dependency injection](dependency-injection.md) for the container and the `AddXxx` / `RegisterXxx` pattern.
3333

34+
## OnConfigLoaded
35+
36+
After the configuration is loaded - and before the logger is built and services start - the
37+
bootstrap applies any typed config hooks. Use them to inspect or override loaded sections at
38+
startup; changes are in-memory only:
39+
40+
```csharp
41+
var bootstrap = SquidStdBootstrap.Create(o => o.ConfigName = "myapp");
42+
bootstrap.ConfigureServices(c => c.RegisterCoreServices());
43+
bootstrap.OnConfigLoaded<SquidStdLoggerOptions>(o => o.MinimumLevel = LogLevelType.Debug);
44+
45+
await bootstrap.StartAsync();
46+
```
47+
48+
The effective order is: load configuration, apply config hooks, configure the logger, start
49+
services. Hooks are re-applied on every configuration load, so overrides are never lost. See
50+
[Inspecting and overriding loaded configuration](../guides/configuration.md#inspecting-and-overriding-loaded-configuration) for more examples.
51+
3452
## Migrating to 0.15: explicit core services
3553

3654
Up to 0.14, `SquidStdBootstrap.Create` registered every core service on creation. From 0.15 the bootstrap registers only the configuration core - `DirectoriesConfig`, the `logger` config section and the config manager. The remaining core services (JSON serializer, event bus, job system, main-thread dispatcher, timer wheel, metrics collection, secrets) are opted into with the parameterless `RegisterCoreServices()`:
@@ -53,7 +71,7 @@ builder.UseSquidStd(options => options.ConfigName = "myapp", c => c.RegisterCore
5371

5472
## Start and stop over ISquidStdService
5573

56-
Services implementing `ISquidStdService` participate in the lifecycle. On `StartAsync` they are started in registration order; on `StopAsync` they are stopped in reverse order, so dependencies remain available while their dependents shut down.
74+
Services implementing `ISquidStdService` participate in the lifecycle. On `StartAsync` the configuration is loaded and the config hooks are applied, then services are started in registration order; on `StopAsync` they are stopped in reverse order, so dependencies remain available while their dependents shut down.
5775

5876
The bootstrap logs its whole lifecycle: a startup banner with the application name and version (set `SquidStdOptions.AppName`; it defaults to the entry assembly name and is attached to every event as the `Application` / `ApplicationVersion` properties), a registration summary (per-registration detail at Debug), one line per service started with its duration, and the shutdown sequence. A service that fails to stop is logged as a warning and the remaining services are still stopped. Extra Serilog sinks can be plugged by registering `ILogEventSink` instances in the container before start.
5977

docs/articles/guides/configuration.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,3 +43,39 @@ myService:
4343
endpoint: "https://$API_HOST"
4444
retries: 5
4545
```
46+
47+
## Inspecting and overriding loaded configuration
48+
49+
The config manager loads sections transparently, but nothing stays hidden:
50+
51+
```csharp
52+
var config = bootstrap.Resolve<IConfigManagerService>();
53+
Console.WriteLine(config.Compose()); // full current configuration as YAML
54+
var logger = config.GetConfig<SquidStdLoggerOptions>(); // one typed section
55+
```
56+
57+
To inspect or tweak a section at startup - before the logger and the services consume it -
58+
register a typed hook on the bootstrap. Hooks run after every configuration load and mutate
59+
the section in memory only: the YAML file is never rewritten (call `Save()` explicitly if you
60+
want to persist).
61+
62+
```csharp
63+
// Tune a section at startup
64+
bootstrap.OnConfigLoaded<SquidStdLoggerOptions>(o => o.MinimumLevel = LogLevelType.Debug);
65+
66+
// Override from the environment (WorkersConfig comes with SquidStd.Workers)
67+
bootstrap.OnConfigLoaded<WorkersConfig>(w =>
68+
{
69+
if (int.TryParse(Environment.GetEnvironmentVariable("WORKERS_MAX"), out var max))
70+
{
71+
w.MaxConcurrency = max;
72+
}
73+
});
74+
75+
// Inspect a loaded section before services start
76+
bootstrap.OnConfigLoaded<StorageConfig>(s => Console.WriteLine($"storage root: {s.RootDirectory}"));
77+
```
78+
79+
Hooks compose: register as many as you need, also on the same section - they run in
80+
registration order. Registering a hook for a type that is not a config section fails at
81+
startup with a clear error; registering one after the bootstrap has started throws.

docs/tutorials/getting-started.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ The host starts, logs the service lifecycle, and waits until you press Ctrl+C.
4848
## How it works
4949

5050
`SquidStdBootstrap` is the composition root: it builds the container, registers the configuration core, loads
51-
the config sections, and orchestrates the `ISquidStdService` lifecycle. The core services come from the
51+
the config sections, and orchestrates the `ISquidStdService` lifecycle. Need to inspect or tweak a loaded
52+
value before services start? See [Inspecting and overriding loaded configuration](../articles/guides/configuration.md#inspecting-and-overriding-loaded-configuration). The core services come from the
5253
explicit `RegisterCoreServices()` call in `ConfigureServices`, and every other SquidStd module plugs into the
5354
same container through its `Add…` extension methods.
5455

src/SquidStd.Core/Interfaces/Bootstrap/ISquidStdBootstrap.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,17 @@ public interface ISquidStdBootstrap : IAsyncDisposable
3232
/// <returns>The same bootstrap instance for chaining.</returns>
3333
ISquidStdBootstrap ConfigureServices(Func<IContainer, IContainer> configure);
3434

35+
/// <summary>
36+
/// Registers a callback invoked after the configuration is loaded (and re-applied on every
37+
/// reload performed through the bootstrap) and before the logger and services consume it.
38+
/// The callback receives the section singleton and may inspect or mutate it; changes are
39+
/// in-memory only - the YAML file is not rewritten.
40+
/// </summary>
41+
/// <typeparam name="TConfig">The configuration section type.</typeparam>
42+
/// <param name="configure">Callback that receives the loaded section.</param>
43+
/// <returns>The same bootstrap for chaining.</returns>
44+
ISquidStdBootstrap OnConfigLoaded<TConfig>(Action<TConfig> configure) where TConfig : class;
45+
3546
/// <summary>
3647
/// Resolves a service from the owned dependency injection container.
3748
/// </summary>

src/SquidStd.Core/Interfaces/Config/IConfigManagerService.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@ public interface IConfigManagerService
2525
/// </summary>
2626
IReadOnlyCollection<IConfigEntry> Entries { get; }
2727

28+
/// <summary>
29+
/// Raised after every successful <see cref="Load" />, once the section instances are
30+
/// registered into DI.
31+
/// </summary>
32+
event Action? ConfigLoaded;
33+
2834
/// <summary>
2935
/// Composes the currently loaded sections into YAML.
3036
/// </summary>

src/SquidStd.Services.Core/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,13 @@ bootstrap.ConfigureServices(c => c.RegisterCoreServices());
3939
await bootstrap.StartAsync();
4040
```
4141

42+
Need to inspect or override a loaded config section before services start? Register a typed hook
43+
(in-memory only - the YAML file is untouched):
44+
45+
```csharp
46+
bootstrap.OnConfigLoaded<SquidStdLoggerOptions>(o => o.MinimumLevel = LogLevelType.Debug);
47+
```
48+
4249
### Command dispatch
4350

4451
```csharp

src/SquidStd.Services.Core/Services/Bootstrap/SquidStdBootstrap.cs

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,11 @@ namespace SquidStd.Services.Core.Services.Bootstrap;
2323
/// </summary>
2424
public sealed class SquidStdBootstrap : ISquidStdBootstrap
2525
{
26+
private readonly List<(Type ConfigType, Action<object> Apply)> _configHooks = [];
2627
private readonly bool _ownsContainer;
2728
private readonly List<ISquidStdService> _startedServices = [];
2829
private readonly Lock _syncRoot = new();
30+
private bool _configHooksSubscribed;
2931
private int _disposed;
3032
private bool _loggerConfigured;
3133
private BootstrapStateType _state;
@@ -91,6 +93,25 @@ public ISquidStdBootstrap ConfigureServices(Func<IContainer, IContainer> configu
9193
: this;
9294
}
9395

96+
/// <inheritdoc />
97+
public ISquidStdBootstrap OnConfigLoaded<TConfig>(Action<TConfig> configure) where TConfig : class
98+
{
99+
ArgumentNullException.ThrowIfNull(configure);
100+
ThrowIfDisposed();
101+
102+
lock (_syncRoot)
103+
{
104+
if (_state != BootstrapStateType.Created)
105+
{
106+
throw new InvalidOperationException("Config hooks cannot be registered after bootstrap start.");
107+
}
108+
}
109+
110+
_configHooks.Add((typeof(TConfig), instance => configure((TConfig)instance)));
111+
112+
return this;
113+
}
114+
94115
/// <inheritdoc />
95116
public async ValueTask DisposeAsync()
96117
{
@@ -135,7 +156,15 @@ public void ConfigureLogging()
135156
return;
136157
}
137158

138-
Container.Resolve<IConfigManagerService>().Load();
159+
var configManager = Container.Resolve<IConfigManagerService>();
160+
161+
if (!_configHooksSubscribed)
162+
{
163+
configManager.ConfigLoaded += ApplyConfigHooks;
164+
_configHooksSubscribed = true;
165+
}
166+
167+
configManager.Load();
139168
ConfigureLogger();
140169
}
141170

@@ -382,6 +411,33 @@ private void ConfigureLogger()
382411
_loggerConfigured = true;
383412
}
384413

414+
private void ApplyConfigHooks()
415+
{
416+
if (_configHooks.Count == 0)
417+
{
418+
return;
419+
}
420+
421+
List<ConfigRegistrationData> sections = Container.IsRegistered<List<ConfigRegistrationData>>()
422+
? Container.Resolve<List<ConfigRegistrationData>>()
423+
: [];
424+
var logger = Log.ForContext<SquidStdBootstrap>();
425+
426+
foreach (var (configType, apply) in _configHooks)
427+
{
428+
if (!sections.Any(section => section.ConfigType == configType))
429+
{
430+
throw new InvalidOperationException(
431+
$"No config section registered for type '{configType.Name}'. "
432+
+ "Register it with RegisterConfigSection before using OnConfigLoaded."
433+
);
434+
}
435+
436+
apply(Container.Resolve(configType));
437+
logger.Debug("Applied config hook to {Section:l}", configType.Name);
438+
}
439+
}
440+
385441
private void LogRegistrations(ILogger logger, ServiceRegistrationData[] lifecycleRegistrations)
386442
{
387443
List<ConfigRegistrationData> sections = Container.IsRegistered<List<ConfigRegistrationData>>()

src/SquidStd.Services.Core/Services/ConfigManagerService.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ public sealed class ConfigManagerService : IConfigManagerService, ISquidStdServi
2929
/// <inheritdoc />
3030
public IReadOnlyCollection<IConfigEntry> Entries => GetEntries();
3131

32+
/// <inheritdoc />
33+
public event Action? ConfigLoaded;
34+
3235
/// <summary>
3336
/// Initializes the config manager service.
3437
/// </summary>
@@ -80,6 +83,8 @@ public void Load()
8083
{
8184
Save();
8285
}
86+
87+
ConfigLoaded?.Invoke();
8388
}
8489

8590
/// <inheritdoc />

tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,13 @@ public ISquidStdBootstrap ConfigureServices(Func<IContainer, IContainer> configu
2929
: throw new InvalidOperationException("ConfigureServices must return the bootstrap container instance.");
3030
}
3131

32+
public ISquidStdBootstrap OnConfigLoaded<TConfig>(Action<TConfig> configure) where TConfig : class
33+
{
34+
ArgumentNullException.ThrowIfNull(configure);
35+
36+
return this;
37+
}
38+
3239
public ValueTask DisposeAsync()
3340
{
3441
Container.Dispose();

0 commit comments

Comments
 (0)