77using JsonApiDotNetCore . Resources ;
88using Microsoft . AspNetCore . Mvc ;
99using Microsoft . AspNetCore . Mvc . ApplicationModels ;
10+ using Microsoft . Extensions . Logging ;
1011
1112namespace JsonApiDotNetCore . Middleware ;
1213
@@ -30,17 +31,20 @@ public sealed class JsonApiRoutingConvention : IJsonApiRoutingConvention
3031{
3132 private readonly IJsonApiOptions _options ;
3233 private readonly IResourceGraph _resourceGraph ;
34+ private readonly ILogger < JsonApiRoutingConvention > _logger ;
3335 private readonly Dictionary < string , string > _registeredControllerNameByTemplate = new ( ) ;
3436 private readonly Dictionary < Type , ResourceType > _resourceTypePerControllerTypeMap = new ( ) ;
3537 private readonly Dictionary < ResourceType , ControllerModel > _controllerPerResourceTypeMap = new ( ) ;
3638
37- public JsonApiRoutingConvention ( IJsonApiOptions options , IResourceGraph resourceGraph )
39+ public JsonApiRoutingConvention ( IJsonApiOptions options , IResourceGraph resourceGraph , ILogger < JsonApiRoutingConvention > logger )
3840 {
3941 ArgumentGuard . NotNull ( options ) ;
4042 ArgumentGuard . NotNull ( resourceGraph ) ;
43+ ArgumentGuard . NotNull ( logger ) ;
4144
4245 _options = options ;
4346 _resourceGraph = resourceGraph ;
47+ _logger = logger ;
4448 }
4549
4650 /// <inheritdoc />
@@ -64,36 +68,51 @@ public void Apply(ApplicationModel application)
6468
6569 foreach ( ControllerModel controller in application . Controllers )
6670 {
67- bool isOperationsController = IsOperationsController ( controller . ControllerType ) ;
71+ if ( ! IsJsonApiController ( controller ) )
72+ {
73+ continue ;
74+ }
75+
76+ if ( HasApiControllerAttribute ( controller ) )
77+ {
78+ // Although recommended by Microsoft for hard-written controllers, the opinionated behavior of [ApiController] violates the JSON:API specification.
79+ // See https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#apicontroller-attribute for its effects.
80+ // JsonApiDotNetCore already handles all of these concerns, but in a JSON:API-compliant way. So the attribute doesn't do any good.
6881
69- if ( ! isOperationsController )
82+ // While we try our best when [ApiController] is used, we can't completely avoid a degraded experience. ModelState validation errors are turned into
83+ // ProblemDetails, where the origin of the error gets lost. As a result, we can't populate the source pointer in JSON:API error responses.
84+ // For backwards-compatibility, we log a warning instead of throwing. But we can't think of any use cases where having [ApiController] makes sense.
85+
86+ _logger . LogWarning (
87+ $ "Found JSON:API controller '{ controller . ControllerType } ' with [ApiController]. Please remove this attribute for optimal JSON:API compliance.") ;
88+ }
89+
90+ if ( ! IsOperationsController ( controller . ControllerType ) )
7091 {
7192 Type ? resourceClrType = ExtractResourceClrTypeFromController ( controller . ControllerType ) ;
7293
7394 if ( resourceClrType != null )
7495 {
7596 ResourceType ? resourceType = _resourceGraph . FindResourceType ( resourceClrType ) ;
7697
77- if ( resourceType != null )
78- {
79- if ( _controllerPerResourceTypeMap . ContainsKey ( resourceType ) )
80- {
81- throw new InvalidConfigurationException (
82- $ "Multiple controllers found for resource type '{ resourceType } ': '{ _controllerPerResourceTypeMap [ resourceType ] . ControllerType } ' and '{ controller . ControllerType } '.") ;
83- }
84-
85- _resourceTypePerControllerTypeMap . Add ( controller . ControllerType , resourceType ) ;
86- _controllerPerResourceTypeMap . Add ( resourceType , controller ) ;
87- }
88- else
98+ if ( resourceType == null )
8999 {
90100 throw new InvalidConfigurationException ( $ "Controller '{ controller . ControllerType } ' depends on " +
91101 $ "resource type '{ resourceClrType } ', which does not exist in the resource graph.") ;
92102 }
103+
104+ if ( _controllerPerResourceTypeMap . ContainsKey ( resourceType ) )
105+ {
106+ throw new InvalidConfigurationException (
107+ $ "Multiple controllers found for resource type '{ resourceType } ': '{ _controllerPerResourceTypeMap [ resourceType ] . ControllerType } ' and '{ controller . ControllerType } '.") ;
108+ }
109+
110+ _resourceTypePerControllerTypeMap . Add ( controller . ControllerType , resourceType ) ;
111+ _controllerPerResourceTypeMap . Add ( resourceType , controller ) ;
93112 }
94113 }
95114
96- if ( ! IsRoutingConventionEnabled ( controller ) )
115+ if ( IsRoutingConventionDisabled ( controller ) )
97116 {
98117 continue ;
99118 }
@@ -115,10 +134,19 @@ public void Apply(ApplicationModel application)
115134 }
116135 }
117136
118- private bool IsRoutingConventionEnabled ( ControllerModel controller )
137+ private static bool IsJsonApiController ( ControllerModel controller )
138+ {
139+ return controller . ControllerType . IsSubclassOf ( typeof ( CoreJsonApiController ) ) ;
140+ }
141+
142+ private static bool HasApiControllerAttribute ( ControllerModel controller )
143+ {
144+ return controller . ControllerType . GetCustomAttribute < ApiControllerAttribute > ( ) != null ;
145+ }
146+
147+ private static bool IsRoutingConventionDisabled ( ControllerModel controller )
119148 {
120- return controller . ControllerType . IsSubclassOf ( typeof ( CoreJsonApiController ) ) &&
121- controller . ControllerType . GetCustomAttribute < DisableRoutingConventionAttribute > ( true ) == null ;
149+ return controller . ControllerType . GetCustomAttribute < DisableRoutingConventionAttribute > ( true ) != null ;
122150 }
123151
124152 /// <summary>
0 commit comments