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
18 changes: 18 additions & 0 deletions docs/examples/fulfillment-health-endpoint-monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Fulfillment Health Endpoint Monitoring

The fulfillment health endpoint example evaluates database reachability, message broker connectivity, and fulfillment queue depth.

```csharp
services.AddFulfillmentHealthEndpointDemo();

var service = provider.GetRequiredService<FulfillmentHealthEndpointService>();
var report = service.Evaluate();
```

ASP.NET Core applications can expose the same endpoint through the route builder extension:

```csharp
app.MapFulfillmentHealthEndpoint("/health/fulfillment");
```

The example includes fluent and source-generated construction, `IServiceCollection` registration, Generic Host-friendly service composition, and ASP.NET Core minimal API integration.
3 changes: 3 additions & 0 deletions docs/examples/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ Welcome! This section collects small, focused demos that show **how to compose b
* **Fulfillment Priority Queue**
Shows fluent and source-generated business-priority queues with an importable `IServiceCollection` extension. See [Fulfillment Priority Queue](fulfillment-priority-queue.md).

* **Fulfillment Health Endpoint Monitoring**
Shows fluent and source-generated health checks with `IServiceCollection`, Generic Host-friendly services, and ASP.NET Core route mapping. See [Fulfillment Health Endpoint Monitoring](fulfillment-health-endpoint-monitoring.md).

* **Generated Message Envelope**
Shows fluent and source-generated message envelope contracts side by side, with an importable `IServiceCollection` extension. See [Generated Message Envelope](generated-message-envelope.md).

Expand Down
3 changes: 3 additions & 0 deletions docs/examples/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
- name: Fulfillment Pipes and Filters
href: fulfillment-pipes-and-filters.md

- name: Fulfillment Health Endpoint Monitoring
href: fulfillment-health-endpoint-monitoring.md

- name: CQRS Dispatcher
href: cqrs-dispatcher.md

Expand Down
23 changes: 23 additions & 0 deletions docs/generators/health-endpoint-monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Health Endpoint Monitoring Generator

`[GenerateHealthEndpoint]` creates a typed `HealthEndpoint<TContext>` factory from static check methods.

```csharp
[GenerateHealthEndpoint(typeof(FulfillmentHealthSnapshot), FactoryMethodName = "Create", EndpointName = "fulfillment-health")]
public static partial class FulfillmentHealthEndpoint
{
[HealthEndpointCheck("database", Order = 1)]
private static HealthEndpointCheckResult CheckDatabase(FulfillmentHealthSnapshot snapshot)
=> snapshot.DatabaseOnline
? HealthEndpointCheckResult.HealthyCheck("database")
: HealthEndpointCheckResult.UnhealthyCheck("database", "offline");
}
```

The generated factory is parameterless, so applications can register it in `IServiceCollection` and inject the endpoint into hosted services, readiness checks, or ASP.NET Core route handlers.

Diagnostics:

- `PKHEM001`: host type must be partial.
- `PKHEM002`: at least one health check is required.
- `PKHEM003`: health check signature is invalid.
1 change: 1 addition & 0 deletions docs/generators/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ PatternKit includes a Roslyn incremental generator package (`PatternKit.Generato
| [**Circuit Breaker**](circuit-breaker.md) | Dependency isolation policy factories with open and half-open states | `[GenerateCircuitBreakerPolicy]` |
| [**Bulkhead**](bulkhead.md) | Bounded concurrency and queue isolation policy factories | `[GenerateBulkheadPolicy]` |
| [**Queue Load Leveling**](queue-load-leveling.md) | Bounded worker queue policy factories | `[GenerateQueueLoadLevelingPolicy]` |
| [**Health Endpoint Monitoring**](health-endpoint-monitoring.md) | Typed service health endpoint factories | `[GenerateHealthEndpoint]` |
| [**Priority Queue**](priority-queue.md) | Business-priority queue factories | `[GeneratePriorityQueue]` |
| [**Cache-Aside**](cache-aside.md) | Read-through cache policy factories with TTL and cache predicates | `[GenerateCacheAsidePolicy]` |
| [**Rate Limiting**](rate-limiting.md) | Key-partitioned fixed-window rate limit policy factories | `[GenerateRateLimitPolicy]` |
Expand Down
3 changes: 3 additions & 0 deletions docs/generators/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@
- name: Flyweight
href: flyweight.md

- name: Health Endpoint Monitoring
href: health-endpoint-monitoring.md

- name: Interpreter
href: interpreter.md

Expand Down
1 change: 1 addition & 0 deletions docs/guides/pattern-coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ The source of truth is `PatternKitPatternCatalog` in `src/PatternKit.Examples/Pr
| Cloud Architecture | Circuit Breaker | `CircuitBreakerPolicy<T>` | Circuit Breaker generator |
| Cloud Architecture | Bulkhead | `BulkheadPolicy<T>` | Bulkhead generator |
| Cloud Architecture | Queue-Based Load Leveling | `QueueLoadLevelingPolicy<T>` | Queue Load Leveling generator |
| Cloud Architecture | Health Endpoint Monitoring | `HealthEndpoint<TContext>` | Health Endpoint Monitoring generator |
| Cloud Architecture | Priority Queue | `PriorityQueuePolicy<TItem, TPriority>` | Priority Queue generator |
| Cloud Architecture | Cache-Aside | `CacheAsidePolicy<T>` | Cache-Aside generator |
| Cloud Architecture | Rate Limiting | `RateLimitPolicy<T>` | Rate Limiting generator |
Expand Down
21 changes: 21 additions & 0 deletions docs/patterns/cloud/health-endpoint-monitoring.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Health Endpoint Monitoring

Health Endpoint Monitoring exposes a typed status endpoint that evaluates application dependencies and returns a deterministic health report.

```csharp
var endpoint = HealthEndpoint<FulfillmentHealthSnapshot>
.Create("fulfillment-health")
.WithCheck("database", snapshot => snapshot.DatabaseOnline
? HealthEndpointCheckResult.HealthyCheck("database")
: HealthEndpointCheckResult.UnhealthyCheck("database", "offline"))
.WithCheck("queue-depth", snapshot => snapshot.QueueDepth <= 100
? HealthEndpointCheckResult.HealthyCheck("queue-depth")
: HealthEndpointCheckResult.UnhealthyCheck("queue-depth", "backlog above target"))
.Build();

var report = endpoint.Evaluate(snapshot);
```

Use it when service health needs to be composed from multiple business and infrastructure checks before it is exposed to load balancers, orchestrators, Generic Host startup validation, or ASP.NET Core endpoints.

The source-generated path uses `[GenerateHealthEndpoint]` and `[HealthEndpointCheck]`. Import the fulfillment example through `AddFulfillmentHealthEndpointDemo()`, map it through `MapFulfillmentHealthEndpoint()`, or include it in `AddPatternKitExamples()`.
2 changes: 2 additions & 0 deletions docs/patterns/toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,8 @@
href: cloud/bulkhead.md
- name: Queue-Based Load Leveling
href: cloud/queue-load-leveling.md
- name: Health Endpoint Monitoring
href: cloud/health-endpoint-monitoring.md
- name: Priority Queue
href: cloud/priority-queue.md
- name: Cache-Aside
Expand Down
120 changes: 120 additions & 0 deletions src/PatternKit.Core/Cloud/HealthEndpointMonitoring/HealthEndpoint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
namespace PatternKit.Cloud.HealthEndpointMonitoring;

public sealed class HealthEndpointCheckResult
{
private HealthEndpointCheckResult(string name, bool healthy, string message)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Health check name is required.", nameof(name));

Name = name;
Healthy = healthy;
Message = message ?? string.Empty;
}

public string Name { get; }

public bool Healthy { get; }

public string Message { get; }

public static HealthEndpointCheckResult HealthyCheck(string name, string message = "")
=> new(name, true, message);

public static HealthEndpointCheckResult UnhealthyCheck(string name, string message)
=> new(name, false, message);
}

public sealed class HealthEndpointReport
{
public HealthEndpointReport(string endpointName, IReadOnlyList<HealthEndpointCheckResult> checks)
{
if (string.IsNullOrWhiteSpace(endpointName))
throw new ArgumentException("Health endpoint name is required.", nameof(endpointName));

EndpointName = endpointName;
Checks = checks ?? throw new ArgumentNullException(nameof(checks));
Healthy = checks.All(static check => check.Healthy);
PassedCount = checks.Count(static check => check.Healthy);
FailedCount = checks.Count - PassedCount;
}

public string EndpointName { get; }

public bool Healthy { get; }

public int PassedCount { get; }

public int FailedCount { get; }

public IReadOnlyList<HealthEndpointCheckResult> Checks { get; }
}

public sealed class HealthEndpoint<TContext>
{
private readonly IReadOnlyList<ConfiguredCheck> _checks;

private HealthEndpoint(string name, IReadOnlyList<ConfiguredCheck> checks)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Health endpoint name is required.", nameof(name));

if (checks is null)
throw new ArgumentNullException(nameof(checks));
if (checks.Count == 0)
throw new InvalidOperationException("Health endpoint requires at least one check.");

Name = name;
_checks = checks;
}

public string Name { get; }

public HealthEndpointReport Evaluate(TContext context)
{
if (context is null)
throw new ArgumentNullException(nameof(context));

var results = new List<HealthEndpointCheckResult>(_checks.Count);
foreach (var check in _checks)
{
var result = check.Evaluate(context) ?? throw new InvalidOperationException($"Health check '{check.Name}' returned null.");
results.Add(result);
}

return new(Name, results);
}

public static Builder Create(string name = "health-endpoint") => new(name);

public sealed class Builder
{
private readonly string _name;
private readonly List<ConfiguredCheck> _checks = [];

internal Builder(string name) => _name = name;

public Builder WithCheck(string name, Func<TContext, HealthEndpointCheckResult> check)
{
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException("Health check name is required.", nameof(name));
if (check is null)
throw new ArgumentNullException(nameof(check));

_checks.Add(new(name, check));
return this;
}

public HealthEndpoint<TContext> Build() => new(_name, _checks.ToArray());
}

private sealed class ConfiguredCheck
{
public ConfiguredCheck(string name, Func<TContext, HealthEndpointCheckResult> evaluate)
=> (Name, Evaluate) = (name, evaluate);

public string Name { get; }

public Func<TContext, HealthEndpointCheckResult> Evaluate { get; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using PatternKit.Cloud.Bulkhead;
using PatternKit.Cloud.CacheAside;
using PatternKit.Cloud.CircuitBreaker;
using PatternKit.Cloud.HealthEndpointMonitoring;
using PatternKit.Cloud.PriorityQueue;
using PatternKit.Cloud.RateLimiting;
using PatternKit.Cloud.QueueLoadLeveling;
Expand All @@ -35,6 +36,7 @@
using PatternKit.Examples.FlyweightDemo;
using PatternKit.Examples.Generators.Builders.CorporateApplicationBuilderDemo;
using PatternKit.Examples.Generators.Visitors;
using PatternKit.Examples.HealthEndpointMonitoringDemo;
using PatternKit.Examples.IdentityMapDemo;
using PatternKit.Examples.MaterializedViewDemo;
using PatternKit.Examples.MementoDemo;
Expand Down Expand Up @@ -187,6 +189,7 @@ public sealed record InventoryRetryExample(RetryPolicy<InventoryResponse> Policy
public sealed record FulfillmentCircuitBreakerExample(CircuitBreakerPolicy<FulfillmentResponse> Policy, FulfillmentCircuitBreakerService Service);
public sealed record ShippingBulkheadExample(BulkheadPolicy<ShippingAllocation> Policy, ShippingBulkheadService Service);
public sealed record FulfillmentQueueLoadLevelingExample(QueueLoadLevelingPolicy<FulfillmentQueueResult> Policy, FulfillmentQueueLoadLevelingService Service);
public sealed record FulfillmentHealthEndpointExample(HealthEndpoint<FulfillmentHealthSnapshot> Endpoint, FulfillmentHealthEndpointService Service);
public sealed record FulfillmentPriorityQueueExample(PriorityQueuePolicy<FulfillmentPriorityWork, int> Queue, FulfillmentPriorityQueueService Service);
public sealed record ProductCatalogCacheAsideExample(CacheAsidePolicy<ProductReadModel> Policy, ProductCatalogCacheAsideService Service);
public sealed record ProductSearchRateLimitingExample(RateLimitPolicy<SearchResponse> Policy, ProductSearchRateLimitService Service);
Expand Down Expand Up @@ -272,6 +275,7 @@ public static IServiceCollection AddPatternKitExamples(this IServiceCollection s
.AddFulfillmentCircuitBreakerExample()
.AddShippingBulkheadExample()
.AddFulfillmentQueueLoadLevelingExample()
.AddFulfillmentHealthEndpointExample()
.AddFulfillmentPriorityQueueExample()
.AddProductCatalogCacheAsideExample()
.AddProductSearchRateLimitingExample()
Expand Down Expand Up @@ -941,6 +945,15 @@ public static IServiceCollection AddFulfillmentPriorityQueueExample(this IServic
return services.RegisterExample<FulfillmentPriorityQueueExample>("Fulfillment Priority Queue", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost);
}

public static IServiceCollection AddFulfillmentHealthEndpointExample(this IServiceCollection services)
{
services.AddFulfillmentHealthEndpointDemo();
services.AddSingleton<FulfillmentHealthEndpointExample>(sp => new(
sp.GetRequiredService<HealthEndpoint<FulfillmentHealthSnapshot>>(),
sp.GetRequiredService<FulfillmentHealthEndpointService>()));
return services.RegisterExample<FulfillmentHealthEndpointExample>("Fulfillment Health Endpoint Monitoring", ExampleIntegrationSurface.LibraryOnly | ExampleIntegrationSurface.SourceGenerator | ExampleIntegrationSurface.DependencyInjection | ExampleIntegrationSurface.GenericHost | ExampleIntegrationSurface.AspNetCore);
}

public static IServiceCollection AddProductCatalogCacheAsideExample(this IServiceCollection services)
{
services.AddProductCatalogCacheAsideDemo();
Expand Down
Loading
Loading