Skip to content

Commit 823336d

Browse files
committed
Fixed: produce helpful error when [FromBody] is missing, add tests
1 parent b736298 commit 823336d

9 files changed

+132
-6
lines changed

src/JsonApiDotNetCore.OpenApi/JsonApiActionDescriptorCollectionProvider.cs

+8-3
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
using System.Reflection;
2+
using JsonApiDotNetCore.Errors;
13
using JsonApiDotNetCore.Middleware;
24
using JsonApiDotNetCore.OpenApi.JsonApiMetadata;
35
using Microsoft.AspNetCore.Mvc;
@@ -38,7 +40,8 @@ private ActionDescriptorCollection GetActionDescriptors()
3840

3941
foreach (ActionDescriptor endpoint in endpoints)
4042
{
41-
JsonApiEndpointMetadataContainer endpointMetadataContainer = _jsonApiEndpointMetadataProvider.Get(endpoint.GetActionMethod());
43+
MethodInfo actionMethod = endpoint.GetActionMethod();
44+
JsonApiEndpointMetadataContainer endpointMetadataContainer = _jsonApiEndpointMetadataProvider.Get(actionMethod);
4245

4346
List<ActionDescriptor> replacementDescriptorsForEndpoint =
4447
[
@@ -148,8 +151,10 @@ private static void UpdateBodyParameterDescriptor(ActionDescriptor endpoint, Typ
148151

149152
if (requestBodyDescriptor == null)
150153
{
151-
// ASP.NET model binding picks up on [FromBody] in base classes, so even when it is left out in an override, this should not be reachable.
152-
throw new UnreachableCodeException();
154+
MethodInfo actionMethod = endpoint.GetActionMethod();
155+
156+
throw new InvalidConfigurationException(
157+
$"The action method '{actionMethod}' on type '{actionMethod.ReflectedType?.FullName}' contains no parameter with a [FromBody] attribute.");
153158
}
154159

155160
requestBodyDescriptor.ParameterType = documentType;

src/JsonApiDotNetCore.OpenApi/JsonApiOperationIdSelector.cs

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System.Reflection;
12
using System.Text.Json;
23
using Humanizer;
34
using JsonApiDotNetCore.Configuration;
@@ -50,7 +51,8 @@ public string GetOperationId(ApiDescription endpoint)
5051
{
5152
ArgumentGuard.NotNull(endpoint);
5253

53-
ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(endpoint.ActionDescriptor.GetActionMethod().ReflectedType);
54+
MethodInfo actionMethod = endpoint.ActionDescriptor.GetActionMethod();
55+
ResourceType? primaryResourceType = _controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
5456

5557
if (primaryResourceType == null)
5658
{

src/JsonApiDotNetCore.OpenApi/SwaggerComponents/JsonApiOperationDocumentationFilter.cs

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System.Net;
2+
using System.Reflection;
23
using Humanizer;
34
using JetBrains.Annotations;
45
using JsonApiDotNetCore.Configuration;
@@ -79,8 +80,8 @@ public void Apply(OpenApiOperation operation, OperationFilterContext context)
7980
operation.Responses.Clear();
8081
}
8182

82-
ResourceType? resourceType =
83-
_controllerResourceMapping.GetResourceTypeForController(context.ApiDescription.ActionDescriptor.GetActionMethod().ReflectedType);
83+
MethodInfo actionMethod = context.ApiDescription.ActionDescriptor.GetActionMethod();
84+
ResourceType? resourceType = _controllerResourceMapping.GetResourceTypeForController(actionMethod.ReflectedType);
8485

8586
if (resourceType != null)
8687
{
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
using JetBrains.Annotations;
2+
using Microsoft.EntityFrameworkCore;
3+
using TestBuildingBlocks;
4+
5+
namespace OpenApiTests.OpenApiGenerationFailures.MissingFromBody;
6+
7+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
8+
public sealed class MissingFromBodyDbContext(DbContextOptions<MissingFromBodyDbContext> options) : TestableDbContext(options)
9+
{
10+
public DbSet<RecycleBin> RecycleBins => Set<RecycleBin>();
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using JsonApiDotNetCore.Configuration;
3+
using JsonApiDotNetCore.Controllers;
4+
using JsonApiDotNetCore.Services;
5+
using Microsoft.AspNetCore.Mvc;
6+
using Microsoft.Extensions.Logging;
7+
8+
namespace OpenApiTests.OpenApiGenerationFailures.MissingFromBody;
9+
10+
public sealed class MissingFromBodyOnPatchController(
11+
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<RecycleBin, long> resourceService)
12+
: BaseJsonApiController<RecycleBin, long>(options, resourceGraph, loggerFactory, resourceService)
13+
{
14+
// Not overriding the base method, to trigger the error that [FromBody] is missing.
15+
[HttpPatch("{id}")]
16+
public Task<IActionResult> AlternatePatchAsync([Required] long id, [Required] RecycleBin resource, CancellationToken cancellationToken)
17+
{
18+
return PatchAsync(id, resource, cancellationToken);
19+
}
20+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using FluentAssertions;
2+
using JsonApiDotNetCore.Errors;
3+
using Xunit;
4+
5+
namespace OpenApiTests.OpenApiGenerationFailures.MissingFromBody;
6+
7+
public sealed class MissingFromBodyOnPatchMethodTests : OpenApiTestContext<OpenApiStartup<MissingFromBodyDbContext>, MissingFromBodyDbContext>
8+
{
9+
public MissingFromBodyOnPatchMethodTests()
10+
{
11+
UseController<MissingFromBodyOnPatchController>();
12+
}
13+
14+
[Fact]
15+
public async Task Cannot_use_Patch_controller_action_method_without_FromBody_attribute()
16+
{
17+
// Act
18+
Func<Task> action = async () => _ = await GetSwaggerDocumentAsync();
19+
20+
// Assert
21+
string? actionMethod = typeof(MissingFromBodyOnPatchController).GetMethod(nameof(MissingFromBodyOnPatchController.AlternatePatchAsync))!.ToString();
22+
string containingType = typeof(MissingFromBodyOnPatchController).ToString();
23+
24+
await action.Should().ThrowExactlyAsync<InvalidConfigurationException>().WithMessage(
25+
$"The action method '{actionMethod}' on type '{containingType}' contains no parameter with a [FromBody] attribute.");
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
using JsonApiDotNetCore.Configuration;
2+
using JsonApiDotNetCore.Controllers;
3+
using JsonApiDotNetCore.Services;
4+
using Microsoft.AspNetCore.Mvc;
5+
using Microsoft.Extensions.Logging;
6+
7+
namespace OpenApiTests.OpenApiGenerationFailures.MissingFromBody;
8+
9+
public sealed class MissingFromBodyOnPostController(
10+
IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IResourceService<RecycleBin, long> resourceService)
11+
: BaseJsonApiController<RecycleBin, long>(options, resourceGraph, loggerFactory, resourceService)
12+
{
13+
// Not overriding the base method, to trigger the error that [FromBody] is missing.
14+
[HttpPost]
15+
public Task<IActionResult> AlternatePostAsync(RecycleBin resource, CancellationToken cancellationToken)
16+
{
17+
return PostAsync(resource, cancellationToken);
18+
}
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
using FluentAssertions;
2+
using JsonApiDotNetCore.Errors;
3+
using Xunit;
4+
5+
namespace OpenApiTests.OpenApiGenerationFailures.MissingFromBody;
6+
7+
public sealed class MissingFromBodyOnPostMethodTests : OpenApiTestContext<OpenApiStartup<MissingFromBodyDbContext>, MissingFromBodyDbContext>
8+
{
9+
public MissingFromBodyOnPostMethodTests()
10+
{
11+
UseController<MissingFromBodyOnPostController>();
12+
}
13+
14+
[Fact]
15+
public async Task Cannot_use_Post_controller_action_method_without_FromBody_attribute()
16+
{
17+
// Act
18+
Func<Task> action = async () => _ = await GetSwaggerDocumentAsync();
19+
20+
// Assert
21+
string? actionMethod = typeof(MissingFromBodyOnPostController).GetMethod(nameof(MissingFromBodyOnPostController.AlternatePostAsync))!.ToString();
22+
string containingType = typeof(MissingFromBodyOnPostController).ToString();
23+
24+
await action.Should().ThrowExactlyAsync<InvalidConfigurationException>().WithMessage(
25+
$"The action method '{actionMethod}' on type '{containingType}' contains no parameter with a [FromBody] attribute.");
26+
}
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using JetBrains.Annotations;
2+
using JsonApiDotNetCore.Controllers;
3+
using JsonApiDotNetCore.Resources;
4+
using JsonApiDotNetCore.Resources.Annotations;
5+
6+
namespace OpenApiTests.OpenApiGenerationFailures.MissingFromBody;
7+
8+
[UsedImplicitly(ImplicitUseTargetFlags.Members)]
9+
[Resource(ControllerNamespace = "OpenApiTests.OpenApiGenerationFailures.MissingFromBody", GenerateControllerEndpoints = JsonApiEndpoints.None)]
10+
public sealed class RecycleBin : Identifiable<long>
11+
{
12+
[Attr]
13+
public bool IsEmpty { get; set; }
14+
}

0 commit comments

Comments
 (0)