Skip to content

Use native NullabilityInfoContext #1167

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

Closed
wants to merge 5 commits into from
Closed
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
2 changes: 2 additions & 0 deletions JsonApiDotNetCore.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ JsonApiDotNetCore.ArgumentGuard.NotNull($EXPR$, $NAME$);</s:String>
<s:Int64 x:Key="/Default/CodeEditing/NullCheckPatterns/PatternTypeNamesToPriority/=JetBrains_002EReSharper_002EFeature_002EServices_002ECSharp_002ENullChecking_002ETraceAssertPattern/@EntryIndexedValue">50</s:Int64>
<s:Boolean x:Key="/Default/CodeInspection/CodeAnnotations/PropagateAnnotations/@EntryValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=swagger_002Ejson/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/ExcludedFiles/FileMasksToSkip/=_002A_002A_002Fobj_002F_002A_002A/@EntryIndexedValue">True</s:Boolean>

<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=swagger_002Eg_002Ejson/@EntryIndexedValue">swagger.g.json</s:String>
<s:String x:Key="/Default/CodeInspection/GeneratedCode/GeneratedFileMasks/=swagger_002Ejson/@EntryIndexedValue">swagger.json</s:String>
<s:String x:Key="/Default/CodeInspection/Highlighting/AnalysisEnabled/@EntryValue">SOLUTION</s:String>
Expand Down
36 changes: 0 additions & 36 deletions src/JsonApiDotNetCore.OpenApi/MemberInfoExtensions.cs

This file was deleted.

31 changes: 25 additions & 6 deletions src/JsonApiDotNetCore.OpenApi/ResourceFieldAttributeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,40 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using JsonApiDotNetCore.Resources.Annotations;
using Swashbuckle.AspNetCore.SwaggerGen;

namespace JsonApiDotNetCore.OpenApi;

internal static class ResourceFieldAttributeExtensions
{
private static readonly NullabilityInfoContext NullabilityInfoContext = new();

public static bool IsNullable(this ResourceFieldAttribute source)
{
TypeCategory fieldTypeCategory = source.Property.GetTypeCategory();
bool hasRequiredAttribute = source.Property.HasAttribute<RequiredAttribute>();

return fieldTypeCategory switch
if (hasRequiredAttribute)
{
TypeCategory.NonNullableReferenceType or TypeCategory.ValueType => false,
TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => !hasRequiredAttribute,
_ => throw new UnreachableCodeException()
};
// Reflects the following cases, independent of NRT setting
// `[Required] int? Number` => not nullable
// `[Required] int Number` => not nullable
// `[Required] string Text` => not nullable
// `[Required] string? Text` => not nullable
// `[Required] string Text` => not nullable
return false;
}

NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(source.Property);

// Reflects the following cases:
// Independent of NRT:
// `int? Number` => nullable
// `int Number` => not nullable
// If NRT is enabled:
// `string? Text` => nullable
// `string Text` => not nullable
// If NRT is disabled:
// `string Text` => nullable
return nullabilityInfo.ReadState is not NullabilityState.NotNull;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ internal sealed class JsonApiSchemaGenerator : ISchemaGenerator
typeof(NullableToOneRelationshipInRequest<>)
};

private static readonly Type[] JsonApiDocumentWithNullableDataOpenTypes =
{
typeof(NullableSecondaryResourceResponseDocument<>),
typeof(NullableResourceIdentifierResponseDocument<>),
typeof(NullableToOneRelationshipInRequest<>)
};

private readonly ISchemaGenerator _defaultSchemaGenerator;
private readonly ResourceObjectSchemaGenerator _resourceObjectSchemaGenerator;
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
Expand Down Expand Up @@ -58,7 +65,7 @@ public OpenApiSchema GenerateSchema(Type type, SchemaRepository schemaRepository
{
OpenApiSchema schema = GenerateJsonApiDocumentSchema(type);

if (IsDataPropertyNullable(type))
if (IsDataPropertyNullableInDocument(type))
{
SetDataObjectSchemaToNullable(schema);
}
Expand Down Expand Up @@ -98,18 +105,11 @@ private static bool IsManyDataDocument(Type documentType)
return documentType.BaseType!.GetGenericTypeDefinition() == typeof(ManyData<>);
}

private static bool IsDataPropertyNullable(Type type)
private static bool IsDataPropertyNullableInDocument(Type documentType)
{
PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data));

if (dataProperty == null)
{
throw new UnreachableCodeException();
}

TypeCategory typeCategory = dataProperty.GetTypeCategory();
Type documentOpenType = documentType.GetGenericTypeDefinition();

return typeCategory == TypeCategory.NullableReferenceType;
return JsonApiDocumentWithNullableDataOpenTypes.Contains(documentOpenType);
}

private void SetDataObjectSchemaToNullable(OpenApiSchema referenceSchemaForDocument)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
using System.ComponentModel.DataAnnotations;
using System.Reflection;
using System.Text.Json;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.OpenApi.JsonApiObjects;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships;
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
Expand All @@ -12,35 +12,46 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents;

internal sealed class ResourceFieldObjectSchemaBuilder
{
private static readonly Type[] RelationshipInResponseOpenTypes =
private static readonly NullabilityInfoContext NullabilityInfoContext = new();

private static readonly Type[] RelationshipSchemaInResponseOpenTypes =
{
typeof(ToOneRelationshipInResponse<>),
typeof(ToManyRelationshipInResponse<>),
typeof(NullableToOneRelationshipInResponse<>)
};

private static readonly Type[] NullableRelationshipSchemaOpenTypes =
{
typeof(NullableToOneRelationshipInRequest<>),
typeof(NullableToOneRelationshipInResponse<>)
};

private readonly ResourceTypeInfo _resourceTypeInfo;
private readonly ISchemaRepositoryAccessor _schemaRepositoryAccessor;
private readonly SchemaGenerator _defaultSchemaGenerator;
private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator;
private readonly IJsonApiOptions _options;
private readonly SchemaRepository _resourceSchemaRepository = new();
private readonly NullableReferenceSchemaGenerator _nullableReferenceSchemaGenerator;
private readonly IDictionary<string, OpenApiSchema> _schemasForResourceFields;

public ResourceFieldObjectSchemaBuilder(ResourceTypeInfo resourceTypeInfo, ISchemaRepositoryAccessor schemaRepositoryAccessor,
SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, JsonNamingPolicy? namingPolicy)
SchemaGenerator defaultSchemaGenerator, ResourceTypeSchemaGenerator resourceTypeSchemaGenerator, IJsonApiOptions options)
{
ArgumentGuard.NotNull(resourceTypeInfo, nameof(resourceTypeInfo));
ArgumentGuard.NotNull(schemaRepositoryAccessor, nameof(schemaRepositoryAccessor));
ArgumentGuard.NotNull(defaultSchemaGenerator, nameof(defaultSchemaGenerator));
ArgumentGuard.NotNull(resourceTypeSchemaGenerator, nameof(resourceTypeSchemaGenerator));
ArgumentGuard.NotNull(options, nameof(options));

_resourceTypeInfo = resourceTypeInfo;
_schemaRepositoryAccessor = schemaRepositoryAccessor;
_defaultSchemaGenerator = defaultSchemaGenerator;
_resourceTypeSchemaGenerator = resourceTypeSchemaGenerator;
_options = options;

_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, namingPolicy);
_nullableReferenceSchemaGenerator = new NullableReferenceSchemaGenerator(schemaRepositoryAccessor, options.SerializerOptions.PropertyNamingPolicy);
_schemasForResourceFields = GetFieldSchemas();
}

Expand Down Expand Up @@ -108,15 +119,14 @@ private bool IsFieldRequired(ResourceFieldAttribute field)
return false;
}

TypeCategory fieldTypeCategory = field.Property.GetTypeCategory();
bool hasRequiredAttribute = field.Property.HasAttribute<RequiredAttribute>();

return fieldTypeCategory switch
NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(field.Property);

return field.Property.PropertyType.IsValueType switch
{
TypeCategory.NonNullableReferenceType => true,
TypeCategory.ValueType => hasRequiredAttribute,
TypeCategory.NullableReferenceType or TypeCategory.NullableValueType => hasRequiredAttribute,
_ => throw new UnreachableCodeException()
true => hasRequiredAttribute,
false => _options.ValidateModelState ? nullabilityInfo.ReadState == NullabilityState.NotNull || hasRequiredAttribute : hasRequiredAttribute
};
}

Expand Down Expand Up @@ -194,7 +204,9 @@ private OpenApiSchema CreateRelationshipSchema(Type relationshipSchemaType)

OpenApiSchema fullSchema = _schemaRepositoryAccessor.Current.Schemas[referenceSchema.Reference.Id];

if (IsDataPropertyNullable(relationshipSchemaType))
Console.WriteLine(relationshipSchemaType.FullName);

if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType))
{
fullSchema.Properties[JsonApiObjectPropertyName.Data] =
_nullableReferenceSchemaGenerator.GenerateSchema(fullSchema.Properties[JsonApiObjectPropertyName.Data]);
Expand All @@ -212,18 +224,12 @@ private static bool IsRelationshipInResponseType(Type relationshipSchemaType)
{
Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition();

return RelationshipInResponseOpenTypes.Contains(relationshipSchemaOpenType);
return RelationshipSchemaInResponseOpenTypes.Contains(relationshipSchemaOpenType);
}

private static bool IsDataPropertyNullable(Type type)
private static bool IsDataPropertyNullableInRelationshipSchemaType(Type relationshipSchemaType)
{
PropertyInfo? dataProperty = type.GetProperty(nameof(JsonApiObjectPropertyName.Data));

if (dataProperty == null)
{
throw new UnreachableCodeException();
}

return dataProperty.GetTypeCategory() == TypeCategory.NullableReferenceType;
Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition();
return NullableRelationshipSchemaOpenTypes.Contains(relationshipSchemaOpenType);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public ResourceObjectSchemaGenerator(SchemaGenerator defaultSchemaGenerator, IRe
_allowClientGeneratedIds = options.AllowClientGeneratedIds;

_resourceFieldObjectSchemaBuilderFactory = resourceTypeInfo => new ResourceFieldObjectSchemaBuilder(resourceTypeInfo, schemaRepositoryAccessor,
defaultSchemaGenerator, _resourceTypeSchemaGenerator, options.SerializerOptions.PropertyNamingPolicy);
defaultSchemaGenerator, _resourceTypeSchemaGenerator, options);
}

public OpenApiSchema GenerateSchema(Type resourceObjectType)
Expand Down
9 changes: 0 additions & 9 deletions src/JsonApiDotNetCore.OpenApi/TypeCategory.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@

#pragma warning disable AV1008 // Class should not be static

namespace OpenApiClientTests.LegacyClient;
namespace OpenApiClientTests;

internal static class ApiResponse
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Text;
using JsonApiDotNetCore.OpenApi.Client;

namespace OpenApiClientTests.LegacyClient;
namespace OpenApiClientTests;

/// <summary>
/// Enables to inject an outgoing response body and inspect the incoming request.
Expand Down
18 changes: 14 additions & 4 deletions test/OpenApiClientTests/OpenApiClientTests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,6 @@
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(AspNetVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(TestSdkVersion)" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NSwag.ApiDescription.Client" Version="13.10.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.Extensions.ApiDescription.Client" Version="5.0.9">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down Expand Up @@ -62,5 +58,19 @@
<CodeGenerator>NSwagCSharp</CodeGenerator>
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions</Options>
</OpenApiReference>
<OpenApiReference Include="SchemaProperties\NullableReferenceTypesEnabled\swagger.g.json">
<Namespace>OpenApiClientTests.SchemaProperties.NullableReferenceTypesEnabled.GeneratedCode</Namespace>
<ClassName>NullableReferenceTypesEnabledClient</ClassName>
<OutputPath>NullableReferenceTypesEnabledClient.cs</OutputPath>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:true</Options>
</OpenApiReference>
<OpenApiReference Include="SchemaProperties\NullableReferenceTypesDisabled\swagger.g.json">
<Namespace>OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode</Namespace>
<ClassName>NullableReferenceTypesDisabledClient</ClassName>
<OutputPath>NullableReferenceTypesDisabledClient.cs</OutputPath>
<CodeGenerator>NSwagCSharp</CodeGenerator>
<Options>/UseBaseUrl:false /ClientClassAccessModifier:internal /GenerateExceptionClasses:false /AdditionalNamespaceUsages:JsonApiDotNetCore.OpenApi.Client.Exceptions /GenerateNullableReferenceTypes:false</Options>
</OpenApiReference>
</ItemGroup>
</Project>
30 changes: 30 additions & 0 deletions test/OpenApiClientTests/PropertyInfoAssertionsExtension.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Reflection;
using FluentAssertions;
using FluentAssertions.Types;

namespace OpenApiClientTests;

internal static class PropertyInfoAssertionsExtensions
{
private static readonly NullabilityInfoContext NullabilityInfoContext = new();

[CustomAssertion]
public static void BeNullable(this PropertyInfoAssertions source, string because = "", params object[] becauseArgs)
{
PropertyInfo propertyInfo = source.Subject;

NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(propertyInfo);

nullabilityInfo.ReadState.Should().NotBe(NullabilityState.NotNull, because, becauseArgs);
}

[CustomAssertion]
public static void BeNonNullable(this PropertyInfoAssertions source, string because = "", params object[] becauseArgs)
{
PropertyInfo propertyInfo = source.Subject;

NullabilityInfo nullabilityInfo = NullabilityInfoContext.Create(propertyInfo);

nullabilityInfo.ReadState.Should().Be(NullabilityState.NotNull, because, becauseArgs);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
using JsonApiDotNetCore.OpenApi.Client;
using Newtonsoft.Json;

namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;

internal partial class NullableReferenceTypesDisabledClient : JsonApiClient
{
partial void UpdateJsonSerializerSettings(JsonSerializerSettings settings)
{
SetSerializerSettingsForJsonApi(settings);

settings.Formatting = Formatting.Indented;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Reflection;
using FluentAssertions;
using OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled.GeneratedCode;
using Xunit;

namespace OpenApiClientTests.SchemaProperties.NullableReferenceTypesDisabled;

public sealed class NullabilityTests
{
[Fact]
public void Nullability_of_generated_types_is_as_expected()
{
PropertyInfo[] propertyInfos = typeof(ChickenAttributesInResponse).GetProperties();

PropertyInfo? propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.Name));
propertyInfo.Should().BeNullable();

propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.NameOfCurrentFarm));
propertyInfo.Should().BeNullable();

propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.Age));
propertyInfo.Should().BeNonNullable();

propertyInfo = propertyInfos.FirstOrDefault(property => property.Name == nameof(ChickenAttributesInResponse.TimeAtCurrentFarmInDays));
propertyInfo.Should().BeNullable();
}
}
Loading