Skip to content

Commit 00890c9

Browse files
Avoid exception checking nullability
- Do not throw if we cannot determine the nullability of a dictionary. - Clean-up some code analysis suggestions. Resolves #3070. Resolves #2793.
1 parent b8e1f0f commit 00890c9

File tree

3 files changed

+107
-15
lines changed

3 files changed

+107
-15
lines changed

src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/MemberInfoExtensions.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,19 @@ public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo)
8585
{
8686
#if NET6_0_OR_GREATER
8787
var nullableInfo = GetNullabilityInfo(memberInfo);
88-
if (nullableInfo.GenericTypeArguments.Length != 2)
89-
{
90-
var length = nullableInfo.GenericTypeArguments.Length;
91-
var type = nullableInfo.Type.FullName;
92-
var container = memberInfo.DeclaringType.FullName;
93-
var member = memberInfo.Name;
94-
throw new InvalidOperationException($"Expected Dictionary to have two generic type arguments but it had {length}. Member: {container}.{member} Type: {type}.");
95-
}
9688

97-
return nullableInfo.GenericTypeArguments[1].ReadState == NullabilityState.NotNull;
89+
// Assume one generic argument means TKey and TValue are the same type.
90+
// Assume two generic arguments match TKey and TValue for a dictionary.
91+
// A better solution would be to inspect the type declaration (base types,
92+
// interfaces, etc.) to determine if the type is a dictionary, but the
93+
// nullability information is not available to be able to do that.
94+
// See https://stackoverflow.com/q/75786306/1064169.
95+
return nullableInfo.GenericTypeArguments.Length switch
96+
{
97+
1 => nullableInfo.GenericTypeArguments[0].ReadState == NullabilityState.NotNull,
98+
2 => nullableInfo.GenericTypeArguments[1].ReadState == NullabilityState.NotNull,
99+
_ => false,
100+
};
98101
#else
99102
var memberType = memberInfo.MemberType == MemberTypes.Field
100103
? ((FieldInfo)memberInfo).FieldType
@@ -156,19 +159,19 @@ private static bool GetNullableFallbackValue(this MemberInfo memberInfo)
156159
{
157160
var declaringTypes = memberInfo.DeclaringType.IsNested
158161
? GetDeclaringTypeChain(memberInfo)
159-
: new List<Type>(1) { memberInfo.DeclaringType };
162+
: [memberInfo.DeclaringType];
160163

161164
foreach (var declaringType in declaringTypes)
162165
{
163-
var attributes = (IEnumerable<object>)declaringType.GetCustomAttributes(false);
166+
IEnumerable<object> attributes = declaringType.GetCustomAttributes(false);
164167

165168
var nullableContext = attributes
166169
.FirstOrDefault(attr => string.Equals(attr.GetType().FullName, NullableContextAttributeFullTypeName));
167170

168171
if (nullableContext != null)
169172
{
170173
if (nullableContext.GetType().GetField(FlagFieldName) is FieldInfo field &&
171-
field.GetValue(nullableContext) is byte flag && flag == NotAnnotated)
174+
field.GetValue(nullableContext) is byte flag && flag == NotAnnotated)
172175
{
173176
return true;
174177
}

src/Swashbuckle.AspNetCore.SwaggerGen/SchemaGenerator/SchemaGenerator.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ public SchemaGenerator(SchemaGeneratorOptions generatorOptions, ISerializerDataC
2727
{
2828
}
2929

30-
public SchemaGenerator(SchemaGeneratorOptions generatorOptions, ISerializerDataContractResolver serializerDataContractResolver, IOptions<MvcOptions> mvcOptions)
30+
public SchemaGenerator(
31+
SchemaGeneratorOptions generatorOptions,
32+
ISerializerDataContractResolver serializerDataContractResolver,
33+
IOptions<MvcOptions> mvcOptions)
3134
{
3235
_generatorOptions = generatorOptions;
3336
_serializerDataContractResolver = serializerDataContractResolver;
@@ -104,7 +107,7 @@ private OpenApiSchema GenerateSchemaForMember(
104107
var genericTypes = modelType
105108
.GetInterfaces()
106109
#if NETSTANDARD2_0
107-
.Concat(new[] { modelType })
110+
.Concat([modelType])
108111
#else
109112
.Append(modelType)
110113
#endif
@@ -309,7 +312,7 @@ private static OpenApiSchema CreatePrimitiveSchema(DataContract dataContract)
309312
};
310313

311314
#pragma warning disable CS0618 // Type or member is obsolete
312-
// For backcompat only - EnumValues is obsolete
315+
// For backwards compatibility only - EnumValues is obsolete
313316
if (dataContract.EnumValues != null)
314317
{
315318
schema.Enum = dataContract.EnumValues
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using Xunit;
5+
6+
namespace Swashbuckle.AspNetCore.SwaggerGen;
7+
8+
#nullable enable
9+
10+
public static class MemberInfoExtensionsTests
11+
{
12+
[Theory]
13+
[InlineData(typeof(MyClass), nameof(MyClass.DictionaryInt32NonNullable), true)]
14+
[InlineData(typeof(MyClass), nameof(MyClass.DictionaryInt32Nullable), false)]
15+
[InlineData(typeof(MyClass), nameof(MyClass.DictionaryStringNonNullable), true)]
16+
[InlineData(typeof(MyClass), nameof(MyClass.DictionaryStringNullable), false)]
17+
[InlineData(typeof(MyClass), nameof(MyClass.IDictionaryInt32NonNullable), true)]
18+
[InlineData(typeof(MyClass), nameof(MyClass.IDictionaryInt32Nullable), false)]
19+
[InlineData(typeof(MyClass), nameof(MyClass.IDictionaryStringNonNullable), true)]
20+
[InlineData(typeof(MyClass), nameof(MyClass.IDictionaryStringNullable), false)]
21+
[InlineData(typeof(MyClass), nameof(MyClass.IReadOnlyDictionaryInt32NonNullable), true)]
22+
[InlineData(typeof(MyClass), nameof(MyClass.IReadOnlyDictionaryInt32Nullable), false)]
23+
[InlineData(typeof(MyClass), nameof(MyClass.IReadOnlyDictionaryStringNonNullable), true)]
24+
[InlineData(typeof(MyClass), nameof(MyClass.IReadOnlyDictionaryStringNullable), false)]
25+
[InlineData(typeof(MyClass), nameof(MyClass.StringDictionary), false)] // There is no way to inspect the nullability of the base class' TValue argument
26+
[InlineData(typeof(MyClass), nameof(MyClass.NullableStringDictionary), false)]
27+
[InlineData(typeof(MyClass), nameof(MyClass.SameTypesDictionary), true)]
28+
[InlineData(typeof(MyClass), nameof(MyClass.CustomDictionaryStringNullable), false)]
29+
[InlineData(typeof(MyClass), nameof(MyClass.CustomDictionaryStringNonNullable), true)]
30+
public static void IsDictionaryValueNonNullable_Returns_Correct_Value(Type type, string memberName, bool expected)
31+
{
32+
// Arrange
33+
var memberInfo = type.GetMember(memberName).First();
34+
35+
// Act
36+
var actual = memberInfo.IsDictionaryValueNonNullable();
37+
38+
// Assert
39+
Assert.Equal(expected, actual);
40+
}
41+
42+
public class MyClass
43+
{
44+
public Dictionary<string, int> DictionaryInt32NonNullable { get; set; } = [];
45+
46+
public Dictionary<string, int?> DictionaryInt32Nullable { get; set; } = [];
47+
48+
public Dictionary<string, string> DictionaryStringNonNullable { get; set; } = [];
49+
50+
public Dictionary<string, string?> DictionaryStringNullable { get; set; } = [];
51+
52+
public IDictionary<string, int> IDictionaryInt32NonNullable { get; set; } = new Dictionary<string, int>();
53+
54+
public IDictionary<string, int?> IDictionaryInt32Nullable { get; set; } = new Dictionary<string, int?>();
55+
56+
public IDictionary<string, string> IDictionaryStringNonNullable { get; set; } = new Dictionary<string, string>();
57+
58+
public IDictionary<string, string?> IDictionaryStringNullable { get; set; } = new Dictionary<string, string?>();
59+
60+
public IReadOnlyDictionary<string, int> IReadOnlyDictionaryInt32NonNullable { get; set; } = new Dictionary<string, int>();
61+
62+
public IReadOnlyDictionary<string, int?> IReadOnlyDictionaryInt32Nullable { get; set; } = new Dictionary<string, int?>();
63+
64+
public IReadOnlyDictionary<string, string> IReadOnlyDictionaryStringNonNullable { get; set; } = new Dictionary<string, string>();
65+
66+
public IReadOnlyDictionary<string, string?> IReadOnlyDictionaryStringNullable { get; set; } = new Dictionary<string, string?>();
67+
68+
public StringDictionary StringDictionary { get; set; } = [];
69+
70+
public NullableStringDictionary NullableStringDictionary { get; set; } = [];
71+
72+
public SameTypesDictionary<string> SameTypesDictionary { get; set; } = [];
73+
74+
public CustomDictionary<string, string?> CustomDictionaryStringNullable { get; set; } = [];
75+
76+
public CustomDictionary<string, string> CustomDictionaryStringNonNullable { get; set; } = [];
77+
}
78+
79+
public class StringDictionary : Dictionary<string, string>;
80+
81+
public class NullableStringDictionary : Dictionary<string, string?>;
82+
83+
public class SameTypesDictionary<T> : Dictionary<T, T> where T : notnull;
84+
85+
public class CustomDictionary<TKey, TValue> : Dictionary<TKey, TValue> where TKey : notnull;
86+
}

0 commit comments

Comments
 (0)