Skip to content

Commit 1c099e7

Browse files
committed
Copied integration and fixed compilation errors
1 parent d226a0e commit 1c099e7

File tree

66 files changed

+2817
-4
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+2817
-4
lines changed
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System.Linq;
2+
using System.Reflection;
3+
using Microsoft.AspNetCore.Mvc.Abstractions;
4+
using Microsoft.AspNetCore.Mvc.Controllers;
5+
using Microsoft.AspNetCore.Mvc.Filters;
6+
using Microsoft.AspNetCore.Mvc.ModelBinding;
7+
8+
namespace JsonApiDotNetCore.OpenApi
9+
{
10+
internal static class ActionDescriptorExtensions
11+
{
12+
public static MethodInfo GetActionMethod(this ActionDescriptor descriptor)
13+
{
14+
ArgumentGuard.NotNull(descriptor, nameof(descriptor));
15+
16+
return ((ControllerActionDescriptor)descriptor).MethodInfo;
17+
}
18+
19+
public static TFilterMetaData GetFilterMetadata<TFilterMetaData>(this ActionDescriptor descriptor)
20+
where TFilterMetaData : IFilterMetadata
21+
{
22+
ArgumentGuard.NotNull(descriptor, nameof(descriptor));
23+
24+
IFilterMetadata filterMetadata = descriptor.FilterDescriptors.Select(filterDescriptor => filterDescriptor.Filter)
25+
.FirstOrDefault(filter => filter is TFilterMetaData);
26+
27+
return (TFilterMetaData)filterMetadata;
28+
}
29+
30+
public static ControllerParameterDescriptor GetBodyParameterDescriptor(this ActionDescriptor descriptor)
31+
{
32+
ArgumentGuard.NotNull(descriptor, nameof(descriptor));
33+
34+
return (ControllerParameterDescriptor)descriptor.Parameters.FirstOrDefault(parameterDescriptor =>
35+
// ReSharper disable once ConstantConditionalAccessQualifier Motivation: see https://github.com/dotnet/aspnetcore/issues/32538
36+
parameterDescriptor.BindingInfo?.BindingSource == BindingSource.Body);
37+
}
38+
}
39+
}
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
using JsonApiDotNetCore.OpenApi.JsonApiMetadata;
6+
using JsonApiDotNetCore.Configuration;
7+
using JsonApiDotNetCore.Middleware;
8+
using Microsoft.AspNetCore.Mvc;
9+
using Microsoft.AspNetCore.Mvc.Abstractions;
10+
using Microsoft.AspNetCore.Mvc.Controllers;
11+
using Microsoft.AspNetCore.Mvc.Filters;
12+
using Microsoft.AspNetCore.Mvc.Infrastructure;
13+
using Microsoft.AspNetCore.Mvc.Routing;
14+
15+
namespace JsonApiDotNetCore.OpenApi
16+
{
17+
/// <summary>
18+
/// Adds JsonApiDotNetCore metadata to <see cref="ControllerActionDescriptor" />s if available. This translates to updating response types in
19+
/// <see cref="ProducesResponseTypeAttribute" /> and performing an expansion for secondary and relationship endpoints (eg
20+
/// /article/{id}/{relationshipName} -> /article/{id}/author, /article/{id}/revisions, etc).
21+
/// </summary>
22+
internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescriptorCollectionProvider
23+
{
24+
private readonly IActionDescriptorCollectionProvider _defaultProvider;
25+
private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider;
26+
27+
public ActionDescriptorCollection ActionDescriptors => GetActionDescriptors();
28+
29+
public JsonApiActionDescriptorCollectionProvider(IResourceGraph resourceGraph, IControllerResourceMapping controllerResourceMapping,
30+
IActionDescriptorCollectionProvider defaultProvider)
31+
{
32+
ArgumentGuard.NotNull(resourceGraph, nameof(resourceGraph));
33+
ArgumentGuard.NotNull(controllerResourceMapping, nameof(controllerResourceMapping));
34+
ArgumentGuard.NotNull(defaultProvider, nameof(defaultProvider));
35+
36+
_defaultProvider = defaultProvider;
37+
_jsonApiEndpointMetadataProvider = new JsonApiEndpointMetadataProvider(resourceGraph, controllerResourceMapping);
38+
}
39+
40+
private ActionDescriptorCollection GetActionDescriptors()
41+
{
42+
List<ActionDescriptor> newDescriptors = _defaultProvider.ActionDescriptors.Items.ToList();
43+
List<ActionDescriptor> endpoints = newDescriptors.Where(IsVisibleJsonApiEndpoint).ToList();
44+
45+
foreach (ActionDescriptor endpoint in endpoints)
46+
{
47+
JsonApiEndpointMetadataContainer endpointMetadataContainer = _jsonApiEndpointMetadataProvider.Get(endpoint.GetActionMethod());
48+
49+
List<ActionDescriptor> replacementDescriptorsForEndpoint = new();
50+
replacementDescriptorsForEndpoint.AddRange(AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.RequestMetadata));
51+
replacementDescriptorsForEndpoint.AddRange(AddJsonApiMetadataToAction(endpoint, endpointMetadataContainer.ResponseMetadata));
52+
53+
if (replacementDescriptorsForEndpoint.Any())
54+
{
55+
newDescriptors.InsertRange(newDescriptors.IndexOf(endpoint) - 1, replacementDescriptorsForEndpoint);
56+
newDescriptors.Remove(endpoint);
57+
}
58+
}
59+
60+
int descriptorVersion = _defaultProvider.ActionDescriptors.Version;
61+
return new ActionDescriptorCollection(newDescriptors.AsReadOnly(), descriptorVersion);
62+
}
63+
64+
private static bool IsVisibleJsonApiEndpoint(ActionDescriptor descriptor)
65+
{
66+
// Only if in a convention ApiExplorer.IsVisible was set to false, the ApiDescriptionActionData will not be present.
67+
return descriptor is ControllerActionDescriptor controllerAction && controllerAction.Properties.ContainsKey(typeof(ApiDescriptionActionData));
68+
}
69+
70+
private static IList<ActionDescriptor> AddJsonApiMetadataToAction(ActionDescriptor endpoint, IJsonApiEndpointMetadata jsonApiEndpointMetadata)
71+
{
72+
switch (jsonApiEndpointMetadata)
73+
{
74+
case PrimaryResponseMetadata primaryMetadata:
75+
{
76+
UpdateProducesResponseTypeAttribute(endpoint, primaryMetadata.Type);
77+
return Array.Empty<ActionDescriptor>();
78+
}
79+
case PrimaryRequestMetadata primaryMetadata:
80+
{
81+
UpdateBodyParameterDescriptor(endpoint, primaryMetadata.Type);
82+
return Array.Empty<ActionDescriptor>();
83+
}
84+
case ExpansibleEndpointMetadata expansibleMetadata
85+
when expansibleMetadata is RelationshipResponseMetadata || expansibleMetadata is SecondaryResponseMetadata:
86+
{
87+
return Expand(endpoint, expansibleMetadata,
88+
(expandedEndpoint, relationshipType, _) => UpdateProducesResponseTypeAttribute(expandedEndpoint, relationshipType));
89+
}
90+
case ExpansibleEndpointMetadata expansibleMetadata when expansibleMetadata is RelationshipRequestMetadata:
91+
{
92+
return Expand(endpoint, expansibleMetadata, UpdateBodyParameterDescriptor);
93+
}
94+
default:
95+
{
96+
return Array.Empty<ActionDescriptor>();
97+
}
98+
}
99+
}
100+
101+
private static void UpdateProducesResponseTypeAttribute(ActionDescriptor endpoint, Type responseTypeToSet)
102+
{
103+
// TODO: is this check really adding anything? is the "else" ever hit, and if so, is that even meaningful?
104+
if (ProducesJsonApiResponseBody(endpoint))
105+
{
106+
var producesResponse = endpoint.GetFilterMetadata<ProducesResponseTypeAttribute>();
107+
producesResponse.Type = responseTypeToSet;
108+
}
109+
}
110+
111+
private static bool ProducesJsonApiResponseBody(ActionDescriptor endpoint)
112+
{
113+
var produces = endpoint.GetFilterMetadata<ProducesAttribute>();
114+
115+
return produces != null && produces.ContentTypes.Any(contentType => contentType == HeaderConstants.MediaType);
116+
}
117+
118+
private static IList<ActionDescriptor> Expand(ActionDescriptor genericEndpoint, ExpansibleEndpointMetadata metadata,
119+
Action<ActionDescriptor, Type, string> expansionCallback)
120+
{
121+
var expansion = new List<ActionDescriptor>();
122+
123+
foreach ((string relationshipName, Type relationshipType) in metadata.ExpansionElements)
124+
{
125+
ActionDescriptor expandedEndpoint = Clone(genericEndpoint);
126+
RemovePathParameter(expandedEndpoint.Parameters, JsonApiPathParameter.RelationshipName);
127+
ExpandTemplate(expandedEndpoint.AttributeRouteInfo, relationshipName);
128+
129+
expansionCallback(expandedEndpoint, relationshipType, relationshipName);
130+
131+
expansion.Add(expandedEndpoint);
132+
}
133+
134+
return expansion;
135+
}
136+
137+
private static void UpdateBodyParameterDescriptor(ActionDescriptor endpoint, Type bodyType, string parameterName = null)
138+
{
139+
ControllerParameterDescriptor requestBodyDescriptor = endpoint.GetBodyParameterDescriptor();
140+
requestBodyDescriptor.ParameterType = bodyType;
141+
ParameterInfo replacementParameterInfo = requestBodyDescriptor.ParameterInfo.WithParameterType(bodyType);
142+
143+
if (parameterName != null)
144+
{
145+
replacementParameterInfo = replacementParameterInfo.WithName(parameterName);
146+
}
147+
148+
requestBodyDescriptor.ParameterInfo = replacementParameterInfo;
149+
}
150+
151+
private static ActionDescriptor Clone(ActionDescriptor descriptor)
152+
{
153+
var clonedDescriptor = (ActionDescriptor)descriptor.MemberwiseClone();
154+
155+
clonedDescriptor.AttributeRouteInfo = (AttributeRouteInfo)descriptor.AttributeRouteInfo.MemberwiseClone();
156+
157+
clonedDescriptor.FilterDescriptors = new List<FilterDescriptor>();
158+
159+
foreach (FilterDescriptor filter in descriptor.FilterDescriptors)
160+
{
161+
clonedDescriptor.FilterDescriptors.Add(Clone(filter));
162+
}
163+
164+
clonedDescriptor.Parameters = new List<ParameterDescriptor>();
165+
166+
foreach (ParameterDescriptor parameter in descriptor.Parameters)
167+
{
168+
clonedDescriptor.Parameters.Add((ParameterDescriptor)parameter.MemberwiseClone());
169+
}
170+
171+
return clonedDescriptor;
172+
}
173+
174+
private static FilterDescriptor Clone(FilterDescriptor descriptor)
175+
{
176+
var clonedFilter = (IFilterMetadata)descriptor.Filter.MemberwiseClone();
177+
178+
return new FilterDescriptor(clonedFilter, descriptor.Scope)
179+
{
180+
Order = descriptor.Order
181+
};
182+
}
183+
184+
private static void RemovePathParameter(ICollection<ParameterDescriptor> parameters, string parameterName)
185+
{
186+
ParameterDescriptor relationshipName = parameters.Single(parameterDescriptor => parameterDescriptor.Name == parameterName);
187+
188+
parameters.Remove(relationshipName);
189+
}
190+
191+
private static void ExpandTemplate(AttributeRouteInfo route, string expansionParameter)
192+
{
193+
route.Template = route.Template!.Replace(JsonApiRoutingTemplate.RelationshipNameUrlPlaceholder, expansionParameter);
194+
}
195+
}
196+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
namespace JsonApiDotNetCore.OpenApi
2+
{
3+
internal enum JsonApiEndpoint
4+
{
5+
GetCollection,
6+
GetSingle,
7+
GetSecondary,
8+
GetRelationship,
9+
Post,
10+
PostRelationship,
11+
Patch,
12+
PatchRelationship,
13+
#pragma warning disable AV1711 // Name members and local functions similarly to members of .NET Framework classes
14+
Delete,
15+
#pragma warning restore AV1711 // Name members and local functions similarly to members of .NET Framework classes
16+
DeleteRelationship
17+
}
18+
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using JsonApiDotNetCore.Middleware;
5+
using JsonApiDotNetCore.Serialization;
6+
using Microsoft.AspNetCore.Mvc.ApiExplorer;
7+
using Microsoft.AspNetCore.Mvc.Formatters;
8+
using Microsoft.Extensions.DependencyInjection;
9+
using Microsoft.Net.Http.Headers;
10+
11+
namespace JsonApiDotNetCore.OpenApi
12+
{
13+
internal sealed class JsonApiInputFormatterWithMetadata : IJsonApiInputFormatter, IApiRequestFormatMetadataProvider
14+
{
15+
public bool CanRead(InputFormatterContext context)
16+
{
17+
ArgumentGuard.NotNull(context, nameof(context));
18+
19+
return context.HttpContext.IsJsonApiRequest();
20+
}
21+
22+
public async Task<InputFormatterResult> ReadAsync(InputFormatterContext context)
23+
{
24+
ArgumentGuard.NotNull(context, nameof(context));
25+
26+
var reader = context.HttpContext.RequestServices.GetRequiredService<IJsonApiReader>();
27+
return await reader.ReadAsync(context);
28+
}
29+
30+
public IReadOnlyList<string> GetSupportedContentTypes(string contentType, Type objectType)
31+
{
32+
ArgumentGuard.NotNullNorEmpty(contentType, nameof(contentType));
33+
ArgumentGuard.NotNull(objectType, nameof(objectType));
34+
35+
return new MediaTypeCollection
36+
{
37+
new MediaTypeHeaderValue(HeaderConstants.MediaType)
38+
};
39+
}
40+
}
41+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
using System.Linq;
2+
using System.Reflection;
3+
using Microsoft.AspNetCore.Mvc;
4+
using Microsoft.AspNetCore.Mvc.Routing;
5+
6+
namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
7+
{
8+
internal sealed class EndpointResolver
9+
{
10+
public JsonApiEndpoint? Get(MethodInfo controllerAction)
11+
{
12+
ArgumentGuard.NotNull(controllerAction, nameof(controllerAction));
13+
14+
HttpMethodAttribute method = controllerAction.GetCustomAttributes(true).OfType<HttpMethodAttribute>().FirstOrDefault();
15+
16+
return ResolveJsonApiEndpoint(method);
17+
}
18+
19+
private static JsonApiEndpoint? ResolveJsonApiEndpoint(HttpMethodAttribute httpMethod)
20+
{
21+
return httpMethod switch
22+
{
23+
HttpGetAttribute attr => attr.Template switch
24+
{
25+
null => JsonApiEndpoint.GetCollection,
26+
JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.GetSingle,
27+
JsonApiRoutingTemplate.SecondaryEndpoint => JsonApiEndpoint.GetSecondary,
28+
JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.GetRelationship,
29+
_ => null
30+
},
31+
HttpPostAttribute attr => attr.Template switch
32+
{
33+
null => JsonApiEndpoint.Post,
34+
JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.PostRelationship,
35+
_ => null
36+
},
37+
HttpPatchAttribute attr => attr.Template switch
38+
{
39+
JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.Patch,
40+
JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.PatchRelationship,
41+
_ => null
42+
},
43+
HttpDeleteAttribute attr => attr.Template switch
44+
{
45+
JsonApiRoutingTemplate.PrimaryEndpoint => JsonApiEndpoint.Delete,
46+
JsonApiRoutingTemplate.RelationshipEndpoint => JsonApiEndpoint.DeleteRelationship,
47+
_ => null
48+
},
49+
_ => null
50+
};
51+
}
52+
}
53+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
5+
{
6+
internal abstract class ExpansibleEndpointMetadata
7+
{
8+
public abstract IDictionary<string, Type> ExpansionElements { get; }
9+
}
10+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
2+
{
3+
internal interface IJsonApiEndpointMetadata
4+
{
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
2+
{
3+
internal interface IJsonApiRequestMetadata : IJsonApiEndpointMetadata
4+
{
5+
}
6+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
2+
{
3+
internal interface IJsonApiResponseMetadata : IJsonApiEndpointMetadata
4+
{
5+
}
6+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
namespace JsonApiDotNetCore.OpenApi.JsonApiMetadata
2+
{
3+
/// <summary>
4+
/// Metadata available at runtime about a JsonApiDotNetCore endpoint.
5+
/// </summary>
6+
internal sealed class JsonApiEndpointMetadataContainer
7+
{
8+
public IJsonApiRequestMetadata RequestMetadata { get; init; }
9+
10+
public IJsonApiResponseMetadata ResponseMetadata { get; init; }
11+
}
12+
}

0 commit comments

Comments
 (0)