Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 23 additions & 9 deletions docs/articles/concepts/bootstrap-lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,15 @@ var bootstrap = SquidStdBootstrap.Create(new SquidStdOptions
});
```

`ConfigName` selects the configuration file and `RootDirectory` anchors relative paths. Creating the bootstrap registers the configuration core - `DirectoriesConfig`, the `logger` config section and the config manager. Everything else is registered explicitly in `ConfigureServices`.
`ConfigName` selects the configuration file and `RootDirectory` anchors relative paths. `Create`
loads the configuration eagerly - the YAML file, or an empty document when it does not exist
yet - into a standalone `SquidStdConfig`, before any service registration happens. It also
registers the configuration core: `DirectoriesConfig`, the `logger` config section (bound
immediately against that `SquidStdConfig`) and the config manager. Everything else is
registered explicitly in `ConfigureServices`. To supply an already-loaded configuration
yourself - useful when values from the file must drive registration decisions - use the
`Create(SquidStdConfig, SquidStdOptions)` overload; see
[two-phase setup](../guides/configuration.md#two-phase-setup-moongate-style).

## ConfigureServices

Expand All @@ -29,13 +37,18 @@ bootstrap.ConfigureServices(container =>
});
```

Config bound at registration: every `RegisterConfigSection` call inside this callback - direct,
or through a `RegisterXxx`/`AddXxx` helper - binds its section immediately, so the section
instance is resolvable as soon as the callback returns. There is no need to wait for
`StartAsync`.

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

## OnConfigLoaded

After the configuration is loaded - and before the logger is built and services start - the
bootstrap applies any typed config hooks. Use them to inspect or override loaded sections at
startup; changes are in-memory only:
Once, at `StartAsync` - before the logger is built and services start - the bootstrap applies
any typed config hooks you registered. Use them to inspect or override the already-bound
sections at startup; changes are in-memory only:

```csharp
var bootstrap = SquidStdBootstrap.Create(o => o.ConfigName = "myapp");
Expand All @@ -45,10 +58,11 @@ bootstrap.OnConfigLoaded<SquidStdLoggerOptions>(o => o.MinimumLevel = LogLevelTy
await bootstrap.StartAsync();
```

The effective order is: load configuration, apply config hooks, configure the logger, start
services. Hooks are re-applied on every configuration load, so overrides are never lost. To
receive the whole configuration manager once every typed hook has run, use
`bootstrap.OnConfigReady(cfg => ...)`. See
The effective order is: sections bind at registration (during `ConfigureServices`), config
hooks apply once at `StartAsync` entry, the logger is configured, then services start. Hooks
are re-applied whenever you call `IConfigManagerService.Load()` for an explicit reload, so
overrides are never lost across a reload. To receive the whole configuration manager once
every typed hook has run, use `bootstrap.OnConfigReady(cfg => ...)`. See
[Inspecting and overriding loaded configuration](../guides/configuration.md#inspecting-and-overriding-loaded-configuration) for more examples.

## Migrating to 0.15: explicit core services
Expand All @@ -73,7 +87,7 @@ builder.UseSquidStd(options => options.ConfigName = "myapp", c => c.RegisterCore

## Start and stop over ISquidStdService

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.
Services implementing `ISquidStdService` participate in the lifecycle. On `StartAsync` the config hooks are applied once (mutating the already-bound sections in place) and, if the configuration file does not exist yet, it is written with defaults; then services are started in registration order. On `StopAsync` they are stopped in reverse order, so dependencies remain available while their dependents shut down.

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.

Expand Down
142 changes: 124 additions & 18 deletions docs/articles/guides/configuration.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
# Configuration

SquidStd loads configuration from a single YAML file. Each module registers a
strongly-typed section; the config manager deserializes it, expands environment
variables, and publishes the populated object into the container so you can
resolve it like any other service.
SquidStd loads configuration from a single YAML file, eagerly, before any service is
registered. Each module binds a strongly-typed section at registration time: the value is
already in the container as soon as the `Register*` call returns, expanded and ready to
resolve like any other service.

## The config-first flow

1. **Create loads eagerly.** `SquidStdBootstrap.Create(options)` reads the YAML file into a
standalone `SquidStdConfig` before any container registration happens (a missing file
yields an empty document; nothing is written yet).
2. **Registrations bind immediately.** Every `RegisterConfigSection<TConfig>` call - direct,
or through a `RegisterXxx` helper - binds its section against that `SquidStdConfig` right
away and registers the resulting instance as a singleton. There is no "config not ready
yet" window: resolve the type straight after the `Register*` call.
3. **Hooks run once, at `StartAsync`.** Typed `OnConfigLoaded<T>` hooks and `OnConfigReady`
callbacks are applied a single time, right before the logger is configured and services
start. They mutate the already-bound instances in place.
4. **Explicit `Load()` is a reload.** Call `IConfigManagerService.Load()` to re-read the file
from disk, re-bind every tracked section, and re-apply the hooks. This is the only way
configuration changes after startup - there is no background file watch.

## Steps

Expand All @@ -12,28 +28,42 @@ resolve it like any other service.
`RootDirectory` is the directory it is searched in.
2. **Register a section.** Call `RegisterConfigSection<TConfig>` with the section
name (the top-level YAML key). Provide a `createDefault` factory so the file
is generated with sensible defaults on first run.
3. **Use environment variables.** Any `string` property whose value contains a
`$VAR` token is expanded from the environment when the section is loaded.
Unknown tokens are left untouched.
4. **Read values.** Resolve the config type from the container wherever you need it.
is generated with sensible defaults on first run (the file is written at `StartAsync`
when it does not exist yet, once every section is known).
3. **Use environment variables in SquidStd sections.** Any `string` property of a type whose
namespace starts with `SquidStd` (the framework's own sections and any section type shipped
in a `SquidStd.*` package) is expanded from the environment when the section is bound: a
`$VAR` token is replaced with the matching environment variable, unknown tokens are left
untouched. Application-defined sections, like `MyServiceConfig` below, are not walked
automatically - call the same expansion yourself with the `string` extension `ReplaceEnv()`
(`SquidStd.Core.Extensions.Env`) wherever you read the value.
4. **Read values.** Resolve the config type from the container wherever you need it - the
value is available as soon as the registration call returns.

```csharp
public sealed class MyServiceConfig
{
public string Endpoint { get; set; } = string.Empty; // e.g. "https://$API_HOST"
public int Retries { get; set; } = 3;

// MyServiceConfig lives outside the SquidStd namespace, so it is not walked by the
// automatic substitution pass. Expand tokens explicitly where you read the value:
// config.Endpoint = config.Endpoint.ReplaceEnv();
}

var bootstrap = SquidStdBootstrap.Create(
new SquidStdOptions { ConfigName = "squidstd", RootDirectory = AppContext.BaseDirectory });

bootstrap.ConfigureServices(container =>
container.RegisterConfigSection("myService", static () => new MyServiceConfig()));
{
container.RegisterConfigSection("myService", static () => new MyServiceConfig());

await bootstrap.StartAsync();
var config = container.Resolve<MyServiceConfig>(); // already bound, no need to wait for StartAsync

var config = container.Resolve<MyServiceConfig>();
return container;
});

await bootstrap.StartAsync();
```

The corresponding `squidstd.yaml`:
Expand All @@ -44,9 +74,73 @@ myService:
retries: 5
```

## Four ways to configure a service

Sectioned registrations (`RegisterEventLoop`, `RegisterJobSystemService`,
`RegisterTimerWheelService`, `RegisterMetricsCollectionService`, `RegisterSecretServices`, and
the other `RegisterXxx`/`AddXxx` helpers) accept an optional explicit config instance, so there
are four ways to supply a value, from least to most code:

1. **Standard section from file** - call the registration with no config; it binds the
named YAML section (or the `createDefault` factory when the section is absent):

```csharp
container.RegisterEventLoop(); // binds the "eventLoop" section
```

2. **`OnConfigLoaded<T>` hook** - keep the standard binding, but adjust the bound instance
once at startup:

```csharp
bootstrap.OnConfigLoaded<EventLoopConfig>(c => c.IdleSleepMs = 5);
```

3. **Derived from another section** - bind a parent section yourself and pass one of its
nested values through:

```csharp
var appConfig = config.GetSection<MyServerConfig>("myapp");
container.RegisterEventLoop(appConfig.EventLoop);
```

4. **Explicit instance** - bypass the file entirely:

```csharp
container.RegisterEventLoop(new EventLoopConfig { IdleSleepMs = 0 });
```

Options 3 and 4 register the instance directly (`IfAlreadyRegistered.Replace`) and skip
`RegisterConfigSection`, so the YAML file is never read for that section.

## Two-phase setup (Moongate-style)

Apps that need configuration before any service is registered - to size worker pools, pick a
storage backend, and so on - load the `SquidStdConfig` themselves and pass it to `Create`:

```csharp
// Phase 1 - config loaded immediately, no container involved
var config = SquidStdConfig.Load("moongate", "~/.moongate");

// Phase 2 - registrations with real config objects
var bootstrap = SquidStdBootstrap.Create(
config,
new SquidStdOptions { ConfigName = "moongate", RootDirectory = "~/.moongate" }
)
.ConfigureServices(c =>
{
c.RegisterEventLoop(config.GetSection<EventLoopConfig>("eventLoop")); // from file, direct
c.RegisterTimerWheelService(config.GetSection<MoongateServerConfig>("moongate").TimerWheel); // derived
c.RegisterJobSystemService(new JobsConfig { WorkerThreadCount = 8 }); // code only
c.RegisterMetricsCollectionService(); // standard section, binds at registration
return c;
});

await bootstrap.RunAsync();
```

## Inspecting and overriding loaded configuration

The config manager loads sections transparently, but nothing stays hidden:
The config manager binds sections transparently, but nothing stays hidden:

```csharp
var config = bootstrap.Resolve<IConfigManagerService>();
Expand All @@ -55,9 +149,9 @@ var logger = config.GetConfig<SquidStdLoggerOptions>(); // one typed section
```

To inspect or tweak a section at startup - before the logger and the services consume it -
register a typed hook on the bootstrap. Hooks run after every configuration load and mutate
the section in memory only: the YAML file is never rewritten (call `Save()` explicitly if you
want to persist).
register a typed hook on the bootstrap. Hooks run once, right after the configuration is
loaded, and mutate the section in memory only: the YAML file is never rewritten (call `Save()`
explicitly if you want to persist).

```csharp
// Tune a section at startup
Expand Down Expand Up @@ -91,5 +185,17 @@ bootstrap.OnConfigReady(cfg =>
});
```

`OnConfigReady` follows the same rules as the typed hooks: it runs after every configuration
load, before the logger and the services, and must be registered before the bootstrap starts.
`OnConfigReady` follows the same rules as the typed hooks: it runs once, after the
configuration is loaded and before the logger and the services, and must be registered
before the bootstrap starts.

## Reloading configuration

Both the typed hooks and `OnConfigReady` also run again on an explicit reload:
`bootstrap.Resolve<IConfigManagerService>().Load()` re-reads the YAML file, re-binds every
tracked section, and re-applies every hook - so overrides made with `OnConfigLoaded` are never
lost across a reload. There is no automatic file watch: call `Load()` yourself when you know
the file changed.

Rebinding replaces the section's registration in the container - a service that already holds
a direct reference to the old instance keeps using it until it resolves the section again.
2 changes: 2 additions & 0 deletions docs/articles/scheduler.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ eventLoop:
SlowTickThresholdMs: 250 # warn above this per-tick time
```

Skip the file entirely with an explicit instance: `RegisterEventLoop(new EventLoopConfig { IdleSleepMs = 0 })`.

```mermaid
flowchart TD
subgraph Drivers["Drivers - register exactly one"]
Expand Down
9 changes: 5 additions & 4 deletions docs/tutorials/scripting-lua.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,11 @@ result = hello from C# and lua

## How it works

`IScriptEngineService` wraps a MoonSharp `Script`. Registering it with `RegisterStdService` lets the bootstrap
call its `StartAsync` during `StartAsync`, which wires up modules, constants, and a file watcher over the
configured scripts directory. Globals you register become Lua variables, and `ExecuteFunction` evaluates a
`return <expression>` and surfaces the value through `ScriptResult.Data`.
`IScriptEngineService` wraps a MoonSharp `Script`. `RegisterLuaEngine` registers the `LuaEngineConfig` instance
and the engine as the standard script engine service in one call, so the bootstrap calls its `StartAsync`
during `StartAsync`, wiring up modules, constants, and a file watcher over the configured scripts directory.
Globals you register become Lua variables, and `ExecuteFunction` evaluates a `return <expression>` and
surfaces the value through `ScriptResult.Data`.

## See also

Expand Down
4 changes: 1 addition & 3 deletions samples/SquidStd.Samples.ScriptingLua/Program.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using DryIoc;
using SquidStd.Abstractions.Extensions.Services;
using SquidStd.Generators.Scripting.Lua;
using SquidStd.Scripting.Lua.Attributes;
using SquidStd.Scripting.Lua.Attributes.Scripts;
Expand Down Expand Up @@ -37,8 +36,7 @@
"1.0.0"
);

container.RegisterInstance(engineConfig);
container.RegisterStdService<IScriptEngineService, LuaScriptEngineService>();
container.RegisterLuaEngine(engineConfig);
container.RegisterGeneratedScriptModules();
container.RegisterScriptModule<LogModule>();
container.RegisterLuaEvents();
Expand Down

This file was deleted.

Loading
Loading