From fd77a91eb369e16c5f952edd42c4c17a8de1aba6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?M=C3=BCller=20Roman?= Date: Mon, 19 Oct 2026 13:33:42 +0200 Subject: [PATCH] fix: defer Newtonsoft resolver detection in the OpenAPI filters ThinktectureSchemaFilter and ThinktectureParameterFilter are created inside the SwaggerGenOptions configuration of AddThinktectureOpenApiFilters. Their constructors resolved ISerializerDataContractResolver to detect Swashbuckle.AspNetCore.Newtonsoft. Since Swashbuckle 10.3.0 the default resolver is built from SchemaGeneratorOptions, which are configured from SwaggerGenOptions. Resolving it during that configuration re-enters it, so building the OpenAPI document recurses endlessly and hangs (e.g. dotnet-getdocument times out at build time). Detect the resolver lazily on first use instead. --- .../Internal/ThinktectureParameterFilter.cs | 8 ++++---- .../Internal/ThinktectureSchemaFilter.cs | 6 +++--- ...ectureSchemaFilterTests.ObjectFactories.cs | 20 +++++++++++++++++++ .../ThinktectureSchemaFilterTests.cs | 15 ++++++++++++++ 4 files changed, 42 insertions(+), 7 deletions(-) diff --git a/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureParameterFilter.cs b/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureParameterFilter.cs index 09fb53df..d7b02f3b 100644 --- a/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureParameterFilter.cs +++ b/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureParameterFilter.cs @@ -14,7 +14,7 @@ namespace Thinktecture.Swashbuckle.Internal; public class ThinktectureParameterFilter : IParameterFilter { private readonly bool _createExtraSchemasForParameters; - private readonly bool _documentNewtonsoftJsonSerialization; + private readonly Lazy _documentNewtonsoftJsonSerialization; /// /// This is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to @@ -27,7 +27,7 @@ public ThinktectureParameterFilter( IOptions options) { _createExtraSchemasForParameters = options.Value.CreateExtraSchemasForParameters; - _documentNewtonsoftJsonSerialization = ThinktectureSchemaFilter.IsNewtonsoftDataContractResolverRegistered(serviceProvider); + _documentNewtonsoftJsonSerialization = new Lazy(() => ThinktectureSchemaFilter.IsNewtonsoftDataContractResolverRegistered(serviceProvider)); } /// @@ -44,7 +44,7 @@ public void Apply( return; modelBindingType = context.ParameterInfo.ParameterType.NormalizeStructType(modelBindingType); - var serializationType = ThinktectureSchemaFilter.GetSerializationType(context.ParameterInfo.ParameterType, _documentNewtonsoftJsonSerialization); + var serializationType = ThinktectureSchemaFilter.GetSerializationType(context.ParameterInfo.ParameterType, _documentNewtonsoftJsonSerialization.Value); if (parameter is not OpenApiParameter openApiParameter || (parameter.Schema?.AllOf?.Count > 0 // Wrapper made by UseAllOfToExtendReferenceSchemas. @@ -81,7 +81,7 @@ public void Apply( // return the key type, because ThinktectureModelBinderProvider falls back to the key for model binding. if (MetadataLookup.Find(parameterType) is Metadata.Keyed keyedMetadata) { - return ThinktectureSchemaFilter.GetSerializationType(parameterType, _documentNewtonsoftJsonSerialization) == parameterType + return ThinktectureSchemaFilter.GetSerializationType(parameterType, _documentNewtonsoftJsonSerialization.Value) == parameterType ? keyedMetadata.Type : keyedMetadata.KeyType; } diff --git a/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureSchemaFilter.cs b/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureSchemaFilter.cs index ad3ffe82..e901bd4a 100644 --- a/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureSchemaFilter.cs +++ b/src/Thinktecture.Runtime.Extensions.Swashbuckle/Swashbuckle/Internal/ThinktectureSchemaFilter.cs @@ -26,7 +26,7 @@ public class ThinktectureSchemaFilter : ISchemaFilter private readonly IAdHocUnionSchemaFilter _adHocUnionSchemaFilter; private readonly IRegularUnionSchemaFilter _regularUnionSchemaFilter; private readonly SwaggerGenOptions _swaggerGenOptions; - private readonly bool _documentNewtonsoftJsonSerialization; + private readonly Lazy _documentNewtonsoftJsonSerialization; /// /// This is an internal API that supports the Thinktecture.Runtime.Extensions infrastructure and not subject to @@ -51,7 +51,7 @@ public ThinktectureSchemaFilter( _adHocUnionSchemaFilter = adHocUnionSchemaFilter; _regularUnionSchemaFilter = regularUnionSchemaFilter; _swaggerGenOptions = swaggerGenOptions; - _documentNewtonsoftJsonSerialization = IsNewtonsoftDataContractResolverRegistered(serviceProvider); + _documentNewtonsoftJsonSerialization = new Lazy(() => IsNewtonsoftDataContractResolverRegistered(serviceProvider)); } internal static bool IsNewtonsoftDataContractResolverRegistered(IServiceProvider serviceProvider) @@ -139,7 +139,7 @@ private bool TryHandleTypeWithObjectFactory( OpenApiSchema schema, SchemaFilterContext context) { - var serializationType = GetSerializationType(context.Type, _documentNewtonsoftJsonSerialization); + var serializationType = GetSerializationType(context.Type, _documentNewtonsoftJsonSerialization.Value); // Return to prevent infinite recursion if (context.Type == serializationType) diff --git a/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.ObjectFactories.cs b/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.ObjectFactories.cs index a7dea29c..21f46983 100644 --- a/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.ObjectFactories.cs +++ b/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.ObjectFactories.cs @@ -98,6 +98,26 @@ public async Task Should_use_Newtonsoft_only_object_factory_when_Newtonsoft_supp schema.TryGetProperty("properties", out _).Should().BeFalse(); } + [Fact] + public async Task Should_generate_document_when_data_contract_resolver_depends_on_SchemaGeneratorOptions() + { + // Regression: the filters looked up ISerializerDataContractResolver while SwaggerGenOptions were being + // configured. With Swashbuckle 10.3+ resolving it re-enters that configuration and recurses endlessly. + _buildDataContractResolverFromSchemaGeneratorOptions = true; + + App.MapPost("/test", ([FromBody] ComplexValueObjectWithSystemTextJsonOnlyObjectFactory value) => value); + + var openApi = await GetOpenApiJsonAsync(); + + using var document = JsonDocument.Parse(openApi); + + document.RootElement + .GetProperty("components") + .GetProperty("schemas") + .TryGetProperty(nameof(ComplexValueObjectWithSystemTextJsonOnlyObjectFactory), out _) + .Should().BeTrue(); + } + [Fact] public async Task Should_ignore_MessagePack_only_object_factory_when_Newtonsoft_support_is_registered() { diff --git a/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.cs b/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.cs index daef4a98..0c65d87b 100644 --- a/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.cs +++ b/test/Thinktecture.Runtime.Extensions.Swashbuckle.Tests.Sources/Swashbuckle/ThinktectureSchemaFilterTests.cs @@ -7,10 +7,13 @@ using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.OpenApi; using NSubstitute; +using Swashbuckle.AspNetCore.SwaggerGen; using Thinktecture.Runtime.Tests.Swashbuckle.Helpers; using Thinktecture.Swashbuckle; @@ -31,6 +34,7 @@ public abstract partial class ThinktectureSchemaFilterTests : IAsyncLifetime private bool _useStringEnumConverter; private bool _keepOrphanedKeyTypeSchemas; private bool _addSwaggerGenNewtonsoftSupport; + private bool _buildDataContractResolverFromSchemaGeneratorOptions; private Type? _controllerType; protected ThinktectureSchemaFilterTests(ITestOutputHelper testOutputHelper) @@ -76,6 +80,17 @@ private WebApplication CreateApp() appBuilder.Services.AddSwaggerGenNewtonsoftSupport(); } + if (_buildDataContractResolverFromSchemaGeneratorOptions) + { + // Mirrors the registration of Swashbuckle 10.3+, whose ISerializerDataContractResolver depends on + // SchemaGeneratorOptions, which in turn are configured from SwaggerGenOptions. + appBuilder.Services.Replace(ServiceDescriptor.Singleton(serviceProvider => + { + _ = serviceProvider.GetRequiredService>().Value; + return new JsonSerializerDataContractResolver(new System.Text.Json.JsonSerializerOptions()); + })); + } + if (_useStringEnumConverter) { // Swashbuckle's schema generator and Thinktecture's value factory both read the MVC JSON options,