Skip to content
Open
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
5 changes: 5 additions & 0 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
}
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions .github/plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -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."
}
]
}
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
6 changes: 6 additions & 0 deletions plugins/dotnet-aspnet/plugin.json
Original file line number Diff line number Diff line change
@@ -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/"
}
175 changes: 175 additions & 0 deletions plugins/dotnet-aspnet/skills/implementing-health-checks/SKILL.md
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are packages for SqlServer, Redis and Uris required for the skill about health-checks for Kubernetes and load balancer integration? Skill has to be very specific, and it feels like this one supports multiple different resources not really following the single idea.

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();
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

app.MapHealthChecksUI() exposes the HealthChecks UI endpoint with detailed health information but no authentication or access control, which can leak internal dependency names, connection details, and health statuses to unauthenticated clients. An attacker who can reach this endpoint could use this operational data to map internal services or assist further attacks. Restrict access to the HealthChecks UI (for example by applying authorization, IP/network restrictions, or hosting it on an internal-only path) rather than exposing it directly on the public app pipeline.

Suggested change
app.MapHealthChecksUI();
// Expose UI on a custom path and require authorization to avoid leaking details
app.MapHealthChecksUI(options =>
{
options.UIPath = "/health-ui"; // UI endpoint
options.ApiPath = "/health-ui-api"; // backend API
}).RequireAuthorization();

Copilot uses AI. Check for mistakes.
```

## 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 |
70 changes: 70 additions & 0 deletions tests/dotnet-aspnet/implementing-health-checks/eval.yaml
Original file line number Diff line number Diff line change
@@ -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."
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prompt is overfitting for the name of skill. I dont imagine myself writing "i need to add health checks for MY ASPNETCORE API", I will simply ask agent "need to add health checks dependant on db" for example.

assertions:
- type: "output_matches"
pattern: "(AddHealthChecks|MapHealthChecks)"
- type: "output_matches"
pattern: "(liveness|readiness|live|ready)"
- type: "output_matches"
pattern: "(healthz|health)"
rubric:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skill clearly relies on installing specific NuGet package(s), and rubric does not validate that at all. Are those packages really required, or agent can generate code building GET endpoint handlers from scratch?

- "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)"
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The assertion pattern (UseHttpLogging|UseSerilog|logging|middleware) is so broad that a response mentioning unrelated “middleware” could satisfy it without actually recommending request logging. Tighten the pattern to require logging-specific terms (e.g., UseHttpLogging, Serilog, request logging, etc.) so this scenario can’t pass on false positives.

Suggested change
pattern: "(UseHttpLogging|UseSerilog|logging|middleware)"
pattern: "(UseHttpLogging|UseSerilog|request logging|http logging|HTTP logging|logging middleware)"

Copilot uses AI. Check for mistakes.
rubric:
- "Did NOT suggest health checks for a request logging scenario"
- "Focused on HTTP logging or request logging middleware configuration"
timeout: 60
timeout: 60
Copy link

Copilot AI Mar 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This scenario defines timeout twice. YAML duplicate keys are easy to miss and the last value will win, which can hide edits or cause confusing behavior during evaluation. Remove the duplicate timeout entry so the scenario has a single, unambiguous timeout value.

Suggested change
timeout: 60

Copilot uses AI. Check for mistakes.
Loading