# Phase 00 — Bootstrap Scaffold the .NET 10 Worker Service project and the test project. Wire up Generic Host, Serilog, Windows-Service registration, and `MbproxyOptions` POCOs bound via `IOptionsMonitor`. No proxy logic yet — the service starts, logs "ready", and stops cleanly. **Depends on:** nothing. Must run alone. **Parallel-safe with:** nothing. Phase 00 owns the initial `.csproj` and solution; subsequent phases append. ## Goal Produce a minimal but production-shaped host that all subsequent phases plug into. The host must: - Target `.NET 10` (`net10.0`), be registered as a Windows Service via `Microsoft.Extensions.Hosting.WindowsServices`, and also run as a console under `dotnet run` for local dev. - Load `appsettings.json` with `reloadOnChange: true`, bind the `"Mbproxy"` section to typed POCOs, and expose them via `IOptionsMonitor`. - Use Serilog with console + rolling-file sinks under `%ProgramData%\mbproxy\logs\` (configurable, but default that location). - Set `true` and `enable` in the csproj. These stay set forever. ## Outputs (files created in this phase) ``` Mbproxy.slnx src/Mbproxy/Mbproxy.csproj src/Mbproxy/Program.cs src/Mbproxy/HostingExtensions.cs # AddMbproxyOptions, AddMbproxySerilog src/Mbproxy/Options/MbproxyOptions.cs src/Mbproxy/Options/BcdTagOptions.cs src/Mbproxy/Options/PlcOptions.cs src/Mbproxy/Options/ConnectionOptions.cs src/Mbproxy/Options/ResilienceOptions.cs src/Mbproxy/Options/BcdTagListOptions.cs # the Global + per-PLC Add/Remove DTOs src/Mbproxy/Workers/HeartbeatWorker.cs # one-line "service alive" worker; deleted by phase 03 src/Mbproxy/appsettings.json # minimal default with empty Plcs array tests/Mbproxy.Tests/Mbproxy.Tests.csproj tests/Mbproxy.Tests/HostSmokeTests.cs tests/Mbproxy.Tests/Options/MbproxyOptionsBindingTests.cs .gitignore # add bin/, obj/, .vs/, *.user, tests/sim/.venv/, %ProgramData%\mbproxy\ ``` No other files. Phase 00 does NOT create: - BCD codec types (phase 02) - Proxy types (phase 03) - Listener supervisor (phase 05) - Status page (phase 07) ## Tasks 1. **Create `Mbproxy.slnx`** referencing the two csprojs. 2. **`src/Mbproxy/Mbproxy.csproj`** — ``, `TargetFramework=net10.0`, `OutputType=Exe`, `Nullable=enable`, `TreatWarningsAsErrors=true`, `ImplicitUsings=enable`. PackageReferences: - `Microsoft.Extensions.Hosting` (latest stable for .NET 10) - `Microsoft.Extensions.Hosting.WindowsServices` - `Serilog.Extensions.Hosting` - `Serilog.Settings.Configuration` - `Serilog.Sinks.Console` - `Serilog.Sinks.File` - `Polly` (referenced now so phase 04/05 don't have to touch this csproj for the package; usage is deferred) 3. **`Options/MbproxyOptions.cs`** and siblings — typed POCOs that mirror the appsettings schema in [`../design.md`](../design.md) → Configuration. Keep them plain DTOs (`public sealed class` with init-only properties). Use `IValidateOptions` for cross-field checks at the **schema** level only (no business rules like "duplicate addresses" — those move to phase 06 along with hot-reload). 4. **`HostingExtensions.cs`** — extension methods on `IHostApplicationBuilder` named `AddMbproxyOptions(IConfiguration)` and `AddMbproxySerilog(IConfiguration)`. Keep `Program.cs` thin: read config, call the two extensions, register `HeartbeatWorker`, run. 5. **`Program.cs`** — Generic Host with `.UseWindowsService()`. `await Host.CreateApplicationBuilder(args)...Build().RunAsync()`. Honour `--console` as a no-op flag for documentation symmetry with the design (the worker SDK + UseWindowsService combo already runs in console mode under `dotnet run`). 6. **`Workers/HeartbeatWorker.cs`** — `BackgroundService` that logs `mbproxy.startup.ready` once after `Task.Delay(100)` (so Serilog has flushed) and then idles. This worker is deleted in phase 03 when the real listener supervisor takes over; it exists so phase 00's smoke test has something to assert. 7. **`appsettings.json`** — minimal, valid against the POCOs, with `Plcs: []`. Include the full key shape (`BcdTags.Global`, `AdminPort`, `Connection`, `Resilience`) so future phases just fill in values. 8. **`tests/Mbproxy.Tests/Mbproxy.Tests.csproj`** — Microsoft.NET.Sdk, `TargetFramework=net10.0`, same `Nullable`/`TreatWarningsAsErrors`. ProjectReference to `src/Mbproxy/Mbproxy.csproj`. PackageReferences: - `Microsoft.NET.Test.Sdk` - `xunit` (v3 if a stable release exists; v2 otherwise — record the decision in the csproj comment) - `xunit.runner.visualstudio` - `Shouldly` 9. **`HostSmokeTests.cs`** — build the host with `Host.CreateApplicationBuilder` against a synthetic config, start it on a `CancellationTokenSource` with a short deadline, assert it logged `mbproxy.startup.ready` and shut down without unhandled exceptions. 10. **`MbproxyOptionsBindingTests.cs`** — bind a hand-written `Dictionary` config source into `MbproxyOptions`, assert all fields populate correctly (including a `Plcs` entry with `BcdTags.Add` and `BcdTags.Remove`). ## Public surface declared in this phase ```csharp namespace Mbproxy.Options; public sealed class MbproxyOptions { public BcdTagListOptions BcdTags { get; init; } = new(); public IReadOnlyList Plcs { get; init; } = []; public int AdminPort { get; init; } = 8080; public ConnectionOptions Connection { get; init; } = new(); public ResilienceOptions Resilience { get; init; } = new(); } public sealed class BcdTagListOptions { public IReadOnlyList Global { get; init; } = []; } public sealed class BcdTagOptions { public ushort Address { get; init; } public byte Width { get; init; } // 16 or 32 } public sealed class PlcOptions { public string Name { get; init; } = ""; public int ListenPort { get; init; } public string Host { get; init; } = ""; public PlcBcdOverrides? BcdTags { get; init; } } public sealed class PlcBcdOverrides { public IReadOnlyList Add { get; init; } = []; public IReadOnlyList Remove { get; init; } = []; } public sealed class ConnectionOptions { public int BackendConnectTimeoutMs { get; init; } = 3000; public int BackendRequestTimeoutMs { get; init; } = 3000; } public sealed class ResilienceOptions { public RetryProfile BackendConnect { get; init; } = new() { MaxAttempts = 3, BackoffMs = [100, 500, 2000] }; public RecoveryProfile ListenerRecovery { get; init; } = new() { InitialBackoffMs = [1000, 2000, 5000, 15000, 30000], SteadyStateMs = 30000, }; } public sealed class RetryProfile { public int MaxAttempts { get; init; } public IReadOnlyList BackoffMs { get; init; } = []; } public sealed class RecoveryProfile { public IReadOnlyList InitialBackoffMs { get; init; } = []; public int SteadyStateMs { get; init; } } ``` ```csharp namespace Mbproxy; internal static class HostingExtensions { public static IHostApplicationBuilder AddMbproxyOptions(this IHostApplicationBuilder b); public static IHostApplicationBuilder AddMbproxySerilog(this IHostApplicationBuilder b); } ``` ```csharp namespace Mbproxy.Workers; internal sealed class HeartbeatWorker : BackgroundService { /* logs mbproxy.startup.ready */ } ``` No other public types in this phase. ## Tests required ### Unit (`Category = Unit`, default) 1. `MbproxyOptionsBinding_BindsGlobalBcdTags_From_appsettings` 2. `MbproxyOptionsBinding_BindsPerPlcAddAndRemove` 3. `MbproxyOptionsBinding_DefaultsAreApplied_WhenSectionMissing` (AdminPort=8080, Resilience defaults) 4. `MbproxyOptionsBinding_RejectsInvalidWidth` — IValidateOptions returns Fail for `Width != 16 && Width != 32`. Schema-level only; address-overlap validation is phase 06. 5. `HostSmoke_StartsAndStops_Cleanly_AndLogs_StartupReady` — uses a Serilog sink that captures events to memory; asserts the `mbproxy.startup.ready` event fired at Information. 6. `HostSmoke_ShutdownIsOrdered` — host responds to `StopAsync` within 2 s. ### E2E (`Category = E2E`) None in this phase. The simulator harness is phase 01. ## Phase gate - [ ] `dotnet build Mbproxy.slnx -c Debug` — zero warnings. - [ ] `dotnet test --filter Category!=E2E` — all green, ≥6 tests. - [ ] `dotnet run --project src/Mbproxy` — service starts, logs `mbproxy.startup.ready` to console within 5 s, exits cleanly on Ctrl-C. - [ ] `appsettings.json` is a valid JSON document and parses into a populated `MbproxyOptions` instance via the test harness. - [ ] [`../design.md`](../design.md) is unchanged (this phase introduces no new design decisions). - [ ] Resource index entry for `docs/plan/00-bootstrap.md` is not needed (the plan README routes there). ## Out of scope - BCD encode/decode logic (phase 02). - TcpListener / Modbus framing / byte forwarding (phase 03). - Polly retry pipelines (referenced as a NuGet, used starting in phase 04/05). - Address-overlap / duplicate-port validation (phase 06). - AdminPort HTTP endpoint (phase 07). - Service install / uninstall scripts (phase 08). ## Notes for the subagent - Do not create `README.md` for the tool root yet — that's a phase 08 deliverable when there's something installable to document. - If the `xunit` v3 vs v2 question is unclear at implementation time, prefer v3 if available on NuGet — record the choice in a single-line comment at the top of the test csproj. Future phases must not silently switch. - Use `LoggerMessage`-source-generated logging (`[LoggerMessage]`) for the heartbeat event so phases that add more log events can follow the same pattern. Set `EventId.Name = "mbproxy.startup.ready"`.