Skip to content

Commit

Permalink
Auto-generated JSON:API controllers using source generators (#1117)
Browse files Browse the repository at this point in the history
* Removed Interface target from custom attributes, because it does not work (see https://stackoverflow.com/questions/540749/can-a-c-sharp-class-inherit-attributes-from-its-interface). Fixed detection of attribute usage on base classes.

* Auto-generation of JSON:API controllers (using source generators)

* Updated integration tests to use auto-generated controllers

* Fixed: throw at startup when multiple controllers are registered for the same resource type

* Addressed cleanupcode/inspectcode issues

* Add dependency from JsonApiDotNetCore to SourceGenerators, so it gets pulled in via NuGet

* Added unit tests for controller source generator

* Update ROADMAP.md

* Updated documentation

* Produce NuGet package in cibuild
This lets each project opt-in for producing a NuGet package, instead of listing them globally

* Addressed review feedback
  • Loading branch information
Bart Koelman authored Dec 3, 2021
1 parent e65b65a commit 9c9a4f2
Show file tree
Hide file tree
Showing 250 changed files with 2,931 additions and 1,385 deletions.
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

0 comments on commit 9c9a4f2

Please sign in to comment.