Skip to content

Commit 4c323e4

Browse files
committed
Add doc expansion and sorting options to Swagger UI
Introduce DocExpansion enum and new options to control tag/operation expansion and sorting in SignalR Swagger UI. Update DI, middleware, and README to support DocExpansion, SortTagsAlphabetically, and SortOperationsAlphabetically. Add integration tests to verify configuration and UI output.
1 parent e6649ac commit 4c323e4

6 files changed

Lines changed: 219 additions & 0 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,9 @@ builder.Services.AddSignalRSwaggerUi(options =>
9292
options.StripAsyncSuffix = true; // Strip "Async" from display names (default)
9393
options.SyntaxHighlight = true; // Enable syntax highlighting (default)
9494
options.DefaultModelsExpandDepth = -1; // Hide models section (default), 1 to show
95+
options.DocExpansion = DocExpansion.List; // Tag expand mode: List (default), Full, None
96+
options.SortTagsAlphabetically = false; // Sort tags A-Z (default: document order)
97+
options.SortOperationsAlphabetically = false; // Sort operations A-Z (default: document order)
9598
9699
// Static headers sent with every SignalR hub connection
97100
options.Headers["X-Custom-Header"] = "MyValue";
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
namespace SignalR.OpenApi.SwaggerUi;
4+
5+
/// <summary>
6+
/// Controls the default expansion setting for the operations and tags in SwaggerUI.
7+
/// </summary>
8+
public enum DocExpansion
9+
{
10+
/// <summary>
11+
/// Expands only the tags (groups). Operations within each tag are collapsed.
12+
/// This is the default SwaggerUI behavior.
13+
/// </summary>
14+
List,
15+
16+
/// <summary>
17+
/// Expands the tags and operations fully.
18+
/// </summary>
19+
Full,
20+
21+
/// <summary>
22+
/// Collapses everything (both tags and operations).
23+
/// </summary>
24+
None,
25+
}

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,21 @@ public static IApplicationBuilder UseSignalRSwaggerUi(
6868
// Set default models expand depth
6969
c.DefaultModelsExpandDepth(options.DefaultModelsExpandDepth);
7070

71+
// Set doc expansion mode
72+
c.DocExpansion((Swashbuckle.AspNetCore.SwaggerUI.DocExpansion)options.DocExpansion);
73+
74+
// Sort tags alphabetically if configured
75+
if (options.SortTagsAlphabetically)
76+
{
77+
c.ConfigObject.AdditionalItems["tagsSorter"] = "alpha";
78+
}
79+
80+
// Sort operations alphabetically if configured
81+
if (options.SortOperationsAlphabetically)
82+
{
83+
c.ConfigObject.AdditionalItems["operationsSorter"] = "alpha";
84+
}
85+
7186
// Configure auth UI for JWT Bearer (standard SwaggerUI behavior)
7287
c.OAuthUsePkce();
7388
});

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ public static IServiceCollection AddSignalRSwaggerUi(
3232
o.StripAsyncSuffix = options.StripAsyncSuffix;
3333
o.SyntaxHighlight = options.SyntaxHighlight;
3434
o.DefaultModelsExpandDepth = options.DefaultModelsExpandDepth;
35+
o.DocExpansion = options.DocExpansion;
36+
o.SortTagsAlphabetically = options.SortTagsAlphabetically;
37+
o.SortOperationsAlphabetically = options.SortOperationsAlphabetically;
3538

3639
foreach (var header in options.Headers)
3740
{

src/SignalR.OpenApi.SwaggerUi/SignalRSwaggerUiOptions.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,26 @@ public class SignalRSwaggerUiOptions
5151
/// </summary>
5252
public int DefaultModelsExpandDepth { get; set; } = -1;
5353

54+
/// <summary>
55+
/// Gets or sets the default expansion setting for tags and operations in SwaggerUI.
56+
/// Defaults to <see cref="SwaggerUi.DocExpansion.List"/> (tags expanded, operations collapsed).
57+
/// </summary>
58+
public DocExpansion DocExpansion { get; set; } = DocExpansion.List;
59+
60+
/// <summary>
61+
/// Gets or sets a value indicating whether tags (groups) are sorted
62+
/// alphabetically in SwaggerUI. Defaults to <see langword="false"/>
63+
/// (tags appear in the order defined by the OpenAPI document).
64+
/// </summary>
65+
public bool SortTagsAlphabetically { get; set; }
66+
67+
/// <summary>
68+
/// Gets or sets a value indicating whether operations (endpoints)
69+
/// within each tag are sorted alphabetically in SwaggerUI.
70+
/// Defaults to <see langword="false"/> (operations appear in document order).
71+
/// </summary>
72+
public bool SortOperationsAlphabetically { get; set; }
73+
5474
/// <summary>
5575
/// Gets the custom HTTP headers to include on every SignalR hub connection.
5676
/// These headers are sent with the initial negotiate request and all long-polling

test/SignalR.OpenApi.Tests/SwaggerUiIntegrationTests.cs

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ public void AddSignalRSwaggerUi_DefaultOptions()
5858
Assert.AreEqual(0, options.Headers.Count);
5959
Assert.IsTrue(options.SyntaxHighlight);
6060
Assert.AreEqual(-1, options.DefaultModelsExpandDepth);
61+
Assert.AreEqual(DocExpansion.List, options.DocExpansion);
62+
Assert.IsFalse(options.SortTagsAlphabetically);
63+
Assert.IsFalse(options.SortOperationsAlphabetically);
6164
}
6265

6366
/// <summary>
@@ -117,6 +120,60 @@ public void AddSignalRSwaggerUi_CustomDefaultModelsExpandDepth()
117120
Assert.AreEqual(2, options.DefaultModelsExpandDepth);
118121
}
119122

123+
/// <summary>
124+
/// Verifies that DocExpansion can be configured via DI.
125+
/// </summary>
126+
[TestMethod]
127+
public void AddSignalRSwaggerUi_CustomDocExpansion()
128+
{
129+
var services = new ServiceCollection();
130+
services.AddSignalRSwaggerUi(o =>
131+
{
132+
o.DocExpansion = DocExpansion.None;
133+
});
134+
135+
using var provider = services.BuildServiceProvider();
136+
var options = provider.GetRequiredService<IOptions<SignalRSwaggerUiOptions>>().Value;
137+
138+
Assert.AreEqual(DocExpansion.None, options.DocExpansion);
139+
}
140+
141+
/// <summary>
142+
/// Verifies that SortTagsAlphabetically can be configured via DI.
143+
/// </summary>
144+
[TestMethod]
145+
public void AddSignalRSwaggerUi_SortTagsAlphabetically()
146+
{
147+
var services = new ServiceCollection();
148+
services.AddSignalRSwaggerUi(o =>
149+
{
150+
o.SortTagsAlphabetically = true;
151+
});
152+
153+
using var provider = services.BuildServiceProvider();
154+
var options = provider.GetRequiredService<IOptions<SignalRSwaggerUiOptions>>().Value;
155+
156+
Assert.IsTrue(options.SortTagsAlphabetically);
157+
}
158+
159+
/// <summary>
160+
/// Verifies that SortOperationsAlphabetically can be configured via DI.
161+
/// </summary>
162+
[TestMethod]
163+
public void AddSignalRSwaggerUi_SortOperationsAlphabetically()
164+
{
165+
var services = new ServiceCollection();
166+
services.AddSignalRSwaggerUi(o =>
167+
{
168+
o.SortOperationsAlphabetically = true;
169+
});
170+
171+
using var provider = services.BuildServiceProvider();
172+
var options = provider.GetRequiredService<IOptions<SignalRSwaggerUiOptions>>().Value;
173+
174+
Assert.IsTrue(options.SortOperationsAlphabetically);
175+
}
176+
120177
/// <summary>
121178
/// Verifies embedded signalr.min.js resource is served.
122179
/// </summary>
@@ -284,6 +341,102 @@ public async Task SwaggerUi_CustomDefaultModelsExpandDepthApplied()
284341
Assert.IsTrue(content.Contains("defaultModelsExpandDepth"), "Config should include defaultModelsExpandDepth");
285342
}
286343

344+
/// <summary>
345+
/// Verifies that DocExpansion defaults to "list" in the SwaggerUI configObject.
346+
/// </summary>
347+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
348+
[TestMethod]
349+
public async Task SwaggerUi_DocExpansionDefaultIsList()
350+
{
351+
using var host = await CreateTestHost();
352+
using var client = host.GetTestClient();
353+
354+
using var response = await client.GetAsync("/signalr-swagger/index.js");
355+
var content = await response.Content.ReadAsStringAsync();
356+
357+
Assert.IsTrue(content.Contains("\"docExpansion\":\"list\""), "Default DocExpansion should be list");
358+
}
359+
360+
/// <summary>
361+
/// Verifies that DocExpansion.None is applied to the SwaggerUI configObject.
362+
/// </summary>
363+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
364+
[TestMethod]
365+
public async Task SwaggerUi_DocExpansionNoneApplied()
366+
{
367+
using var host = await CreateTestHost(o => o.DocExpansion = DocExpansion.None);
368+
using var client = host.GetTestClient();
369+
370+
using var response = await client.GetAsync("/signalr-swagger/index.js");
371+
var content = await response.Content.ReadAsStringAsync();
372+
373+
Assert.IsTrue(content.Contains("\"docExpansion\":\"none\""), "DocExpansion should be none");
374+
}
375+
376+
/// <summary>
377+
/// Verifies that tagsSorter is not present by default.
378+
/// </summary>
379+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
380+
[TestMethod]
381+
public async Task SwaggerUi_TagsSorterNotPresentByDefault()
382+
{
383+
using var host = await CreateTestHost();
384+
using var client = host.GetTestClient();
385+
386+
using var response = await client.GetAsync("/signalr-swagger/index.js");
387+
var content = await response.Content.ReadAsStringAsync();
388+
389+
Assert.IsFalse(content.Contains("tagsSorter"), "tagsSorter should not be present by default");
390+
}
391+
392+
/// <summary>
393+
/// Verifies that tagsSorter is set to "alpha" when SortTagsAlphabetically is true.
394+
/// </summary>
395+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
396+
[TestMethod]
397+
public async Task SwaggerUi_SortTagsAlphabeticallyApplied()
398+
{
399+
using var host = await CreateTestHost(o => o.SortTagsAlphabetically = true);
400+
using var client = host.GetTestClient();
401+
402+
using var response = await client.GetAsync("/signalr-swagger/index.js");
403+
var content = await response.Content.ReadAsStringAsync();
404+
405+
Assert.IsTrue(content.Contains("\"tagsSorter\":\"alpha\""), "tagsSorter should be alpha when enabled");
406+
}
407+
408+
/// <summary>
409+
/// Verifies that operationsSorter is not present by default.
410+
/// </summary>
411+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
412+
[TestMethod]
413+
public async Task SwaggerUi_OperationsSorterNotPresentByDefault()
414+
{
415+
using var host = await CreateTestHost();
416+
using var client = host.GetTestClient();
417+
418+
using var response = await client.GetAsync("/signalr-swagger/index.js");
419+
var content = await response.Content.ReadAsStringAsync();
420+
421+
Assert.IsFalse(content.Contains("operationsSorter"), "operationsSorter should not be present by default");
422+
}
423+
424+
/// <summary>
425+
/// Verifies that operationsSorter is set to "alpha" when SortOperationsAlphabetically is true.
426+
/// </summary>
427+
/// <returns>A <see cref="Task"/> representing the asynchronous test.</returns>
428+
[TestMethod]
429+
public async Task SwaggerUi_SortOperationsAlphabeticallyApplied()
430+
{
431+
using var host = await CreateTestHost(o => o.SortOperationsAlphabetically = true);
432+
using var client = host.GetTestClient();
433+
434+
using var response = await client.GetAsync("/signalr-swagger/index.js");
435+
var content = await response.Content.ReadAsStringAsync();
436+
437+
Assert.IsTrue(content.Contains("\"operationsSorter\":\"alpha\""), "operationsSorter should be alpha when enabled");
438+
}
439+
287440
/// <summary>
288441
/// Verifies that the document includes hubPath in x-signalr extension.
289442
/// </summary>

0 commit comments

Comments
 (0)