基于 SOC 职业分类
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
直接命令不会经过审查 Prompt;运行前请先检查来源。
npx skills add https://github.com/rudironsoni/Synaxis --skill dotnet-realtime-communication命令会保持在同一行。复制前请横向滚动并检查完整内容。
想先保存到本地?可下载 SkillsMP 当前能够提供的文件。
正在显示 SKILL.md
AI-powered wiki generation for code repositories with commands, agents, and skills
Routes .NET/C# work to domain skills. Loads coding-standards for code paths.
Skill manifest management for dotnet-agent-harness. Tracks skill dependencies, conflicts, version compatibility, and provides validation and resolution tools. Triggers on: skill manifest, dependency resolution, skill compatibility, version conflicts, build manifest, validate dependencies.
| name | dotnet-realtime-communication |
| category | architecture |
| subcategory | messaging |
| description | Builds real-time features. SignalR hubs, SSE (.NET 10), JSON-RPC 2.0, gRPC streaming, scaling. |
| license | MIT |
| targets | ["*"] |
| tags | ["api","dotnet","skill"] |
| version | 0.0.1 |
| author | dotnet-agent-harness |
| invocable | true |
| claudecode | {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} |
| codexcli | {"short-description":".NET skill guidance for api tasks"} |
| opencode | {"allowed-tools":["Read","Grep","Glob","Bash","Write","Edit"]} |
| copilot | {} |
| geminicli | {} |
| antigravity | {} |
Real-time communication patterns for .NET applications. Compares SignalR (full-duplex over WebSockets with automatic fallback), Server-Sent Events (SSE, built-in to ASP.NET Core in .NET 10), JSON-RPC 2.0 (structured request-response over any transport), and gRPC streaming (high-performance binary streaming). Provides decision guidance for choosing the right protocol based on requirements.
Cross-references: [skill:dotnet-grpc] for gRPC streaming implementation details and all four streaming patterns. See [skill:dotnet-integration-testing] for testing real-time communication endpoints. See [skill:dotnet-blazor-patterns] for Blazor-specific SignalR circuit management and render mode interaction.
| Protocol | Direction | Transport | Format | Browser Support | Best For |
|---|---|---|---|---|---|
| SignalR | Full-duplex | WebSocket, SSE, Long Polling (auto-negotiation) | JSON or MessagePack | Yes (JS/TS client) | Interactive apps, chat, dashboards, collaborative editing |
| SSE (.NET 10) | Server-to-client only | HTTP/1.1+ | Text (typically JSON lines) | Yes (native EventSource API) | Notifications, live feeds, status updates |
| JSON-RPC 2.0 | Request-response | Any (HTTP, WebSocket, stdio) | JSON | Depends on transport | Tooling protocols (LSP), structured RPC over simple transports |
| gRPC streaming | All four patterns | HTTP/2 | Protobuf (binary) | Limited (gRPC-Web) | Service-to-service, high-throughput, low-latency streaming |
EventSource API.SignalR provides real-time web functionality with automatic connection management and transport negotiation.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR(options =>
{
options.EnableDetailedErrors = builder.Environment.IsDevelopment();
options.MaximumReceiveMessageSize = 64 * 1024; // 64 KB
options.KeepAliveInterval = TimeSpan.FromSeconds(15);
});
var app = builder.Build();
app.MapHub<NotificationHub>("/hubs/notifications");
```text
### Hub Implementation
```csharp
public sealed class NotificationHub(
ILogger<NotificationHub> logger) : Hub
{
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier;
if (userId is not null)
{
await Groups.AddToGroupAsync(Context.ConnectionId, $"user:{userId}");
}
await base.OnConnectedAsync();
}
// Client-to-server method
public async Task SendMessage(string channel, string message)
{
// Broadcast to all clients in the channel group
await Clients.Group(channel).SendAsync("ReceiveMessage",
Context.UserIdentifier, message);
}
// Server-to-client streaming
public async IAsyncEnumerable<StockPrice> StreamPrices(
string symbol,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
yield return await GetLatestPrice(symbol, cancellationToken);
await Task.Delay(1000, cancellationToken);
}
}
}
```text
### Strongly-Typed Hubs
Use interfaces to get compile-time safety for client method calls:
```csharp
public interface INotificationClient
{
Task ReceiveMessage(string user, string message);
Task OrderStatusChanged(int orderId, string status);
}
public sealed class NotificationHub(
ILogger<NotificationHub> logger) : Hub<INotificationClient>
{
public async Task SendMessage(string channel, string message)
{
// Compile-time checked -- no magic strings
await Clients.Group(channel).ReceiveMessage(
Context.UserIdentifier!, message);
}
}
```text
### Sending from Outside Hubs
Inject `IHubContext` to send messages from background services or controllers:
```csharp
public sealed class OrderService(
IHubContext<NotificationHub, INotificationClient> hubContext)
{
public async Task UpdateOrderStatus(int orderId, string userId, string status)
{
// Send to specific user group
await hubContext.Clients.Group($"user:{userId}")
.OrderStatusChanged(orderId, status);
}
}
```text
### Transport Negotiation
SignalR automatically negotiates the best transport:
1. **WebSocket** (preferred) -- full-duplex, lowest latency
2. **Server-Sent Events** -- server-to-client only, falls back when WebSockets unavailable
3. **Long Polling** -- universal fallback, highest latency
Force a specific transport when needed:
```csharp
// Server: disable specific transports
app.MapHub<NotificationHub>("/hubs/notifications", options =>
{
options.Transports = HttpTransportType.WebSockets |
HttpTransportType.ServerSentEvents;
// Disables Long Polling
});
```text
### MessagePack Protocol
Use MessagePack for smaller payloads and faster serialization:
```csharp
// Server
builder.Services.AddSignalR()
.AddMessagePackProtocol();
// Client (JavaScript)
// new signalR.HubConnectionBuilder()
// .withUrl("/hubs/notifications")
// .withHubProtocol(new signalR.protocols.msgpack.MessagePackHubProtocol())
// .build();
```text
### Connection Lifecycle
Override `OnConnectedAsync` and `OnDisconnectedAsync` to manage connection state:
```csharp
public sealed class NotificationHub(
ILogger<NotificationHub> logger,
IConnectionTracker tracker) : Hub<INotificationClient>
{
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier;
var connectionId = Context.ConnectionId;
logger.LogInformation("Client {ConnectionId} connected (user: {UserId})",
connectionId, userId);
// Track connection for presence features
if (userId is not null)
{
await tracker.AddConnectionAsync(userId, connectionId);
await Groups.AddToGroupAsync(connectionId, $"user:{userId}");
}
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.UserIdentifier;
var connectionId = Context.ConnectionId;
if (exception is not null)
{
logger.LogWarning(exception,
"Client {ConnectionId} disconnected with error", connectionId);
}
if (userId is not null)
{
await tracker.RemoveConnectionAsync(userId, connectionId);
}
await base.OnDisconnectedAsync(exception);
}
}
```text
### Groups Management
Groups provide a lightweight pub/sub mechanism. Connections can belong to multiple groups and group membership is managed per-connection:
```csharp
public sealed class ChatHub : Hub<IChatClient>
{
// Join a room (called by clients)
public async Task JoinRoom(string roomName)
{
await Groups.AddToGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).UserJoined(Context.UserIdentifier!, roomName);
}
// Leave a room
public async Task LeaveRoom(string roomName)
{
await Groups.RemoveFromGroupAsync(Context.ConnectionId, roomName);
await Clients.Group(roomName).UserLeft(Context.UserIdentifier!, roomName);
}
// Send to specific group
public async Task SendToRoom(string roomName, string message)
{
await Clients.Group(roomName).ReceiveMessage(
Context.UserIdentifier!, message);
}
// Send to all except caller
public async Task BroadcastExceptSelf(string message)
{
await Clients.Others.ReceiveMessage(
Context.UserIdentifier!, message);
}
}
```text
Groups are not persisted -- they are cleared when a connection disconnects. Re-add connections to groups in `OnConnectedAsync` if needed (e.g., from a database or cache).
### Client-to-Server Streaming
Clients can stream data to the hub using `IAsyncEnumerable<T>` or `ChannelReader<T>`:
```csharp
public sealed class UploadHub : Hub
{
// Accept a stream of items from the client
public async Task UploadData(
IAsyncEnumerable<SensorReading> stream,
CancellationToken cancellationToken)
{
await foreach (var reading in stream.WithCancellation(cancellationToken))
{
await ProcessReading(reading);
}
}
}
```text
### Authentication
SignalR uses the same authentication as the ASP.NET Core host. For WebSocket connections, the access token is sent via query string because WebSocket does not support custom headers:
```csharp
// Server: configure JWT for SignalR
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.Authority = "https://identity.example.com";
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
// Read token from query string for WebSocket requests
var accessToken = context.Request.Query["access_token"];
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
path.StartsWithSegments("/hubs"))
{
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapHub<NotificationHub>("/hubs/notifications")
.RequireAuthorization();
```text
Access `Context.UserIdentifier` in the hub to identify the authenticated user. By default this maps to the `ClaimTypes.NameIdentifier` claim. Customize with `IUserIdProvider`:
```csharp
public sealed class EmailUserIdProvider : IUserIdProvider
{
public string? GetUserId(HubConnectionContext connection)
{
return connection.User?.FindFirst(ClaimTypes.Email)?.Value;
}
}
// Register
builder.Services.AddSingleton<IUserIdProvider, EmailUserIdProvider>();
```text
### Scaling with Backplane
For multi-server deployments, use a backplane to synchronize messages across instances. Without a backplane, messages sent on one server are not visible to connections on other servers.
**Redis backplane:**
```csharp
builder.Services.AddSignalR()
.AddStackExchangeRedis(builder.Configuration.GetConnectionString("Redis")!,
options =>
{
options.Configuration.ChannelPrefix =
RedisChannel.Literal("MyApp:");
});
```text
**Azure SignalR Service (managed backplane):**
```csharp
builder.Services.AddSignalR()
.AddAzureSignalR(builder.Configuration["Azure:SignalR:ConnectionString"]);
```csharp
Azure SignalR Service offloads connection management entirely -- the ASP.NET Core server handles hub logic while Azure manages WebSocket connections, scaling, and message routing.
---
## Server-Sent Events (SSE) -- .NET 10
.NET 10 adds built-in SSE support to ASP.NET Core, making server-to-client streaming straightforward without additional packages.
### Minimal API Endpoint
```csharp
app.MapGet("/events/orders", async (
OrderEventService eventService,
CancellationToken cancellationToken) =>
{
// TypedResults.ServerSentEvents returns an SSE response
return TypedResults.ServerSentEvents(
eventService.GetOrderEventsAsync(cancellationToken));
});
```text
### Event Source Implementation
```csharp
public sealed class OrderEventService
{
public async IAsyncEnumerable<SseItem<OrderEvent>> GetOrderEventsAsync(
[EnumeratorCancellation] CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
var evt = await WaitForNextEvent(cancellationToken);
yield return new SseItem<OrderEvent>(evt, "order-update");
}
}
}
```text
### Browser Client
```javascript
const source = new EventSource('/events/orders');
source.addEventListener('order-update', (event) => {
const order = JSON.parse(event.data);
updateDashboard(order);
});
source.onerror = () => {
// EventSource automatically reconnects
console.log('SSE connection lost, reconnecting...');
};
```text
### When to Use SSE Over SignalR
- **One-way push only** -- SSE is simpler when you do not need client-to-server messages
- **Browser native** -- no JavaScript library needed (uses `EventSource` API)
- **Automatic reconnection** -- browsers reconnect automatically with `Last-Event-ID`
- **HTTP/1.1 compatible** -- works through proxies that do not support WebSocket upgrade
---
## JSON-RPC 2.0
JSON-RPC 2.0 is a stateless, transport-agnostic remote procedure call protocol encoded in JSON. It is the foundation of the Language Server Protocol (LSP) and is used in some .NET tooling scenarios.
### Protocol Structure
```json
// Request
{"jsonrpc": "2.0", "method": "textDocument/completion", "params": {...}, "id": 1}
// Response
{"jsonrpc": "2.0", "result": {...}, "id": 1}
// Notification (no response expected)
{"jsonrpc": "2.0", "method": "textDocument/didChange", "params": {...}}
// Error
{"jsonrpc": "2.0", "error": {"code": -32600, "message": "Invalid Request"}, "id": 1}
```json
### StreamJsonRpc (.NET Library)
`StreamJsonRpc` is the primary .NET library for JSON-RPC 2.0:
```xml
<PackageReference Include="StreamJsonRpc" Version="2.*" />
```json
```csharp
// Server: expose methods via JSON-RPC over a stream
using StreamJsonRpc;
public sealed class CalculatorService
{
public int Add(int a, int b) => a + b;
public Task<double> DivideAsync(double a, double b) =>
b == 0 ? throw new ArgumentException("Division by zero")
: Task.FromResult(a / b);
}
// Wire up over a WebSocket -- UseWebSockets() is required for upgrade handling
app.UseWebSockets();
app.Map("/jsonrpc", async (HttpContext context) =>
{
if (!context.WebSockets.IsWebSocketRequest)
{
context.Response.StatusCode = 400;
return;
}
var ws = await context.WebSockets.AcceptWebSocketAsync();
using var rpc = new JsonRpc(new WebSocketMessageHandler(ws));
rpc.AddLocalRpcTarget(new CalculatorService());
rpc.StartListening();
await rpc.Completion;
});
```text
```csharp
// Client
using var ws = new ClientWebSocket();
await ws.ConnectAsync(new Uri("ws://localhost:5000/jsonrpc"),
CancellationToken.None);
using var rpc = new JsonRpc(new WebSocketMessageHandler(ws));
rpc.StartListening();
var result = await rpc.InvokeAsync<int>("Add", 2, 3);
// result == 5
```text
### When to Use JSON-RPC 2.0
- Building or integrating with Language Server Protocol (LSP) implementations
- Simple RPC over WebSocket or stdio where gRPC is too heavyweight
- Interoperating with non-.NET systems that speak JSON-RPC
- Tooling and editor integrations
---
## gRPC Streaming
See [skill:dotnet-grpc] for complete gRPC implementation details including all four streaming patterns (unary, server streaming, client streaming, bidirectional streaming), authentication, load balancing, and health checks.
### Quick Decision: gRPC Streaming vs SignalR vs SSE
| Requirement | Choose |
|-------------|--------|
| Service-to-service, both .NET | gRPC streaming |
| Browser client needs bidirectional | SignalR |
| Browser client needs server push only | SSE |
| Maximum throughput, binary payloads | gRPC streaming |
| Automatic reconnection with browser clients | SSE (native) or SignalR (built-in) |
| Multiple client platforms (JS, mobile, .NET) | SignalR |
---
## Key Principles
- **Default to SignalR for browser-facing real-time** -- it handles transport negotiation, reconnection, and grouping out of the box
- **Use SSE for simple server push** -- .NET 10 built-in support makes it the lightest option for one-way notifications
- **Use gRPC streaming for service-to-service** -- highest performance, strongly typed contracts, all four streaming patterns
- **Use JSON-RPC 2.0 for tooling protocols** -- when you need structured RPC over simple transports (WebSocket, stdio)
- **Use strongly-typed hubs** -- `Hub<T>` catches method name typos at compile time instead of runtime
- **Scale SignalR with a backplane** -- Redis Azure SignalR Service multi-server deployments
See [skill:dotnet-native-aot] AOT compilation pipeline [skill:dotnet-aot-architecture] AOT-compatible real-time communication patterns.
---
## Agent Gotchas
1. **Do use SignalR SSE suffices** -- you only need server-to-client push without bidirectional communication, SSE simpler lighter.
2. **Do forget `()` the server the client uses MessagePack** -- mismatched protocols cause silent connection failures.
3. **Do use Long Polling transport SignalR unless ** -- it has significantly higher latency server resource usage compared to WebSockets.
4. **Do store connection IDs -term** -- SignalR connection IDs change reconnection. Use user identifiers groups addressing.
5. **Do use gRPC streaming to browsers directly** -- browsers support HTTP/2 trailers natively. Use gRPC-Web a proxy choose SignalR/SSE instead.
6. **Do confuse SSE WebSocket** -- SSE (). If you need client-to-server messages, use SignalR WebSocket directly.
7. **Do forget `OnMessageReceived` JWT SignalR** -- WebSocket connections cannot send custom HTTP headers after the initial handshake. The access token must be read the query `JwtBearerEvents.OnMessageReceived`.
8. **Do assume membership persists across reconnections** -- groups are tied to connection IDs, which change reconnect. Re- connections to groups `OnConnectedAsync`.
9. **Do deploy multi-server SignalR without a backplane** -- without Redis Azure SignalR Service, messages sent one server instance are invisible to connections other instances.
---
## Attribution
Adapted [Aaronontheweb/dotnet-skills]() ().
---
## References
- [SignalR overview]()
- [SignalR hubs]()
- [Server-Sent Events .NET 10]()
- [StreamJsonRpc]()
- [gRPC streaming]()
- [SignalR scaling Redis]()
- [SignalR authentication authorization]()
- [Azure SignalR Service]()
Primary approach: Use Serena symbol operations for efficient code navigation:
serena_find_symbol instead of text searchserena_get_symbols_overview for file organizationserena_find_referencing_symbols for impact analysisserena_replace_symbol_body for clean modificationsWhen to use Serena vs traditional tools:
Example workflow:
# Instead of:
Read: src/Services/OrderService.cs
Grep: "public void ProcessOrder"
# Use:
serena_find_symbol: "OrderService/ProcessOrder"
serena_get_symbols_overview: "src/Services/OrderService.cs"