7
7
using JsonApiDotNetCore . Resources ;
8
8
using Microsoft . AspNetCore . Mvc ;
9
9
using Microsoft . AspNetCore . Mvc . ApplicationModels ;
10
+ using Microsoft . Extensions . Logging ;
10
11
11
12
namespace JsonApiDotNetCore . Middleware ;
12
13
@@ -30,17 +31,20 @@ public sealed class JsonApiRoutingConvention : IJsonApiRoutingConvention
30
31
{
31
32
private readonly IJsonApiOptions _options ;
32
33
private readonly IResourceGraph _resourceGraph ;
34
+ private readonly ILogger < JsonApiRoutingConvention > _logger ;
33
35
private readonly Dictionary < string , string > _registeredControllerNameByTemplate = new ( ) ;
34
36
private readonly Dictionary < Type , ResourceType > _resourceTypePerControllerTypeMap = new ( ) ;
35
37
private readonly Dictionary < ResourceType , ControllerModel > _controllerPerResourceTypeMap = new ( ) ;
36
38
37
- public JsonApiRoutingConvention ( IJsonApiOptions options , IResourceGraph resourceGraph )
39
+ public JsonApiRoutingConvention ( IJsonApiOptions options , IResourceGraph resourceGraph , ILogger < JsonApiRoutingConvention > logger )
38
40
{
39
41
ArgumentGuard . NotNull ( options ) ;
40
42
ArgumentGuard . NotNull ( resourceGraph ) ;
43
+ ArgumentGuard . NotNull ( logger ) ;
41
44
42
45
_options = options ;
43
46
_resourceGraph = resourceGraph ;
47
+ _logger = logger ;
44
48
}
45
49
46
50
/// <inheritdoc />
@@ -64,36 +68,51 @@ public void Apply(ApplicationModel application)
64
68
65
69
foreach ( ControllerModel controller in application . Controllers )
66
70
{
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.
68
81
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 ) )
70
91
{
71
92
Type ? resourceClrType = ExtractResourceClrTypeFromController ( controller . ControllerType ) ;
72
93
73
94
if ( resourceClrType != null )
74
95
{
75
96
ResourceType ? resourceType = _resourceGraph . FindResourceType ( resourceClrType ) ;
76
97
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 )
89
99
{
90
100
throw new InvalidConfigurationException ( $ "Controller '{ controller . ControllerType } ' depends on " +
91
101
$ "resource type '{ resourceClrType } ', which does not exist in the resource graph.") ;
92
102
}
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 ) ;
93
112
}
94
113
}
95
114
96
- if ( ! IsRoutingConventionEnabled ( controller ) )
115
+ if ( IsRoutingConventionDisabled ( controller ) )
97
116
{
98
117
continue ;
99
118
}
@@ -115,10 +134,19 @@ public void Apply(ApplicationModel application)
115
134
}
116
135
}
117
136
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 )
119
148
{
120
- return controller . ControllerType . IsSubclassOf ( typeof ( CoreJsonApiController ) ) &&
121
- controller . ControllerType . GetCustomAttribute < DisableRoutingConventionAttribute > ( true ) == null ;
149
+ return controller . ControllerType . GetCustomAttribute < DisableRoutingConventionAttribute > ( true ) != null ;
122
150
}
123
151
124
152
/// <summary>
0 commit comments