From 575ddc607ff102ca290e4c9df2397aa2ff20178c Mon Sep 17 00:00:00 2001 From: Patrik Westerlund Date: Fri, 23 Aug 2024 09:21:46 +0200 Subject: [PATCH 1/3] Use NullabilityInfoContext to determine dictionary value nullability --- .../SchemaGenerator/MemberInfoExtensions.cs | 14 ++ .../JsonSerializerSchemaGeneratorTests.cs | 159 +++++++++++++----- .../Fixtures/TypeWithNullableContext.cs | 112 +++++++++++- 3 files changed, 241 insertions(+), 44 deletions(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs index 34f9a51e28..b34a1bf7ef 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs @@ -61,6 +61,19 @@ public static bool IsNonNullableReferenceType(this MemberInfo memberInfo) public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo) { +#if NET6_0_OR_GREATER + var context = new NullabilityInfoContext(); + var nullableInfo = memberInfo.MemberType == MemberTypes.Field + ? context.Create((FieldInfo)memberInfo) + : context.Create((PropertyInfo)memberInfo); + + if (nullableInfo.GenericTypeArguments.Length != 2) + { + throw new InvalidOperationException("Expected Dictionary to have two generic type arguments."); + } + + return nullableInfo.GenericTypeArguments[1].ReadState == NullabilityState.NotNull; +#else var memberType = memberInfo.MemberType == MemberTypes.Field ? ((FieldInfo)memberInfo).FieldType : ((PropertyInfo)memberInfo).PropertyType; @@ -104,6 +117,7 @@ public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo) } return false; +#endif } private static object GetNullableAttribute(this MemberInfo memberInfo) diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index 1aaaa43d31..ba1a818861 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -5,6 +5,7 @@ using System.Dynamic; using System.Linq; using System.Net; +using System.Reflection; using System.Text.Json; using System.Text.Json.Serialization; using Microsoft.AspNetCore.Http; @@ -670,15 +671,55 @@ public void GenerateSchema_SupportsOption_UseInlineDefinitionsForEnums() Assert.NotNull(schema.Enum); } + [Fact] + public void TypeWithNullableContextAnnotated_IsAnnotated() + { + const string Name = "System.Runtime.CompilerServices.NullableContextAttribute"; + + var nullableContext = typeof(TypeWithNullableContextAnnotated) + .GetCustomAttributes() + .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, Name)); + + Assert.NotNull(nullableContext); + + var flag = nullableContext?.GetType().GetField("Flag")?.GetValue(nullableContext); + + Assert.Equal((byte)2, flag); + } + + [Fact] + public void TypeWithNullableContextNotAnnotated_IsNotAnnotated() + { + const string Name = "System.Runtime.CompilerServices.NullableContextAttribute"; + + var nullableContext = typeof(TypeWithNullableContextNotAnnotated) + .GetCustomAttributes() + .FirstOrDefault(attr => string.Equals(attr.GetType().FullName, Name)); + + Assert.NotNull(nullableContext); + + var flag = nullableContext?.GetType().GetField("Flag")?.GetValue(nullableContext); + + Assert.Equal((byte)1, flag); + } + [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableInt), true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableInt), false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableString), true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableString), false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableArray), true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableArray), false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableList), true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableList), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableInt), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableInt), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableString), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableString), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableArray), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableArray), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableList), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableList), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableInt), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableInt), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableString), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableString), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableArray), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableArray), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableList), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableList), false)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes( Type declaringType, string propertyName, @@ -696,10 +737,14 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes( } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableDictionaryInNonNullableContent), true, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableDictionaryInNonNullableContent), false, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableDictionaryInNullableContent), false, true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableDictionaryInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableDictionaryInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableDictionaryInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableDictionaryInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableDictionaryInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableDictionaryInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableDictionaryInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableDictionaryInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableDictionaryInNullableContent), true, true)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_Dictionary( Type declaringType, string propertyName, @@ -720,10 +765,14 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_Nulla } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIDictionaryInNonNullableContent), true, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIDictionaryInNonNullableContent), false, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIDictionaryInNullableContent), false, true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIDictionaryInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIDictionaryInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIDictionaryInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIDictionaryInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIDictionaryInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIDictionaryInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIDictionaryInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIDictionaryInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIDictionaryInNullableContent), true, true)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IDictionary( Type declaringType, string propertyName, @@ -744,10 +793,14 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_Nulla } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIReadOnlyDictionaryInNonNullableContent), true, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIReadOnlyDictionaryInNonNullableContent), false, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIReadOnlyDictionaryInNullableContent), false, true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIReadOnlyDictionaryInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIReadOnlyDictionaryInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIReadOnlyDictionaryInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIReadOnlyDictionaryInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIReadOnlyDictionaryInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIReadOnlyDictionaryInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIReadOnlyDictionaryInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIReadOnlyDictionaryInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIReadOnlyDictionaryInNullableContent), true, true)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IReadOnlyDictionary( Type declaringType, string propertyName, @@ -768,10 +821,14 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_Nulla } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableDictionaryWithValueTypeInNonNullableContent), true, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableDictionaryWithValueTypeInNonNullableContent), false, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableDictionaryWithValueTypeInNullableContent), false, true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableDictionaryWithValueTypeInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableDictionaryWithValueTypeInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableDictionaryWithValueTypeInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableDictionaryWithValueTypeInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableDictionaryWithValueTypeInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableDictionaryWithValueTypeInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableDictionaryWithValueTypeInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableDictionaryWithValueTypeInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableDictionaryWithValueTypeInNullableContent), true, true)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_DictionaryWithValueType( Type declaringType, string propertyName, @@ -792,10 +849,14 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_Nulla } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIDictionaryWithValueTypeInNonNullableContent), true, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIDictionaryWithValueTypeInNonNullableContent), false, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIDictionaryWithValueTypeInNullableContent), false, true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIDictionaryWithValueTypeInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIDictionaryWithValueTypeInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIDictionaryWithValueTypeInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIDictionaryWithValueTypeInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIDictionaryWithValueTypeInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIDictionaryWithValueTypeInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIDictionaryWithValueTypeInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIDictionaryWithValueTypeInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIDictionaryWithValueTypeInNullableContent), true, true)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IDictionaryWithValueType( Type declaringType, string propertyName, @@ -816,10 +877,14 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_Nulla } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIReadOnlyDictionaryWithValueTypeInNonNullableContent), true, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIReadOnlyDictionaryWithValueTypeInNonNullableContent), false, false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableIReadOnlyDictionaryWithValueTypeInNullableContent), false, true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableIReadOnlyDictionaryWithValueTypeInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIReadOnlyDictionaryWithValueTypeInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIReadOnlyDictionaryWithValueTypeInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NonNullableIReadOnlyDictionaryWithValueTypeInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.NullableIReadOnlyDictionaryWithValueTypeInNullableContent), true, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIReadOnlyDictionaryWithValueTypeInNonNullableContent), true, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIReadOnlyDictionaryWithValueTypeInNonNullableContent), false, false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NonNullableIReadOnlyDictionaryWithValueTypeInNullableContent), false, true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.NullableIReadOnlyDictionaryWithValueTypeInNullableContent), true, true)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IReadOnlyDictionaryWithValueType( Type declaringType, string propertyName, @@ -840,8 +905,10 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_Nulla } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNullableContent), nameof(TypeWithNullableContext.NullableString), true)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNullableContent), nameof(TypeWithNullableContextAnnotated.NullableString), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextAnnotated.NonNullableString), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.SubTypeWithOneNullableContent), nameof(TypeWithNullableContextNotAnnotated.NullableString), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextNotAnnotated.NonNullableString), false)] public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypesInDictionary_NullableAttribute_Compiler_Optimizations_Situations( Type declaringType, string subType, @@ -860,8 +927,10 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypesInDict } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNullableContent), nameof(TypeWithNullableContext.NullableString), false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNullableContent), nameof(TypeWithNullableContextAnnotated.NullableString), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextAnnotated.NonNullableString), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.SubTypeWithOneNullableContent), nameof(TypeWithNullableContextNotAnnotated.NullableString), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextNotAnnotated.NonNullableString), true)] public void GenerateSchema_SupportsOption_NonNullableReferenceTypesAsRequired_RequiredAttribute_Compiler_Optimizations_Situations( Type declaringType, string subType, @@ -880,8 +949,10 @@ public void GenerateSchema_SupportsOption_NonNullableReferenceTypesAsRequired_Re } [Theory] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), false)] - [InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContext.NonNullableString), true)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextAnnotated.NonNullableString), false)] + [InlineData(typeof(TypeWithNullableContextAnnotated), nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextAnnotated.NonNullableString), true)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextNotAnnotated.NonNullableString), false)] + [InlineData(typeof(TypeWithNullableContextNotAnnotated), nameof(TypeWithNullableContextNotAnnotated.SubTypeWithOneNonNullableContent), nameof(TypeWithNullableContextNotAnnotated.NonNullableString), true)] public void GenerateSchema_SupportsOption_SuppressImplicitRequiredAttributeForNonNullableReferenceTypes( Type declaringType, string subType, @@ -900,8 +971,10 @@ public void GenerateSchema_SupportsOption_SuppressImplicitRequiredAttributeForNo Assert.Equal(!suppress, propertyIsRequired); } - [Fact] - public void GenerateSchema_Works_IfNotProvidingMvcOptions() + [Theory] + [InlineData(typeof(TypeWithNullableContextAnnotated))] + [InlineData(typeof(TypeWithNullableContextNotAnnotated))] + public void GenerateSchema_Works_IfNotProvidingMvcOptions(Type type) { var generatorOptions = new SchemaGeneratorOptions { @@ -913,10 +986,10 @@ public void GenerateSchema_Works_IfNotProvidingMvcOptions() var subject = new SchemaGenerator(generatorOptions, new JsonSerializerDataContractResolver(serializerOptions)); var schemaRepository = new SchemaRepository(); - subject.GenerateSchema(typeof(TypeWithNullableContext), schemaRepository); + subject.GenerateSchema(type, schemaRepository); - var subType = nameof(TypeWithNullableContext.SubTypeWithOneNonNullableContent); - var propertyName = nameof(TypeWithNullableContext.NonNullableString); + var subType = nameof(TypeWithNullableContextAnnotated.SubTypeWithOneNonNullableContent); + var propertyName = nameof(TypeWithNullableContextAnnotated.NonNullableString); var propertyIsRequired = schemaRepository.Schemas[subType].Required.Contains(propertyName); Assert.True(propertyIsRequired); } diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs index 481b950dc5..6fef5eeca7 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs @@ -3,8 +3,41 @@ namespace Swashbuckle.AspNetCore.TestSupport { #nullable enable - public class TypeWithNullableContext + + // These types are used to test our handling of nullable references types. + // NRT results in NullableContextAttribute and NullableAttribute being placed on + // types and members by the compiler. + + // Remember to mirror both types and use both types in tests. + + /// + /// We expect this type to receive NullableContext(1) (NotAnnotated) from the compiler. + /// + public class TypeWithNullableContextNotAnnotated { + // Dummies to affect the NullableContextAttribute value. + // It seems to default to the most common nullable state. + public string Dummy1 { get; } = default!; + public string Dummy2 { get; } = default!; + public string Dummy3 { get; } = default!; + public string Dummy4 { get; } = default!; + public string Dummy5 { get; } = default!; + public string Dummy6 { get; } = default!; + public string Dummy7 { get; } = default!; + public string Dummy8 { get; } = default!; + public string Dummy9 { get; } = default!; + public string Dummy10 { get; } = default!; + public string Dummy11 { get; set; } = default!; + public string Dummy12 { get; set; } = default!; + public string Dummy13 { get; set; } = default!; + public string Dummy14 { get; set; } = default!; + public string Dummy15 { get; set; } = default!; + public string Dummy16 { get; set; } = default!; + public string Dummy17 { get; set; } = default!; + public string Dummy18 { get; set; } = default!; + public string Dummy19 { get; set; } = default!; + public string Dummy20 { get; set; } = default!; + public int? NullableInt { get; set; } public int NonNullableInt { get; set; } public string? NullableString { get; set; } @@ -54,8 +87,85 @@ public class SubTypeWithOneNonNullableContent { public string NonNullableString { get; set; } = default!; } + } + + /// + /// We expect this type to receive NullableContext(2) (Annotated) from the compiler. + /// + public class TypeWithNullableContextAnnotated + { + // Dummies to affect the NullableContextAttribute value. + // It seems to default to the most common nullable state. + public string? Dummy1 { get; set; } + public string? Dummy2 { get; set; } + public string? Dummy3 { get; set; } + public string? Dummy4 { get; set; } + public string? Dummy5 { get; set; } + public string? Dummy6 { get; set; } + public string? Dummy7 { get; set; } + public string? Dummy8 { get; set; } + public string? Dummy9 { get; set; } + public string? Dummy10 { get; set; } + public string? Dummy11 { get; set; } + public string? Dummy12 { get; set; } + public string? Dummy13 { get; set; } + public string? Dummy14 { get; set; } + public string? Dummy15 { get; set; } + public string? Dummy16 { get; set; } + public string? Dummy17 { get; set; } + public string? Dummy18 { get; set; } + public string? Dummy19 { get; set; } + public string? Dummy20 { get; set; } + + public int? NullableInt { get; set; } + public int NonNullableInt { get; set; } + public string? NullableString { get; set; } + public string NonNullableString { get; set; } = default!; + public int[]? NullableArray { get; set; } + public int[] NonNullableArray { get; set; } = default!; + public List? NullableList { get; set; } + public List NonNullableList { get; set; } = default!; + + public Dictionary? NullableDictionaryInNonNullableContent { get; set; } + public Dictionary NonNullableDictionaryInNonNullableContent { get; set; } = default!; + public Dictionary NonNullableDictionaryInNullableContent { get; set; } = default!; + public Dictionary? NullableDictionaryInNullableContent { get; set; } + public IDictionary? NullableIDictionaryInNonNullableContent { get; set; } + public IDictionary NonNullableIDictionaryInNonNullableContent { get; set; } = default!; + public IDictionary NonNullableIDictionaryInNullableContent { get; set; } = default!; + public IDictionary? NullableIDictionaryInNullableContent { get; set; } + + public IReadOnlyDictionary? NullableIReadOnlyDictionaryInNonNullableContent { get; set; } + public IReadOnlyDictionary NonNullableIReadOnlyDictionaryInNonNullableContent { get; set; } = default!; + public IReadOnlyDictionary NonNullableIReadOnlyDictionaryInNullableContent { get; set; } = default!; + public IReadOnlyDictionary? NullableIReadOnlyDictionaryInNullableContent { get; set; } + + public Dictionary? NullableDictionaryWithValueTypeInNonNullableContent { get; set; } + public Dictionary NonNullableDictionaryWithValueTypeInNonNullableContent { get; set; } = default!; + public Dictionary NonNullableDictionaryWithValueTypeInNullableContent { get; set; } = default!; + public Dictionary? NullableDictionaryWithValueTypeInNullableContent { get; set; } + + public IDictionary? NullableIDictionaryWithValueTypeInNonNullableContent { get; set; } + public IDictionary NonNullableIDictionaryWithValueTypeInNonNullableContent { get; set; } = default!; + public IDictionary NonNullableIDictionaryWithValueTypeInNullableContent { get; set; } = default!; + public IDictionary? NullableIDictionaryWithValueTypeInNullableContent { get; set; } + + public IReadOnlyDictionary? NullableIReadOnlyDictionaryWithValueTypeInNonNullableContent { get; set; } + public IReadOnlyDictionary NonNullableIReadOnlyDictionaryWithValueTypeInNonNullableContent { get; set; } = default!; + public IReadOnlyDictionary NonNullableIReadOnlyDictionaryWithValueTypeInNullableContent { get; set; } = default!; + public IReadOnlyDictionary? NullableIReadOnlyDictionaryWithValueTypeInNullableContent { get; set; } + + public class SubTypeWithOneNullableContent + { + public string? NullableString { get; set; } + } + + public class SubTypeWithOneNonNullableContent + { + public string NonNullableString { get; set; } = default!; + } } #nullable restore } From 2679cc12efa7a8bbb909c0017852b81071e695b9 Mon Sep 17 00:00:00 2001 From: Patrik Westerlund Date: Sat, 24 Aug 2024 12:05:01 +0200 Subject: [PATCH 2/3] More informative exception message --- .../SchemaGenerator/MemberInfoExtensions.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs index b34a1bf7ef..574c305f32 100644 --- a/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs +++ b/src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs @@ -69,7 +69,11 @@ public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo) if (nullableInfo.GenericTypeArguments.Length != 2) { - throw new InvalidOperationException("Expected Dictionary to have two generic type arguments."); + var length = nullableInfo.GenericTypeArguments.Length; + var type = nullableInfo.Type.FullName; + var container = memberInfo.DeclaringType.FullName; + var member = memberInfo.Name; + throw new InvalidOperationException($"Expected Dictionary to have two generic type arguments but it had {length}. Member: {container}.{member} Type: {type}."); } return nullableInfo.GenericTypeArguments[1].ReadState == NullabilityState.NotNull; From bf5c554c6e10594a2999c7c6d5eff10f3c381ee4 Mon Sep 17 00:00:00 2001 From: Patrik Westerlund Date: Sat, 24 Aug 2024 12:05:16 +0200 Subject: [PATCH 3/3] Additional comments in tests --- .../JsonSerializerSchemaGeneratorTests.cs | 10 ++++++++++ .../Fixtures/TypeWithNullableContext.cs | 14 ++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs index ba1a818861..88e20d03f9 100644 --- a/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs +++ b/test/Swashbuckle.AspNetCore.SwaggerGen.Test/SchemaGenerator/JsonSerializerSchemaGeneratorTests.cs @@ -674,6 +674,11 @@ public void GenerateSchema_SupportsOption_UseInlineDefinitionsForEnums() [Fact] public void TypeWithNullableContextAnnotated_IsAnnotated() { + // This is a sanity check to ensure that TypeWithNullableContextAnnotated + // is annotated with NullableContext(Flag=2) by the compiler. If this is no + // longer the case, you may need to add more of the Dummy properties to + // coerce the compiler. + const string Name = "System.Runtime.CompilerServices.NullableContextAttribute"; var nullableContext = typeof(TypeWithNullableContextAnnotated) @@ -690,6 +695,11 @@ public void TypeWithNullableContextAnnotated_IsAnnotated() [Fact] public void TypeWithNullableContextNotAnnotated_IsNotAnnotated() { + // This is a sanity check to ensure that TypeWithNullableContextNotAnnotated + // is annotated with NullableContext(Flag=1) by the compiler. If this is no + // longer the case, you may need to add more of the Dummy properties to + // coerce the compiler. + const string Name = "System.Runtime.CompilerServices.NullableContextAttribute"; var nullableContext = typeof(TypeWithNullableContextNotAnnotated) diff --git a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs index 6fef5eeca7..81cf7dcb5c 100644 --- a/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs +++ b/test/Swashbuckle.AspNetCore.TestSupport/Fixtures/TypeWithNullableContext.cs @@ -11,12 +11,13 @@ namespace Swashbuckle.AspNetCore.TestSupport // Remember to mirror both types and use both types in tests. /// - /// We expect this type to receive NullableContext(1) (NotAnnotated) from the compiler. + /// We expect this type to receive NullableContext(Flag=1) (NotAnnotated) from the compiler. /// public class TypeWithNullableContextNotAnnotated { - // Dummies to affect the NullableContextAttribute value. - // It seems to default to the most common nullable state. + // Dummy properties to affect the NullableContextAttribute placed on the type. + // It seems to default to the most common nullable state, so we overwhelm + // it with non-nullable properties in order to coerce it. public string Dummy1 { get; } = default!; public string Dummy2 { get; } = default!; public string Dummy3 { get; } = default!; @@ -90,12 +91,13 @@ public class SubTypeWithOneNonNullableContent } /// - /// We expect this type to receive NullableContext(2) (Annotated) from the compiler. + /// We expect this type to receive NullableContext(Flag=2) (Annotated) from the compiler. /// public class TypeWithNullableContextAnnotated { - // Dummies to affect the NullableContextAttribute value. - // It seems to default to the most common nullable state. + // Dummy properties to affect the NullableContextAttribute placed on the type. + // It seems to default to the most common nullable state, so we overwhelm + // it with nullable properties in order to coerce it. public string? Dummy1 { get; set; } public string? Dummy2 { get; set; } public string? Dummy3 { get; set; }