Skip to content

Auto-generated JSON:API controllers using source generators #1117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Dec 3, 2021
7 changes: 4 additions & 3 deletions Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ function CheckLastExitCode {

function RunInspectCode {
$outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml')
dotnet jb inspectcode JsonApiDotNetCore.sln --no-build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal
# passing --build instead of --no-build as workaround for https://youtrack.jetbrains.com/issue/RSRP-487054
dotnet jb inspectcode JsonApiDotNetCore.sln --build --output="$outputPath" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal
CheckLastExitCode

[xml]$xml = Get-Content "$outputPath"
Expand Down Expand Up @@ -84,10 +85,10 @@ function CreateNuGetPackage {
}

if ([string]::IsNullOrWhitespace($versionSuffix)) {
dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts
dotnet pack --no-restore --no-build --configuration Release --output .\artifacts
}
else {
dotnet pack .\src\JsonApiDotNetCore -c Release -o .\artifacts --version-suffix=$versionSuffix
dotnet pack --no-restore --no-build --configuration Release --output .\artifacts --version-suffix=$versionSuffix
}

CheckLastExitCode
Expand Down
5 changes: 5 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,14 @@
<AspNetCoreVersion>5.0.*</AspNetCoreVersion>
<EFCoreVersion>5.0.*</EFCoreVersion>
<NpgsqlPostgreSQLVersion>5.0.*</NpgsqlPostgreSQLVersion>
<MicrosoftCodeAnalysisVersion>3.*</MicrosoftCodeAnalysisVersion>
<HumanizerVersion>2.11.10</HumanizerVersion>
<JsonApiDotNetCoreVersionPrefix>5.0.0</JsonApiDotNetCoreVersionPrefix>
<CodeAnalysisRuleSet>$(MSBuildThisFileDirectory)CodingGuidelines.ruleset</CodeAnalysisRuleSet>
<WarningLevel>9999</WarningLevel>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<WarnOnPackingNonPackableProject>false</WarnOnPackingNonPackableProject>
</PropertyGroup>

<ItemGroup>
Expand Down
69 changes: 57 additions & 12 deletions JsonApiDotNetCore.sln
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiDbContextTests", "test
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestBuildingBlocks", "test\TestBuildingBlocks\TestBuildingBlocks.csproj", "{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JsonApiDotNetCore.SourceGenerators", "src\JsonApiDotNetCore.SourceGenerators\JsonApiDotNetCore.SourceGenerators.csproj", "{952C0FDE-AFC8-455C-986F-6CC882ED8953}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SourceGeneratorDebugger", "test\SourceGeneratorDebugger\SourceGeneratorDebugger.csproj", "{87D066F9-3540-4AC7-A748-134900969EE5}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SourceGeneratorTests", "test\SourceGeneratorTests\SourceGeneratorTests.csproj", "{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -54,6 +60,18 @@ Global
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.ActiveCfg = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.Build.0 = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.ActiveCfg = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.Build.0 = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.Build.0 = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.ActiveCfg = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.Build.0 = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.ActiveCfg = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.Build.0 = Release|Any CPU
{CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CAF331F8-9255-4D72-A1A8-A54141E99F1E}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -162,18 +180,6 @@ Global
{21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x64.Build.0 = Release|Any CPU
{21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x86.ActiveCfg = Release|Any CPU
{21D27239-138D-4604-8E49-DCBE41BCE4C8}.Release|x86.Build.0 = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.ActiveCfg = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x64.Build.0 = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.ActiveCfg = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Debug|x86.Build.0 = Debug|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|Any CPU.Build.0 = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.ActiveCfg = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x64.Build.0 = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.ActiveCfg = Release|Any CPU
{067FFD7A-C66B-473D-8471-37F5C95DF61C}.Release|x86.Build.0 = Release|Any CPU
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26}.Debug|x64.ActiveCfg = Debug|Any CPU
Expand Down Expand Up @@ -210,6 +216,42 @@ Global
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x64.Build.0 = Release|Any CPU
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x86.ActiveCfg = Release|Any CPU
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21}.Release|x86.Build.0 = Release|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Debug|Any CPU.Build.0 = Debug|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Debug|x64.ActiveCfg = Debug|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Debug|x64.Build.0 = Debug|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Debug|x86.ActiveCfg = Debug|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Debug|x86.Build.0 = Debug|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Release|Any CPU.ActiveCfg = Release|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Release|Any CPU.Build.0 = Release|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Release|x64.ActiveCfg = Release|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Release|x64.Build.0 = Release|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Release|x86.ActiveCfg = Release|Any CPU
{952C0FDE-AFC8-455C-986F-6CC882ED8953}.Release|x86.Build.0 = Release|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Debug|x64.ActiveCfg = Debug|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Debug|x64.Build.0 = Debug|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Debug|x86.ActiveCfg = Debug|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Debug|x86.Build.0 = Debug|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Release|Any CPU.Build.0 = Release|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Release|x64.ActiveCfg = Release|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Release|x64.Build.0 = Release|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Release|x86.ActiveCfg = Release|Any CPU
{87D066F9-3540-4AC7-A748-134900969EE5}.Release|x86.Build.0 = Release|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Debug|x64.ActiveCfg = Debug|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Debug|x64.Build.0 = Debug|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Debug|x86.ActiveCfg = Debug|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Debug|x86.Build.0 = Debug|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Release|Any CPU.Build.0 = Release|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Release|x64.ActiveCfg = Release|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Release|x64.Build.0 = Release|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Release|x86.ActiveCfg = Release|Any CPU
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9}.Release|x86.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand All @@ -228,6 +270,9 @@ Global
{6CAFDDBE-00AB-4784-801B-AB419C3C3A26} = {026FBC6C-AF76-4568-9B87-EC73457899FD}
{EC3202C6-1D4C-4B14-A599-B9D3F27FE3BA} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{210FD61E-FF5D-4CEE-8E0D-C739ECCCBA21} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{952C0FDE-AFC8-455C-986F-6CC882ED8953} = {7A2B7ADD-ECB5-4D00-AA6A-D45BD11C97CF}
{87D066F9-3540-4AC7-A748-134900969EE5} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
{0E0B5C51-F7E2-4F40-A4E4-DED0E9731DC9} = {24B15015-62E5-42E1-9BA0-ECE6BE7AA15F}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A2421882-8F0A-4905-928F-B550B192F9A4}
Expand Down
14 changes: 1 addition & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,14 @@ See [our documentation](https://www.jsonapi.net/) for detailed usage.
```c#
#nullable enable

[Resource]
public class Article : Identifiable<int>
{
[Attr]
public string Name { get; set; } = null!;
}
```

### Controllers

```c#
public class ArticlesController : JsonApiController<Article, int>
{
public ArticlesController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IResourceService<Article, int> resourceService)
: base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
```

### Middleware

```c#
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,11 @@ The need for breaking changes has blocked several efforts in the v4.x release, s
- [x] Nullable reference types [#1029](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1029)
- [x] Improved paging links [#1010](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1010)
- [x] Configuration validation [#170](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/170)
- [x] Auto-generated controllers [#732](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/732) [#365](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/365)
- [ ] Support .NET 6 with EF Core 6 [#1109](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1109)

Aside from the list above, we have interest in the following topics. It's too soon yet to decide whether they'll make it into v5.x or in a later major version.

- Auto-generated controllers [#732](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/732) [#365](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/365)
- Optimistic concurrency [#1004](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1004)
- Extract annotations into separate package [#730](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/730)
- OpenAPI (Swagger) [#1046](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1046)
Expand Down
18 changes: 1 addition & 17 deletions docs/getting-started/step-by-step.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ The shortest path to a running API looks like:
- Install
- Define models
- Define the DbContext
- Define controllers
- Add Middleware and Services
- Seed the database
- Start the app
Expand Down Expand Up @@ -40,6 +39,7 @@ The easiest way to do this is to inherit from `Identifiable<TId>`.
```c#
#nullable enable

[Resource]
public class Person : Identifiable<int>
{
[Attr]
Expand All @@ -63,22 +63,6 @@ public class AppDbContext : DbContext
}
```

### Define Controllers

You need to create controllers that inherit from `JsonApiController<TResource, TId>`
where `TResource` is the model that inherits from `Identifiable<TId>`.

```c#
public class PeopleController : JsonApiController<Person, int>
{
public PeopleController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IResourceService<Person, int> resourceService)
: base(options, resourceGraph, loggerFactory, resourceService)
{
}
}
```

### Middleware and Services

Finally, add the services by adding the following to your Startup.ConfigureServices:
Expand Down
119 changes: 95 additions & 24 deletions docs/usage/extensibility/controllers.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,99 @@
# Controllers

You need to create controllers that inherit from `JsonApiController<TResource, TId>`
To expose API endpoints, ASP.NET controllers need to be defined.

_since v5_

Controllers are auto-generated (using [source generators](https://docs.microsoft.com/en-us/dotnet/csharp/roslyn-sdk/source-generators-overview)) when you add `[Resource]` on your model class:

```c#
[Resource] // Generates ArticlesController.g.cs
public class Article : Identifiable<Guid>
{
// ...
}
```

## Resource Access Control

It is often desirable to limit which endpoints are exposed on your controller.
A subset can be specified too:

```c#
[Resource(GenerateControllerEndpoints =
JsonApiEndpoints.GetCollection | JsonApiEndpoints.GetSingle)]
public class Article : Identifiable<Guid>
{
// ...
}
```

Instead of passing a set of endpoints, you can use `JsonApiEndpoints.Query` to generate all read-only endpoints or `JsonApiEndpoints.Command` for all write-only endpoints.

When an endpoint is blocked, an HTTP 403 Forbidden response is returned.

```http
DELETE http://localhost:14140/articles/1 HTTP/1.1
```

```json
{
"links": {
"self": "/articles"
},
"errors": [
{
"id": "dde7f219-2274-4473-97ef-baac3e7c1487",
"status": "403",
"title": "The requested endpoint is not accessible.",
"detail": "Endpoint '/articles/1' is not accessible for DELETE requests."
}
]
}
```

## Augmenting controllers

Auto-generated controllers can easily be augmented because they are partial classes. For example:

```c#
[DisableRoutingConvention]
[Route("some/custom/route")]
[DisableQueryString(JsonApiQueryStringParameters.Include)]
partial class ArticlesController
{
[HttpPost]
public IActionResult Upload()
{
// ...
}
}
```

If you need to inject extra dependencies, tell the IoC container with `[ActivatorUtilitiesConstructor]` to prefer your constructor:

```c#
partial class ArticlesController
{
private IAuthenticationService _authService;

[ActivatorUtilitiesConstructor]
public ArticlesController(IAuthenticationService authService, IJsonApiOptions options,
IResourceGraph resourceGraph, ILoggerFactory loggerFactory,
IResourceService<Article, Guid> resourceService)
: base(options, resourceGraph, loggerFactory, resourceService)
{
_authService = authService;
}
}
```

In case you don't want to use auto-generated controllers and define them yourself (see below), remove
`[Resource]` from your models or use `[Resource(GenerateControllerEndpoints = JsonApiEndpoints.None)]`.

## Earlier versions

In earlier versions of JsonApiDotNetCore, you needed to create controllers that inherit from `JsonApiController<TResource, TId>`. For example:

```c#
public class ArticlesController : JsonApiController<Article, Guid>
Expand All @@ -15,7 +108,7 @@ public class ArticlesController : JsonApiController<Article, Guid>

If you want to setup routes yourself, you can instead inherit from `BaseJsonApiController<TResource, TId>` and override its methods with your own `[HttpGet]`, `[HttpHead]`, `[HttpPost]`, `[HttpPatch]` and `[HttpDelete]` attributes added on them. Don't forget to add `[FromBody]` on parameters where needed.

## Resource Access Control
### Resource Access Control

It is often desirable to limit which routes are exposed on your controller.

Expand All @@ -37,25 +130,3 @@ public class ReportsController : JsonApiController<Report, int>
```

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).

When a route is blocked, an HTTP 403 Forbidden response is returned.

```http
DELETE http://localhost:14140/people/1 HTTP/1.1
```

```json
{
"links": {
"self": "/api/v1/people"
},
"errors": [
{
"id": "dde7f219-2274-4473-97ef-baac3e7c1487",
"status": "403",
"title": "The requested endpoint is not accessible.",
"detail": "Endpoint '/people/1' is not accessible for DELETE requests."
}
]
}
```
18 changes: 15 additions & 3 deletions docs/usage/extensibility/services.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
# Resource Services

The `IResourceService` acts as a service layer between the controller and the data access layer.
This allows you to customize it however you want. This is also a good place to implement custom business logic.
This allows you to customize it however you want. While this is still a potential place to implement custom business logic,
since v4, [Resource Definitions](~/usage/extensibility/resource-definitions.md) are more suitable for that.

## Supplementing Default Behavior

Expand Down Expand Up @@ -77,7 +78,7 @@ public class ProductService : IResourceService<Product, int>

## Limited Requirements

In some cases it may be necessary to only expose a few methods on a resource. For this reason, we have created a hierarchy of service interfaces that can be used to get the exact implementation you require.
In some cases it may be necessary to only expose a few actions on a resource. For this reason, we have created a hierarchy of service interfaces that can be used to get the exact implementation you require.

This interface hierarchy is defined by this tree structure.

Expand Down Expand Up @@ -152,7 +153,18 @@ public class Startup
}
```

Then in the controller, you should inherit from the JSON:API controller and pass the services into the named, optional base parameters:
Then on your model, pass in the set of endpoints to expose (the ones that you've registered services for):

```c#
[Resource(GenerateControllerEndpoints =
JsonApiEndpoints.Create | JsonApiEndpoints.Delete)]
public class Article : Identifiable<int>
{
// ...
}
```

Alternatively, when using a hand-written controller, you should inherit from the JSON:API controller and pass the services into the named, optional base parameters:

```c#
public class ArticlesController : JsonApiController<Article, int>
Expand Down
Loading