Skip to content
Closed
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
21 changes: 21 additions & 0 deletions PR-264-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Summary
Adds the **minimal-api-file-upload** skill for handling file uploads in ASP.NET Core 8 minimal APIs.

> **Note:** Replaces #134 (migrated from skills-old repo). Skill moved to `aspnetcore` plugin per repo restructuring.

## What the Skill Teaches
- `IFormFile` binding in minimal APIs (when `[FromForm]` is needed vs automatic)
- `IFormFileCollection` for multiple file uploads
- Security: file size limits, content type allowlists, magic byte validation
- Safe file naming with `Guid` + validated extension (never trust client filename)
- Streaming uploads with `MultipartReader` for large files
- Antiforgery considerations
- Path traversal prevention

## Eval Scenarios
- (eval.yaml needs scenarios — currently empty)

## Files
- `plugins/aspnetcore/plugin.json` — ASP.NET Core plugin
- `plugins/aspnetcore/skills/minimal-api-file-upload/SKILL.md` — skill instructions
- `tests/aspnetcore/minimal-api-file-upload/eval.yaml` — eval scenarios (to be added)
23 changes: 23 additions & 0 deletions PR-265-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
## Summary
Adds the **implementing-server-sent-events** skill for building SSE endpoints in ASP.NET Core 8 minimal APIs.

> **Note:** Replaces #139 (migrated from skills-old repo). Skill moved to `aspnetcore` plugin per repo restructuring.

## What the Skill Teaches
- Manual SSE protocol implementation (no built-in `MapSSE` — doesn't exist)
- Required headers: `Content-Type: text/event-stream`, `Cache-Control: no-cache`, `Connection: keep-alive`
- Correct SSE event framing with `\n\n` terminators
- `Response.Body.FlushAsync()` for immediate delivery
- Event IDs + `Last-Event-ID` header for reconnection support
- `retry:` field for controlling client reconnection interval
- `X-Accel-Buffering: no` for reverse proxy compatibility
- `HttpContext.RequestAborted` / `CancellationToken` for client disconnect detection
- `Channel<T>` for background service → endpoint communication

## Eval Scenarios
1. Implement SSE notification endpoint in ASP.NET Core 8 minimal API

## Files
- `plugins/aspnetcore/plugin.json` — ASP.NET Core plugin
- `plugins/aspnetcore/skills/implementing-server-sent-events/SKILL.md` — skill instructions
- `tests/aspnetcore/implementing-server-sent-events/eval.yaml` — 1 eval scenario (needs expansion)
Comment on lines +20 to +23

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

This PR description file claims plugins/aspnetcore/plugin.json is part of the change and that the eval has "1 eval scenario (needs expansion)", but the PR as provided does not add plugins/aspnetcore/plugin.json, and the eval.yaml now contains multiple scenarios. Please update or remove this description file so it matches the actual changed files/structure.

Copilot uses AI. Check for mistakes.
25 changes: 25 additions & 0 deletions PR-266-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
## Summary
Adds the **implementing-websocket-endpoints** skill for building WebSocket endpoints in ASP.NET Core 8.

> **Note:** Replaces #142 (migrated from skills-old repo). Skill moved to `aspnetcore` plugin per repo restructuring.

## What the Skill Teaches
- `UseWebSockets()` middleware setup (no `MapWebSocket` — doesn't exist)
- Manual WebSocket upgrade via `AcceptWebSocketAsync`
- Proper receive loop with `EndOfMessage` fragment handling
- **Critical:** Use `CloseOutputAsync` (not `CloseAsync`) when responding to client-initiated close to avoid deadlock
- `ConcurrentDictionary`-based connection manager for broadcast
- `KeepAliveInterval` and `KeepAliveTimeout` configuration
- `AllowedOrigins` for cross-origin protection
- Query string token authentication (browser WebSocket API cannot send custom headers)

## Eval Scenarios
1. Implement WebSocket chat endpoint in ASP.NET Core 8
2. Fix WebSocket CloseAsync deadlock
3. WebSocket should not be used for server-to-client streaming (negative test)
4. Handle fragmented WebSocket messages correctly

## Files
- `plugins/aspnetcore/plugin.json` — ASP.NET Core plugin
- `plugins/aspnetcore/skills/implementing-websocket-endpoints/SKILL.md` — skill instructions
- `tests/aspnetcore/implementing-websocket-endpoints/eval.yaml` — 4 eval scenarios
31 changes: 31 additions & 0 deletions PR-267-description.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Summary
Adds the **implementing-rate-limiting** skill for ASP.NET Core's built-in rate limiting middleware (.NET 7+).

> **Note:** Replaces #131 (migrated from skills-old repo). Skill moved to `aspnetcore` plugin per repo restructuring.

## What the Skill Teaches
- Algorithm selection: fixed window vs sliding window vs token bucket vs concurrency limiter
- **Critical:** Setting `RejectionStatusCode = 429` (default is 503!)
- Per-IP and per-user partitioned rate limiting
- Middleware pipeline ordering (`UseRateLimiter()` after `UseRouting()`)
- `OnRejected` callback with `Retry-After` header
- `DisableRateLimiting()` for health check endpoints
- Named policies with `RequireRateLimiting`

## Eval Results (3-run, `claude-opus-4.6`)

| Scenario | Baseline | With Skill | Verdict |
|----------|----------|------------|---------|
| Add per-client rate limiting to an ASP.NET Core API | 4.0/5 | **4.7/5** | ❌ [1] |
| Rate limiting silently inactive without UseRateLimiter | 4.3/5 | 2.7/5 | ❌ |
| Fix rate limiter returning 503 instead of 429 | 2.3/5 | 2.3/5 | ❌ [2] |
| Rate limiting should not apply to authentication scenarios | 4.0/5 | **4.7/5** | ✅ |
| Choose correct rate limiting algorithm | 4.0/5 | **5.0/5** | ✅ |

[1] Quality improved but weighted score penalized by token/tool/time overhead.
[2] Timeouts impacted scoring. Will iterate with increased timeouts.

## Files
- `plugins/aspnetcore/plugin.json` — new ASP.NET Core plugin
- `plugins/aspnetcore/skills/implementing-rate-limiting/SKILL.md` — skill instructions
- `tests/aspnetcore/implementing-rate-limiting/eval.yaml` — 5 eval scenarios

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

PR-267-description.md documents an "implementing-rate-limiting" skill and references files that are not part of this PR. Unless these PR-xxx description artifacts are intentionally being checked in for another purpose, they add noise and make it harder to review the actual changes; consider removing them from this PR or ensuring they correspond to included code.

Suggested change
- `tests/aspnetcore/implementing-rate-limiting/eval.yaml` — 5 eval scenarios

Copilot uses AI. Check for mistakes.
207 changes: 207 additions & 0 deletions plugins/aspnetcore/skills/implementing-server-sent-events/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
---
name: implementing-server-sent-events
description: Implement Server-Sent Events (SSE) endpoints in ASP.NET Core. Use when building real-time streaming from server to client without WebSockets.
---
Comment on lines +1 to +4

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

The new aspnetcore plugin directory under plugins/aspnetcore/ does not include a plugin.json, unlike the other plugin roots (e.g., plugins/dotnet/plugin.json, plugins/dotnet-data/plugin.json). Adding plugins/aspnetcore/plugin.json is important for plugin manifest validation/discovery and to keep the repo’s plugin structure consistent.

Copilot uses AI. Check for mistakes.

# Implementing Server-Sent Events (SSE) in ASP.NET Core

## When to Use
- Server-to-client real-time push (notifications, live updates, streaming progress)
- When you DON'T need bidirectional communication (use WebSockets for that)
- SSE is simpler than WebSockets and works over standard HTTP
- Automatic reconnection built into EventSource browser API

## When Not to Use
- Bidirectional communication needed → use WebSockets
- Binary data streaming → use WebSockets or gRPC streaming
- Need more than 6 concurrent connections per domain in HTTP/1.1 → use HTTP/2

## Inputs

| Input | Required | Description |
|-------|----------|-------------|
| Event data source | Yes | The data to stream (IAsyncEnumerable, Channel, timer, etc.) |
| Event types | No | Named event types for `event:` field |
| Client reconnection | No | Whether to support Last-Event-ID reconnection |

Comment on lines +21 to +26

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The markdown table under "## Inputs" is malformed (it uses double pipes like || Input | Required | Description |), so it won’t render correctly. Please switch to the standard table format used in other skills (single | delimiters with a separator row).

Copilot uses AI. Check for mistakes.
## Workflow

### Step 1: CRITICAL — There Is No Built-In MapSSE() or MapServerSentEvents()

ASP.NET Core has NO built-in SSE endpoint helper. You must manually write the SSE protocol.

```csharp
// COMMON MISTAKE: Trying to use a non-existent API
// app.MapSSE("/events", ...); // DOES NOT EXIST
// app.MapServerSentEvents("/events", ...); // DOES NOT EXIST
// app.UseServerSentEvents(); // DOES NOT EXIST

// CORRECT: Use a standard minimal API endpoint with manual SSE protocol
app.MapGet("/events", async (HttpContext context, CancellationToken ct) =>
{
// CRITICAL: Set these three headers for SSE
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
Comment on lines +43 to +44

Copilot AI Mar 10, 2026

Copy link

Choose a reason for hiding this comment

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

The ASP.NET Core examples set SSE headers via context.Response.Headers.ContentType / .CacheControl, but IHeaderDictionary doesn't expose these properties (this won't compile). Use context.Response.ContentType = "text/event-stream" and set cache-control via context.Response.Headers["Cache-Control"] = "no-cache" (or GetTypedHeaders().CacheControl). This pattern appears multiple times in this document, so update all occurrences for consistency.

Suggested change
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
context.Response.ContentType = "text/event-stream";
context.Response.Headers["Cache-Control"] = "no-cache";

Copilot uses AI. Check for mistakes.
context.Response.Headers["Connection"] = "keep-alive";

// CRITICAL: Disable response buffering for reverse proxies (nginx, etc.)
context.Response.Headers["X-Accel-Buffering"] = "no";

await context.Response.Body.FlushAsync(ct);

// Stream events...
});
```

### Step 2: CRITICAL — SSE Protocol Format

The SSE format has strict rules. Each field ends with `\n`, and each event ends with `\n\n` (double newline).

```csharp
// CRITICAL: The SSE format is NOT just "send text"
// Each event MUST end with TWO newlines (\n\n)

// Simple data event:
await context.Response.WriteAsync($"data: {message}\n\n", ct);
await context.Response.Body.FlushAsync(ct);

// COMMON MISTAKE: Forgetting the double newline
// await context.Response.WriteAsync($"data: {message}\n", ct); // WRONG - event never completes

// Named event with id (for reconnection):
await context.Response.WriteAsync($"id: {eventId}\n", ct);
await context.Response.WriteAsync($"event: userJoined\n", ct);
await context.Response.WriteAsync($"data: {jsonPayload}\n\n", ct);
await context.Response.Body.FlushAsync(ct);

// Multi-line data (each line needs "data: " prefix):
await context.Response.WriteAsync($"data: line 1\n", ct);
await context.Response.WriteAsync($"data: line 2\n", ct);
await context.Response.WriteAsync($"data: line 3\n\n", ct); // Only last line gets double \n
await context.Response.Body.FlushAsync(ct);
```

### Step 3: CRITICAL — Flush After Every Event

```csharp
// CRITICAL: You MUST flush after every event, otherwise the client
// won't receive anything until the buffer fills up

// Option A: Flush manually after each event
await context.Response.WriteAsync($"data: {msg}\n\n", ct);
await context.Response.Body.FlushAsync(ct); // CRITICAL

// Option B: Use StreamWriter with AutoFlush = true
await using var writer = new StreamWriter(context.Response.Body, leaveOpen: true);
writer.AutoFlush = true; // Flushes after every Write
await writer.WriteLineAsync($"data: {msg}\n"); // Note: WriteLine adds one \n, we add one more
Comment on lines +96 to +97

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

The StreamWriter example is misleading: WriteLineAsync appends TextWriter.NewLine (often \r\n on Windows), not a single \n as the comment states. For SSE it’s safer to explicitly write \n\n (or set writer.NewLine = "\n") so event framing is consistent across OSes.

Suggested change
writer.AutoFlush = true; // Flushes after every Write
await writer.WriteLineAsync($"data: {msg}\n"); // Note: WriteLine adds one \n, we add one more
writer.AutoFlush = true; // Flushes after every Write
writer.NewLine = "\n"; // Ensure consistent '\n' line endings across OSes
await writer.WriteLineAsync($"data: {msg}"); // Writes "data" line ending with '\n'
await writer.WriteLineAsync(); // Writes the blank line that terminates the SSE event

Copilot uses AI. Check for mistakes.

// COMMON MISTAKE: Forgetting FlushAsync — client sees nothing
// await context.Response.WriteAsync($"data: hello\n\n", ct);
// // Missing FlushAsync! Client receives nothing until connection closes.
```

### Step 4: CRITICAL — Handle Client Disconnection with RequestAborted

```csharp
app.MapGet("/events/stream", async (HttpContext context) =>
{
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";

// CRITICAL: Use RequestAborted to detect client disconnect
var ct = context.RequestAborted;

try
{
while (!ct.IsCancellationRequested)
{
var data = await GetNextEvent(ct);
await context.Response.WriteAsync($"data: {data}\n\n", ct);
await context.Response.Body.FlushAsync(ct);
}
}
catch (OperationCanceledException)
{
// Client disconnected — this is normal, not an error
// COMMON MISTAKE: Logging this as an error or letting it propagate
}
});
```

### Step 5: Support Client Reconnection with Last-Event-ID

```csharp
app.MapGet("/events", async (HttpContext context) =>
{
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";

// CRITICAL: When EventSource reconnects, it sends Last-Event-ID header
var lastEventId = context.Request.Headers["Last-Event-ID"].FirstOrDefault();

// Set retry interval (milliseconds) — how long client waits before reconnecting
await context.Response.WriteAsync($"retry: 5000\n\n");
await context.Response.Body.FlushAsync();

var startFrom = lastEventId != null ? int.Parse(lastEventId) + 1 : 0;

Copilot AI Mar 6, 2026

Copy link

Choose a reason for hiding this comment

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

This reconnection example assumes Last-Event-ID is an int (int.Parse(lastEventId)), but SSE event IDs are defined as opaque strings and the header can contain non-numeric values. Prefer treating it as a string (or using int.TryParse if you want to keep the numeric example) to avoid throwing and to better match the SSE spec.

Suggested change
var startFrom = lastEventId != null ? int.Parse(lastEventId) + 1 : 0;
var startFrom = 0;
if (!string.IsNullOrEmpty(lastEventId) && int.TryParse(lastEventId, out var parsedId))
{
startFrom = parsedId + 1;
}

Copilot uses AI. Check for mistakes.

var ct = context.RequestAborted;
var eventId = startFrom;

try
{
while (!ct.IsCancellationRequested)
{
var data = await GetNextEvent(eventId, ct);
// CRITICAL: Send id: field so client can reconnect from this point
await context.Response.WriteAsync($"id: {eventId}\ndata: {data}\n\n", ct);
await context.Response.Body.FlushAsync(ct);
eventId++;
}
}
catch (OperationCanceledException) { }
});
```

### Step 6: Complete Implementation with IAsyncEnumerable

```csharp
app.MapGet("/events/notifications", async (
HttpContext context,
INotificationService notifications) =>
{
context.Response.Headers.ContentType = "text/event-stream";
context.Response.Headers.CacheControl = "no-cache";
context.Response.Headers["Connection"] = "keep-alive";
context.Response.Headers["X-Accel-Buffering"] = "no";

var ct = context.RequestAborted;
var eventId = 0;

try
{
await foreach (var notification in notifications.StreamAsync(ct))
{
var json = JsonSerializer.Serialize(notification);
await context.Response.WriteAsync(
$"id: {eventId++}\nevent: {notification.Type}\ndata: {json}\n\n", ct);
await context.Response.Body.FlushAsync(ct);
}
}
catch (OperationCanceledException) { }

// CRITICAL: Don't return a value — the response is already written to
// COMMON MISTAKE: return Results.Ok() after streaming — this corrupts the response
});
```

## Common Mistakes

1. **Using a non-existent MapSSE() or MapServerSentEvents() method**: ASP.NET Core has no built-in SSE helper. You must manually set headers and write the SSE protocol format.
2. **Forgetting double newline**: Events MUST end with `\n\n`. A single `\n` means the event is not complete and the client won't process it.
3. **Not flushing**: Without `FlushAsync()` after each event, the response is buffered and the client receives nothing until disconnect.
4. **Not handling RequestAborted**: The loop runs forever if you don't check `context.RequestAborted`. `OperationCanceledException` on disconnect is normal.
5. **Returning a result after streaming**: Don't `return Results.Ok()` after writing SSE events — the response body is already being written.
6. **Missing Content-Type header**: Must be exactly `text/event-stream`, not `application/json` or `text/plain`.
7. **Missing X-Accel-Buffering: no**: Reverse proxies (nginx) buffer responses by default, breaking SSE.
Loading
Loading