OpenAPI 3.1 specification generation and SwaggerUI support for ASP.NET Core SignalR hubs.
- Generates OpenAPI 3.1 specifications from SignalR hub methods
- Interactive SwaggerUI with SignalR protocol invocation (no HTTP — real SignalR calls)
- Streaming support:
IAsyncEnumerable<T>andChannelReader<T>with accumulated item history and stream state tracking - Client event monitoring: Auto-subscribes to typed hub (
Hub<TClient>) events with real-time event log panel - Supports standard ASP.NET Core attributes (
[Authorize],[Tags],[EndpointSummary],[Obsolete], etc.) - Document-level tag definitions with descriptions (from options or XML summary fallback)
- XML documentation comments for descriptions and examples
[JsonPolymorphic]/[JsonDerivedType]polymorphic schema support with OData-style sub-endpoints- Data Annotation validation attributes mapped to OpenAPI schema constraints
- FluentValidation rules mapped to OpenAPI schema constraints
- Security scheme detection from
[Authorize]/[AllowAnonymous] - JWT Bearer token support in SwaggerUI (header or query string)
- Custom HTTP headers (static or user-enterable via SwaggerUI Authorize dialog)
- Per-hub Connect / Disconnect toggle button with automatic reconnection handling
- Automatic credential change detection with transparent reconnection
- Form-urlencoded input mode for primitive and flat object parameters
- Multiple named request/response examples via custom attributes
- Enum schema generation (integer or string based on
JsonStringEnumConverter) - Embedded
@microsoft/signalrbundle (no CDN dependency) - Zero proprietary attributes required for core functionality
dotnet add package SignalR.OpenApi
dotnet add package SignalR.OpenApi.SwaggerUivar builder = WebApplication.CreateBuilder(args);
builder.Services.AddSignalR();
builder.Services.AddSignalROpenApi();
builder.Services.AddSignalRSwaggerUi();
var app = builder.Build();
app.MapHub<ChatHub>("/hubs/chat");
app.MapSignalROpenApi();
app.UseSignalRSwaggerUi();
app.Run();The OpenAPI specification is served at /openapi/signalr-v1.json.
The SwaggerUI is available at /signalr-swagger.
builder.Services.AddSignalROpenApi(options =>
{
options.DocumentTitle = "My SignalR API";
options.DocumentVersion = "v1";
// Include type discriminator in JSON examples for polymorphic sub-endpoints (default: true)
options.IncludeDiscriminatorInExamples = true;
// Add descriptions for tags (displayed as group descriptions in SwaggerUI)
options.TagDescriptions["Chat"] = "Real-time chat operations";
options.TagDescriptions["Chat Events"] = "Server-to-client chat notifications";
// Configure JSON property naming (default: camelCase)
options.JsonSerializerOptions = new System.Text.Json.JsonSerializerOptions
{
PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
};
// User-enterable headers shown in the SwaggerUI Authorize dialog.
// Each entry appears as an apiKey security scheme (in: header) so users
// can enter a value at runtime before invoking hub methods.
options.ApiKeyHeaders["X-Custom-Header"] = "A custom header sent with every hub connection.";
// Security schemes applied to operations with [Authorize].
// Define the authentication methods that SwaggerUI exposes in the Authorize dialog.
options.SecuritySchemes["Bearer"] = new Microsoft.OpenApi.Models.OpenApiSecurityScheme
{
Type = Microsoft.OpenApi.Models.SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "JWT Bearer token for SignalR hub authentication.",
};
});
builder.Services.AddSignalRSwaggerUi(options =>
{
options.RoutePrefix = "signalr-swagger"; // SwaggerUI route (default)
options.SpecUrl = "/openapi/signalr-v1.json"; // Spec endpoint (default)
options.DocumentTitle = "SignalR API"; // Browser tab title (default)
options.StripAsyncSuffix = true; // Strip "Async" from display names (default)
options.SyntaxHighlight = true; // Enable syntax highlighting (default)
options.DefaultModelsExpandDepth = -1; // Hide models section (default), 1 to show
options.DocExpansion = DocExpansion.List; // Tag expand mode: List (default), Full, None
options.SortTagsAlphabetically = false; // Sort tags A-Z (default: document order)
options.SortOperationsAlphabetically = false; // Sort operations A-Z (default: document order)
// Static headers sent with every SignalR hub connection
options.Headers["X-Custom-Header"] = "MyValue";
});SignalR operations display custom method labels in SwaggerUI:
| Label | Description |
|---|---|
| INVOKE | Standard hub method invocation |
| STREAM | Streaming method (IAsyncEnumerable<T> / ChannelReader<T>) |
| EVENT | Client event from typed hub (Hub<TClient>) |
Streaming operations accumulate items into a growing response array as they arrive. The response shows:
{
"state": "streaming",
"count": 5,
"items": [10, 9, 8, 7, 6]
}When the stream completes, the state changes to "completed". If an error occurs, it shows "error: ...". A Stop Stream button appears while streaming is active to cancel the subscription.
Client events (from Hub<TClient> interface methods) appear as EVENT operations. When you expand one, an event log panel shows:
- Connect & Listen: Toggle button to establish hub connection and start receiving events (shows "Connected" when active)
- Event log: Real-time list of received events with timestamps and JSON payloads
- Clear Log: Button to reset the event history
Events are automatically subscribed when connecting to a hub via any invoke or stream operation.
Each hub tag section in SwaggerUI displays a connection control bar with a single toggle button:
| Button State | Description |
|---|---|
| Connect | No active connection; click to connect |
| Disconnect | Hub connection is active; click to disconnect |
Auto-connect on Execute: Clicking Execute on any hub method automatically connects if not already connected — you do not need to click Connect first.
Credential change detection: When you change API keys or Bearer tokens in the SwaggerUI Authorize dialog, the plugin automatically detects the change on the next hub method invocation and reconnects with the updated credentials. You can also manually disconnect and reconnect to pick up new credentials immediately.
Hub methods with parameters support two input modes, selectable via a content-type dropdown in SwaggerUI:
| Content Type | Input Mode | Available When |
|---|---|---|
application/json |
Raw JSON textarea | Always |
application/x-www-form-urlencoded |
Individual form fields | Primitive params, single flat objects, polymorphic sub-endpoints |
The dropdown appears above the request body when you click Try it out. Form field values are automatically coerced to the correct type (e.g., "5" → 5 for integers, "true" → true for booleans).
Note: Methods with multi-object parameters (two or more complex objects) only support
application/jsonbecause SwaggerUI cannot render nested objects as form fields.
Parameters using [JsonPolymorphic] / [JsonDerivedType] are supported via two mechanisms:
- Main endpoint (
/hubs/Chat/SendNotification) — usesoneOfwith adiscriminatorfor the type selector dropdown in JSON mode. - OData-style sub-endpoints (
/hubs/Chat/SendNotification/text,/hubs/Chat/SendNotification/alert) — each derived type gets its own endpoint with a flat schema, supporting both JSON and form-urlencoded input.
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
[JsonDerivedType(typeof(TextNotification), "text")]
[JsonDerivedType(typeof(AlertNotification), "alert")]
public abstract class Notification
{
public required string Recipient { get; set; }
}
public class TextNotification : Notification
{
public required string Content { get; set; }
}
public class AlertNotification : Notification
{
public required string Title { get; set; }
public required string Severity { get; set; }
}Note:
System.Text.Jsonpolymorphic deserialization requires the type discriminator property to appear first in the JSON object. The SwaggerUI plugin handles this automatically for sub-endpoints.By default,
IncludeDiscriminatorInExamples = truemakes the discriminator visible in JSON request examples but hidden in form-urlencoded inputs. Set tofalseto hide the discriminator from all examples (the plugin still injects it at invocation time).
| Attribute | OpenAPI Mapping |
|---|---|
[Tags("group")] |
tags on operation + document-level tag definition |
[EndpointSummary("...")] |
summary on operation |
[EndpointDescription("...")] |
description on operation |
[EndpointName("Name")] |
operationId on operation |
[Description("...")] |
description on parameter/property |
[Authorize] / [AllowAnonymous] |
security requirement |
[ApiExplorerSettings(IgnoreApi = true)] |
Excluded from spec |
[ExcludeFromDescription] |
Excluded from spec |
[Produces("application/json")] |
Response content type |
[Obsolete] |
deprecated: true |
[JsonPolymorphic] / [JsonDerivedType] |
discriminator / oneOf with sub-endpoints |
[Required], [StringLength], [Range] |
Schema constraints |
XML <summary>, <param>, <returns> |
Descriptions |
XML <example> |
Example values |
[SignalROpenApiRequestExamples] |
Named request examples |
[SignalROpenApiResponseExamples] |
Named response examples |
Operations are grouped by tags in SwaggerUI. The generator automatically collects all unique tags from operations and adds them to the document-level tags section.
Tag descriptions appear as group headers in SwaggerUI. There are two ways to provide them:
1. Via options (explicit):
builder.Services.AddSignalROpenApi(options =>
{
options.TagDescriptions["Chat"] = "Real-time chat operations";
options.TagDescriptions["Admin"] = "Administrative hub methods";
});2. Via XML summary (automatic fallback): When a tag name matches the hub name (e.g., tag "Chat" matches ChatHub), the hub's XML <summary> is used as the tag description automatically.
/// <summary>
/// Real-time chat operations.
/// </summary>
public class ChatHub : Hub
{
// Methods default to the "Chat" tag → description comes from XML summary above
}Explicit TagDescriptions always take precedence over XML summary fallback.
| Source | Tag Name |
|---|---|
| Hub class (default) | Hub name without Hub suffix (e.g., ChatHub → "Chat") |
[Tags("group")] on hub class or method |
Specified tag name |
Client events (Hub<TClient>) default |
"{HubName} Events" (e.g., "Chat Events") |
[Tags("group")] on client interface method |
Specified tag name (overrides default) |
Hub method parameters are mapped to the OpenAPI request body schema:
- Single complex object parameter (e.g.,
SendMessage(SendMessageRequest request)): The object's properties are flattened directly into the request body — no wrapper property. - Multiple parameters (e.g.,
SendMessage(string user, string message)orReply(ChatMessage original, ChatMessage reply)): Each parameter becomes a named property in a wrapper object. - Primitive parameters (e.g.,
string,int): Always wrapped with the parameter name as the property key.
- 204 No Content: Hub methods returning
voidorTask(no return value) - 200 OK: Hub methods returning
Task<T>or streaming results
Provide multiple named examples for request and response bodies using custom attributes and the ISignalROpenApiExamplesProvider<T> interface.
public class SendMessageRequest
{
public string User { get; set; } = string.Empty;
public string Message { get; set; } = string.Empty;
}
public class SendMessageExamplesProvider : ISignalROpenApiExamplesProvider<SendMessageRequest>
{
public IEnumerable<SignalROpenApiExample<SendMessageRequest>> GetExamples()
{
yield return new SignalROpenApiExample<SendMessageRequest>(
"Greeting",
new SendMessageRequest { User = "Alice", Message = "Hello, everyone!" })
{
Summary = "A friendly greeting",
};
yield return new SignalROpenApiExample<SendMessageRequest>(
"Question",
new SendMessageRequest { User = "Bob", Message = "What time is the meeting?" })
{
Summary = "Asking a question",
};
}
}[SignalROpenApiRequestExamples(typeof(SendMessageExamplesProvider))]
public async Task SendMessage(string user, string message)
{
await Clients.All.ReceiveMessage(user, message);
}The examples appear in SwaggerUI's example dropdown for the request body. Response examples work the same way using [SignalROpenApiResponseExamples].
Example providers are resolved from DI first, with Activator.CreateInstance as a fallback.
The SignalR.OpenApi.FluentValidation package automatically maps FluentValidation rules to OpenAPI schema constraints.
Note: FluentValidation applies to complex object parameters only (classes with properties). Hub methods with primitive parameters (e.g.,
string user, string message) are not validated — use a request object instead (e.g.,SendMessage(SendMessageRequest request)). When a hub method has a single complex object parameter, the schema is flattened — the object's properties appear directly in the request body without a wrapper property.
builder.Services.AddValidatorsFromAssemblyContaining<MyValidator>();
builder.Services.AddSignalROpenApi();
builder.Services.AddSignalRFluentValidation();| FluentValidation Rule | OpenAPI Schema |
|---|---|
NotNull() / NotEmpty() |
required, nullable: false |
NotEmpty() (string) |
minLength: 1 |
Length(min, max) / MaximumLength(n) |
minLength, maxLength |
Matches(regex) |
pattern |
GreaterThan(n) |
minimum + exclusiveMinimum |
GreaterThanOrEqualTo(n) |
minimum |
LessThan(n) |
maximum + exclusiveMaximum |
LessThanOrEqualTo(n) |
maximum |
InclusiveBetween(from, to) |
minimum, maximum |
ExclusiveBetween(from, to) |
minimum, maximum + exclusive flags |
EmailAddress() |
pattern (email regex) |
Validators are resolved from DI via IValidator<T>. Nested child validators are supported.
Custom HTTP headers can be sent with every SignalR hub connection. Two approaches are available depending on whether the value is known at startup or entered by the user at runtime.
Use SignalRSwaggerUiOptions.Headers to configure headers with fixed values. These are included in the negotiate request and all HTTP-based transports (long-polling, server-sent events). WebSocket connections carry them on the initial upgrade request.
builder.Services.AddSignalRSwaggerUi(options =>
{
options.Headers["X-Custom-Header"] = "MyValue";
});Use SignalROpenApiOptions.ApiKeyHeaders to define headers that appear in SwaggerUI's Authorize dialog. Each entry is rendered as an apiKey security scheme (in: header) in the OpenAPI document. Users can enter values at runtime before invoking hub methods.
builder.Services.AddSignalROpenApi(options =>
{
options.ApiKeyHeaders["X-Custom-Header"] = "A custom header sent with every hub connection.";
});When the user clicks the Authorize button in SwaggerUI, they see an input field for each configured header. The entered values are automatically included on every SignalR hub connection.
| Approach | Where configured | User can change at runtime? |
|---|---|---|
SignalRSwaggerUiOptions.Headers |
Static value at startup | No |
SignalROpenApiOptions.ApiKeyHeaders |
Authorize dialog input | Yes |
Both approaches can be combined — static headers provide defaults while apiKey headers allow user overrides. When both define the same header name, the user-entered value from the Authorize dialog takes precedence.
Enum types are automatically mapped to OpenAPI schemas. The schema format depends on whether a JsonStringEnumConverter is configured:
| Converter | Schema Type | Example Values |
|---|---|---|
| None (default) | integer |
0, 1, 2 |
JsonStringEnumConverter |
string with enum |
"Pending", "Active", "Completed" |
The converter is detected from JsonSerializerOptions.Converters (global) or [JsonConverter] on the enum type.
// Global: all enums serialize as strings
options.JsonSerializerOptions.Converters.Add(new System.Text.Json.Serialization.JsonStringEnumConverter());
// Per-type: only this enum serializes as strings
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum Status { Pending, Active, Completed }| Package | Description |
|---|---|
| SignalR.OpenApi | Core library: hub discovery, OpenAPI generation |
| SignalR.OpenApi.FluentValidation | FluentValidation rules → OpenAPI schema constraints |
| SignalR.OpenApi.SwaggerUi | SwaggerUI with interactive SignalR invocation, streaming, and event monitoring |
The following open-source projects also provide OpenAPI, SwaggerUI, or developer tooling for ASP.NET Core SignalR hubs. SignalR.OpenApi was designed with awareness of these projects and aims to combine the best aspects of each.
| Feature | SignalR.OpenApi | SigSpec | SignalRSwaggerGen | TypedSignalR.Client.DevTools | NSwag4SignalR |
|---|---|---|---|---|---|
| OpenAPI spec generation | ✅ 3.1 | ✅ Custom (SigSpec) | ✅ Swagger 2.0 / OAS 3.0 | ✅ Custom (spec.json) | ✅ 3.0 |
| OpenAPI library | Microsoft.AspNetCore.OpenApi |
Custom | Swashbuckle IDocumentFilter |
Custom | NSwag IDocumentProcessor |
| Interactive UI | ✅ SwaggerUI (Swashbuckle) | ❌ | ❌ Spec only | ✅ Custom (Next.js / Bulma) | ✅ SwaggerUI (NSwag) |
| Real SignalR invocation | ✅ @microsoft/signalr |
❌ | ❌ | ✅ @microsoft/signalr |
✅ @microsoft/signalr |
| Streaming UI | ✅ Accumulated history, state tracking, stop button | ❌ | ❌ | ✅ Server-to-client & client-to-server | ✅ PUT operations |
| Client event monitoring | ✅ Real-time event log panel | ❌ | ❌ | ✅ Event subscription | ✅ GET operations |
| Hub discovery | Reflection | Reflection | Attribute-based ([SignalRHub]) |
Source generator (MapHub<T>()) |
Endpoint metadata (HubMetadata) |
| FluentValidation → schema | ✅ | ❌ | ❌ | ❌ | ❌ |
| Polymorphic types | ✅ oneOf/discriminator + sub-endpoints |
❌ | ❌ | ❌ | ❌ |
| Form-urlencoded input | ✅ Flat params & objects | ❌ | ❌ | ❌ | ❌ |
| Named examples | ✅ Custom attributes + providers | ❌ | ❌ | ❌ | ❌ |
| Auth (JWT / Windows) | ✅ Built-in SwaggerUI | ❌ | ✅ [Authorize] detection |
✅ accessTokenFactory |
❌ |
| Custom headers | ✅ Static + Authorize dialog | ❌ | ❌ | ❌ | ❌ |
| Tag descriptions | ✅ Options + XML summary fallback | ❌ | Partial (tags only) | ❌ | ❌ |
| Enum schema | ✅ Integer + JsonStringEnumConverter |
❌ | Partial (via Swashbuckle) | ❌ | ✅ (via NSwag) |
| Standard attributes | ✅ [Tags], [EndpointSummary], [Authorize], [Obsolete], etc. |
Partial | ✅ [Authorize], custom |
Partial | Partial |
| Target framework | .NET 8+ | .NET Core 3.1+ | .NET 5+ | .NET 6+ | .NET 10 |
| NuGet packages | 3 packages | ❌ | ✅ | ✅ | ✅ |
| Project | Description |
|---|---|
| nswag-fluentvalidation (ZymLabs) | FluentValidation → OpenAPI schema mapping for NSwag; rule-based architecture pattern |
| Swashbuckle.AspNetCore.Filters | Request/response examples and security filters for Swashbuckle |
| Signalr.Hubs.TypeScriptGenerator | TypeScript type generation from SignalR hubs (legacy .NET Framework / SignalR 2.x) |
MIT