Skip to content

Commit 55d7cb0

Browse files
committed
Log warning at startup when [ApiController] found on a JSON:API controller
1 parent be99234 commit 55d7cb0

File tree

2 files changed

+94
-19
lines changed

2 files changed

+94
-19
lines changed

src/JsonApiDotNetCore/Middleware/JsonApiRoutingConvention.cs

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
using JsonApiDotNetCore.Resources;
88
using Microsoft.AspNetCore.Mvc;
99
using Microsoft.AspNetCore.Mvc.ApplicationModels;
10+
using Microsoft.Extensions.Logging;
1011

1112
namespace 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>
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
using FluentAssertions;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
4+
using TestBuildingBlocks;
5+
using Xunit;
6+
7+
namespace JsonApiDotNetCoreTests.IntegrationTests.CustomRoutes;
8+
9+
public sealed class ApiControllerAttributeLogTests : IntegrationTestContext<TestableStartup<CustomRouteDbContext>, CustomRouteDbContext>
10+
{
11+
private readonly FakeLoggerFactory _loggerFactory;
12+
13+
public ApiControllerAttributeLogTests()
14+
{
15+
UseController<CiviliansController>();
16+
17+
_loggerFactory = new FakeLoggerFactory(LogLevel.Warning);
18+
19+
ConfigureLogging(options =>
20+
{
21+
options.ClearProviders();
22+
options.AddProvider(_loggerFactory);
23+
});
24+
25+
ConfigureServicesBeforeStartup(services =>
26+
{
27+
services.AddSingleton(_loggerFactory);
28+
});
29+
}
30+
31+
[Fact]
32+
public void Logs_warning_at_startup_when_ApiControllerAttribute_found()
33+
{
34+
// Arrange
35+
_loggerFactory.Logger.Clear();
36+
37+
// Act
38+
_ = Factory;
39+
40+
// Assert
41+
_loggerFactory.Logger.Messages.ShouldHaveCount(1);
42+
_loggerFactory.Logger.Messages.Single().LogLevel.Should().Be(LogLevel.Warning);
43+
44+
_loggerFactory.Logger.Messages.Single().Text.Should().Be(
45+
$"Found JSON:API controller '{typeof(CiviliansController)}' with [ApiController]. Please remove this attribute for optimal JSON:API compliance.");
46+
}
47+
}

0 commit comments

Comments
 (0)