From 1d7bdc72b3818501b66667852eb35222e6245897 Mon Sep 17 00:00:00 2001 From: Gaoyang Date: Tue, 9 Jun 2026 11:21:15 +0800 Subject: [PATCH] chore: add ConfigureAwait(false) to all awaits in library and examples Also expand CLAUDE.md with test conventions, DI details, CI/versioning, and case-insensitive name behavior in DynamicHealthMonitorManager. --- CLAUDE.md | 14 ++++++++++---- examples/HealthMonitor.Examples/Program.cs | 2 +- .../Services/HealthMonitorHostedService.cs | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 320897a..c275a0a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,22 +33,28 @@ Build artifacts go to `/artifacts/bin/` (configured in `Directory.Build.props`). - **Healthy → Degraded**: no `Signal()` received within `DegradedThreshold` (default 30 s) - **Degraded → Healthy**: `Signal()` received while degraded; fires `Recovered` event immediately -`HealthMonitorBase` uses three independent `IStopwatch` instances: `signalStopwatch` (reset on every `Signal()`), `checkStopwatch` (gates background tick frequency per `CheckInterval`), and `stateStopwatch` (measures time-in-state for event args). All shared state is guarded by a single `lock` so `Signal()` is safe to call from any thread concurrently with `Tick()`. +`HealthMonitorBase` uses three independent `IStopwatch` instances: `signalStopwatch` (reset on every `Signal()`), `checkStopwatch` (gates background tick frequency per `CheckInterval`), and `stateStopwatch` (measures time-in-state for event args). All shared state is guarded by a single `lock` so `Signal()` is safe to call from any thread concurrently with `Tick()`. Events are built inside the lock but invoked outside it to avoid deadlocks. ### DI registration -`AddHealthMonitor()` registers monitors as keyed services (by name, .NET 8+) and via `IEnumerable` on all targets. The hosted service resolves all monitors through the coordinator. The call is idempotent for the shared infrastructure (hosted service, coordinator) — call it once per monitor name. +`AddHealthMonitor(name, configure?)` registers monitors as keyed services (by name, .NET 8+) and via `IEnumerable` on all targets. Shared infrastructure (`ISystemTimeProvider`, coordinator, hosted service) is registered once via `TryAddSingleton`/`TryAddEnumerable` — the call is idempotent for infrastructure, call it once per monitor name. On `netstandard2.0` (no keyed DI), resolve a specific monitor by filtering `IEnumerable` by `Name`. ### Dynamic registration (no DI) -`DynamicHealthMonitorManager` is a standalone `IDisposable` class for runtime monitor management. Each monitor added via `Add(name, options?)` gets its own `System.Threading.Timer` firing at its `CheckInterval`. The manager exposes aggregate `Degraded` / `Recovered` events that forward from all registered monitors — subscribers added before or after `Add()` both receive events. `Remove()` disposes the timer and unsubscribes event forwarding atomically under a lock. +`DynamicHealthMonitorManager` is a standalone `IDisposable` class for runtime monitor management. Each monitor added via `Add(name, options?)` gets its own `System.Threading.Timer` firing at its `CheckInterval`. Monitor names are case-insensitive (`OrdinalIgnoreCase`). The manager exposes aggregate `Degraded` / `Recovered` events that forward from all registered monitors — subscribers added before or after `Add()` both receive events. `Remove()` disposes the timer and unsubscribes event forwarding atomically under a lock. ### Testability `IStopwatch` and `ISystemTimeProvider` abstractions allow time-controlled unit tests. The `tests/HealthMonitor.Tests/Fakes/` directory provides `FakeStopwatch` and related fakes. Tests use **xunit.v3** (not v2 — the assertion API differs). +Test classes follow the `[ComponentName]Tests` naming convention; test methods use `Verb_Scenario_Expected` (e.g., `Signal_WhenDegraded_FiresRecoveredImmediately`). Most test classes use a `CreateMonitor()` factory that returns a tuple of the monitor plus all injected fakes for precise time control without boilerplate. + ### Multi-targeting -`HealthMonitor.Core` targets `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`. Keyed DI services are only available on `net8.0`+. Use `#if NET8_0_OR_GREATER` guards when adding version-specific APIs. +`HealthMonitor.Core` targets `netstandard2.0`, `net8.0`, `net9.0`, and `net10.0`. Tests target `net8.0`, `net9.0`, and `net10.0` only. Keyed DI services are only available on `net8.0`+. Use `#if NET8_0_OR_GREATER` guards when adding version-specific APIs. + +### Versioning and CI + +Versioning uses **MinVer** — the NuGet package version is derived automatically from git tags (`v*` prefix). CI runs on GitHub Actions (`ubuntu-latest`) and publishes to NuGet on version tags. No additional linters or analyzers are configured; the project relies on `LangVersion: latest`, `Nullable: enable`, and `ImplicitUsings: enable` set globally in `Directory.Build.props`. diff --git a/examples/HealthMonitor.Examples/Program.cs b/examples/HealthMonitor.Examples/Program.cs index 9bf6b6c..45971ae 100644 --- a/examples/HealthMonitor.Examples/Program.cs +++ b/examples/HealthMonitor.Examples/Program.cs @@ -211,7 +211,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) { await Task.WhenAll( SimulateFastFeed(stoppingToken), - SimulateSlowFeed(stoppingToken)); + SimulateSlowFeed(stoppingToken)).ConfigureAwait(false); } private async Task SimulateFastFeed(CancellationToken ct) diff --git a/src/HealthMonitor.Core/Services/HealthMonitorHostedService.cs b/src/HealthMonitor.Core/Services/HealthMonitorHostedService.cs index ed1fbe8..1519f82 100644 --- a/src/HealthMonitor.Core/Services/HealthMonitorHostedService.cs +++ b/src/HealthMonitor.Core/Services/HealthMonitorHostedService.cs @@ -41,7 +41,7 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken) try { - await Task.Delay(coordinator.MinCheckInterval, stoppingToken); + await Task.Delay(coordinator.MinCheckInterval, stoppingToken).ConfigureAwait(false); } catch (OperationCanceledException) {