Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
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
}
}