Skip to content

Commit fcc2c9b

Browse files
authored
Add OpenAPI support for custom JSON:API action methods (#1787)
* Simplify ordering calls * Add switch to build-dev.ps1 to skip opening docs in new browser tab * Remove redundant type parameter * Add OpenAPI support for custom JSON:API action methods Moves success/error status codes into metadata to support custom JSON:API action methods. Refactored internal type hierarchy: [Before] OpenApiActionMethod CustomControllerActionMethod BuiltinJsonApiActionMethod { ControllerType } AtomicOperationsActionMethod JsonApiActionMethod { Endpoint } [After] JsonApiActionMethod { ControllerType } OperationsActionMethod ResourceActionMethod BuiltinResourceActionMethod { Endpoint } CustomResourceActionMethod { Descriptor }
1 parent 5f85f93 commit fcc2c9b

File tree

62 files changed

+3766
-393
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

62 files changed

+3766
-393
lines changed

docs/build-dev.ps1

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@
44

55
param(
66
# Specify -NoBuild to skip code build and examples generation. This runs faster, so handy when only editing Markdown files.
7-
[switch] $NoBuild=$False
7+
[switch] $NoBuild=$False,
8+
# Specify -NoOpen to skip opening the documentation website in a web browser.
9+
[switch] $NoOpen=$False
810
)
911

1012
function VerifySuccessExitCode {
@@ -53,9 +55,12 @@ Copy-Item -Force -Recurse home/assets/* _site/styles/
5355

5456
cd _site
5557
$webServerJob = httpserver &
56-
Start-Process "http://localhost:8080/"
5758
cd ..
5859

60+
if (-Not $NoOpen) {
61+
Start-Process "http://localhost:8080/"
62+
}
63+
5964
Write-Host ""
6065
Write-Host "Web server started. Press Enter to close."
6166
$key = [Console]::ReadKey()

docs/usage/extensibility/controllers.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,56 @@ public class ReportsController : JsonApiController<Report, int>
137137
```
138138

139139
For more information about resource service injection, see [Replacing injected services](~/usage/extensibility/layer-overview.md#replacing-injected-services) and [Resource Services](~/usage/extensibility/services.md).
140+
141+
## Custom action methods
142+
143+
Aside from adding custom ASP.NET controllers and Minimal API endpoints to your project that are unrelated to JSON:API,
144+
you can also augment JsonApiDotNetCore controllers with custom action methods.
145+
This applies to both auto-generated and explicit controllers.
146+
147+
When doing so, they participate in the JsonApiDotNetCore pipeline, which means that JSON:API query string parameters are available,
148+
exceptions are handled, and the request/response bodies match the JSON:API structure. As a result, the following restrictions apply:
149+
150+
- The input/output resource types used must exist in the resource graph.
151+
- For primary endpoints, the input/output resource types must match the controller resource type.
152+
- An action method can only return a resource, a collection of resources, an error, or null.
153+
154+
For example, the following custom POST endpoint doesn't take a request body and returns a collection of resources:
155+
156+
```c#
157+
partial class TagsController
158+
{
159+
// POST /tags/defaults
160+
[HttpPost("defaults")]
161+
public async Task<IActionResult> CreateDefaultTagsAsync()
162+
{
163+
List<string> defaultTagNames =
164+
[
165+
"Create design",
166+
"Implement feature",
167+
"Write tests",
168+
"Update documentation",
169+
"Deploy changes"
170+
];
171+
172+
bool hasDefaultTags = await _appDbContext.Tags.AnyAsync(tag => defaultTagNames.Contains(tag.Name));
173+
if (hasDefaultTags)
174+
{
175+
throw new JsonApiException(new ErrorObject(HttpStatusCode.Conflict)
176+
{
177+
Title = "Default tags already exist."
178+
});
179+
}
180+
181+
List<Tag> defaultTags = defaultTagNames.Select(name => new Tag
182+
{
183+
Name = name
184+
}).ToList();
185+
186+
_appDbContext.Tags.AddRange(defaultTags);
187+
await _appDbContext.SaveChangesAsync();
188+
189+
return Ok(defaultTags);
190+
}
191+
}
192+
```

docs/usage/openapi.md

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,13 @@ provides OpenAPI support for JSON:API by integrating with [Swashbuckle](https://
3737
3838
By default, the OpenAPI document will be available at `http://localhost:<port>/swagger/v1/swagger.json`.
3939
40+
> [!TIP]
41+
> In addition to the documentation here, various examples can be found in the [OpenApiTests project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/OpenApiTests).
42+
4043
### Customizing the Route Template
4144
4245
Because Swashbuckle doesn't properly implement the ASP.NET Options pattern, you must *not* use its
43-
[documented way](https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#change-the-path-for-swagger-json-endpoints)
46+
[documented way](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/docs/configure-and-customize-swagger.md#change-the-path-for-swagger-json-endpoints)
4447
to change the route template:
4548
4649
```c#
@@ -78,6 +81,40 @@ The `NoWarn` line is optional, which suppresses build warnings for undocumented
7881
</PropertyGroup>
7982
```
8083

81-
You can combine this with the documentation that Swagger itself supports, by enabling it as described
82-
[here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments).
84+
You can combine this with the documentation that Swashbuckle itself supports, by enabling it as described
85+
[here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore/blob/master/docs/configure-and-customize-swaggergen.md#include-descriptions-from-xml-comments).
8386
This adds documentation for additional types, such as triple-slash comments on enums used in your resource models.
87+
88+
## Custom JSON:API action methods
89+
90+
To express the metadata of [custom action methods](~/usage/extensibility/controllers.md#custom-action-methods) in OpenAPI,
91+
use the following attributes on your controller action method:
92+
93+
- The `Name` property on `HttpMethodAttribute` to specify the OpenAPI operation ID, for example:
94+
```c#
95+
[HttpGet("active", Name = "get-active-users")]
96+
```
97+
98+
- `EndpointDescriptionAttribute` to specify the OpenAPI endpoint description, for example:
99+
```c#
100+
[EndpointDescription("Provides access to user accounts.")]
101+
```
102+
103+
- `ConsumesAttribute` to specify the resource type of the request body, for example:
104+
```c#
105+
[Consumes(typeof(UserAccount), "application/vnd.api+json")]
106+
```
107+
> [!NOTE]
108+
> The `contentType` parameter is required, but effectively ignored.
109+
110+
- `ProducesResponseTypeAttribute` attribute(s) to specify the response types and status codes, for example:
111+
```c#
112+
[ProducesResponseType<ICollection<UserAccount>>(StatusCodes.Status200OK)]
113+
[ProducesResponseType(StatusCodes.Status204NoContent)]
114+
[ProducesResponseType(StatusCodes.Status404NotFound)]
115+
[ProducesResponseType(StatusCodes.Status409Conflict)]
116+
```
117+
> [!NOTE]
118+
> For non-success response status codes, the type should be omitted.
119+
120+
Custom parameters on action methods can be decorated with the usual attributes, such as `[Required]`, `[Description]`, etc.

src/JsonApiDotNetCore.OpenApi.Swashbuckle/ActionDescriptorExtensions.cs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ namespace JsonApiDotNetCore.OpenApi.Swashbuckle;
77

88
internal static class ActionDescriptorExtensions
99
{
10-
public static MethodInfo GetActionMethod(this ActionDescriptor descriptor)
10+
public static MethodInfo? TryGetActionMethod(this ActionDescriptor descriptor)
1111
{
1212
ArgumentNullException.ThrowIfNull(descriptor);
1313

@@ -16,10 +16,7 @@ public static MethodInfo GetActionMethod(this ActionDescriptor descriptor)
1616
return controllerActionDescriptor.MethodInfo;
1717
}
1818

19-
MethodInfo? methodInfo = descriptor.EndpointMetadata.OfType<MethodInfo>().FirstOrDefault();
20-
ConsistencyGuard.ThrowIf(methodInfo == null);
21-
22-
return methodInfo;
19+
return descriptor.EndpointMetadata.OfType<MethodInfo>().FirstOrDefault();
2320
}
2421

2522
public static ControllerParameterDescriptor? GetBodyParameterDescriptor(this ActionDescriptor descriptor)

src/JsonApiDotNetCore.OpenApi.Swashbuckle/ConfigureSwaggerGenOptions.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,17 +146,17 @@ private static void IncludeDerivedTypes(ResourceType baseType, List<Type> clrTyp
146146

147147
private static IList<string> GetOpenApiOperationTags(ApiDescription description, IControllerResourceMapping controllerResourceMapping)
148148
{
149-
var actionMethod = OpenApiActionMethod.Create(description.ActionDescriptor);
149+
var actionMethod = JsonApiActionMethod.TryCreate(description.ActionDescriptor);
150150

151151
switch (actionMethod)
152152
{
153-
case AtomicOperationsActionMethod:
153+
case OperationsActionMethod:
154154
{
155155
return ["operations"];
156156
}
157-
case JsonApiActionMethod jsonApiActionMethod:
157+
case ResourceActionMethod resourceActionMethod:
158158
{
159-
ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(jsonApiActionMethod.ControllerType);
159+
ResourceType? resourceType = controllerResourceMapping.GetResourceTypeForController(resourceActionMethod.ControllerType);
160160
ConsistencyGuard.ThrowIf(resourceType == null);
161161

162162
return [resourceType.PublicName];

0 commit comments

Comments
 (0)