Skip to content

Commit 834aa25

Browse files
committed
Improved Swagger UI perf & UX for SignalR integration and added SyntaxHighlight and DefaultModelsExpandDepth options
- Disable syntax highlighting and model rendering by default to speed up Swagger UI and reduce clutter. - Add caching for OpenAPI spec JS conversion in the SignalR plugin to avoid repeated .toJS() calls. - Use ImmutableJS getIn() for targeted spec access, improving efficiency and memory usage for large specs. - Introduce SyntaxHighlight and DefaultModelsExpandDepth options to SignalRSwaggerUiOptions for improved SwaggerUI configurability. SyntaxHighlight allows toggling response syntax highlighting (default: true), while DefaultModelsExpandDepth controls model section visibility (default: -1, hidden). Update middleware to respect these settings, enhance documentation and usage examples, optimize JS plugin for spec access, and expand tests to cover new options and config output.
1 parent f881cb3 commit 834aa25

7 files changed

Lines changed: 159 additions & 12 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ builder.Services.AddSignalRSwaggerUi(options =>
9090
options.SpecUrl = "/openapi/signalr-v1.json"; // Spec endpoint (default)
9191
options.DocumentTitle = "SignalR API"; // Browser tab title (default)
9292
options.StripAsyncSuffix = true; // Strip "Async" from display names (default)
93+
options.SyntaxHighlight = true; // Enable syntax highlighting (default)
94+
options.DefaultModelsExpandDepth = -1; // Hide models section (default), 1 to show
9395
9496
// Static headers sent with every SignalR hub connection
9597
options.Headers["X-Custom-Header"] = "MyValue";

samples/SignalR.OpenApi.Sample/Program.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
builder.Services.AddSignalRSwaggerUi(options =>
3939
{
4040
options.StripAsyncSuffix = true;
41+
options.DefaultModelsExpandDepth = 1;
4142

4243
// Custom headers sent with every SignalR hub connection.
4344
// These are included in the negotiate request and all HTTP-based transports.

src/SignalR.OpenApi.SwaggerUi/Extensions/SwaggerUiApplicationBuilderExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,15 @@ public static IApplicationBuilder UseSignalRSwaggerUi(
5959
c.ConfigObject.AdditionalItems["signalRHeaders"] = options.Headers;
6060
}
6161

62+
// Disable syntax highlighting if configured
63+
if (!options.SyntaxHighlight)
64+
{
65+
c.ConfigObject.AdditionalItems["syntaxHighlight"] = false;
66+
}
67+
68+
// Set default models expand depth
69+
c.DefaultModelsExpandDepth(options.DefaultModelsExpandDepth);
70+
6271
// Configure auth UI for JWT Bearer (standard SwaggerUI behavior)
6372
c.OAuthUsePkce();
6473
});

src/SignalR.OpenApi.SwaggerUi/Extensions/SwaggerUiServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ public static IServiceCollection AddSignalRSwaggerUi(
3030
o.DocumentTitle = options.DocumentTitle;
3131
o.UseDefaultCredentials = options.UseDefaultCredentials;
3232
o.StripAsyncSuffix = options.StripAsyncSuffix;
33+
o.SyntaxHighlight = options.SyntaxHighlight;
34+
o.DefaultModelsExpandDepth = options.DefaultModelsExpandDepth;
3335

3436
foreach (var header in options.Headers)
3537
{

src/SignalR.OpenApi.SwaggerUi/Resources/signalr-openapi-plugin.js

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@ var SignalROpenApiPlugin = function (system) {
2121
// Event log subscribers (components that need re-render on new events)
2222
var _eventListeners = {};
2323

24+
// Cached spec data to avoid repeated .toJS() conversions
25+
var _cachedSpecVersion = null;
26+
var _cachedSpecJs = null;
27+
2428
// Get the Bearer token from the SwaggerUI authorize dialog
2529
var _getAccessToken = function () {
2630
var state = system.getState().get("auth").get("authorized");
@@ -479,6 +483,16 @@ var SignalROpenApiPlugin = function (system) {
479483
);
480484
}
481485

486+
var _getSpecJs = function () {
487+
var specIm = system.specSelectors.specJson();
488+
// Use the ImmutableJS hash or identity for cache invalidation
489+
if (specIm !== _cachedSpecVersion) {
490+
_cachedSpecVersion = specIm;
491+
_cachedSpecJs = specIm.toJS();
492+
}
493+
return _cachedSpecJs;
494+
};
495+
482496
return {
483497
statePlugins: {
484498
spec: {
@@ -634,11 +648,11 @@ var SignalROpenApiPlugin = function (system) {
634648
if (method === "get") {
635649
props.operationProps = props.operationProps.set("method", "event");
636650
} else {
637-
// Check if this is a streaming operation
638-
var specJson = system.specSelectors.specJson().toJS();
639-
var opSpec = specJson.paths && specJson.paths[path] && specJson.paths[path][method];
640-
var ext = opSpec && opSpec["x-signalr"];
641-
if (ext && ext.stream === true) {
651+
// Use getIn() to access only the specific extension value
652+
// instead of converting the entire spec with .toJS()
653+
var specIm = system.specSelectors.specJson();
654+
var streamFlag = specIm.getIn(["paths", path, method, "x-signalr", "stream"]);
655+
if (streamFlag === true) {
642656
props.operationProps = props.operationProps.set("method", "stream");
643657
} else {
644658
props.operationProps = props.operationProps.set("method", "invoke");
@@ -745,11 +759,15 @@ var SignalROpenApiPlugin = function (system) {
745759
return React.createElement(Original, props);
746760
}
747761

748-
// Get the x-signalr extension
749-
var specJson = system.specSelectors.specJson().toJS();
750-
var opSpec = specJson.paths && specJson.paths[path] && specJson.paths[path][method];
751-
var ext = opSpec && opSpec["x-signalr"];
752-
if (!ext || !ext.clientEvent) {
762+
// Use getIn() to access only the specific extension instead of .toJS()
763+
var specIm = system.specSelectors.specJson();
764+
var extIm = specIm.getIn(["paths", path, method, "x-signalr"]);
765+
if (!extIm) {
766+
return React.createElement(Original, props);
767+
}
768+
769+
var ext = extIm.toJS ? extIm.toJS() : extIm;
770+
if (!ext.clientEvent) {
753771
return React.createElement(Original, props);
754772
}
755773

src/SignalR.OpenApi.SwaggerUi/SignalRSwaggerUiOptions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,19 @@ public class SignalRSwaggerUiOptions
3838
/// </summary>
3939
public bool StripAsyncSuffix { get; set; } = true;
4040

41+
/// <summary>
42+
/// Gets or sets a value indicating whether syntax highlighting is enabled
43+
/// in SwaggerUI response bodies. Defaults to <see langword="true"/>.
44+
/// </summary>
45+
public bool SyntaxHighlight { get; set; } = true;
46+
47+
/// <summary>
48+
/// Gets or sets the default expand depth for models in SwaggerUI.
49+
/// Set to <c>-1</c> to hide the models section entirely.
50+
/// Defaults to <c>-1</c>.
51+
/// </summary>
52+
public int DefaultModelsExpandDepth { get; set; } = -1;
53+
4154
/// <summary>
4255
/// Gets the custom HTTP headers to include on every SignalR hub connection.
4356
/// These headers are sent with the initial negotiate request and all long-polling

test/SignalR.OpenApi.Tests/SwaggerUiIntegrationTests.cs

Lines changed: 104 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,8 @@ public void AddSignalRSwaggerUi_DefaultOptions()
5656
Assert.IsFalse(options.UseDefaultCredentials);
5757
Assert.IsTrue(options.StripAsyncSuffix);
5858
Assert.AreEqual(0, options.Headers.Count);
59+
Assert.IsTrue(options.SyntaxHighlight);
60+
Assert.AreEqual(-1, options.DefaultModelsExpandDepth);
5961
}
6062

6163
/// <summary>
@@ -79,6 +81,42 @@ public void AddSignalRSwaggerUi_CustomHeaders()
7981
Assert.AreEqual("Other", options.Headers["X-Another"]);
8082
}
8183

84+
/// <summary>
85+
/// Verifies that syntax highlighting can be disabled.
86+
/// </summary>
87+
[TestMethod]
88+
public void AddSignalRSwaggerUi_SyntaxHighlightDisabled()
89+
{
90+
var services = new ServiceCollection();
91+
services.AddSignalRSwaggerUi(o =>
92+
{
93+
o.SyntaxHighlight = false;
94+
});
95+
96+
using var provider = services.BuildServiceProvider();
97+
var options = provider.GetRequiredService<IOptions<SignalRSwaggerUiOptions>>().Value;
98+
99+
Assert.IsFalse(options.SyntaxHighlight);
100+
}
101+
102+
/// <summary>
103+
/// Verifies that the default models expand depth can be customized.
104+
/// </summary>
105+
[TestMethod]
106+
public void AddSignalRSwaggerUi_CustomDefaultModelsExpandDepth()
107+
{
108+
var services = new ServiceCollection();
109+
services.AddSignalRSwaggerUi(o =>
110+
{
111+
o.DefaultModelsExpandDepth = 2;
112+
});
113+
114+
using var provider = services.BuildServiceProvider();
115+
var options = provider.GetRequiredService<IOptions<SignalRSwaggerUiOptions>>().Value;
116+
117+
Assert.AreEqual(2, options.DefaultModelsExpandDepth);
118+
}
119+
82120
/// <summary>
83121
/// Verifies embedded signalr.min.js resource is served.
84122
/// </summary>
@@ -182,6 +220,70 @@ public async Task SwaggerUi_RegistersPluginInConfigObject()
182220
Assert.IsTrue(content.Contains("SignalROpenApiPlugin"), "ConfigObject should reference SignalROpenApiPlugin in index.js");
183221
}
184222

223+
/// <summary>
224+
/// Verifies that syntax highlighting is not disabled when SyntaxHighlight is true (default).
225+
/// </summary>
226+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
227+
[TestMethod]
228+
public async Task SwaggerUi_SyntaxHighlightEnabledByDefault()
229+
{
230+
using var host = await CreateTestHost();
231+
using var client = host.GetTestClient();
232+
233+
using var response = await client.GetAsync("/signalr-swagger/index.js");
234+
var content = await response.Content.ReadAsStringAsync();
235+
236+
Assert.IsFalse(content.Contains("\"syntaxHighlight\":false"), "Syntax highlighting should be enabled by default");
237+
}
238+
239+
/// <summary>
240+
/// Verifies that syntax highlighting is disabled when configured.
241+
/// </summary>
242+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
243+
[TestMethod]
244+
public async Task SwaggerUi_SyntaxHighlightDisabledWhenConfigured()
245+
{
246+
using var host = await CreateTestHost(o => o.SyntaxHighlight = false);
247+
using var client = host.GetTestClient();
248+
249+
using var response = await client.GetAsync("/signalr-swagger/index.js");
250+
var content = await response.Content.ReadAsStringAsync();
251+
252+
Assert.IsTrue(content.Contains("syntaxHighlight"), "Syntax highlighting should be disabled in config");
253+
}
254+
255+
/// <summary>
256+
/// Verifies that the default models expand depth is set to -1 by default.
257+
/// </summary>
258+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
259+
[TestMethod]
260+
public async Task SwaggerUi_DefaultModelsExpandDepthHiddenByDefault()
261+
{
262+
using var host = await CreateTestHost();
263+
using var client = host.GetTestClient();
264+
265+
using var response = await client.GetAsync("/signalr-swagger/index.js");
266+
var content = await response.Content.ReadAsStringAsync();
267+
268+
Assert.IsTrue(content.Contains("defaultModelsExpandDepth"), "Config should include defaultModelsExpandDepth");
269+
}
270+
271+
/// <summary>
272+
/// Verifies that a custom default models expand depth is applied.
273+
/// </summary>
274+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
275+
[TestMethod]
276+
public async Task SwaggerUi_CustomDefaultModelsExpandDepthApplied()
277+
{
278+
using var host = await CreateTestHost(o => o.DefaultModelsExpandDepth = 2);
279+
using var client = host.GetTestClient();
280+
281+
using var response = await client.GetAsync("/signalr-swagger/index.js");
282+
var content = await response.Content.ReadAsStringAsync();
283+
284+
Assert.IsTrue(content.Contains("defaultModelsExpandDepth"), "Config should include defaultModelsExpandDepth");
285+
}
286+
185287
/// <summary>
186288
/// Verifies that the document includes hubPath in x-signalr extension.
187289
/// </summary>
@@ -211,7 +313,7 @@ public void GenerateDocument_IncludesHubPath()
211313
Assert.AreEqual("/hubs/basic", ((Microsoft.OpenApi.Any.OpenApiString)extension["hubPath"]).Value);
212314
}
213315

214-
private static async Task<IHost> CreateTestHost()
316+
private static async Task<IHost> CreateTestHost(Action<SignalRSwaggerUiOptions>? configureUi = null)
215317
{
216318
return await new HostBuilder()
217319
.ConfigureWebHost(webBuilder =>
@@ -221,7 +323,7 @@ private static async Task<IHost> CreateTestHost()
221323
{
222324
services.AddSignalR();
223325
services.AddSignalROpenApi();
224-
services.AddSignalRSwaggerUi();
326+
services.AddSignalRSwaggerUi(configureUi ?? (_ => { }));
225327
services.AddRouting();
226328
});
227329
webBuilder.Configure(app =>

0 commit comments

Comments
 (0)