.claude/skills/gen-signalr-hub/SKILL.md
Use when adding real-time push communication to an ASP.NET Core application. Generates a strongly-typed SignalR hub with a client interface, group management, DI registration, and authorization attributes. Also invoke when the user mentions: SignalR hub, real-time hub, WebSocket hub, push notifications, Hub class. Domain: Code Generation, Real-Time. Level: Intermediate.
npx skillsauth add klod68/littlerae gen-signalr-hubInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Generate a SignalR hub for ${1:Feature}.
${2:MyApp}${3:Brief description of real-time feature}${4:yes | no}${2}.Hubs.I${1}HubClient.cs
/// <summary>
/// Defines methods the server can invoke on connected <see cref="${1}Hub"/> clients.
/// </summary>
public interface I${1}HubClient
{
/// <summary>Notifies the client of a new event.</summary>
Task Receive${1}Update(${1}UpdateDto update);
/// <summary>Notifies the client that a user joined the group.</summary>
Task UserJoined(string userId);
/// <summary>Notifies the client that a user left the group.</summary>
Task UserLeft(string userId);
}
${2}.Hubs.${1}Hub.cs
/// <summary>
/// Real-time hub for ${3:description}.
/// </summary>
[Authorize] // Remove if ${4} is "no"
internal sealed class ${1}Hub : Hub<I${1}HubClient>
{
private readonly ILogger<${1}Hub> _logger;
public ${1}Hub(ILogger<${1}Hub> logger)
{
ArgumentNullException.ThrowIfNull(logger);
_logger = logger;
}
public override async Task OnConnectedAsync()
{
var userId = Context.UserIdentifier
?? throw new InvalidOperationException("User identity not available.");
_logger.LogDebug("User {UserId} connected to {Hub}",
userId, nameof(${1}Hub));
await base.OnConnectedAsync();
}
public override async Task OnDisconnectedAsync(Exception? exception)
{
var userId = Context.UserIdentifier ?? "unknown";
_logger.LogDebug("User {UserId} disconnected from {Hub}",
userId, nameof(${1}Hub));
await base.OnDisconnectedAsync(exception);
}
/// <summary>
/// Joins the caller to a named group for targeted broadcasting.
/// </summary>
public async Task JoinGroup(string groupName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(groupName);
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
var userId = Context.UserIdentifier ?? "anonymous";
await Clients.OthersInGroup(groupName).UserJoined(userId);
_logger.LogDebug("User {UserId} joined group {Group}",
userId, groupName);
}
/// <summary>
/// Removes the caller from a named group.
/// </summary>
public async Task LeaveGroup(string groupName)
{
ArgumentException.ThrowIfNullOrWhiteSpace(groupName);
var userId = Context.UserIdentifier ?? "anonymous";
await Clients.OthersInGroup(groupName).UserLeft(userId);
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
_logger.LogDebug("User {UserId} left group {Group}",
userId, groupName);
}
}
${2}.Hubs.${1}UpdateDto.cs
/// <summary>
/// Data sent to clients when a ${1} update occurs.
/// </summary>
public sealed record ${1}UpdateDto
{
public required string Id { get; init; }
public required string Message { get; init; }
public required DateTime OccurredAtUtc { get; init; }
}
${2}.Extensions.${1}HubExtensions.cs
/// <summary>
/// Registers and maps the <see cref="${1}Hub"/>.
/// </summary>
public static class ${1}HubExtensions
{
public static IServiceCollection Add${1}Hub(this IServiceCollection services)
{
ArgumentNullException.ThrowIfNull(services);
services.AddSignalR();
return services;
}
public static IEndpointRouteBuilder Map${1}Hub(this IEndpointRouteBuilder endpoints)
{
ArgumentNullException.ThrowIfNull(endpoints);
endpoints.MapHub<${1}Hub>("/hubs/${1:kebab}");
return endpoints;
}
}
// Inject IHubContext<${1}Hub, I${1}HubClient> in any service:
internal sealed class ${1}NotificationService
{
private readonly IHubContext<${1}Hub, I${1}HubClient> _hubContext;
public ${1}NotificationService(IHubContext<${1}Hub, I${1}HubClient> hubContext)
{
ArgumentNullException.ThrowIfNull(hubContext);
_hubContext = hubContext;
}
public async Task NotifyGroupAsync(string groupName, ${1}UpdateDto update, CancellationToken ct)
{
ArgumentException.ThrowIfNullOrWhiteSpace(groupName);
ArgumentNullException.ThrowIfNull(update);
await _hubContext.Clients.Group(groupName).Receive${1}Update(update);
}
}
internal sealed inheriting Hub<TClient> — always strongly-typedpublic (may live in shared contracts project)Context.UserIdentifier — never parse tokensIHubContext<THub, TClient> to send from outside the hub — never store hub references[Authorize] on the hub class or individual methodssignalr rule filetools
Use when cross-cutting concerns (logging, metrics, validation, authorization) are tangled into command handlers or service methods, when building database command pipelines with reorderable concerns, or when HTTP client pipelines or message handlers need composable, independently-replaceable processing stages. Covers ICommandInterceptor interface, InterceptorPipeline with reverse-chain construction, zero-cost Empty sentinel to skip overhead when no interceptors are registered, and ConfigureAwait(false) discipline for library code. Domain: Architecture, Cross-Cutting Concerns. Level: Intermediate. Tags: interceptor, pipeline, middleware, decorator, cross-cutting-concerns.
development
Use when writing integration tests for Razor Pages, MVC, or Minimal API applications to validate routing, middleware, page rendering, and HTTP behavior without a browser or live server, or when adding fast smoke tests to a CI pipeline. Covers WebApplicationFactory<Program> setup with public partial class Program, in-memory test server, AngleSharp HTML parsing, CSS selector assertions, redirect and status code testing, and a shared static fixture pattern for minimal per-test startup overhead. Domain: Testing, ASP.NET Core. Level: Intermediate. Tags: integration-testing, webapplicationfactory, razor-pages, anglesharp, http-testing.
development
Use when designing indexes for new tables, diagnosing slow queries that are not using indexes efficiently, reviewing index fragmentation and maintenance, or when the current indexing strategy results in key lookups, table scans, or missing index warnings. Covers clustered index key selection (narrow, unique, ever-increasing), non-clustered index design for query patterns, covering indexes with INCLUDE columns, filtered indexes for subset queries, composite index column ordering, DMV-based monitoring for missing and unused indexes, and rebuild vs reorganize maintenance thresholds. Domain: Database, Performance. Level: Intermediate. Tags: index, sql-server, covering-index, filtered-index, performance, dmv, maintenance.
development
Use when building a searchable in-memory catalog or registry for documentation sites, admin panels, or type/API browsers where you need keyword matching, fuzzy search, and ranked results without an external search engine or database. Covers RegistryService with weighted scoring across name, description, keywords, and method names; Levenshtein fuzzy matching; synonym expansion; category and subcategory filtering; and singleton DI registration for datasets of hundreds to low thousands of items. Domain: Search, Data Access Patterns. Level: Intermediate. Tags: search, registry, fuzzy-matching, in-memory, catalog, filtering.