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
14 changes: 10 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<IHealthMonitor>` 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<IHealthMonitor>` 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<IHealthMonitor>` 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`.
2 changes: 1 addition & 1 deletion examples/HealthMonitor.Examples/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
Loading