diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs index b25869be1f..8cb61ed2a1 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/DependencyInjection/NewtonsoftServiceCollectionExtensions.cs @@ -25,7 +25,7 @@ public static IServiceCollection AddSwaggerGenNewtonsoftSupport(this IServiceCol { var serializerSettings = s.GetRequiredService>().Value?.SerializerSettings ?? new(); - var generatorOptions = s.GetRequiredService>().Value; + var generatorOptions = s.GetRequiredService>(); return new NewtonsoftDataContractResolver(serializerSettings, generatorOptions); })); diff --git a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs index 99e2b3442f..6e348853df 100644 --- a/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.Newtonsoft/SchemaGenerator/NewtonsoftDataContractResolver.cs @@ -1,4 +1,5 @@ using System.Reflection; +using Microsoft.Extensions.Options; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Newtonsoft.Json.Serialization; @@ -13,7 +14,7 @@ namespace Swashbuckle.AspNetCore.Newtonsoft; public class NewtonsoftDataContractResolver : ISerializerDataContractResolver { private readonly JsonSerializerSettings _serializerSettings; - private readonly SchemaGeneratorOptions _generatorOptions; + private readonly IOptions _generatorOptions; private readonly IContractResolver _contractResolver; /// @@ -31,9 +32,14 @@ public NewtonsoftDataContractResolver(JsonSerializerSettings serializerSettings) /// The to use. /// The to use. public NewtonsoftDataContractResolver(JsonSerializerSettings serializerSettings, SchemaGeneratorOptions generatorOptions) + : this(serializerSettings, Options.Create(generatorOptions ?? new SchemaGeneratorOptions())) + { + } + + internal NewtonsoftDataContractResolver(JsonSerializerSettings serializerSettings, IOptions generatorOptions) { _serializerSettings = serializerSettings; - _generatorOptions = generatorOptions ?? new SchemaGeneratorOptions(); + _generatorOptions = generatorOptions; _contractResolver = serializerSettings.ContractResolver ?? new DefaultContractResolver(); } @@ -218,8 +224,9 @@ private List GetDataPropertiesFor(JsonObjectContract jsonObjectCon private bool IsNullable(MemberInfo memberInfo, Type propertyType) { var nullable = propertyType.IsReferenceOrNullableType(); + var generatorOptions = _generatorOptions.Value; - if ((_generatorOptions.SupportNonNullableReferenceTypes || _generatorOptions.NonNullableReferenceTypesAsRequired) + if ((generatorOptions.SupportNonNullableReferenceTypes || generatorOptions.NonNullableReferenceTypesAsRequired) && memberInfo != null) { nullable &= !memberInfo.IsNonNullableReferenceType(); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs index 09083330ca..f99fd40a65 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/DependencyInjection/SwaggerGenServiceCollectionExtensions.cs @@ -33,7 +33,7 @@ public static IServiceCollection AddSwaggerGen( services.TryAddSingleton(s => { var serializerOptions = s.GetRequiredService().Options; - var generatorOptions = s.GetRequiredService>().Value; + var generatorOptions = s.GetRequiredService>(); return new JsonSerializerDataContractResolver(serializerOptions, generatorOptions); }); diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs index 70ec5404b2..1276c5abe4 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/JsonSerializerDataContractResolver.cs @@ -2,6 +2,7 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; +using Microsoft.Extensions.Options; using Swashbuckle.AspNetCore.Annotations; namespace Swashbuckle.AspNetCore.SwaggerGen; @@ -9,7 +10,7 @@ namespace Swashbuckle.AspNetCore.SwaggerGen; public class JsonSerializerDataContractResolver : ISerializerDataContractResolver { private readonly JsonSerializerOptions _serializerOptions; - private readonly SchemaGeneratorOptions _generatorOptions; + private readonly IOptions _generatorOptions; public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOptions) : this(serializerOptions, new SchemaGeneratorOptions()) @@ -17,9 +18,14 @@ public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOption } public JsonSerializerDataContractResolver(JsonSerializerOptions serializerOptions, SchemaGeneratorOptions generatorOptions) + : this(serializerOptions, Options.Create(generatorOptions ?? new SchemaGeneratorOptions())) + { + } + + internal JsonSerializerDataContractResolver(JsonSerializerOptions serializerOptions, IOptions generatorOptions) { _serializerOptions = serializerOptions; - _generatorOptions = generatorOptions ?? new SchemaGeneratorOptions(); + _generatorOptions = generatorOptions; } public DataContract GetDataContractForType(Type type) @@ -285,9 +291,10 @@ private List GetDataPropertiesFor(Type objectType, out Type extens private bool IsNullable(PropertyInfo propertyInfo, Type propertyType) { var nullable = propertyType.IsReferenceOrNullableType(); + var generatorOptions = _generatorOptions.Value; - if (_generatorOptions.SupportNonNullableReferenceTypes || - _generatorOptions.NonNullableReferenceTypesAsRequired) + if (generatorOptions.SupportNonNullableReferenceTypes || + generatorOptions.NonNullableReferenceTypesAsRequired) { nullable &= !propertyInfo.IsNonNullableReferenceType(); } diff --git a/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftServiceCollectionExtensionsTests.cs b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftServiceCollectionExtensionsTests.cs new file mode 100644 index 0000000000..e2506d7e80 --- /dev/null +++ b/test/Swashbuckle.AspNetCore.Newtonsoft.Test/NewtonsoftServiceCollectionExtensionsTests.cs @@ -0,0 +1,33 @@ +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi; +using Swashbuckle.AspNetCore.SwaggerGen; + +namespace Swashbuckle.AspNetCore.Newtonsoft.Test; + +public static class NewtonsoftServiceCollectionExtensionsTests +{ + [Fact] + public static async Task AddSwaggerGenNewtonsoftSupport_CreatesSchemaFilter_WhenItDependsOnDataContractResolver() + { + var services = new ServiceCollection(); + services.AddOptions(); + services.AddSwaggerGen((options) => options.SchemaFilter()); + services.AddSwaggerGenNewtonsoftSupport(); + + var provider = services.BuildServiceProvider(); + + // A dependency cycle through the options hangs instead of throwing, so resolve + // them on another thread to fail the test with a timeout. + var options = await Task.Run(() => provider.GetRequiredService>().Value) + .WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); + + Assert.IsType(Assert.Single(options.SchemaFilters)); + await provider.DisposeAsync(); + } + + private sealed class DataContractResolverSchemaFilter(ISerializerDataContractResolver resolver) : ISchemaFilter + { + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) => _ = resolver; + } +} diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenServiceCollectionExtensionsTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenServiceCollectionExtensionsTests.cs index 2e9f11596f..56ff7273ca 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenServiceCollectionExtensionsTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SwaggerGenServiceCollectionExtensionsTests.cs @@ -2,6 +2,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Options; +using Microsoft.OpenApi; using NSubstitute; namespace Swashbuckle.AspNetCore.SwaggerGen.Test; @@ -62,6 +64,23 @@ public static void AddSwaggerGen_UsesCamelCase_WhenNothingIsConfigured() Assert.Equal("helloWorld", Assert.Single(contract.ObjectProperties).Name); } + [Fact] + public static async Task AddSwaggerGen_CreatesSchemaFilter_WhenItDependsOnDataContractResolver() + { + var services = CreateServices(); + services.AddSwaggerGen((options) => options.SchemaFilter()); + + var provider = services.BuildServiceProvider(); + + // A dependency cycle through the options hangs instead of throwing, so resolve + // them on another thread to fail the test with a timeout. + var options = await Task.Run(() => provider.GetRequiredService>().Value) + .WaitAsync(TimeSpan.FromSeconds(10), TestContext.Current.CancellationToken); + + Assert.IsType(Assert.Single(options.SchemaFilters)); + await provider.DisposeAsync(); + } + private static JsonNamingPolicy MinimalApiNamingPolicy { get; } = new SuffixNamingPolicy("-minimal-api"); private static JsonNamingPolicy MvcNamingPolicy { get; } = new SuffixNamingPolicy("-mvc"); @@ -96,4 +115,9 @@ private class Dto { public string HelloWorld { get; set; } } + + private sealed class DataContractResolverSchemaFilter(ISerializerDataContractResolver resolver) : ISchemaFilter + { + public void Apply(IOpenApiSchema schema, SchemaFilterContext context) => _ = resolver; + } }