diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 02a140fe0..7600c43a0 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -33,6 +33,11 @@ "name": "dotnet-maui", "source": "./plugins/dotnet-maui", "description": "Skills for .NET MAUI development: environment setup, diagnostics, and troubleshooting." + }, + { + "name": "dotnet-aspnet", + "source": "./plugins/dotnet-aspnet", + "description": "Skills for ASP.NET Core web development: health checks, middleware, authentication, and API patterns." } ] } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 3ccf48955..4bb4260b2 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -58,3 +58,7 @@ # dotnet-maui /plugins/dotnet-maui/ @Redth @jfversluis /tests/dotnet-maui/ @Redth @jfversluis + +# dotnet-aspnet (ASP.NET) +/plugins/dotnet-aspnet/skills/implementing-health-checks/ @BrennanConroy @artl93 +/tests/dotnet-aspnet/implementing-health-checks/ @BrennanConroy @artl93 diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 02a140fe0..7600c43a0 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -33,6 +33,11 @@ "name": "dotnet-maui", "source": "./plugins/dotnet-maui", "description": "Skills for .NET MAUI development: environment setup, diagnostics, and troubleshooting." + }, + { + "name": "dotnet-aspnet", + "source": "./plugins/dotnet-aspnet", + "description": "Skills for ASP.NET Core web development: health checks, middleware, authentication, and API patterns." } ] } diff --git a/README.md b/README.md index d487802a7..d5beaeb43 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ This repository contains the .NET team's curated set of core skills and custom a | [dotnet-msbuild](plugins/dotnet-msbuild/) | Comprehensive MSBuild and .NET build skills: failure diagnosis, performance optimization, code quality, and modernization. | | [dotnet-upgrade](plugins/dotnet-upgrade/) | Skills for migrating and upgrading .NET projects across framework versions, language features, and compatibility targets. | | [dotnet-maui](plugins/dotnet-maui/) | Skills for .NET MAUI development: environment setup, diagnostics, and troubleshooting. | +| [dotnet-aspnet](plugins/dotnet-aspnet/) | Skills for ASP.NET Core web development: health checks, middleware, authentication, and API patterns. | ## Installation diff --git a/plugins/dotnet-aspnet/plugin.json b/plugins/dotnet-aspnet/plugin.json new file mode 100644 index 000000000..a21b64868 --- /dev/null +++ b/plugins/dotnet-aspnet/plugin.json @@ -0,0 +1,6 @@ +{ + "name": "dotnet-aspnet", + "version": "0.1.0", + "description": "Skills for ASP.NET Core web development: health checks, middleware, authentication, and API patterns.", + "skills": "./skills/" +} diff --git a/plugins/dotnet-aspnet/skills/implementing-health-checks/SKILL.md b/plugins/dotnet-aspnet/skills/implementing-health-checks/SKILL.md new file mode 100644 index 000000000..cf97362f3 --- /dev/null +++ b/plugins/dotnet-aspnet/skills/implementing-health-checks/SKILL.md @@ -0,0 +1,175 @@ +--- +name: implementing-health-checks +description: Implement ASP.NET Core health checks with liveness, readiness, and startup probes for Kubernetes and load balancer integration. Use when configuring health endpoints, monitoring dependencies, or setting up container orchestration probes. +--- + +# Implementing Health Checks + +## When to Use + +- Adding health check endpoints to an ASP.NET Core app +- Configuring Kubernetes liveness, readiness, and startup probes +- Monitoring database, cache, or external service availability +- Load balancer health endpoint configuration + +## When Not to Use + +- The app is not ASP.NET Core +- The user wants application performance monitoring (use OpenTelemetry instead) +- The user needs business-level monitoring (use custom metrics) + +## Inputs + +| Input | Required | Description | +|-------|----------|-------------| +| ASP.NET Core project | Yes | The project to add health checks to | +| Dependencies to monitor | No | Database, Redis, message queue, etc. | + +## Workflow + +### Step 1: Add the health checks packages + +```bash +dotnet add package AspNetCore.HealthChecks.SqlServer # for SQL Server +dotnet add package AspNetCore.HealthChecks.Redis # for Redis +dotnet add package AspNetCore.HealthChecks.Uris # for HTTP dependencies +``` + +> `Microsoft.Extensions.Diagnostics.HealthChecks` is already included in the ASP.NET Core framework — no explicit install needed. + +### Step 2: Register health checks with SEPARATE liveness and readiness + +**Critical distinction most implementations get wrong:** + +- **Liveness** = "Is the process alive?" — Only checks the process isn't deadlocked. Failure → Kubernetes RESTARTS the pod. +- **Readiness** = "Can the process serve traffic?" — Checks dependencies. Failure → Kubernetes STOPS SENDING traffic (but doesn't restart). +- **Startup** = "Has the initial startup completed?" — One-time check. Failure during grace period is expected. + +```csharp +builder.Services.AddHealthChecks() + // Liveness checks: ONLY check the process itself, NEVER external dependencies + .AddCheck("self", () => HealthCheckResult.Healthy(), tags: new[] { "live" }) + + // Readiness checks: check external dependencies + .AddSqlServer( + connectionString: builder.Configuration.GetConnectionString("Default")!, + name: "database", + tags: new[] { "ready" }, + timeout: TimeSpan.FromSeconds(5)) + .AddRedis( + redisConnectionString: builder.Configuration.GetConnectionString("Redis")!, + name: "redis", + tags: new[] { "ready" }, + timeout: TimeSpan.FromSeconds(5)) + .AddUrlGroup( + new Uri("https://api.external-service.com/health"), + name: "external-api", + tags: new[] { "ready" }, + timeout: TimeSpan.FromSeconds(5)); +``` + +### Step 3: Map separate health endpoints + +```csharp +// Liveness: Kubernetes livenessProbe hits this +app.MapHealthChecks("/healthz/live", new HealthCheckOptions +{ + Predicate = check => check.Tags.Contains("live"), + ResponseWriter = WriteMinimalResponse +}); + +// Readiness: Kubernetes readinessProbe hits this +// Use minimal response by default; only expose detailed responses on protected/internal endpoints +app.MapHealthChecks("/healthz/ready", new HealthCheckOptions +{ + Predicate = check => check.Tags.Contains("ready"), + ResponseWriter = WriteMinimalResponse +}); + +// Startup: Kubernetes startupProbe hits this — check process health only (same as liveness) +app.MapHealthChecks("/healthz/startup", new HealthCheckOptions +{ + Predicate = check => check.Tags.Contains("live"), + ResponseWriter = WriteMinimalResponse +}); +``` + +### Step 4: Write response formatters + +```csharp +static Task WriteMinimalResponse(HttpContext context, HealthReport report) +{ + context.Response.ContentType = "application/json"; + var result = new { status = report.Status.ToString() }; + return context.Response.WriteAsJsonAsync(result); +} + +static Task WriteDetailedResponse(HttpContext context, HealthReport report) +{ + context.Response.ContentType = "application/json"; + var result = new + { + status = report.Status.ToString(), + checks = report.Entries.Select(e => new + { + name = e.Key, + status = e.Value.Status.ToString(), + description = e.Value.Description, + duration = e.Value.Duration.TotalMilliseconds + }) + }; + return context.Response.WriteAsJsonAsync(result); +} +``` + +### Step 5: Configure Kubernetes probes + +```yaml +# In the Kubernetes deployment spec: +containers: + - name: myapp + livenessProbe: + httpGet: + path: /healthz/live + port: 8080 + initialDelaySeconds: 0 # Start checking immediately + periodSeconds: 10 + failureThreshold: 3 # Restart after 3 failures + readinessProbe: + httpGet: + path: /healthz/ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 # Stop traffic after 3 failures + startupProbe: + httpGet: + path: /healthz/startup + port: 8080 + initialDelaySeconds: 0 + periodSeconds: 5 + failureThreshold: 30 # Allow up to 150s for startup +``` + +### Step 6: Add health check UI (optional) + +```bash +dotnet add package AspNetCore.HealthChecks.UI +dotnet add package AspNetCore.HealthChecks.UI.InMemory.Storage +``` + +```csharp +builder.Services.AddHealthChecksUI().AddInMemoryStorage(); +app.MapHealthChecksUI(); +``` + +## Common Pitfalls + +| Pitfall | Solution | +|---------|----------| +| Checking DB in liveness probe | DB down → pod restarts → makes outage worse. DB checks go in READINESS only | +| No timeout on health checks | Add `timeout: TimeSpan.FromSeconds(5)` to each check registration | +| Health endpoint not excluded from auth | Add `.AllowAnonymous()` to `MapHealthChecks` or exclude path in auth middleware | +| Startup probe missing | Without it, liveness probe kills pods during slow cold starts | +| All checks on one endpoint | Separate live/ready/startup — mixing them causes cascading restarts | +| Detailed response on public endpoint | Detailed health responses can leak internal dependency names. Use minimal responses by default; only expose details on protected/internal endpoints | diff --git a/tests/dotnet-aspnet/implementing-health-checks/eval.yaml b/tests/dotnet-aspnet/implementing-health-checks/eval.yaml new file mode 100644 index 000000000..747252964 --- /dev/null +++ b/tests/dotnet-aspnet/implementing-health-checks/eval.yaml @@ -0,0 +1,70 @@ +scenarios: + - name: "Add health checks with Kubernetes probes" + prompt: "I need to add health checks to my ASP.NET Core API for Kubernetes deployment. It should check the database and Redis connections and have separate liveness and readiness endpoints." + assertions: + - type: "output_matches" + pattern: "(AddHealthChecks|MapHealthChecks)" + - type: "output_matches" + pattern: "(liveness|readiness|live|ready)" + - type: "output_matches" + pattern: "(healthz|health)" + rubric: + - "Separated liveness and readiness health checks using tags" + - "Liveness probe does NOT check external dependencies (database, Redis) — only process health" + - "Readiness probe checks database and Redis connectivity" + - "Mapped separate endpoints for liveness and readiness (e.g., /healthz/live and /healthz/ready)" + - "Explained WHY liveness should not check dependencies (restart cascading)" + - "Provided Kubernetes probe YAML configuration or explained probe settings" + timeout: 120 + + - name: "Health check skill should not activate for monitoring setup" + prompt: "I want to add application performance monitoring with OpenTelemetry to track request latency and error rates." + assertions: + - type: "output_not_matches" + pattern: "\\b(AddHealthChecks|MapHealthChecks)\\s*\\(" + - type: "output_matches" + pattern: "(OpenTelemetry|AddOpenTelemetry|tracing|metrics|exporter)" + rubric: + - "Did NOT suggest health checks for an APM/observability request" + - "Focused on OpenTelemetry setup (tracing, metrics, exporters)" + timeout: 60 + + - name: "Separate startup probe from liveness" + prompt: "My ASP.NET Core app takes about 60 seconds to warm up and Kubernetes keeps restarting it. I already have a liveness probe at /health. How do I fix this?" + assertions: + - type: "output_matches" + pattern: "(startupProbe|startup)" + - type: "output_matches" + pattern: "(initialDelaySeconds|failureThreshold|periodSeconds)" + rubric: + - "Recommended adding a startup probe to handle slow cold starts" + - "Explained that without a startup probe, the liveness probe kills pods during startup" + - "Suggested appropriate failureThreshold and periodSeconds to cover the 60s warmup" + - "Startup probe should check process health only, not external dependencies" + timeout: 120 + + - name: "Health checks with load balancer instead of Kubernetes" + prompt: "I'm deploying my ASP.NET Core app behind an Azure Application Gateway. I need a health endpoint that the gateway can hit to determine if the instance is healthy." + assertions: + - type: "output_matches" + pattern: "(MapHealthChecks|AddHealthChecks)" + - type: "output_matches" + pattern: "(health|healthz)" + rubric: + - "Suggested a health check endpoint that a load balancer can use" + - "Checked external dependencies (database, cache) that affect ability to serve traffic" + - "Did not over-engineer with separate liveness/readiness/startup unless context warranted it" + timeout: 120 + + - name: "Do not confuse health checks with logging middleware" + prompt: "I want to add request logging middleware to my ASP.NET Core API so I can see all incoming HTTP requests and their response times." + assertions: + - type: "output_not_matches" + pattern: "\\b(AddHealthChecks|MapHealthChecks)\\s*\\(" + - type: "output_matches" + pattern: "(UseHttpLogging|UseSerilog|logging|middleware)" + rubric: + - "Did NOT suggest health checks for a request logging scenario" + - "Focused on HTTP logging or request logging middleware configuration" + timeout: 60 + timeout: 60