From 1ec31d23570b738458bb97d6cbafcc968f79f46d Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 30 Mar 2024 00:21:23 +0100 Subject: [PATCH 1/2] Declare Data properties as members instead of via base type --- ...lableResourceIdentifierResponseDocument.cs | 8 ++- ...llableSecondaryResourceResponseDocument.cs | 6 +- .../PrimaryResourceResponseDocument.cs | 6 +- .../ResourceCollectionResponseDocument.cs | 6 +- ...rceIdentifierCollectionResponseDocument.cs | 6 +- .../ResourceIdentifierResponseDocument.cs | 6 +- .../Documents/ResourcePatchRequestDocument.cs | 11 +++- .../Documents/ResourcePostRequestDocument.cs | 11 +++- .../SecondaryResourceResponseDocument.cs | 6 +- .../JsonApiObjects/ManyData.cs | 15 ----- .../JsonApiObjects/NullableSingleData.cs | 15 ----- .../NullableToOneRelationshipInRequest.cs | 11 +++- .../NullableToOneRelationshipInResponse.cs | 7 ++- .../ToManyRelationshipInRequest.cs | 11 +++- .../ToManyRelationshipInResponse.cs | 7 ++- .../ToOneRelationshipInRequest.cs | 11 +++- .../ToOneRelationshipInResponse.cs | 7 ++- .../JsonApiObjects/SingleData.cs | 15 ----- .../JsonApiPropertyName.cs | 2 - .../OpenApiSchemaExtensions.cs | 23 -------- .../DocumentSchemaGenerator.cs | 59 +++++-------------- .../JsonApiDataContractResolver.cs | 25 ++++++++ .../ResourceDataSchemaGenerator.cs | 12 ---- .../ResourceFieldSchemaBuilder.cs | 9 --- 24 files changed, 140 insertions(+), 155 deletions(-) delete mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs delete mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs delete mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs index 12eeee42fd..b86acb14f9 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableResourceIdentifierResponseDocument.cs @@ -7,10 +7,10 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; -// Types in the current namespace are never touched by ASP.NET ModelState validation, therefore using a non-nullable reference type for a property does not +// Types in the JsonApiObjects namespace are never touched by ASP.NET ModelState validation, therefore using a non-nullable reference type for a property does not // imply this property is required. Instead, we use [Required] explicitly, because this is how Swashbuckle is instructed to mark properties as required. [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class NullableResourceIdentifierResponseDocument : NullableSingleData> +internal sealed class NullableResourceIdentifierResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -20,6 +20,10 @@ internal sealed class NullableResourceIdentifierResponseDocument : Nu [JsonPropertyName("links")] public ResourceIdentifierTopLevelLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ResourceIdentifier? Data { get; set; } + [JsonPropertyName("meta")] public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs index a6a9551428..3924585989 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/NullableSecondaryResourceResponseDocument.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class NullableSecondaryResourceResponseDocument : NullableSingleData> +internal sealed class NullableSecondaryResourceResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -18,6 +18,10 @@ internal sealed class NullableSecondaryResourceResponseDocument : Nul [JsonPropertyName("links")] public ResourceTopLevelLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ResourceDataInResponse? Data { get; set; } + [JsonPropertyName("included")] public IList Included { get; set; } = null!; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs index da834d2070..ad23955ec6 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class PrimaryResourceResponseDocument : SingleData> +internal sealed class PrimaryResourceResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -18,6 +18,10 @@ internal sealed class PrimaryResourceResponseDocument : SingleData Data { get; set; } = null!; + [JsonPropertyName("included")] public IList Included { get; set; } = null!; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs index 3f92d06059..3ca8783313 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceCollectionResponseDocument.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ResourceCollectionResponseDocument : ManyData> +internal sealed class ResourceCollectionResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -18,6 +18,10 @@ internal sealed class ResourceCollectionResponseDocument : ManyData> Data { get; set; } = null!; + [JsonPropertyName("included")] public IList Included { get; set; } = null!; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs index b423fad146..5d400f48f5 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierCollectionResponseDocument.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ResourceIdentifierCollectionResponseDocument : ManyData> +internal sealed class ResourceIdentifierCollectionResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -18,6 +18,10 @@ internal sealed class ResourceIdentifierCollectionResponseDocument : [JsonPropertyName("links")] public ResourceIdentifierCollectionTopLevelLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ICollection> Data { get; set; } = null!; + [JsonPropertyName("meta")] public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs index 5d0d741c7b..f47a3306d5 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourceIdentifierResponseDocument.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ResourceIdentifierResponseDocument : SingleData> +internal sealed class ResourceIdentifierResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -18,6 +18,10 @@ internal sealed class ResourceIdentifierResponseDocument : SingleData [JsonPropertyName("links")] public ResourceIdentifierTopLevelLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ResourceIdentifier Data { get; set; } = null!; + [JsonPropertyName("meta")] public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs index c06427b29c..cb51ab223b 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePatchRequestDocument.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources; @@ -5,5 +7,10 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ResourcePatchRequestDocument : SingleData> - where TResource : IIdentifiable; +internal sealed class ResourcePatchRequestDocument + where TResource : IIdentifiable +{ + [Required] + [JsonPropertyName("data")] + public ResourceDataInPatchRequest Data { get; set; } = null!; +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs index 1c9975b3e4..d00e215597 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/ResourcePostRequestDocument.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources; @@ -5,5 +7,10 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ResourcePostRequestDocument : SingleData> - where TResource : IIdentifiable; +internal sealed class ResourcePostRequestDocument + where TResource : IIdentifiable +{ + [Required] + [JsonPropertyName("data")] + public ResourceDataInPostRequest Data { get; set; } = null!; +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs index 9c26fea16e..bfe70b0ea4 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/SecondaryResourceResponseDocument.cs @@ -8,7 +8,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class SecondaryResourceResponseDocument : SingleData> +internal sealed class SecondaryResourceResponseDocument where TResource : IIdentifiable { [JsonPropertyName("jsonapi")] @@ -18,6 +18,10 @@ internal sealed class SecondaryResourceResponseDocument : SingleData< [JsonPropertyName("links")] public ResourceTopLevelLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ResourceDataInResponse Data { get; set; } = null!; + [JsonPropertyName("included")] public IList Included { get; set; } = null!; diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs deleted file mode 100644 index 34099658d9..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/ManyData.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using JetBrains.Annotations; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; - -namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; - -[UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal abstract class ManyData - where TData : class, IResourceIdentity -{ - [Required] - [JsonPropertyName("data")] - public ICollection Data { get; set; } = null!; -} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs deleted file mode 100644 index 5bfad0cabe..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/NullableSingleData.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using JetBrains.Annotations; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; - -namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; - -[UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal abstract class NullableSingleData - where TData : class, IResourceIdentity -{ - [Required] - [JsonPropertyName("data")] - public TData? Data { get; set; } -} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs index 95b7821f48..247f7ad14e 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInRequest.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources; @@ -5,5 +7,10 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class NullableToOneRelationshipInRequest : NullableSingleData> - where TResource : IIdentifiable; +internal sealed class NullableToOneRelationshipInRequest + where TResource : IIdentifiable +{ + [Required] + [JsonPropertyName("data")] + public ResourceIdentifier? Data { get; set; } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs index a2e6cc63ac..52f1bbc120 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/NullableToOneRelationshipInResponse.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; @@ -7,13 +8,17 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class NullableToOneRelationshipInResponse : NullableSingleData> +internal sealed class NullableToOneRelationshipInResponse where TResource : IIdentifiable { // Non-required because the related controller may be unavailable when used in an include. [JsonPropertyName("links")] public RelationshipLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ResourceIdentifier? Data { get; set; } + [JsonPropertyName("meta")] public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs index e2a3865ba8..e8a4698186 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInRequest.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources; @@ -5,5 +7,10 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ToManyRelationshipInRequest : ManyData> - where TResource : IIdentifiable; +internal sealed class ToManyRelationshipInRequest + where TResource : IIdentifiable +{ + [Required] + [JsonPropertyName("data")] + public ICollection> Data { get; set; } = null!; +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs index b78b52dbce..8c78f09adb 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToManyRelationshipInResponse.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; @@ -7,13 +8,17 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ToManyRelationshipInResponse : ManyData> +internal sealed class ToManyRelationshipInResponse where TResource : IIdentifiable { // Non-required because the related controller may be unavailable when used in an include. [JsonPropertyName("links")] public RelationshipLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ICollection> Data { get; set; } = null!; + [JsonPropertyName("meta")] public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs index 7fdc2275d0..2ec7e1dc96 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInRequest.cs @@ -1,3 +1,5 @@ +using System.ComponentModel.DataAnnotations; +using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources; @@ -5,5 +7,10 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ToOneRelationshipInRequest : SingleData> - where TResource : IIdentifiable; +internal sealed class ToOneRelationshipInRequest + where TResource : IIdentifiable +{ + [Required] + [JsonPropertyName("data")] + public ResourceIdentifier Data { get; set; } = null!; +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs index 64a3fcf881..bc66ae7581 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Relationships/ToOneRelationshipInResponse.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using System.Text.Json.Serialization; using JetBrains.Annotations; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links; @@ -7,13 +8,17 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; [UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal sealed class ToOneRelationshipInResponse : SingleData> +internal sealed class ToOneRelationshipInResponse where TResource : IIdentifiable { // Non-required because the related controller may be unavailable when used in an include. [JsonPropertyName("links")] public RelationshipLinks Links { get; set; } = null!; + [Required] + [JsonPropertyName("data")] + public ResourceIdentifier Data { get; set; } = null!; + [JsonPropertyName("meta")] public IDictionary Meta { get; set; } = null!; } diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs deleted file mode 100644 index 451b2a974f..0000000000 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; -using JetBrains.Annotations; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; - -namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; - -[UsedImplicitly(ImplicitUseTargetFlags.Members)] -internal abstract class SingleData - where TData : class, IResourceIdentity -{ - [Required] - [JsonPropertyName("data")] - public TData Data { get; set; } = null!; -} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiPropertyName.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiPropertyName.cs index 906015e40b..5c4ad366b3 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiPropertyName.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiPropertyName.cs @@ -11,7 +11,5 @@ internal static class JsonApiPropertyName public const string Id = "id"; public const string Attributes = "attributes"; public const string Relationships = "relationships"; - public const string Errors = "errors"; - public const string Included = "included"; public const string Meta = "meta"; } diff --git a/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs b/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs index 8894d68600..3ca7f7d623 100644 --- a/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs +++ b/src/JsonApiDotNetCore.OpenApi/OpenApiSchemaExtensions.cs @@ -4,29 +4,6 @@ namespace JsonApiDotNetCore.OpenApi; internal static class OpenApiSchemaExtensions { - public static void ReorderProperties(this OpenApiSchema fullSchema, IEnumerable propertyNamesInOrder) - { - ArgumentGuard.NotNull(fullSchema); - ArgumentGuard.NotNull(propertyNamesInOrder); - - var propertiesInOrder = new Dictionary(); - - foreach (string propertyName in propertyNamesInOrder) - { - if (fullSchema.Properties.TryGetValue(propertyName, out OpenApiSchema? schema)) - { - propertiesInOrder.Add(propertyName, schema); - } - } - - if (fullSchema.Properties.Count != propertiesInOrder.Count) - { - throw new UnreachableCodeException(); - } - - fullSchema.Properties = propertiesInOrder; - } - public static OpenApiSchema UnwrapExtendedReferenceSchema(this OpenApiSchema source) { ArgumentGuard.NotNull(source); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs index daec740e51..07d628c6e5 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs @@ -1,5 +1,5 @@ +using System.Reflection; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; @@ -18,16 +18,6 @@ internal sealed class DocumentSchemaGenerator typeof(NullableToOneRelationshipInRequest<>) ]; - private static readonly string[] DocumentPropertyNamesInOrder = - [ - JsonApiPropertyName.Jsonapi, - JsonApiPropertyName.Links, - JsonApiPropertyName.Data, - JsonApiPropertyName.Errors, - JsonApiPropertyName.Included, - JsonApiPropertyName.Meta - ]; - private readonly SchemaGenerator _defaultSchemaGenerator; private readonly AbstractResourceDataSchemaGenerator _abstractResourceDataSchemaGenerator; private readonly ResourceDataSchemaGenerator _resourceDataSchemaGenerator; @@ -84,7 +74,7 @@ private OpenApiSchema GenerateJsonApiDocumentSchema(Type documentType, SchemaRep // for a derived type is generated, we'll add it to the discriminator mapping. _ = _abstractResourceDataSchemaGenerator.Get(schemaRepository); - Type resourceDataConstructedType = documentType.BaseType!.GenericTypeArguments[0]; + Type resourceDataConstructedType = GetInnerTypeOfDataProperty(documentType); // Ensure all reachable related resource types are available in the discriminator mapping so includes work. // Doing this matters when not all endpoints are exposed. @@ -96,20 +86,28 @@ private OpenApiSchema GenerateJsonApiDocumentSchema(Type documentType, SchemaRep OpenApiSchema referenceSchemaForDocument = _defaultSchemaGenerator.GenerateSchema(documentType, schemaRepository); OpenApiSchema fullSchemaForDocument = schemaRepository.Schemas[referenceSchemaForDocument.Reference.Id]; - fullSchemaForDocument.Properties[JsonApiPropertyName.Data] = IsManyDataDocument(documentType) - ? CreateArrayTypeDataSchema(referenceSchemaForResourceData) - : CreateExtendedReferenceSchema(referenceSchemaForResourceData); - if (IsDataPropertyNullableInDocument(documentType)) { SetDataSchemaToNullable(fullSchemaForDocument); } - fullSchemaForDocument.ReorderProperties(DocumentPropertyNamesInOrder); - return referenceSchemaForDocument; } + private static Type GetInnerTypeOfDataProperty(Type documentType) + { + PropertyInfo? dataProperty = documentType.GetProperty("Data"); + + if (dataProperty == null) + { + throw new UnreachableCodeException(); + } + + return dataProperty.PropertyType.GetGenericTypeDefinition().IsAssignableTo(typeof(ICollection<>)) + ? dataProperty.PropertyType.GenericTypeArguments[0] + : dataProperty.PropertyType; + } + private void EnsureResourceTypesAreMappedInDiscriminator(Type resourceDataConstructedType, SchemaRepository schemaRepository) { Type resourceDataOpenType = resourceDataConstructedType.GetGenericTypeDefinition(); @@ -131,11 +129,6 @@ private void EnsureResourceTypesAreMappedInDiscriminator(Type resourceDataConstr } } - private static bool IsManyDataDocument(Type documentType) - { - return documentType.BaseType!.GetGenericTypeDefinition() == typeof(ManyData<>); - } - private static bool IsDataPropertyNullableInDocument(Type documentType) { Type documentOpenType = documentType.GetGenericTypeDefinition(); @@ -143,15 +136,6 @@ private static bool IsDataPropertyNullableInDocument(Type documentType) return JsonApiDocumentWithNullableDataOpenTypes.Contains(documentOpenType); } - private static OpenApiSchema CreateArrayTypeDataSchema(OpenApiSchema referenceSchemaForResourceData) - { - return new OpenApiSchema - { - Items = referenceSchemaForResourceData, - Type = "array" - }; - } - private static void SetDataSchemaToNullable(OpenApiSchema fullSchemaForDocument) { OpenApiSchema referenceSchemaForData = fullSchemaForDocument.Properties[JsonApiPropertyName.Data]; @@ -176,15 +160,4 @@ private void SetJsonApiVersion(OpenApiSchema fullSchemaForDocument, SchemaReposi } } } - - private static OpenApiSchema CreateExtendedReferenceSchema(OpenApiSchema referenceSchema) - { - return new OpenApiSchema - { - AllOf = new List - { - referenceSchema - } - }; - } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs index 4b6ae76cef..876eef26bc 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiDataContractResolver.cs @@ -47,6 +47,8 @@ public DataContract GetDataContractForType(Type type) dataContract = ReplacePropertiesInDataContract(dataContract, replacementProperties); } + dataContract = OrderPropertiesInDataContract(dataContract); + return dataContract; } @@ -56,6 +58,29 @@ private static DataContract ReplacePropertiesInDataContract(DataContract dataCon dataContract.ObjectTypeNameValue); } + private static DataContract OrderPropertiesInDataContract(DataContract dataContract) + { +#if NET6_0 + // From https://learn.microsoft.com/en-us/dotnet/api/system.type.getproperties#system-type-getproperties: + // In .NET 6 and earlier versions, the GetProperties method does not return properties in a particular order, such as alphabetical or declaration + // order. Your code must not depend on the order in which properties are returned, because that order varies. However, starting with .NET 7, the + // ordering is deterministic based upon the metadata ordering in the assembly. + + if (dataContract.ObjectProperties != null) + { + DataProperty[] dataPropertiesInOrder = dataContract.ObjectProperties.OrderBy(dataProperty => dataProperty.MemberInfo.MetadataToken).ToArray(); + + // @formatter:keep_existing_linebreaks true + + return DataContract.ForObject(dataContract.UnderlyingType, dataPropertiesInOrder, dataContract.ObjectExtensionDataType, + dataContract.ObjectTypeNameProperty, dataContract.ObjectTypeNameValue); + + // @formatter:keep_existing_linebreaks restore + } +#endif + return dataContract; + } + private IList GetDataPropertiesThatExistInResourceClrType(Type resourceClrType, DataContract dataContract) { ResourceType resourceType = _resourceGraph.GetResourceType(resourceClrType); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceDataSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceDataSchemaGenerator.cs index 3db1bc7fe1..dbfe667691 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceDataSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceDataSchemaGenerator.cs @@ -9,16 +9,6 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class ResourceDataSchemaGenerator { - private static readonly string[] ResourceDataPropertyNamesInOrder = - [ - JsonApiPropertyName.Type, - JsonApiPropertyName.Id, - JsonApiPropertyName.Attributes, - JsonApiPropertyName.Relationships, - JsonApiPropertyName.Links, - JsonApiPropertyName.Meta - ]; - private readonly SchemaGenerator _defaultSchemaGenerator; private readonly ResourceTypeSchemaGenerator _resourceTypeSchemaGenerator; private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator; @@ -92,8 +82,6 @@ public OpenApiSchema GenerateSchema(Type resourceDataConstructedType, SchemaRepo effectiveFullSchemaForResourceData.SetValuesInMetaToNullable(); - effectiveFullSchemaForResourceData.ReorderProperties(ResourceDataPropertyNamesInOrder); - return referenceSchemaForResourceData; } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs index 5b94bbeb23..5db4d9fffb 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs @@ -24,13 +24,6 @@ internal sealed class ResourceFieldSchemaBuilder typeof(NullableToOneRelationshipInResponse<>) ]; - private static readonly string[] RelationshipPropertyNamesInOrder = - [ - JsonApiPropertyName.Links, - JsonApiPropertyName.Data, - JsonApiPropertyName.Meta - ]; - private readonly SchemaGenerator _defaultSchemaGenerator; private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator; private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator; @@ -224,8 +217,6 @@ private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaT fullSchema.Required.Remove(JsonApiPropertyName.Data); fullSchema.SetValuesInMetaToNullable(); - - fullSchema.ReorderProperties(RelationshipPropertyNamesInOrder); } return referenceSchema; From 10980e659c01af953556e3e90ef425d2658c8516 Mon Sep 17 00:00:00 2001 From: Bart Koelman <10324372+bkoelman@users.noreply.github.com> Date: Sat, 30 Mar 2024 00:58:27 +0100 Subject: [PATCH 2/2] Centralize some schema type arrays, extension to unwrap constructed generic type --- .../JsonApiObjects/JsonApiSchemaFacts.cs | 73 +++++++++++++++++++ .../JsonApiOperationIdSelector.cs | 9 +-- .../JsonApiRequestFormatMetadataProvider.cs | 15 +--- .../JsonApiSchemaIdSelector.cs | 6 +- .../DocumentSchemaGenerator.cs | 21 +----- .../JsonApiSchemaGenerator.cs | 29 +------- .../LinksVisibilitySchemaGenerator.cs | 12 +-- .../ResourceFieldSchemaBuilder.cs | 41 ++--------- .../TypeExtensions.cs | 11 +++ 9 files changed, 108 insertions(+), 109 deletions(-) create mode 100644 src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonApiSchemaFacts.cs create mode 100644 src/JsonApiDotNetCore.OpenApi/TypeExtensions.cs diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonApiSchemaFacts.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonApiSchemaFacts.cs new file mode 100644 index 0000000000..55be9d1e49 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiObjects/JsonApiSchemaFacts.cs @@ -0,0 +1,73 @@ +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; +using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; + +#pragma warning disable AV1008 // Class should not be static + +namespace JsonApiDotNetCore.OpenApi.JsonApiObjects; + +internal static class JsonApiSchemaFacts +{ + private static readonly Type[] RequestSchemaTypes = + [ + typeof(ResourcePostRequestDocument<>), + typeof(ResourcePatchRequestDocument<>), + typeof(ToOneRelationshipInRequest<>), + typeof(NullableToOneRelationshipInRequest<>), + typeof(ToManyRelationshipInRequest<>) + ]; + + private static readonly Type[] ResponseSchemaTypes = + [ + typeof(ResourceCollectionResponseDocument<>), + typeof(PrimaryResourceResponseDocument<>), + typeof(SecondaryResourceResponseDocument<>), + typeof(NullableSecondaryResourceResponseDocument<>), + typeof(ResourceIdentifierCollectionResponseDocument<>), + typeof(ResourceIdentifierResponseDocument<>), + typeof(NullableResourceIdentifierResponseDocument<>), + typeof(ErrorResponseDocument) + ]; + + private static readonly Type[] SchemaTypesHavingNullableDataProperty = + [ + typeof(NullableSecondaryResourceResponseDocument<>), + typeof(NullableResourceIdentifierResponseDocument<>), + typeof(NullableToOneRelationshipInRequest<>), + typeof(NullableToOneRelationshipInResponse<>) + ]; + + private static readonly Type[] RelationshipInResponseSchemaTypes = + [ + typeof(ToOneRelationshipInResponse<>), + typeof(ToManyRelationshipInResponse<>), + typeof(NullableToOneRelationshipInResponse<>) + ]; + + public static bool RequiresCustomSchemaGenerator(Type schemaType) + { + // Subset of the entry types Swashbuckle calls us for, which must be handled by our custom schema generators. + + Type lookupType = schemaType.ConstructedToOpenType(); + return RequestSchemaTypes.Contains(lookupType) || ResponseSchemaTypes.Contains(lookupType); + } + + public static bool IsRequestSchemaType(Type schemaType) + { + Type lookupType = schemaType.ConstructedToOpenType(); + return RequestSchemaTypes.Contains(lookupType); + } + + public static bool HasNullableDataProperty(Type schemaType) + { + // Swashbuckle infers non-nullable because our Data properties are [Required]. + + Type lookupType = schemaType.ConstructedToOpenType(); + return SchemaTypesHavingNullableDataProperty.Contains(lookupType); + } + + public static bool IsRelationshipInResponseType(Type schemaType) + { + Type lookupType = schemaType.ConstructedToOpenType(); + return RelationshipInResponseSchemaTypes.Contains(lookupType); + } +} diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs index cc200014bb..762b7b4344 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs @@ -86,9 +86,7 @@ private static Type GetDocumentType(ApiDescription endpoint) } ControllerParameterDescriptor? requestBodyDescriptor = endpoint.ActionDescriptor.GetBodyParameterDescriptor(); - - Type documentType = requestBodyDescriptor?.ParameterType.GetGenericTypeDefinition() ?? - GetGenericTypeDefinition(producesResponseTypeAttribute.Type) ?? producesResponseTypeAttribute.Type; + Type documentType = (requestBodyDescriptor?.ParameterType ?? producesResponseTypeAttribute.Type).ConstructedToOpenType(); if (documentType == typeof(ResourceCollectionResponseDocument<>) && endpoint.ParameterDescriptions.Count > 0) { @@ -98,11 +96,6 @@ private static Type GetDocumentType(ApiDescription endpoint) return documentType; } - private static Type? GetGenericTypeDefinition(Type type) - { - return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null; - } - private string ApplyTemplate(string operationIdTemplate, ResourceType resourceType, ApiDescription endpoint) { if (endpoint.RelativePath == null || endpoint.HttpMethod == null) diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs index 011696cc51..4f2a1edd53 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs @@ -1,6 +1,5 @@ using JsonApiDotNetCore.Middleware; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; +using JsonApiDotNetCore.OpenApi.JsonApiObjects; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; @@ -9,15 +8,6 @@ namespace JsonApiDotNetCore.OpenApi; internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IApiRequestFormatMetadataProvider { - private static readonly Type[] JsonApiRequestOpenTypes = - [ - typeof(ToManyRelationshipInRequest<>), - typeof(ToOneRelationshipInRequest<>), - typeof(NullableToOneRelationshipInRequest<>), - typeof(ResourcePostRequestDocument<>), - typeof(ResourcePatchRequestDocument<>) - ]; - /// public bool CanRead(InputFormatterContext context) { @@ -36,8 +26,7 @@ public IReadOnlyList GetSupportedContentTypes(string contentType, Type o ArgumentGuard.NotNullNorEmpty(contentType); ArgumentGuard.NotNull(objectType); - if (contentType == HeaderConstants.MediaType && objectType.IsConstructedGenericType && - JsonApiRequestOpenTypes.Contains(objectType.GetGenericTypeDefinition())) + if (contentType == HeaderConstants.MediaType && JsonApiSchemaFacts.IsRequestSchemaType(objectType)) { return new MediaTypeCollection { diff --git a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs index f83a736a30..308357b2f3 100644 --- a/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs +++ b/src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs @@ -64,10 +64,10 @@ public string GetSchemaId(Type type) return resourceType.PublicName.Singularize(); } - if (type.IsConstructedGenericType) - { - Type openType = type.GetGenericTypeDefinition(); + Type openType = type.ConstructedToOpenType(); + if (openType != type) + { if (TypeToSchemaTemplateMap.TryGetValue(openType, out string? schemaTemplate)) { Type resourceClrType = type.GetGenericArguments().First(); diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs index 07d628c6e5..02d5f2098e 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/DocumentSchemaGenerator.cs @@ -1,7 +1,6 @@ using System.Reflection; using JsonApiDotNetCore.Configuration; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; +using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -11,13 +10,6 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class DocumentSchemaGenerator { - private static readonly Type[] JsonApiDocumentWithNullableDataOpenTypes = - [ - typeof(NullableSecondaryResourceResponseDocument<>), - typeof(NullableResourceIdentifierResponseDocument<>), - typeof(NullableToOneRelationshipInRequest<>) - ]; - private readonly SchemaGenerator _defaultSchemaGenerator; private readonly AbstractResourceDataSchemaGenerator _abstractResourceDataSchemaGenerator; private readonly ResourceDataSchemaGenerator _resourceDataSchemaGenerator; @@ -86,7 +78,7 @@ private OpenApiSchema GenerateJsonApiDocumentSchema(Type documentType, SchemaRep OpenApiSchema referenceSchemaForDocument = _defaultSchemaGenerator.GenerateSchema(documentType, schemaRepository); OpenApiSchema fullSchemaForDocument = schemaRepository.Schemas[referenceSchemaForDocument.Reference.Id]; - if (IsDataPropertyNullableInDocument(documentType)) + if (JsonApiSchemaFacts.HasNullableDataProperty(documentType)) { SetDataSchemaToNullable(fullSchemaForDocument); } @@ -103,7 +95,7 @@ private static Type GetInnerTypeOfDataProperty(Type documentType) throw new UnreachableCodeException(); } - return dataProperty.PropertyType.GetGenericTypeDefinition().IsAssignableTo(typeof(ICollection<>)) + return dataProperty.PropertyType.ConstructedToOpenType().IsAssignableTo(typeof(ICollection<>)) ? dataProperty.PropertyType.GenericTypeArguments[0] : dataProperty.PropertyType; } @@ -129,13 +121,6 @@ private void EnsureResourceTypesAreMappedInDiscriminator(Type resourceDataConstr } } - private static bool IsDataPropertyNullableInDocument(Type documentType) - { - Type documentOpenType = documentType.GetGenericTypeDefinition(); - - return JsonApiDocumentWithNullableDataOpenTypes.Contains(documentOpenType); - } - private static void SetDataSchemaToNullable(OpenApiSchema fullSchemaForDocument) { OpenApiSchema referenceSchemaForData = fullSchemaForDocument.Properties[JsonApiPropertyName.Data]; diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs index 8d64640e02..aca57a94cf 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiSchemaGenerator.cs @@ -1,7 +1,6 @@ using System.Reflection; using JsonApiDotNetCore.Controllers; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; +using JsonApiDotNetCore.OpenApi.JsonApiObjects; using Microsoft.AspNetCore.Mvc.ApiExplorer; using Microsoft.OpenApi.Models; using Swashbuckle.AspNetCore.SwaggerGen; @@ -11,24 +10,6 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class JsonApiSchemaGenerator : ISchemaGenerator { - // List of all the root types we're being called for, that need to be handled by our custom schema generators. - private static readonly Type[] JsonApiDocumentTypes = - [ - typeof(ResourceCollectionResponseDocument<>), - typeof(PrimaryResourceResponseDocument<>), - typeof(SecondaryResourceResponseDocument<>), - typeof(NullableSecondaryResourceResponseDocument<>), - typeof(ResourcePostRequestDocument<>), - typeof(ResourcePatchRequestDocument<>), - typeof(ResourceIdentifierCollectionResponseDocument<>), - typeof(ResourceIdentifierResponseDocument<>), - typeof(NullableResourceIdentifierResponseDocument<>), - typeof(ErrorResponseDocument), - typeof(ToManyRelationshipInRequest<>), - typeof(ToOneRelationshipInRequest<>), - typeof(NullableToOneRelationshipInRequest<>) - ]; - private static readonly OpenApiSchema IdTypeSchema = new() { Type = "string" @@ -66,7 +47,7 @@ public OpenApiSchema GenerateSchema(Type modelType, SchemaRepository schemaRepos : jsonApiDocumentSchema; } - if (IsJsonApiDocument(modelType)) + if (JsonApiSchemaFacts.RequiresCustomSchemaGenerator(modelType)) { _ = _documentSchemaGenerator.GenerateSchema(modelType, schemaRepository); @@ -80,10 +61,4 @@ private static bool IsJsonApiParameter(ParameterInfo parameter) { return parameter.Member.DeclaringType != null && parameter.Member.DeclaringType.IsAssignableTo(typeof(CoreJsonApiController)); } - - private static bool IsJsonApiDocument(Type type) - { - Type documentType = type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; - return JsonApiDocumentTypes.Contains(documentType); - } } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/LinksVisibilitySchemaGenerator.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/LinksVisibilitySchemaGenerator.cs index 9cfdaba0bf..c1bdcf33ae 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/LinksVisibilitySchemaGenerator.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/LinksVisibilitySchemaGenerator.cs @@ -28,7 +28,7 @@ internal sealed class LinksVisibilitySchemaGenerator private const LinkTypes RelationshipLinkTypes = LinkTypes.Self | LinkTypes.Related; private const LinkTypes ResourceLinkTypes = LinkTypes.Self; - private static readonly Dictionary LinksInJsonApiComponentTypes = new() + private static readonly Dictionary LinksInJsonApiSchemaTypes = new() { [typeof(NullableSecondaryResourceResponseDocument<>)] = ResourceTopLinkTypes, [typeof(PrimaryResourceResponseDocument<>)] = ResourceTopLinkTypes, @@ -73,9 +73,9 @@ public void UpdateSchemaForTopLevel(Type modelType, OpenApiSchema fullSchemaForL ArgumentGuard.NotNull(modelType); ArgumentGuard.NotNull(fullSchemaForLinksContainer); - Type lookupType = modelType.IsConstructedGenericType ? modelType.GetGenericTypeDefinition() : modelType; + Type lookupType = modelType.ConstructedToOpenType(); - if (LinksInJsonApiComponentTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes)) + if (LinksInJsonApiSchemaTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes)) { UpdateLinksProperty(fullSchemaForLinksContainer, _lazyLinksVisibility.Value.TopLevelLinks, possibleLinkTypes, schemaRepository); } @@ -86,7 +86,7 @@ public void UpdateSchemaForResource(ResourceTypeInfo resourceTypeInfo, OpenApiSc ArgumentGuard.NotNull(resourceTypeInfo); ArgumentGuard.NotNull(fullSchemaForResourceData); - if (LinksInJsonApiComponentTypes.TryGetValue(resourceTypeInfo.ResourceDataOpenType, out LinkTypes possibleLinkTypes)) + if (LinksInJsonApiSchemaTypes.TryGetValue(resourceTypeInfo.ResourceDataOpenType, out LinkTypes possibleLinkTypes)) { UpdateLinksProperty(fullSchemaForResourceData, _lazyLinksVisibility.Value.ResourceLinks, possibleLinkTypes, schemaRepository); } @@ -97,9 +97,9 @@ public void UpdateSchemaForRelationship(Type modelType, OpenApiSchema fullSchema ArgumentGuard.NotNull(modelType); ArgumentGuard.NotNull(fullSchemaForRelationship); - Type lookupType = modelType.GetGenericTypeDefinition(); + Type lookupType = modelType.ConstructedToOpenType(); - if (LinksInJsonApiComponentTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes)) + if (LinksInJsonApiSchemaTypes.TryGetValue(lookupType, out LinkTypes possibleLinkTypes)) { UpdateLinksProperty(fullSchemaForRelationship, _lazyLinksVisibility.Value.RelationshipLinks, possibleLinkTypes, schemaRepository); } diff --git a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs index 5db4d9fffb..fd9852d45a 100644 --- a/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs +++ b/src/JsonApiDotNetCore.OpenApi/SwaggerComponents/ResourceFieldSchemaBuilder.cs @@ -1,6 +1,6 @@ using System.Reflection; using JsonApiDotNetCore.OpenApi.JsonApiMetadata; -using JsonApiDotNetCore.OpenApi.JsonApiObjects.Relationships; +using JsonApiDotNetCore.OpenApi.JsonApiObjects; using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects; using JsonApiDotNetCore.Resources.Annotations; using Microsoft.OpenApi.Models; @@ -11,19 +11,6 @@ namespace JsonApiDotNetCore.OpenApi.SwaggerComponents; internal sealed class ResourceFieldSchemaBuilder { - private static readonly Type[] RelationshipSchemaInResponseOpenTypes = - [ - typeof(ToOneRelationshipInResponse<>), - typeof(ToManyRelationshipInResponse<>), - typeof(NullableToOneRelationshipInResponse<>) - ]; - - private static readonly Type[] NullableRelationshipSchemaOpenTypes = - [ - typeof(NullableToOneRelationshipInRequest<>), - typeof(NullableToOneRelationshipInResponse<>) - ]; - private readonly SchemaGenerator _defaultSchemaGenerator; private readonly ResourceIdentifierSchemaGenerator _resourceIdentifierSchemaGenerator; private readonly LinksVisibilitySchemaGenerator _linksVisibilitySchemaGenerator; @@ -141,8 +128,8 @@ private Type GetRepresentedTypeForAttributeSchema(AttrAttribute attribute) private bool IsFieldRequired(ResourceFieldAttribute field) { - bool isSchemaForPostResourceRequest = _resourceTypeInfo.ResourceDataOpenType == typeof(ResourceDataInPostRequest<>); - return isSchemaForPostResourceRequest && _resourceFieldValidationMetadataProvider.IsRequired(field); + bool isPostRequestSchemaType = _resourceTypeInfo.ResourceDataOpenType == typeof(ResourceDataInPostRequest<>); + return isPostRequestSchemaType && _resourceFieldValidationMetadataProvider.IsRequired(field); } public void SetMembersOfRelationships(OpenApiSchema fullSchemaForRelationships, SchemaRepository schemaRepository) @@ -189,9 +176,8 @@ private void AddRelationshipSchemaToResourceData(RelationshipAttribute relations private Type GetRelationshipSchemaType(RelationshipAttribute relationship, Type resourceDataConstructedType) { - return resourceDataConstructedType.GetGenericTypeDefinition().IsAssignableTo(typeof(ResourceDataInResponse<>)) - ? _relationshipTypeFactory.GetForResponse(relationship) - : _relationshipTypeFactory.GetForRequest(relationship); + bool isResponseSchemaType = resourceDataConstructedType.ConstructedToOpenType().IsAssignableTo(typeof(ResourceDataInResponse<>)); + return isResponseSchemaType ? _relationshipTypeFactory.GetForResponse(relationship) : _relationshipTypeFactory.GetForRequest(relationship); } private OpenApiSchema? GetReferenceSchemaForRelationship(Type relationshipSchemaType, SchemaRepository schemaRepository) @@ -205,12 +191,12 @@ private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaT OpenApiSchema fullSchema = schemaRepository.Schemas[referenceSchema.Reference.Id]; - if (IsDataPropertyNullableInRelationshipSchemaType(relationshipSchemaType)) + if (JsonApiSchemaFacts.HasNullableDataProperty(relationshipSchemaType)) { fullSchema.Properties[JsonApiPropertyName.Data].Nullable = true; } - if (IsRelationshipInResponseType(relationshipSchemaType)) + if (JsonApiSchemaFacts.IsRelationshipInResponseType(relationshipSchemaType)) { _linksVisibilitySchemaGenerator.UpdateSchemaForRelationship(relationshipSchemaType, fullSchema, schemaRepository); @@ -221,17 +207,4 @@ private OpenApiSchema CreateRelationshipReferenceSchema(Type relationshipSchemaT return referenceSchema; } - - private static bool IsRelationshipInResponseType(Type relationshipSchemaType) - { - Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition(); - - return RelationshipSchemaInResponseOpenTypes.Contains(relationshipSchemaOpenType); - } - - private static bool IsDataPropertyNullableInRelationshipSchemaType(Type relationshipSchemaType) - { - Type relationshipSchemaOpenType = relationshipSchemaType.GetGenericTypeDefinition(); - return NullableRelationshipSchemaOpenTypes.Contains(relationshipSchemaOpenType); - } } diff --git a/src/JsonApiDotNetCore.OpenApi/TypeExtensions.cs b/src/JsonApiDotNetCore.OpenApi/TypeExtensions.cs new file mode 100644 index 0000000000..15a0ef9fb1 --- /dev/null +++ b/src/JsonApiDotNetCore.OpenApi/TypeExtensions.cs @@ -0,0 +1,11 @@ +namespace JsonApiDotNetCore.OpenApi; + +internal static class TypeExtensions +{ + public static Type ConstructedToOpenType(this Type type) + { + ArgumentGuard.NotNull(type); + + return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : type; + } +}