Skip to content

Add implementing-websocket-endpoints skill#266

Open
mrsharm wants to merge 2 commits intodotnet:mainfrom
mrsharm:musharm/implementing-websocket-endpoints
Open

Add implementing-websocket-endpoints skill#266
mrsharm wants to merge 2 commits intodotnet:mainfrom
mrsharm:musharm/implementing-websocket-endpoints

Conversation

@mrsharm
Copy link
Copy Markdown
Member

@mrsharm mrsharm commented Mar 6, 2026

Summary

Adds a skill for implementing raw WebSocket endpoints in ASP.NET Core 8+.

Note: Replaces #142 (migrated from skills-old repo to new plugins/ structure).

Eval Results (3-run)

  • Overall: +28.1% improvement

Files

  • plugins/dotnet/skills/implementing-websocket-endpoints/SKILL.md
  • tests/dotnet/implementing-websocket-endpoints/eval.yaml

Copilot AI review requested due to automatic review settings March 6, 2026 17:23
@mrsharm
Copy link
Copy Markdown
Member Author

mrsharm commented Mar 6, 2026

Migration Note

This PR replaces #142 which was opened from mrsharm/skills-old. The skill and eval files have been migrated to the new plugins/ directory structure:

  • src/dotnet/skills/implementing-websocket-endpoints/plugins/dotnet/skills/implementing-websocket-endpoints/
  • src/dotnet/tests/implementing-websocket-endpoints/tests/dotnet/implementing-websocket-endpoints/

All prior review feedback from #142 still applies — please see that PR for the full discussion history.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new implementing-websocket-endpoints skill to the plugins/dotnet/skills/ directory for ASP.NET Core 8+ WebSocket guidance, along with an evaluation scenario in tests/dotnet/implementing-websocket-endpoints/ and CODEOWNERS entries for the new skill. The skill covers common WebSocket pitfalls such as the absence of MapWebSocket(), proper close semantics (CloseOutputAsync vs CloseAsync), fragmented message handling, thread-safe connection management, and query-string authentication.

Changes:

  • New SKILL.md providing step-by-step guidance and common mistakes for implementing WebSocket endpoints in ASP.NET Core 8+
  • New eval.yaml with a scenario testing the model's ability to correctly advise on WebSocket implementation
  • CODEOWNERS entry for the new skill and test directories

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
plugins/dotnet/skills/implementing-websocket-endpoints/SKILL.md New skill document with WebSocket implementation guidance, code examples, and common pitfalls
tests/dotnet/implementing-websocket-endpoints/eval.yaml Evaluation scenario with assertions and rubric testing WebSocket knowledge
.github/CODEOWNERS Adds ownership entries for the new skill and test directories

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +71 to +86
app.UseWebSockets(new WebSocketOptions
{
// CRITICAL: KeepAliveInterval sends ping frames to keep connection alive
// Default is 2 minutes. Set to match your infrastructure timeouts.
KeepAliveInterval = TimeSpan.FromSeconds(30),

// Allowed origins (for browser CORS protection)
// CRITICAL: Without this, ANY website can open a WebSocket to your API
AllowedOrigins = { "https://myapp.com", "https://www.myapp.com" }
});

// CRITICAL ORDERING: UseWebSockets MUST come before the endpoint that handles WebSockets
app.UseWebSockets(); // ← BEFORE
app.UseRouting();
app.UseAuthorization();
// WebSocket handling endpoint comes after routing
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The Step 2 code block registers UseWebSockets twice, which is misleading and incorrect. Lines 71–80 call app.UseWebSockets(new WebSocketOptions { ... }) with the configured options (the intended "CORRECT" usage), and then line 83 calls app.UseWebSockets() again (without any options) just to illustrate ordering. These two calls would both register the middleware in sequence, with the second one overriding the first's configuration. The ordering comment and the configuration example should be combined into a single app.UseWebSockets(new WebSocketOptions { ... }) call placed before UseRouting() and UseAuthorization() — not separated into two UseWebSockets() calls.

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +121

// Echo back (or process the message)
var responseBytes = Encoding.UTF8.GetBytes($"Echo: {message}");
await webSocket.SendAsync(
new ArraySegment<byte>(responseBytes),
WebSocketMessageType.Text,
endOfMessage: true, // ← MUST set this for the last (or only) fragment
ct);
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The receive loop has a logic error around fragmented messages. The code checks if (!result.EndOfMessage) and the comment says "Don't process partial messages!", but the block is empty and execution falls through to the SendAsync echo on lines 116–121 unconditionally — including when EndOfMessage is false. A partial message will be echoed immediately instead of being accumulated. The code should use else (or a continue/return inside the if block) so that echoing only happens after a complete message is assembled. This is especially ironic given that the comment explicitly calls this a "CRITICAL" pitfall to avoid.

Suggested change
// Echo back (or process the message)
var responseBytes = Encoding.UTF8.GetBytes($"Echo: {message}");
await webSocket.SendAsync(
new ArraySegment<byte>(responseBytes),
WebSocketMessageType.Text,
endOfMessage: true, // ← MUST set this for the last (or only) fragment
ct);
else
{
// Echo back (or process the message) once the full message is received
var responseBytes = Encoding.UTF8.GetBytes($"Echo: {message}");
await webSocket.SendAsync(
new ArraySegment<byte>(responseBytes),
WebSocketMessageType.Text,
endOfMessage: true, // ← MUST set this for the last (or only) fragment
ct);
}

Copilot uses AI. Check for mistakes.
// CRITICAL: ToList() snapshot to avoid modification during iteration
var tasks = _connections.Values
.Where(s => s.State == WebSocketState.Open) // Only open sockets
.Select(s => s.SendAsync(segment, WebSocketMessageType.Text, true, ct));
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The comment on line 166 says "ToList() snapshot to avoid modification during iteration," but ToList() is never called. The LINQ chain .Where(...).Select(...) is deferred/lazy — Task.WhenAll materializes it, but only at execution time, not as a snapshot upfront. While ConcurrentDictionary.Values is a thread-safe snapshot in .NET, the comment is misleading because it promises a ToList() that isn't there. This creates a discrepancy between the stated intent and the actual code, which could mislead developers into thinking the pattern is safe by virtue of ToList() when it actually isn't being called. Either call .ToList() on the LINQ chain or update the comment to accurately reflect why this is safe without it.

Suggested change
.Select(s => s.SendAsync(segment, WebSocketMessageType.Text, true, ct));
.Select(s => s.SendAsync(segment, WebSocketMessageType.Text, true, ct))
.ToList();

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +65
builder.Services.AddWebSockets(options =>
{
// WRONG — this method doesn't exist! Use raw middleware options:
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

The code incorrectly states that builder.Services.AddWebSockets(options => {...}) is a method that "doesn't exist." In fact, AddWebSockets() (without an options lambda) does exist in ASP.NET Core's Microsoft.AspNetCore.WebSockets package — it's used to add WebSocket services (relevant in certain configurations). The claim should be narrowed: AddWebSockets with an options-configuring lambda does not exist, but AddWebSockets() without parameters does. As currently written, this misleads developers into believing AddWebSockets is an entirely fictional API, which is incorrect and could cause confusion.

Suggested change
builder.Services.AddWebSockets(options =>
{
// WRONG — this method doesn't exist! Use raw middleware options:
// Note: A parameterless AddWebSockets() exists in the Microsoft.AspNetCore.WebSockets package.
// WRONG — this overload with an options lambda does NOT exist in ASP.NET Core 8+:
builder.Services.AddWebSockets(options =>
{
// Trying to configure options here will not compile; configure via UseWebSockets instead.

Copilot uses AI. Check for mistakes.
@mrsharm
Copy link
Copy Markdown
Member Author

mrsharm commented Mar 6, 2026

Feedback carried over from #142

PR #142 — Add implementing-websocket-endpoints skill (open)

11 review threads — Reviewers: copilot-pull-request-reviewer, BrennanConroy

Reviewer Feedback
copilot Entire file incorrectly wrapped in ```skill code block
copilot Duplicate UseWebSockets call (line 71 and 83)
copilot Logic inconsistency with EndOfMessage — comment says "Don't process partial messages!" but processes anyway
copilot eval.yaml has leading spaces before scenarios:
copilot Comment about AddWebSockets not existing is inaccurate
copilot Comment says ToList() snapshot but code doesn't call ToList()
copilot Browser WebSocket API doesn't support custom headers at all, not just "after initial handshake"
copilot access_token in query string risks leaking via logs/Referer headers
BrennanConroy Consider setting KeepAliveTimeout as well
BrennanConroy What about binary message handling (else binary??)

@mrsharm mrsharm requested a review from a team March 6, 2026 20:29
@mrsharm
Copy link
Copy Markdown
Member Author

mrsharm commented Mar 6, 2026

/evaluate

@mrsharm
Copy link
Copy Markdown
Member Author

mrsharm commented Mar 6, 2026

CC: @BrennanConroy - just implemented the suggested feedback. Could you please take another look?

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 6, 2026

Skill Validation Results

Skill Scenario Baseline With Skill Δ Skills Loaded Overfit Verdict
implementing-websocket-endpoints Implement WebSocket chat endpoint in ASP.NET Core 8 3.0/5 5.0/5 +2.0 ✅ implementing-websocket-endpoints; tools: skill, report_intent, view ✅ 0.13
implementing-websocket-endpoints Fix WebSocket CloseAsync deadlock 4.0/5 5.0/5 +1.0 ✅ implementing-websocket-endpoints; tools: skill ✅ 0.13
implementing-websocket-endpoints WebSocket should not be used for server-to-client streaming 5.0/5 5.0/5 0.0 ℹ️ not activated (expected) ✅ 0.13
implementing-websocket-endpoints Handle fragmented WebSocket messages correctly 5.0/5 5.0/5 0.0 ⚠️ NOT ACTIVATED ✅ 0.13

Model: claude-opus-4.6 | Judge: claude-opus-4.6

Full results

@ViktorHofer
Copy link
Copy Markdown
Member

WebSocket should not be used for server-to-client streaming

Test doesn't activate. As in the other PR, if this is intentional, specify that in eval.yml via expect_activation: false

@mrsharm
Copy link
Copy Markdown
Member Author

mrsharm commented Mar 8, 2026

/evaluate

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 8, 2026

✅ Evaluation completed. View results | View workflow run

@ManishJayaswal
Copy link
Copy Markdown
Contributor

@mrsharm - - the repo has undergone some restructuring to make everything more organized. Hence, we are asking all open PRs to update the branch. Sorry about this.
This skill should be under ASP plugin. Please update the PR and submit again.
@adityamandaleeka @BrennanConroy - please review

Per repo restructuring feedback, ASP.NET Core specific skills should
be under the aspnetcore plugin rather than the dotnet plugin.
Copilot AI review requested due to automatic review settings March 10, 2026 20:17
@mrsharm mrsharm force-pushed the musharm/implementing-websocket-endpoints branch from 4f6c4b8 to 8756d05 Compare March 10, 2026 20:17
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +1 to +5
scenarios:
- name: "Implement WebSocket chat endpoint in ASP.NET Core 8"
prompt: |
I need to add a WebSocket endpoint to my ASP.NET Core 8 API for a real-time chat feature. Requirements:

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 PR description/file list mentions plugins/dotnet/... and tests/dotnet/..., but this change is under plugins/aspnetcore/... and tests/aspnetcore/.... Please update the PR description (and any related docs) so reviewers and automation can correctly associate the skill with the aspnetcore plugin.

Copilot uses AI. Check for mistakes.
- "Used CloseOutputAsync (not CloseAsync) when responding to client-initiated close to avoid deadlock"
- "Implemented a thread-safe connection manager using ConcurrentDictionary for tracking and broadcasting to connected clients"
- "Handled authentication via query string token since browser WebSocket API cannot send custom headers after handshake"
expect_tools: ["bash"]
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.

expect_tools: ["bash"] forces the scenario to fail unless the model makes a bash tool call, but this scenario has no setup/files and only checks textual output. This constraint can make the eval flaky for no benefit; consider removing expect_tools here (or add a setup/assertion that genuinely requires bash).

Suggested change
expect_tools: ["bash"]

Copilot uses AI. Check for mistakes.
Comment on lines +95 to +104
if (!result.EndOfMessage)
{
// Accumulate into a MemoryStream or larger buffer
// Do NOT process until EndOfMessage == true
result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), ct);
continue;
}

var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
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 text-message fragmentation handling is incorrect: when EndOfMessage is false, the code receives the next fragment but discards the previously received bytes, so large messages will be truncated. Update the sample to actually accumulate fragments (e.g., append result.Count bytes from each fragment into a buffer/MemoryStream) and only decode/deserialise once EndOfMessage is true.

Suggested change
if (!result.EndOfMessage)
{
// Accumulate into a MemoryStream or larger buffer
// Do NOT process until EndOfMessage == true
result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), ct);
continue;
}
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
using var messageBuffer = new System.IO.MemoryStream();
// Write the first fragment
messageBuffer.Write(buffer, 0, result.Count);
// If the message is fragmented, keep reading until EndOfMessage == true
while (!result.EndOfMessage)
{
result = await webSocket.ReceiveAsync(
new ArraySegment<byte>(buffer), ct);
if (result.MessageType != WebSocketMessageType.Text)
{
// Unexpected change of message type; break out to avoid corrupting the buffer
break;
}
messageBuffer.Write(buffer, 0, result.Count);
}
var messageBytes = messageBuffer.ToArray();
var message = Encoding.UTF8.GetString(messageBytes, 0, messageBytes.Length);

Copilot uses AI. Check for mistakes.
Comment on lines +31 to +38
app.MapWebSocket("/ws", handler); // ❌ NOT a real method
app.MapGet("/ws").UseWebSocket(); // ❌ NOT a real method

// CORRECT — WebSocket is middleware-based, not endpoint-routing:
app.UseWebSockets(); // ← Register the middleware

// Then handle WebSocket requests in regular middleware or endpoints:
app.Map("/ws", async (HttpContext context) =>
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 examples use /ws as the WebSocket path (Step 1/Step 5), but the eval scenario and opening requirements describe /ws/chat. To avoid confusion and copy/paste errors, consider making the path consistent throughout the skill (either update the examples to /ws/chat or make the eval prompt generic).

Suggested change
app.MapWebSocket("/ws", handler); // ❌ NOT a real method
app.MapGet("/ws").UseWebSocket(); // ❌ NOT a real method
// CORRECT — WebSocket is middleware-based, not endpoint-routing:
app.UseWebSockets(); // ← Register the middleware
// Then handle WebSocket requests in regular middleware or endpoints:
app.Map("/ws", async (HttpContext context) =>
app.MapWebSocket("/ws/chat", handler); // ❌ NOT a real method
app.MapGet("/ws/chat").UseWebSocket(); // ❌ NOT a real method
// CORRECT — WebSocket is middleware-based, not endpoint-routing:
app.UseWebSockets(); // ← Register the middleware
// Then handle WebSocket requests in regular middleware or endpoints:
app.Map("/ws/chat", async (HttpContext context) =>

Copilot uses AI. Check for mistakes.
@danroth27
Copy link
Copy Markdown
Member

Note that for most WebSocket based endpoints built with ASP.NET Core we would recommend using SignalR. Is this skill overly specialized?

@danmoseley
Copy link
Copy Markdown
Member

If #207 creates a dotnet-aspnet plugin, this skill should presumably move there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants