Skip to content

Commit f03d71e

Browse files
authored
Merge pull request #52 from tgiachi/feature/onconfigready
feat(bootstrap): OnConfigReady callback with the whole config
2 parents 4a732b1 + 70fe2aa commit f03d71e

6 files changed

Lines changed: 162 additions & 2 deletions

File tree

docs/articles/concepts/bootstrap-lifecycle.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ await bootstrap.StartAsync();
4646
```
4747

4848
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
49+
services. Hooks are re-applied on every configuration load, so overrides are never lost. To
50+
receive the whole configuration manager once every typed hook has run, use
51+
`bootstrap.OnConfigReady(cfg => ...)`. See
5052
[Inspecting and overriding loaded configuration](../guides/configuration.md#inspecting-and-overriding-loaded-configuration) for more examples.
5153

5254
## Migrating to 0.15: explicit core services

docs/articles/guides/configuration.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,17 @@ bootstrap.OnConfigLoaded<StorageConfig>(s => Console.WriteLine($"storage root: {
7979
Hooks compose: register as many as you need, also on the same section - they run in
8080
registration order. Registering a hook for a type that is not a config section fails at
8181
startup with a clear error; registering one after the bootstrap has started throws.
82+
83+
To receive the whole configuration in one callback - after every typed hook has been
84+
applied - use `OnConfigReady`. It hands you the config manager, so you can dump or inspect
85+
the final values:
86+
87+
```csharp
88+
bootstrap.OnConfigReady(cfg =>
89+
{
90+
Console.WriteLine(cfg.Compose()); // final configuration, overrides included
91+
});
92+
```
93+
94+
`OnConfigReady` follows the same rules as the typed hooks: it runs after every configuration
95+
load, before the logger and the services, and must be registered before the bootstrap starts.

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DryIoc;
22
using SquidStd.Core.Data.Bootstrap;
3+
using SquidStd.Core.Interfaces.Config;
34

45
namespace SquidStd.Core.Interfaces.Bootstrap;
56

@@ -43,6 +44,16 @@ public interface ISquidStdBootstrap : IAsyncDisposable
4344
/// <returns>The same bootstrap for chaining.</returns>
4445
ISquidStdBootstrap OnConfigLoaded<TConfig>(Action<TConfig> configure) where TConfig : class;
4546

47+
/// <summary>
48+
/// Registers a callback invoked with the whole configuration manager after every load, once
49+
/// all <see cref="OnConfigLoaded{TConfig}" /> hooks have been applied and before the logger
50+
/// and services consume the sections. Use it to inspect (or dump via
51+
/// <see cref="IConfigManagerService.Compose" />) the final configuration.
52+
/// </summary>
53+
/// <param name="ready">Callback that receives the configuration manager.</param>
54+
/// <returns>The same bootstrap for chaining.</returns>
55+
ISquidStdBootstrap OnConfigReady(Action<IConfigManagerService> ready);
56+
4657
/// <summary>
4758
/// Resolves a service from the owned dependency injection container.
4859
/// </summary>

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ namespace SquidStd.Services.Core.Services.Bootstrap;
2424
public sealed class SquidStdBootstrap : ISquidStdBootstrap
2525
{
2626
private readonly List<(Type ConfigType, Action<object> Apply)> _configHooks = [];
27+
private readonly List<Action<IConfigManagerService>> _configReadyCallbacks = [];
2728
private readonly bool _ownsContainer;
2829
private readonly List<ISquidStdService> _startedServices = [];
2930
private readonly Lock _syncRoot = new();
@@ -112,6 +113,25 @@ public ISquidStdBootstrap OnConfigLoaded<TConfig>(Action<TConfig> configure) whe
112113
return this;
113114
}
114115

116+
/// <inheritdoc />
117+
public ISquidStdBootstrap OnConfigReady(Action<IConfigManagerService> ready)
118+
{
119+
ArgumentNullException.ThrowIfNull(ready);
120+
ThrowIfDisposed();
121+
122+
lock (_syncRoot)
123+
{
124+
if (_state != BootstrapStateType.Created)
125+
{
126+
throw new InvalidOperationException("Config callbacks cannot be registered after bootstrap start.");
127+
}
128+
}
129+
130+
_configReadyCallbacks.Add(ready);
131+
132+
return this;
133+
}
134+
115135
/// <inheritdoc />
116136
public async ValueTask DisposeAsync()
117137
{
@@ -413,7 +433,7 @@ private void ConfigureLogger()
413433

414434
private void ApplyConfigHooks()
415435
{
416-
if (_configHooks.Count == 0)
436+
if (_configHooks.Count == 0 && _configReadyCallbacks.Count == 0)
417437
{
418438
return;
419439
}
@@ -436,6 +456,20 @@ private void ApplyConfigHooks()
436456
apply(Container.Resolve(configType));
437457
logger.Debug("Applied config hook to {Section:l}", configType.Name);
438458
}
459+
460+
if (_configReadyCallbacks.Count == 0)
461+
{
462+
return;
463+
}
464+
465+
var configManager = Container.Resolve<IConfigManagerService>();
466+
467+
foreach (var ready in _configReadyCallbacks)
468+
{
469+
ready(configManager);
470+
}
471+
472+
logger.Debug("Invoked {Count} config ready callback(s)", _configReadyCallbacks.Count);
439473
}
440474

441475
private void LogRegistrations(ILogger logger, ServiceRegistrationData[] lifecycleRegistrations)

tests/SquidStd.Tests/AspNetCore/FakeSquidStdBootstrap.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using DryIoc;
22
using SquidStd.Core.Data.Bootstrap;
33
using SquidStd.Core.Interfaces.Bootstrap;
4+
using SquidStd.Core.Interfaces.Config;
45

56
namespace SquidStd.Tests.AspNetCore;
67

@@ -36,6 +37,13 @@ public ISquidStdBootstrap OnConfigLoaded<TConfig>(Action<TConfig> configure) whe
3637
return this;
3738
}
3839

40+
public ISquidStdBootstrap OnConfigReady(Action<IConfigManagerService> ready)
41+
{
42+
ArgumentNullException.ThrowIfNull(ready);
43+
44+
return this;
45+
}
46+
3947
public ValueTask DisposeAsync()
4048
{
4149
Container.Dispose();

tests/SquidStd.Tests/Bootstrap/SquidStdBootstrapConfigHooksTests.cs

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,97 @@ public async Task OnConfigLoaded_DoesNotRewriteYamlFile()
178178
}
179179
}
180180

181+
[Fact]
182+
public async Task OnConfigReady_ReceivesManagerWithHookAdjustedValues()
183+
{
184+
using var temp = new TempDirectory();
185+
await using var bootstrap = NewBootstrap(temp.Path);
186+
var observedLimit = 0;
187+
var invocations = 0;
188+
bootstrap.ConfigureServices(c =>
189+
{
190+
c.RegisterConfigSection("fakeSection", static () => new FakeSectionConfig(), 0);
191+
return c;
192+
});
193+
bootstrap.OnConfigLoaded<FakeSectionConfig>(f => f.Limit = 42);
194+
bootstrap.OnConfigReady(cfg =>
195+
{
196+
invocations++;
197+
observedLimit = cfg.GetConfig<FakeSectionConfig>().Limit;
198+
});
199+
200+
await bootstrap.StartAsync();
201+
try
202+
{
203+
Assert.True(invocations >= 1);
204+
Assert.Equal(42, observedLimit);
205+
}
206+
finally
207+
{
208+
await bootstrap.StopAsync();
209+
}
210+
}
211+
212+
[Fact]
213+
public async Task OnConfigReady_MultipleCallbacks_RunInOrder()
214+
{
215+
using var temp = new TempDirectory();
216+
await using var bootstrap = NewBootstrap(temp.Path);
217+
var order = new List<string>();
218+
bootstrap.OnConfigReady(_ => order.Add("a"));
219+
bootstrap.OnConfigReady(_ => order.Add("b"));
220+
221+
await bootstrap.StartAsync();
222+
try
223+
{
224+
Assert.True(order.Count >= 2);
225+
Assert.Equal("a", order[0]);
226+
Assert.Equal("b", order[1]);
227+
}
228+
finally
229+
{
230+
await bootstrap.StopAsync();
231+
}
232+
}
233+
234+
[Fact]
235+
public async Task OnConfigReady_WithoutTypedHooks_StillRuns()
236+
{
237+
using var temp = new TempDirectory();
238+
await using var bootstrap = NewBootstrap(temp.Path);
239+
var composed = string.Empty;
240+
bootstrap.OnConfigReady(cfg => composed = cfg.Compose());
241+
242+
await bootstrap.StartAsync();
243+
try
244+
{
245+
Assert.Contains("logger", composed, StringComparison.Ordinal);
246+
}
247+
finally
248+
{
249+
await bootstrap.StopAsync();
250+
}
251+
}
252+
253+
[Fact]
254+
public async Task OnConfigReady_AfterStart_Throws()
255+
{
256+
using var temp = new TempDirectory();
257+
await using var bootstrap = NewBootstrap(temp.Path);
258+
259+
await bootstrap.StartAsync();
260+
try
261+
{
262+
Assert.Throws<InvalidOperationException>(
263+
() => bootstrap.OnConfigReady(_ => { })
264+
);
265+
}
266+
finally
267+
{
268+
await bootstrap.StopAsync();
269+
}
270+
}
271+
181272
private static SquidStdBootstrap NewBootstrap(string root)
182273
=> SquidStdBootstrap.Create(new SquidStdOptions { ConfigName = "app", RootDirectory = root });
183274
}

0 commit comments

Comments
 (0)