Skip to content

Commit c6e8542

Browse files
committed
Process review feedback
1 parent e07903c commit c6e8542

21 files changed

+434
-213
lines changed

src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5-
using JsonApiDotNetCore.Configuration;
65
using JsonApiDotNetCore.Middleware;
76
using JsonApiDotNetCore.OpenApi.JsonApiMetadata;
87
using Microsoft.AspNetCore.Mvc;
@@ -26,15 +25,14 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip
2625

2726
public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors();
2827

29-
public JsonApiActionDescriptorCollectionProvider(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping,
28+
public JsonApiActionDescriptorCollectionProvider(IControllerResourceMapping controllerResourceMapping,
3029
IActionDescriptorCollectionProvider defaultProvider)
3130
{
32-
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
3331
ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping));
3432
ArgumentGuard.NotNull(defaultProvider, nameof(defaultProvider));
3533

3634
_defaultProvider = defaultProvider;
37-
_jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(resourceGraph, controllerResourceMapping);
35+
_jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(controllerResourceMapping);
3836
}
3937

4038
private ActionDescriptorCollection GetActionDescriptors()
@@ -107,7 +105,6 @@ private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoin
107105
if (producesResponse != null)
108106
{
109107
producesResponse.Type = responseTypeToSet;
110-
111108
return;
112109
}
113110
}
@@ -134,7 +131,7 @@ private static IList<ActionDescriptor> Expand(ActionDescriptor genericEndpoint,
134131

135132
if (expandedEndpoint.AttributeRouteInfo == null)
136133
{
137-
throw new NotSupportedException("Only attribute based routing is supported for JsonApiDotNetCore endpoints");
134+
throw new UnreachableCodeException();
138135
}
139136

140137
ExpandTemplate(expandedEndpoint.AttributeRouteInfo, relationshipName);
@@ -172,7 +169,12 @@ private static ActionDescriptor Clone(ActionDescriptor descriptor)
172169
{
173170
var clonedDescriptor = (ActionDescriptor)descriptor.MemberwiseClone();
174171

175-
clonedDescriptor.AttributeRouteInfo = (AttributeRouteInfo)descriptor.AttributeRouteInfo!.MemberwiseClone();
172+
if (descriptor.AttributeRouteInfo == null)
173+
{
174+
throw new NotSupportedException("Only attribute routing is supported for JsonApiDotNetCore endpoints.");
175+
}
176+
177+
clonedDescriptor.AttributeRouteInfo = (AttributeRouteInfo)descriptor.AttributeRouteInfo.MemberwiseClone();
176178

177179
clonedDescriptor.FilterDescriptors = new List<FilterDescriptor>();
178180

src/JsonApiDotNetCore.OpenApi/JsonApiMetadata/JsonApiEndpointMetadataProvider.cs

Lines changed: 38 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,13 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
1616
/// </summary>
1717
internal sealed class JsonApiEndpointMetadataProvider
1818
{
19-
private readonly IResourceGraph _resourceGraph;
2019
private readonly IControllerResourceMapping _controllerResourceMapping;
2120
private readonly EndpointResolver _endpointResolver = new();
2221

23-
public JsonApiEndpointMetadataProvider(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping)
22+
public JsonApiEndpointMetadataProvider(IControllerResourceMapping controllerResourceMapping)
2423
{
25-
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
2624
ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping));
2725

28-
_resourceGraph = resourceGraph;
2926
_controllerResourceMapping = controllerResourceMapping;
3027
}
3128

@@ -47,28 +44,28 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction)
4744
throw new UnreachableCodeException();
4845
}
4946

50-
IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint.Value, primaryResourceType.ClrType);
51-
IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint.Value, primaryResourceType.ClrType);
47+
IJsonApiRequestMetadata? requestMetadata = GetRequestMetadata(endpoint.Value, primaryResourceType);
48+
IJsonApiResponseMetadata? responseMetadata = GetResponseMetadata(endpoint.Value, primaryResourceType);
5249
return new JsonApiEndpointMetadataContainer(requestMetadata, responseMetadata);
5350
}
5451

55-
private IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoint endpoint, Type primaryResourceType)
52+
private static IJsonApiRequestMetadata? GetRequestMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType)
5653
{
5754
switch (endpoint)
5855
{
5956
case JsonApiEndpoint.Post:
6057
{
61-
return GetPostRequestMetadata(primaryResourceType);
58+
return GetPostRequestMetadata(primaryResourceType.ClrType);
6259
}
6360
case JsonApiEndpoint.Patch:
6461
{
65-
return GetPatchRequestMetadata(primaryResourceType);
62+
return GetPatchRequestMetadata(primaryResourceType.ClrType);
6663
}
6764
case JsonApiEndpoint.PostRelationship:
6865
case JsonApiEndpoint.PatchRelationship:
6966
case JsonApiEndpoint.DeleteRelationship:
7067
{
71-
return GetRelationshipRequestMetadata(primaryResourceType, endpoint != JsonApiEndpoint.PatchRelationship);
68+
return GetRelationshipRequestMetadata(primaryResourceType.Relationships, endpoint != JsonApiEndpoint.PatchRelationship);
7269
}
7370
default:
7471
{
@@ -77,38 +74,45 @@ public JsonApiEndpointMetadataContainer Get(MethodInfo controllerAction)
7774
}
7875
}
7976

80-
private static PrimaryRequestMetadata GetPostRequestMetadata(Type primaryResourceType)
77+
private static PrimaryRequestMetadata GetPostRequestMetadata(Type resourceClrType)
8178
{
82-
Type documentType = typeof(ResourcePostRequestDocument<>).MakeGenericType(primaryResourceType);
79+
Type documentType = typeof(ResourcePostRequestDocument<>).MakeGenericType(resourceClrType);
8380

8481
return new PrimaryRequestMetadata(documentType);
8582
}
8683

87-
private static PrimaryRequestMetadata GetPatchRequestMetadata(Type primaryResourceType)
84+
private static PrimaryRequestMetadata GetPatchRequestMetadata(Type resourceClrType)
8885
{
89-
Type documentType = typeof(ResourcePatchRequestDocument<>).MakeGenericType(primaryResourceType);
86+
Type documentType = typeof(ResourcePatchRequestDocument<>).MakeGenericType(resourceClrType);
9087

9188
return new PrimaryRequestMetadata(documentType);
9289
}
9390

94-
private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryResourceType, bool ignoreHasOneRelationships)
91+
private static RelationshipRequestMetadata GetRelationshipRequestMetadata(IEnumerable<RelationshipAttribute> relationships,
92+
bool ignoreHasOneRelationships)
9593
{
96-
IEnumerable<RelationshipAttribute> relationships = _resourceGraph.GetResourceType(primaryResourceType).Relationships;
94+
IEnumerable<RelationshipAttribute> relationshipInRequest = ignoreHasOneRelationships ? relationships.OfType<HasManyAttribute>() : relationships;
9795

98-
if (ignoreHasOneRelationships)
99-
{
100-
relationships = relationships.OfType<HasManyAttribute>();
101-
}
96+
IDictionary<string, Type> resourceTypesByRelationshipName = relationshipInRequest.ToDictionary(relationship => relationship.PublicName,
97+
relationship =>
98+
{
99+
// @formatter:nested_ternary_style expanded
102100

103-
IDictionary<string, Type> resourceTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName,
104-
relationship => relationship is HasManyAttribute
105-
? typeof(ToManyRelationshipRequestData<>).MakeGenericType(relationship.RightType.ClrType)
106-
: typeof(ToOneRelationshipRequestData<>).MakeGenericType(relationship.RightType.ClrType));
101+
Type requestDataType = relationship is HasManyAttribute
102+
? typeof(ToManyRelationshipRequestData<>)
103+
: relationship.IsNullable()
104+
? typeof(NullableToOneRelationshipRequestData<>)
105+
: typeof(ToOneRelationshipRequestData<>);
106+
107+
// @formatter:nested_ternary_style restore
108+
109+
return requestDataType.MakeGenericType(relationship.RightType.ClrType);
110+
});
107111

108112
return new RelationshipRequestMetadata(resourceTypesByRelationshipName);
109113
}
110114

111-
private IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoint endpoint, Type primaryResourceType)
115+
private static IJsonApiResponseMetadata? GetResponseMetadata(JsonApiEndpoint endpoint, ResourceType primaryResourceType)
112116
{
113117
switch (endpoint)
114118
{
@@ -117,15 +121,15 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryR
117121
case JsonApiEndpoint.Post:
118122
case JsonApiEndpoint.Patch:
119123
{
120-
return GetPrimaryResponseMetadata(primaryResourceType, endpoint == JsonApiEndpoint.GetCollection);
124+
return GetPrimaryResponseMetadata(primaryResourceType.ClrType, endpoint == JsonApiEndpoint.GetCollection);
121125
}
122126
case JsonApiEndpoint.GetSecondary:
123127
{
124-
return GetSecondaryResponseMetadata(primaryResourceType);
128+
return GetSecondaryResponseMetadata(primaryResourceType.Relationships);
125129
}
126130
case JsonApiEndpoint.GetRelationship:
127131
{
128-
return GetRelationshipResponseMetadata(primaryResourceType);
132+
return GetRelationshipResponseMetadata(primaryResourceType.Relationships);
129133
}
130134
default:
131135
{
@@ -134,17 +138,17 @@ private RelationshipRequestMetadata GetRelationshipRequestMetadata(Type primaryR
134138
}
135139
}
136140

137-
private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type primaryResourceType, bool endpointReturnsCollection)
141+
private static PrimaryResponseMetadata GetPrimaryResponseMetadata(Type resourceClrType, bool endpointReturnsCollection)
138142
{
139143
Type documentOpenType = endpointReturnsCollection ? typeof(ResourceCollectionResponseDocument<>) : typeof(PrimaryResourceResponseDocument<>);
140-
Type documentType = documentOpenType.MakeGenericType(primaryResourceType);
144+
Type documentType = documentOpenType.MakeGenericType(resourceClrType);
141145

142146
return new PrimaryResponseMetadata(documentType);
143147
}
144148

145-
private SecondaryResponseMetadata GetSecondaryResponseMetadata(Type primaryResourceType)
149+
private static SecondaryResponseMetadata GetSecondaryResponseMetadata(IEnumerable<RelationshipAttribute> relationships)
146150
{
147-
IDictionary<string, Type> responseTypesByRelationshipName = GetMetadataByRelationshipName(primaryResourceType, relationship =>
151+
IDictionary<string, Type> responseTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName, relationship =>
148152
{
149153
Type documentType = relationship is HasManyAttribute
150154
? typeof(ResourceCollectionResponseDocument<>)
@@ -156,17 +160,9 @@ private SecondaryResponseMetadata GetSecondaryResponseMetadata(Type primaryResou
156160
return new SecondaryResponseMetadata(responseTypesByRelationshipName);
157161
}
158162

159-
private IDictionary<string, Type> GetMetadataByRelationshipName(Type primaryResourceType,
160-
Func<RelationshipAttribute, Type> extractRelationshipMetadataCallback)
161-
{
162-
IReadOnlyCollection<RelationshipAttribute> relationships = _resourceGraph.GetResourceType(primaryResourceType).Relationships;
163-
164-
return relationships.ToDictionary(relationship => relationship.PublicName, extractRelationshipMetadataCallback);
165-
}
166-
167-
private RelationshipResponseMetadata GetRelationshipResponseMetadata(Type primaryResourceType)
163+
private static RelationshipResponseMetadata GetRelationshipResponseMetadata(IEnumerable<RelationshipAttribute> relationships)
168164
{
169-
IDictionary<string, Type> responseTypesByRelationshipName = GetMetadataByRelationshipName(primaryResourceType,
165+
IDictionary<string, Type> responseTypesByRelationshipName = relationships.ToDictionary(relationship => relationship.PublicName,
170166
relationship => relationship is HasManyAttribute
171167
? typeof(ResourceIdentifierCollectionResponseDocument<>).MakeGenericType(relationship.RightType.ClrType)
172168
: typeof(ResourceIdentifierResponseDocument<>).MakeGenericType(relationship.RightType.ClrType));

src/JsonApiDotNetCore.OpenApi/JsonApiObjects/Documents/PrimaryResourceResponseDocument.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77

88
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.Documents
99
{
10+
/// Types in the <see cref="JsonApiDotNetCore.OpenApi.JsonApiObjects"/> namespace are never touched by ASP.NET ModelState validation, therefore using a
11+
/// non-nullable reference type for a property does not imply this property is required. Instead, we rely on explicitly setting <see cref="RequiredAttribute"/>
12+
/// which is how Swashbuckle is instructed to mark properties as required.
1013
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
1114
internal sealed class PrimaryResourceResponseDocument<TResource> : SingleData<ResourceResponseObject<TResource>>
1215
where TResource : IIdentifiable
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
3+
using JsonApiDotNetCore.Resources;
4+
5+
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData
6+
{
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
internal sealed class NullableToOneRelationshipRequestData<TResource> : SingleData<ResourceIdentifierObject<TResource>?>
9+
where TResource : IIdentifiable
10+
{
11+
}
12+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using System.Collections.Generic;
2+
using System.ComponentModel.DataAnnotations;
3+
using JetBrains.Annotations;
4+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.Links;
5+
using JsonApiDotNetCore.OpenApi.JsonApiObjects.ResourceObjects;
6+
using JsonApiDotNetCore.Resources;
7+
8+
namespace JsonApiDotNetCore.OpenApi.JsonApiObjects.RelationshipData
9+
{
10+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
11+
internal sealed class NullableToOneRelationshipResponseData<TResource> : SingleData<ResourceIdentifierObject<TResource>?>
12+
where TResource : IIdentifiable
13+
{
14+
[Required]
15+
public LinksInRelationshipObject Links { get; set; } = null!;
16+
17+
public IDictionary<string, object> Meta { get; set; } = null!;
18+
}
19+
}

src/JsonApiDotNetCore.OpenApi/JsonApiObjects/SingleData.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ namespace JsonApiDotNetCore.OpenApi.JsonApiObjects
66
{
77
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
88
internal abstract class SingleData<TData>
9-
where TData : ResourceIdentifierObject
9+
where TData : ResourceIdentifierObject?
1010
{
1111
[Required]
1212
public TData Data { get; set; } = null!;

src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ internal sealed class JsonApiOperationIdSelector
3131
[typeof(ResourceIdentifierCollectionResponseDocument<>)] = RelationshipOperationIdTemplate,
3232
[typeof(ResourceIdentifierResponseDocument<>)] = RelationshipOperationIdTemplate,
3333
[typeof(ToOneRelationshipRequestData<>)] = RelationshipOperationIdTemplate,
34+
[typeof(NullableToOneRelationshipRequestData<>)] = RelationshipOperationIdTemplate,
3435
[typeof(ToManyRelationshipRequestData<>)] = RelationshipOperationIdTemplate
3536
};
3637

@@ -64,14 +65,14 @@ public string GetOperationId(ApiDescription endpoint)
6465
return ApplyTemplate(template, primaryResourceType.ClrType, endpoint);
6566
}
6667

67-
private static string GetTemplate(Type primaryResourceType, ApiDescription endpoint)
68+
private static string GetTemplate(Type resourceClrType, ApiDescription endpoint)
6869
{
69-
Type requestDocumentType = GetDocumentType(primaryResourceType, endpoint);
70+
Type requestDocumentType = GetDocumentType(resourceClrType, endpoint);
7071

7172
return DocumentOpenTypeToOperationIdTemplateMap[requestDocumentType];
7273
}
7374

74-
private static Type GetDocumentType(Type primaryResourceType, ApiDescription endpoint)
75+
private static Type GetDocumentType(Type resourceClrType, ApiDescription endpoint)
7576
{
7677
var producesResponseTypeAttribute = endpoint.ActionDescriptor.GetFilterMetadata<ProducesResponseTypeAttribute>();
7778

@@ -89,7 +90,7 @@ private static Type GetDocumentType(Type primaryResourceType, ApiDescription end
8990
{
9091
Type documentResourceType = producesResponseTypeAttribute.Type.GetGenericArguments()[0];
9192

92-
if (documentResourceType != primaryResourceType)
93+
if (documentResourceType != resourceClrType)
9394
{
9495
documentType = typeof(SecondaryResourceResponseDocument<>);
9596
}
@@ -103,10 +104,10 @@ private static Type GetDocumentType(Type primaryResourceType, ApiDescription end
103104
return type.IsConstructedGenericType ? type.GetGenericTypeDefinition() : null;
104105
}
105106

106-
private string ApplyTemplate(string operationIdTemplate, Type primaryResourceType, ApiDescription endpoint)
107+
private string ApplyTemplate(string operationIdTemplate, Type resourceClrType, ApiDescription endpoint)
107108
{
108109
string method = endpoint.HttpMethod!.ToLowerInvariant();
109-
string primaryResourceName = _formatter.FormatResourceName(primaryResourceType).Singularize();
110+
string primaryResourceName = _formatter.FormatResourceName(resourceClrType).Singularize();
110111
string relationshipName = operationIdTemplate.Contains("[RelationshipName]") ? endpoint.RelativePath.Split("/").Last() : string.Empty;
111112

112113
// @formatter:wrap_chained_method_calls chop_always

src/JsonApiDotNetCore.OpenApi/JsonApiRequestFormatMetadataProvider.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ internal sealed class JsonApiRequestFormatMetadataProvider : IInputFormatter, IA
1717
{
1818
typeof(ToManyRelationshipRequestData<>),
1919
typeof(ToOneRelationshipRequestData<>),
20+
typeof(NullableToOneRelationshipRequestData<>),
2021
typeof(ResourcePostRequestDocument<>),
2122
typeof(ResourcePatchRequestDocument<>)
2223
};

src/JsonApiDotNetCore.OpenApi/JsonApiSchemaIdSelector.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,15 @@ internal sealed class JsonApiSchemaIdSelector
1818
[typeof(ResourcePostRequestObject<>)] = "###-data-in-post-request",
1919
[typeof(ResourcePatchRequestObject<>)] = "###-data-in-patch-request",
2020
[typeof(ToOneRelationshipRequestData<>)] = "to-one-###-request-data",
21+
[typeof(NullableToOneRelationshipRequestData<>)] = "nullable-to-one-###-request-data",
2122
[typeof(ToManyRelationshipRequestData<>)] = "to-many-###-request-data",
2223
[typeof(PrimaryResourceResponseDocument<>)] = "###-primary-response-document",
2324
[typeof(SecondaryResourceResponseDocument<>)] = "###-secondary-response-document",
2425
[typeof(ResourceCollectionResponseDocument<>)] = "###-collection-response-document",
2526
[typeof(ResourceIdentifierResponseDocument<>)] = "###-identifier-response-document",
2627
[typeof(ResourceIdentifierCollectionResponseDocument<>)] = "###-identifier-collection-response-document",
2728
[typeof(ToOneRelationshipResponseData<>)] = "to-one-###-response-data",
29+
[typeof(NullableToOneRelationshipResponseData<>)] = "nullable-to-one-###-response-data",
2830
[typeof(ToManyRelationshipResponseData<>)] = "to-many-###-response-data",
2931
[typeof(ResourceResponseObject<>)] = "###-data-in-response",
3032
[typeof(ResourceIdentifierObject<>)] = "###-identifier"

0 commit comments

Comments
 (0)