Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes nullability problems with dictionaries #3023

Merged
merged 13 commits into from
Aug 13, 2024
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public static class MemberInfoExtensions
private const string NullableFlagsFieldName = "NullableFlags";
private const string NullableContextAttributeFullTypeName = "System.Runtime.CompilerServices.NullableContextAttribute";
private const string FlagFieldName = "Flag";
private const int NotAnnotated = 1; // See https://github.com/dotnet/roslyn/blob/af7b0ebe2b0ed5c335a928626c25620566372dd1/docs/features/nullable-metadata.md?plain=1#L40

public static IEnumerable<object> GetInlineAndMetadataAttributes(this MemberInfo memberInfo)
{
Expand Down Expand Up @@ -50,7 +51,7 @@ public static bool IsNonNullableReferenceType(this MemberInfo memberInfo)

if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field &&
field.GetValue(nullableAttribute) is byte[] flags &&
flags.Length >= 1 && flags[0] == 1)
flags.Length >= 1 && flags[0] == NotAnnotated)
{
return true;
}
Expand All @@ -67,17 +68,39 @@ public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo)
if (memberType.IsValueType) return false;

var nullableAttribute = memberInfo.GetNullableAttribute();
var genericArguments = memberType.GetGenericArguments();

if (genericArguments.Length != 2)
{
return false;
}

var valueArgument = genericArguments[1];
var valueArgumentIsNullable = valueArgument.IsGenericType && valueArgument.GetGenericTypeDefinition() == typeof(Nullable<>);

if (nullableAttribute == null)
{
return memberInfo.GetNullableFallbackValue();
return !valueArgumentIsNullable && memberInfo.GetNullableFallbackValue();
}

if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field &&
field.GetValue(nullableAttribute) is byte[] flags &&
flags.Length == 3 && flags[2] == 1)
if (nullableAttribute.GetType().GetField(NullableFlagsFieldName) is FieldInfo field)
{
return true;
if (field.GetValue(nullableAttribute) is byte[] flags)
{
// See https://github.com/dotnet/roslyn/blob/af7b0ebe2b0ed5c335a928626c25620566372dd1/docs/features/nullable-metadata.md
// Observations in the debugger show that the arity of the flags array is 3 only if all 3 items are reference types, i.e.
// Dictionary<string, object> would have arity 3 (one for the Dictionary, one for the string key, one for the object value),
// however Dictionary<string, int> would have arity 2 (one for the Dictionary, one for the string key), the value is skipped
// due it being a value type.
if (flags.Length == 2) // Value in the dictionary is a value type.
{
return !valueArgumentIsNullable;
}
martincostello marked this conversation as resolved.
Show resolved Hide resolved
else if (flags.Length == 3) // Value in the dictionary is a reference type.
{
return flags[2] == NotAnnotated;
}
}
}

return false;
Expand Down Expand Up @@ -108,7 +131,7 @@ private static bool GetNullableFallbackValue(this MemberInfo memberInfo)
if (nullableContext != null)
{
if (nullableContext.GetType().GetField(FlagFieldName) is FieldInfo field &&
field.GetValue(nullableContext) is byte flag && flag == 1)
field.GetValue(nullableContext) is byte flag && flag == NotAnnotated)
{
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,26 @@ private OpenApiSchema GenerateSchemaForMember(
}

// NullableAttribute behaves differently for Dictionaries
if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType &&
modelType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
if (schema.AdditionalPropertiesAllowed && modelType.IsGenericType)
{
schema.AdditionalProperties.Nullable = !memberInfo.IsDictionaryValueNonNullable();
var genericTypes = modelType
.GetInterfaces()
#if NETSTANDARD2_0
.Concat(new[] { modelType })
#else
.Append(modelType)
#endif
.Where(t => t.IsGenericType)
.ToArray();

var isDictionaryType =
genericTypes.Any(t => t.GetGenericTypeDefinition() == typeof(IDictionary<,>)) ||
genericTypes.Any(t => t.GetGenericTypeDefinition() == typeof(IReadOnlyDictionary<,>));

if (isDictionaryType)
{
schema.AdditionalProperties.Nullable = !memberInfo.IsDictionaryValueNonNullable();
}
}

schema.ApplyValidationAttributes(customAttributes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -696,11 +696,131 @@ public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes(
}

[Theory]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableDictionaryWithNonNullableContent), true, false)]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableDictionaryWithNonNullableContent), false, false)]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NonNullableDictionaryWithNullableContent), false, true)]
[InlineData(typeof(TypeWithNullableContext), nameof(TypeWithNullableContext.NullableDictionaryWithNullableContent), true, true)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations(
[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)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_Dictionary(
Type declaringType,
string propertyName,
bool expectedNullableProperty,
bool expectedNullableContent)
{
var subject = Subject(
configureGenerator: c => c.SupportNonNullableReferenceTypes = true
);
var schemaRepository = new SchemaRepository();

var referenceSchema = subject.GenerateSchema(declaringType, schemaRepository);

var propertySchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName];
var contentSchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName].AdditionalProperties;
Assert.Equal(expectedNullableProperty, propertySchema.Nullable);
Assert.Equal(expectedNullableContent, contentSchema.Nullable);
}

[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)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IDictionary(
Type declaringType,
string propertyName,
bool expectedNullableProperty,
bool expectedNullableContent)
{
var subject = Subject(
configureGenerator: c => c.SupportNonNullableReferenceTypes = true
);
var schemaRepository = new SchemaRepository();

var referenceSchema = subject.GenerateSchema(declaringType, schemaRepository);

var propertySchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName];
var contentSchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName].AdditionalProperties;
Assert.Equal(expectedNullableProperty, propertySchema.Nullable);
Assert.Equal(expectedNullableContent, contentSchema.Nullable);
}

[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)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IReadOnlyDictionary(
Type declaringType,
string propertyName,
bool expectedNullableProperty,
bool expectedNullableContent)
{
var subject = Subject(
configureGenerator: c => c.SupportNonNullableReferenceTypes = true
);
var schemaRepository = new SchemaRepository();

var referenceSchema = subject.GenerateSchema(declaringType, schemaRepository);

var propertySchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName];
var contentSchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName].AdditionalProperties;
Assert.Equal(expectedNullableProperty, propertySchema.Nullable);
Assert.Equal(expectedNullableContent, contentSchema.Nullable);
}

[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)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_DictionaryWithValueType(
Type declaringType,
string propertyName,
bool expectedNullableProperty,
bool expectedNullableContent)
{
var subject = Subject(
configureGenerator: c => c.SupportNonNullableReferenceTypes = true
);
var schemaRepository = new SchemaRepository();

var referenceSchema = subject.GenerateSchema(declaringType, schemaRepository);

var propertySchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName];
var contentSchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName].AdditionalProperties;
Assert.Equal(expectedNullableProperty, propertySchema.Nullable);
Assert.Equal(expectedNullableContent, contentSchema.Nullable);
}

[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)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IDictionaryWithValueType(
Type declaringType,
string propertyName,
bool expectedNullableProperty,
bool expectedNullableContent)
{
var subject = Subject(
configureGenerator: c => c.SupportNonNullableReferenceTypes = true
);
var schemaRepository = new SchemaRepository();

var referenceSchema = subject.GenerateSchema(declaringType, schemaRepository);

var propertySchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName];
var contentSchema = schemaRepository.Schemas[referenceSchema.Reference.Id].Properties[propertyName].AdditionalProperties;
Assert.Equal(expectedNullableProperty, propertySchema.Nullable);
Assert.Equal(expectedNullableContent, contentSchema.Nullable);
}

[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)]
public void GenerateSchema_SupportsOption_SupportNonNullableReferenceTypes_NullableAttribute_Compiler_Optimizations_Situations_IReadOnlyDictionaryWithValueType(
Type declaringType,
string propertyName,
bool expectedNullableProperty,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,35 @@ public class TypeWithNullableContext
public List<SubTypeWithOneNullableContent>? NullableList { get; set; }
public List<SubTypeWithOneNonNullableContent> NonNullableList { get; set; } = default!;

public Dictionary<string, string>? NullableDictionaryWithNonNullableContent { get; set; }
public Dictionary<string, string> NonNullableDictionaryWithNonNullableContent { get; set; } = default!;
public Dictionary<string, string?> NonNullableDictionaryWithNullableContent { get; set; } = default!;
public Dictionary<string, string?>? NullableDictionaryWithNullableContent { get; set; }
public Dictionary<string, string>? NullableDictionaryInNonNullableContent { get; set; }
public Dictionary<string, string> NonNullableDictionaryInNonNullableContent { get; set; } = default!;
public Dictionary<string, string?> NonNullableDictionaryInNullableContent { get; set; } = default!;
public Dictionary<string, string?>? NullableDictionaryInNullableContent { get; set; }

public IDictionary<string, string>? NullableIDictionaryInNonNullableContent { get; set; }
public IDictionary<string, string> NonNullableIDictionaryInNonNullableContent { get; set; } = default!;
public IDictionary<string, string?> NonNullableIDictionaryInNullableContent { get; set; } = default!;
public IDictionary<string, string?>? NullableIDictionaryInNullableContent { get; set; }

public IReadOnlyDictionary<string, string>? NullableIReadOnlyDictionaryInNonNullableContent { get; set; }
public IReadOnlyDictionary<string, string> NonNullableIReadOnlyDictionaryInNonNullableContent { get; set; } = default!;
public IReadOnlyDictionary<string, string?> NonNullableIReadOnlyDictionaryInNullableContent { get; set; } = default!;
public IReadOnlyDictionary<string, string?>? NullableIReadOnlyDictionaryInNullableContent { get; set; }

public Dictionary<string, int>? NullableDictionaryWithValueTypeInNonNullableContent { get; set; }
public Dictionary<string, int> NonNullableDictionaryWithValueTypeInNonNullableContent { get; set; } = default!;
public Dictionary<string, int?> NonNullableDictionaryWithValueTypeInNullableContent { get; set; } = default!;
public Dictionary<string, int?>? NullableDictionaryWithValueTypeInNullableContent { get; set; }

public IDictionary<string, int>? NullableIDictionaryWithValueTypeInNonNullableContent { get; set; }
public IDictionary<string, int> NonNullableIDictionaryWithValueTypeInNonNullableContent { get; set; } = default!;
public IDictionary<string, int?> NonNullableIDictionaryWithValueTypeInNullableContent { get; set; } = default!;
public IDictionary<string, int?>? NullableIDictionaryWithValueTypeInNullableContent { get; set; }

public IReadOnlyDictionary<string, int>? NullableIReadOnlyDictionaryWithValueTypeInNonNullableContent { get; set; }
public IReadOnlyDictionary<string, int> NonNullableIReadOnlyDictionaryWithValueTypeInNonNullableContent { get; set; } = default!;
public IReadOnlyDictionary<string, int?> NonNullableIReadOnlyDictionaryWithValueTypeInNullableContent { get; set; } = default!;
public IReadOnlyDictionary<string, int?>? NullableIReadOnlyDictionaryWithValueTypeInNullableContent { get; set; }

public class SubTypeWithOneNullableContent
{
Expand All @@ -33,4 +58,4 @@ public class SubTypeWithOneNonNullableContent

}
#nullable restore
}
}