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 @@ -67,17 +67,28 @@ public static bool IsDictionaryValueNonNullable(this MemberInfo memberInfo)
if (memberType.IsValueType) return false;

var nullableAttribute = memberInfo.GetNullableAttribute();
var valueArgument = memberType.GetGenericArguments()[1];
ozziepeeps marked this conversation as resolved.
Show resolved Hide resolved
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)
{
// Ref.: https://github.com/dotnet/roslyn/blob/main/docs/features/nullable-metadata.md
ozziepeeps marked this conversation as resolved.
Show resolved Hide resolved
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] == 1; // 1 means "Not annotated".
}
}
}

return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,10 +99,18 @@ 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().Concat(new[] { modelType }).Where(t => t.IsGenericType);
ozziepeeps marked this conversation as resolved.
Show resolved Hide resolved
martincostello marked this conversation as resolved.
Show resolved Hide resolved

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

if (dictionaryType)
ozziepeeps marked this conversation as resolved.
Show resolved Hide resolved
{
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)
ozziepeeps marked this conversation as resolved.
Show resolved Hide resolved
{
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
}
}