@@ -31,6 +31,23 @@ internal sealed class JsonApiActionDescriptorCollectionProvider : IActionDescrip
3131 private const int FilterScope = 10 ;
3232 private static readonly Type ErrorDocumentType = typeof ( ErrorResponseDocument ) ;
3333
34+ private static readonly ConsumesMediaTypeCollection RegularMediaTypes = new ( [
35+ JsonApiMediaType . Default ,
36+ OpenApiMediaTypes . OpenApi ,
37+ #pragma warning disable CS0618 // Type or member is obsolete
38+ OpenApiMediaTypes . RelaxedOpenApi
39+ #pragma warning restore CS0618 // Type or member is obsolete
40+ ] ) ;
41+
42+ private static readonly ConsumesMediaTypeCollection OperationsMediaTypes = new ( [
43+ JsonApiMediaType . AtomicOperations ,
44+ OpenApiMediaTypes . AtomicOperationsWithOpenApi ,
45+ #pragma warning disable CS0618 // Type or member is obsolete
46+ JsonApiMediaType . RelaxedAtomicOperations ,
47+ OpenApiMediaTypes . RelaxedAtomicOperationsWithRelaxedOpenApi
48+ #pragma warning restore CS0618 // Type or member is obsolete
49+ ] ) ;
50+
3451 private readonly IActionDescriptorCollectionProvider _defaultProvider ;
3552 private readonly IControllerResourceMapping _controllerResourceMapping ;
3653 private readonly JsonApiEndpointMetadataProvider _jsonApiEndpointMetadataProvider ;
@@ -220,14 +237,14 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor)
220237 {
221238 case AtomicOperationsRequestMetadata atomicOperationsRequestMetadata :
222239 {
223- SetConsumes ( descriptor , atomicOperationsRequestMetadata . DocumentType , JsonApiMediaType . AtomicOperations ) ;
240+ SetConsumes ( descriptor , atomicOperationsRequestMetadata . DocumentType , OperationsMediaTypes ) ;
224241 UpdateRequestBodyParameterDescriptor ( descriptor , atomicOperationsRequestMetadata . DocumentType , null ) ;
225242
226243 break ;
227244 }
228245 case PrimaryRequestMetadata primaryRequestMetadata :
229246 {
230- SetConsumes ( descriptor , primaryRequestMetadata . DocumentType , JsonApiMediaType . Default ) ;
247+ SetConsumes ( descriptor , primaryRequestMetadata . DocumentType , RegularMediaTypes ) ;
231248 UpdateRequestBodyParameterDescriptor ( descriptor , primaryRequestMetadata . DocumentType , null ) ;
232249
233250 break ;
@@ -243,7 +260,7 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor)
243260
244261 RemovePathParameter ( relationshipDescriptor . Parameters , "relationshipName" ) ;
245262 ExpandTemplate ( relationshipDescriptor . AttributeRouteInfo ! , relationship . PublicName ) ;
246- SetConsumes ( descriptor , documentType , JsonApiMediaType . Default ) ;
263+ SetConsumes ( descriptor , documentType , RegularMediaTypes ) ;
247264 UpdateRequestBodyParameterDescriptor ( relationshipDescriptor , documentType , relationship . PublicName ) ;
248265
249266 descriptorsByRelationship [ relationship ] = relationshipDescriptor ;
@@ -302,22 +319,23 @@ private ActionDescriptor[] SetEndpointMetadata(ActionDescriptor descriptor)
302319 return isNonPrimaryEndpoint ? descriptorsByRelationship . Values . ToArray ( ) : [ descriptor ] ;
303320 }
304321
305- private static void SetConsumes ( ActionDescriptor descriptor , Type requestType , JsonApiMediaType mediaType )
322+ private static void SetConsumes ( ActionDescriptor descriptor , Type requestType , ConsumesMediaTypeCollection mediaTypes )
306323 {
307324 RemoveFiltersForRequestBody ( descriptor ) ;
308325
309- // This value doesn't actually appear in the OpenAPI document, but is only used to invoke
310- // JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type.
311- string contentType = mediaType . ToString ( ) ;
312-
313326 if ( descriptor is ControllerActionDescriptor controllerActionDescriptor &&
314327 controllerActionDescriptor . MethodInfo . GetCustomAttributes < ConsumesAttribute > ( ) . Any ( ) )
315328 {
316329 // A custom JSON:API action method with [Consumes] on it. Hide the attribute from Swashbuckle, so it uses our data in API Explorer.
317330 controllerActionDescriptor . MethodInfo = new MethodInfoWrapper ( controllerActionDescriptor . MethodInfo , [ typeof ( ConsumesAttribute ) ] ) ;
318331 }
319332
320- descriptor . FilterDescriptors . Add ( new FilterDescriptor ( new ConsumesAttribute ( requestType , contentType ) , FilterScope ) ) ;
333+ // These media types don't actually appear in the OpenAPI document, but are used to invoke
334+ // JsonApiRequestFormatMetadataProvider.GetSupportedContentTypes(), which determines the actual request content type.
335+ // We need to pass all possible media types, otherwise ASP.NET Core's content negotiation returns HTTP 415.
336+
337+ var consumesAttribute = new ConsumesAttribute ( requestType , mediaTypes . ContentType , mediaTypes . OtherContentTypes ) ;
338+ descriptor . FilterDescriptors . Add ( new FilterDescriptor ( consumesAttribute , FilterScope ) ) ;
321339 }
322340
323341 private static void RemoveFiltersForRequestBody ( ActionDescriptor descriptor )
@@ -474,4 +492,18 @@ private static void SetNonPrimaryResponseMetadata(ActionDescriptor descriptor,
474492
475493 descriptorsByRelationship [ relationship ] = relationshipDescriptor ;
476494 }
495+
496+ private sealed class ConsumesMediaTypeCollection
497+ {
498+ public string ContentType { get ; }
499+ public string [ ] OtherContentTypes { get ; }
500+
501+ public ConsumesMediaTypeCollection ( JsonApiMediaType [ ] mediaTypes )
502+ {
503+ ArgumentGuard . NotNullNorEmpty ( mediaTypes ) ;
504+
505+ ContentType = mediaTypes [ 0 ] . ToString ( ) ;
506+ OtherContentTypes = mediaTypes . Skip ( 1 ) . Select ( mediaType => mediaType . ToString ( ) ) . ToArray ( ) ;
507+ }
508+ }
477509}
0 commit comments