Skip to content

Commit 34b5f75

Browse files
committed
Handle circular references in OpenAPI schema generation
Enhance SignalROpenApiDocumentGenerator to prevent stack overflows by tracking processed types and returning schema references for self-referencing types. Add unit test and new hub/type for circular reference scenarios. Update GitHub Actions to publish non-draft releases.
1 parent 741f397 commit 34b5f75

5 files changed

Lines changed: 84 additions & 3 deletions

File tree

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,6 @@ jobs:
9696
name: v${{ env.PACKAGE_VERSION }}
9797
tag_name: v${{ env.PACKAGE_VERSION }}
9898
generate_release_notes: true
99-
draft: true
99+
draft: false
100100
prerelease: ${{ env.IS_RELEASE != 'True' }}
101101
files: ./artifacts/*.nupkg

src/SignalR.OpenApi/Generation/SignalROpenApiDocumentGenerator.cs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ public sealed class SignalROpenApiDocumentGenerator : ISignalROpenApiDocumentGen
2020
private readonly SignalROpenApiOptions options;
2121
private readonly IServiceProvider serviceProvider;
2222
private readonly IReadOnlyList<ISignalROpenApiSchemaProcessor> schemaProcessors;
23+
private readonly Dictionary<Type, string> schemaRegistry = [];
2324
private OpenApiDocument? currentDocument;
2425

2526
/// <summary>
@@ -57,6 +58,7 @@ public OpenApiDocument GenerateDocument(IReadOnlyList<SignalRHubInfo> hubs)
5758
};
5859

5960
this.currentDocument = document;
61+
this.schemaRegistry.Clear();
6062

6163
var requiresAuth = false;
6264

@@ -510,12 +512,27 @@ private OpenApiSchema CreateSchemaForType(Type type)
510512
};
511513
}
512514

513-
// Complex object
514-
return CreateObjectSchema(type);
515+
// Complex object — check for circular references
516+
if (this.schemaRegistry.TryGetValue(type, out var schemaName))
517+
{
518+
return new OpenApiSchema
519+
{
520+
Reference = new OpenApiReference
521+
{
522+
Type = ReferenceType.Schema,
523+
Id = schemaName,
524+
},
525+
};
526+
}
527+
528+
return this.CreateObjectSchema(type);
515529
}
516530

517531
private OpenApiSchema CreateObjectSchema(Type type)
518532
{
533+
var typeName = type.Name;
534+
this.schemaRegistry[type] = typeName;
535+
519536
var schema = new OpenApiSchema
520537
{
521538
Type = "object",
@@ -551,6 +568,11 @@ private OpenApiSchema CreateObjectSchema(Type type)
551568

552569
this.ApplySchemaProcessors(schema, type);
553570

571+
if (this.currentDocument is not null)
572+
{
573+
this.currentDocument.Components.Schemas[typeName] = schema;
574+
}
575+
554576
return schema;
555577
}
556578

test/SignalR.OpenApi.Tests/SignalROpenApiDocumentGeneratorTests.cs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,20 @@ public void GenerateDocument_FlattenedBody_FalseForPrimitiveParameters()
654654
Assert.IsFalse(flattenedBody, "flattenedBody should be false for primitive parameters.");
655655
}
656656

657+
/// <summary>
658+
/// Verifies that self-referencing types do not cause a StackOverflowException.
659+
/// </summary>
660+
[TestMethod]
661+
public void GenerateDocument_SelfReferencingType_DoesNotStackOverflow()
662+
{
663+
var (discoverer, generator) = CreateServices();
664+
var hubs = discoverer.DiscoverHubs();
665+
var doc = generator.GenerateDocument(hubs);
666+
667+
Assert.IsTrue(doc.Paths.ContainsKey("/hubs/CircularRef/ProcessNode"));
668+
Assert.IsTrue(doc.Components.Schemas.ContainsKey("TreeNode"));
669+
}
670+
657671
private static (ReflectionHubDiscoverer Discoverer, SignalROpenApiDocumentGenerator Generator) CreateServices(
658672
Action<SignalROpenApiOptions>? configure = null)
659673
{
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
using Microsoft.AspNetCore.SignalR;
4+
5+
namespace SignalR.OpenApi.Tests.TestHubs;
6+
7+
/// <summary>
8+
/// A hub for testing circular reference handling.
9+
/// </summary>
10+
public class CircularRefHub : Hub
11+
{
12+
/// <summary>
13+
/// Processes a tree node with self-referencing children.
14+
/// </summary>
15+
/// <param name="node">The tree node.</param>
16+
/// <returns>The processed node.</returns>
17+
public Task<TreeNode> ProcessNode(TreeNode node)
18+
{
19+
return Task.FromResult(node);
20+
}
21+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) SignalR.OpenApi Contributors. Licensed under the MIT License.
2+
3+
namespace SignalR.OpenApi.Tests.TestHubs;
4+
5+
/// <summary>
6+
/// A tree node with self-referencing children.
7+
/// </summary>
8+
public class TreeNode
9+
{
10+
/// <summary>
11+
/// Gets or sets the name.
12+
/// </summary>
13+
public string Name { get; set; } = string.Empty;
14+
15+
/// <summary>
16+
/// Gets or sets the parent node (self-reference).
17+
/// </summary>
18+
public TreeNode? Parent { get; set; }
19+
20+
/// <summary>
21+
/// Gets or sets the child nodes (self-referencing collection).
22+
/// </summary>
23+
public List<TreeNode>? Children { get; set; }
24+
}

0 commit comments

Comments
 (0)