Skip to content

Commit 8e8427e

Browse files
authored
Merge pull request #1245 from json-api-dotnet/docs-tips
Documentation: FAQ and Common Pitfalls
2 parents 2324b69 + 3eb40af commit 8e8427e

25 files changed

+446
-79
lines changed

.config/dotnet-tools.json

+6
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,12 @@
2525
"commands": [
2626
"reportgenerator"
2727
]
28+
},
29+
"docfx": {
30+
"version": "2.60.2",
31+
"commands": [
32+
"docfx"
33+
]
2834
}
2935
}
3036
}

appveyor.yml

+3-7
Original file line numberDiff line numberDiff line change
@@ -45,17 +45,13 @@ for:
4545
# https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html
4646
git checkout $env:APPVEYOR_REPO_BRANCH -q
4747
}
48-
choco install docfx -y
49-
if ($lastexitcode -ne 0) {
50-
throw "docfx install failed with exit code $lastexitcode."
51-
}
5248
after_build:
5349
- pwsh: |
5450
CD ./docs
5551
& ./generate-examples.ps1
56-
& docfx docfx.json
57-
if ($lastexitcode -ne 0) {
58-
throw "docfx build failed with exit code $lastexitcode."
52+
& dotnet docfx docfx.json
53+
if ($LastExitCode -ne 0) {
54+
throw "docfx failed with exit code $LastExitCode."
5955
}
6056
6157
# https://www.appveyor.com/docs/how-to/git-push/

docs/README.md

+2-10
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,9 @@
11
# Intro
2-
Documentation for JsonApiDotNetCore is produced using [DocFX](https://dotnet.github.io/docfx/) from several files in this directory.
2+
Documentation for JsonApiDotNetCore is produced using [docfx](https://dotnet.github.io/docfx/) from several files in this directory.
33
In addition, the example request/response pairs are generated by executing `curl` commands against the GettingStarted project.
44

55
# Installation
6-
Run the following commands once to setup your system:
7-
8-
```
9-
choco install docfx -y
10-
```
11-
12-
```
13-
npm install -g httpserver
14-
```
6+
You need to have 'npm' installed. Download Node.js from https://nodejs.org/.
157

168
# Running
179
The next command regenerates the documentation website and opens it in your default browser:

docs/build-dev.ps1

+42-11
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,48 @@
1-
# This script assumes that you have already installed docfx and httpserver.
2-
# If that's not the case, run the next commands:
3-
# choco install docfx -y
4-
# npm install -g httpserver
1+
#Requires -Version 7.0
52

6-
Remove-Item _site -Recurse -ErrorAction Ignore
3+
# This script builds the documentation website, starts a web server and opens the site in your browser. Intended for local development.
74

8-
dotnet build .. --configuration Release
9-
Invoke-Expression ./generate-examples.ps1
5+
param(
6+
# Specify -NoBuild to skip code build and examples generation. This runs faster, so handy when only editing Markdown files.
7+
[switch] $NoBuild=$False
8+
)
109

11-
docfx ./docfx.json
12-
Copy-Item home/*.html _site/
13-
Copy-Item home/*.ico _site/
14-
Copy-Item -Recurse home/assets/* _site/styles/
10+
function VerifySuccessExitCode {
11+
if ($LastExitCode -ne 0) {
12+
throw "Command failed with exit code $LastExitCode."
13+
}
14+
}
15+
16+
function EnsureHttpServerIsInstalled {
17+
if ((Get-Command "npm" -ErrorAction SilentlyContinue) -eq $null) {
18+
throw "Unable to find npm in your PATH. please install Node.js first."
19+
}
20+
21+
npm list --depth 1 --global httpserver >$null
22+
23+
if ($LastExitCode -eq 1) {
24+
npm install -g httpserver
25+
}
26+
}
27+
28+
EnsureHttpServerIsInstalled
29+
VerifySuccessExitCode
30+
31+
if (-Not $NoBuild -Or -Not (Test-Path -Path _site)) {
32+
Remove-Item _site -Recurse -ErrorAction Ignore
33+
34+
dotnet build .. --configuration Release
35+
VerifySuccessExitCode
36+
37+
Invoke-Expression ./generate-examples.ps1
38+
}
39+
40+
dotnet docfx ./docfx.json
41+
VerifySuccessExitCode
42+
43+
Copy-Item -Force home/*.html _site/
44+
Copy-Item -Force home/*.ico _site/
45+
Copy-Item -Force -Recurse home/assets/* _site/styles/
1546

1647
cd _site
1748
$webServerJob = httpserver &

docs/generate-examples.ps1

+19-12
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ function Get-WebServer-ProcessId {
88
$processId = $(lsof -ti:14141)
99
}
1010
elseif ($IsWindows) {
11-
$processId = $(Get-NetTCPConnection -LocalPort 14141 -ErrorAction SilentlyContinue).OwningProcess
11+
$processId = $(Get-NetTCPConnection -LocalPort 14141 -ErrorAction SilentlyContinue).OwningProcess?[0]
1212
}
1313
else {
1414
throw [System.Exception] "Unsupported operating system."
@@ -22,7 +22,11 @@ function Kill-WebServer {
2222

2323
if ($processId -ne $null) {
2424
Write-Output "Stopping web server"
25-
Get-Process -Id $processId | Stop-Process
25+
Get-Process -Id $processId | Stop-Process -ErrorVariable stopErrorMessage
26+
27+
if ($stopErrorMessage) {
28+
throw "Failed to stop web server: $stopErrorMessage"
29+
}
2630
}
2731
}
2832

@@ -40,18 +44,21 @@ function Start-WebServer {
4044
Kill-WebServer
4145
Start-WebServer
4246

43-
Remove-Item -Force -Path .\request-examples\*.json
47+
try {
48+
Remove-Item -Force -Path .\request-examples\*.json
4449

45-
$scriptFiles = Get-ChildItem .\request-examples\*.ps1
46-
foreach ($scriptFile in $scriptFiles) {
47-
$jsonFileName = [System.IO.Path]::GetFileNameWithoutExtension($scriptFile.Name) + "_Response.json"
50+
$scriptFiles = Get-ChildItem .\request-examples\*.ps1
51+
foreach ($scriptFile in $scriptFiles) {
52+
$jsonFileName = [System.IO.Path]::GetFileNameWithoutExtension($scriptFile.Name) + "_Response.json"
4853

49-
Write-Output "Writing file: $jsonFileName"
50-
& $scriptFile.FullName > .\request-examples\$jsonFileName
54+
Write-Output "Writing file: $jsonFileName"
55+
& $scriptFile.FullName > .\request-examples\$jsonFileName
5156

52-
if ($LastExitCode -ne 0) {
53-
throw [System.Exception] "Example request from '$($scriptFile.Name)' failed with exit code $LastExitCode."
57+
if ($LastExitCode -ne 0) {
58+
throw [System.Exception] "Example request from '$($scriptFile.Name)' failed with exit code $LastExitCode."
59+
}
5460
}
5561
}
56-
57-
Kill-WebServer
62+
finally {
63+
Kill-WebServer
64+
}

docs/getting-started/faq.md

+169
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# Frequently Asked Questions
2+
3+
#### Where can I find documentation and examples?
4+
While the [documentation](~/usage/resources/index.md) covers basic features and a few runnable example projects are available [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples),
5+
many more advanced use cases are available as integration tests [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests), so be sure to check them out!
6+
7+
#### Why can't I use OpenAPI?
8+
Due to the mismatch between the JSON:API structure and the shape of ASP.NET controller methods, this does not work out of the box.
9+
This is high on our agenda and we're steadily making progress, but it's quite complex and far from complete.
10+
See [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1046) for the current status, which includes instructions on trying out the latest build.
11+
12+
#### What's available to implement a JSON:API client?
13+
It depends on the programming language used. There's an overwhelming list of client libraries at https://jsonapi.org/implementations/#client-libraries.
14+
15+
The JSON object model inside JsonApiDotNetCore is tweaked for server-side handling (be tolerant at inputs and strict at outputs).
16+
While you technically *could* use our `JsonSerializer` converters from a .NET client application with some hacks, we don't recommend it.
17+
You'll need to build the resource graph on the client and rely on internal implementation details that are subject to change in future versions.
18+
19+
In the long term, we'd like to solve this through OpenAPI, which enables the generation of a (statically typed) client library in various languages.
20+
21+
#### How can I debug my API project?
22+
Due to auto-generated controllers, you may find it hard to determine where to put your breakpoints.
23+
In Visual Studio, controllers are accessible below **Solution Explorer > Project > Dependencies > Analyzers > JsonApiDotNetCore.SourceGenerators**.
24+
25+
After turning on [Source Link](https://devblogs.microsoft.com/dotnet/improving-debug-time-productivity-with-source-link/#enabling-source-link) (which enables to download the JsonApiDotNetCore source code from GitHub), you can step into our source code and add breakpoints there too.
26+
27+
Here are some key places in the execution pipeline to set a breakpoint:
28+
- `JsonApiRoutingConvention.Apply`: Controllers are registered here (executes once at startup)
29+
- `JsonApiMiddleware.InvokeAsync`: Content negotiation and `IJsonApiRequest` setup
30+
- `QueryStringReader.ReadAll`: Parses the query string parameters
31+
- `JsonApiReader.ReadAsync`: Parses the request body
32+
- `OperationsProcessor.ProcessAsync`: Entry point for handling atomic operations
33+
- `JsonApiResourceService`: Called by controllers, delegating to the repository layer
34+
- `EntityFrameworkCoreRepository.ApplyQueryLayer`: Builds the `IQueryable<>` that is offered to Entity Framework Core (which turns it into SQL)
35+
- `JsonApiWriter.WriteAsync`: Renders the response body
36+
- `ExceptionHandler.HandleException`: Interception point for thrown exceptions
37+
38+
Aside from debugging, you can get more info by:
39+
- Including exception stack traces and incoming request bodies in error responses, as well as writing human-readable JSON:
40+
41+
```c#
42+
// Program.cs
43+
builder.Services.AddJsonApi<AppDbContext>(options =>
44+
{
45+
options.IncludeExceptionStackTraceInErrors = true;
46+
options.IncludeRequestBodyInErrors = true;
47+
options.SerializerOptions.WriteIndented = true;
48+
});
49+
```
50+
- Turning on verbose logging and logging of executed SQL statements, by adding the following to your `appsettings.Development.json`:
51+
52+
```json
53+
{
54+
"Logging": {
55+
"LogLevel": {
56+
"Default": "Warning",
57+
"Microsoft.EntityFrameworkCore.Database.Command": "Information",
58+
"JsonApiDotNetCore": "Verbose"
59+
}
60+
}
61+
}
62+
```
63+
64+
#### What if my JSON:API resources do not exactly match the shape of my database tables?
65+
We often find users trying to write custom code to solve that. They usually get it wrong or incomplete, and it may not perform well.
66+
Or it simply fails because it cannot be translated to SQL.
67+
The good news is that there's an easier solution most of the time: configure Entity Framework Core mappings to do the work.
68+
69+
For example, if your primary key column is named "CustomerId" instead of "Id":
70+
```c#
71+
builder.Entity<Customer>().Property(x => x.Id).HasColumnName("CustomerId");
72+
```
73+
74+
It certainly pays off to read up on these capabilities at [Creating and Configuring a Model](https://learn.microsoft.com/en-us/ef/core/modeling/).
75+
Another great resource is [Learn Entity Framework Core](https://www.learnentityframeworkcore.com/configuration).
76+
77+
#### Can I share my resource models with .NET Framework projects?
78+
Yes, you can. Put your model classes in a separate project that only references [JsonApiDotNetCore.Annotations](https://www.nuget.org/packages/JsonApiDotNetCore.Annotations/).
79+
This package contains just the JSON:API attributes and targets NetStandard 1.0, which makes it flexible to consume.
80+
At startup, use [Auto-discovery](~/usage/resource-graph.md#auto-discovery) and point it to your shared project.
81+
82+
#### What's the best place to put my custom business/validation logic?
83+
For basic input validation, use the attributes from [ASP.NET ModelState Validation](https://learn.microsoft.com/en-us/aspnet/core/mvc/models/validation?source=recommendations&view=aspnetcore-7.0#built-in-attributes) to get the best experience.
84+
JsonApiDotNetCore is aware of them and adjusts behavior accordingly. And it produces the best possible error responses.
85+
86+
For non-trivial business rules that require custom code, the place to be is [Resource Definitions](~/usage/extensibility/resource-definitions.md).
87+
They provide a callback-based model where you can respond to everything going on.
88+
The great thing is that your callbacks are invoked for various endpoints.
89+
For example, the filter callback on Author executes at `GET /authors?filter=`, `GET /books/1/authors?filter=` and `GET /books?include=authors?filter[authors]=`.
90+
Likewise, the callbacks for changing relationships execute for POST/PATCH resource endpoints, as well as POST/PATCH/DELETE relationship endpoints.
91+
92+
#### Can API users send multiple changes in a single request?
93+
Yes, just activate [atomic operations](~/usage/writing/bulk-batch-operations.md).
94+
It enables sending multiple changes in a batch request, which are executed in a database transaction.
95+
If something fails, all changes are rolled back. The error response indicates which operation failed.
96+
97+
#### Is there any way to add `[Authorize(Roles = "...")]` to the generated controllers?
98+
Sure, this is possible. Simply add the attribute at the class level.
99+
See the docs on [Augmenting controllers](~/usage/extensibility/controllers.md#augmenting-controllers).
100+
101+
#### How do I expose non-JSON:API endpoints?
102+
You can add your own controllers that do not derive from `(Base)JsonApiController` or `(Base)JsonApiOperationsController`.
103+
Whatever you do in those is completely ignored by JsonApiDotNetCore.
104+
This is useful if you want to add a few RPC-style endpoints or provide binary file uploads/downloads.
105+
106+
A middle-ground approach is to add custom action methods to existing JSON:API controllers.
107+
While you can route them as you like, they must return JSON:API resources.
108+
And on error, a JSON:API error response is produced.
109+
This is useful if you want to stay in the JSON:API-compliant world, but need to expose something non-standard, for example: `GET /users/me`.
110+
111+
#### How do I optimize for high scalability and prevent denial of service?
112+
Fortunately, JsonApiDotNetCore [scales pretty well](https://github.com/json-api-dotnet/PerformanceReports) under high load and/or large database tables.
113+
It never executes filtering, sorting, or pagination in-memory and tries pretty hard to produce the most efficient query possible.
114+
There are a few things to keep in mind, though:
115+
- Prevent users from executing slow queries by locking down [attribute capabilities](~/usage/resources/attributes.md#capabilities) and [relationship capabilities](~/usage/resources/relationships.md#capabilities).
116+
Ensure the right database indexes are in place for what you enable.
117+
- Prevent users from fetching lots of data by tweaking [maximum page size/number](~/usage/options.md#pagination) and [maximum include depth](~/usage/options.md#maximum-include-depth).
118+
- Avoid long-running transactions by tweaking `MaximumOperationsPerRequest` in options.
119+
- Tell your users to utilize [E-Tags](~/usage/caching.md) to reduce network traffic.
120+
- Not included in JsonApiDotNetCore: Apply general practices such as rate limiting, load balancing, authentication/authorization, blocking very large URLs/request bodies, etc.
121+
122+
#### Can I offload requests to a background process?
123+
Yes, that's possible. Override controller methods to return `HTTP 202 Accepted`, with a `Location` HTTP header where users can retrieve the result.
124+
Your controller method needs to store the request state (URL, query string, and request body) in a queue, which your background process can read from.
125+
From within your background process job handler, reconstruct the request state, execute the appropriate `JsonApiResourceService` method and store the result.
126+
There's a basic example available at https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1144, which processes a captured query string.
127+
128+
### What if I want to use something other than Entity Framework Core?
129+
This basically means you'll need to implement data access yourself. There are two approaches for interception: at the resource service level and at the repository level.
130+
Either way, you can use the built-in query string and request body parsing, as well as routing, error handling, and rendering of responses.
131+
132+
Here are some injectable request-scoped types to be aware of:
133+
- `IJsonApiRequest`: This contains routing information, such as whether a primary, secondary, or relationship endpoint is being accessed.
134+
- `ITargetedFields`: Lists the attributes and relationships from an incoming POST/PATCH resource request. Any fields missing there should not be stored (partial updates).
135+
- `IEnumerable<IQueryConstraintProvider>`: Provides access to the parsed query string parameters.
136+
- `IEvaluatedIncludeCache`: This tells the response serializer which related resources to render, which you need to populate.
137+
- `ISparseFieldSetCache`: This tells the response serializer which fields to render in the attributes and relationship objects. You need to populate this as well.
138+
139+
You may also want to inject the singletons `IJsonApiOptions` (which contains settings such as default page size) and `IResourceGraph` (the JSON:API model of resources and relationships).
140+
141+
So, back to the topic of where to intercept. It helps to familiarize yourself with the [execution pipeline](~/internals/queries.md).
142+
Replacing at the service level is the simplest. But it means you'll need to read the parsed query string parameters and invoke
143+
all resource definition callbacks yourself. And you won't get change detection (HTTP 203 Not Modified).
144+
Take a look at [JsonApiResourceService](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Services/JsonApiResourceService.cs) to see what you're missing out on.
145+
146+
You'll get a lot more out of the box if replacing at the repository level instead. You don't need to apply options, analyze query strings or populate caches for the serializer.
147+
And most resource definition callbacks are handled.
148+
That's because the built-in resource service translates all JSON:API aspects of the request into a database-agnostic data structure called `QueryLayer`.
149+
Now the hard part for you becomes reading that data structure and producing data access calls from that.
150+
If your data store provides a LINQ provider, you may reuse most of [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs),
151+
which drives the translation into [System.Linq.Expressions](https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/concepts/expression-trees/).
152+
Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll likely need to prevent that from happening.
153+
We use this for accessing [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/674889e037334e3f376550178ce12d0842d7560c/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoQueryableBuilder.cs).
154+
155+
> [!TIP]
156+
> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) is very helpful in trying to debug LINQ expression trees!
157+
158+
#### I love JsonApiDotNetCore! How can I support the team?
159+
The best way to express your gratitude is by starring our repository.
160+
This increases our leverage when asking for bug fixes in dependent projects, such as the .NET runtime and Entity Framework Core.
161+
Of course, a simple thank-you message in our [Gitter channel](https://gitter.im/json-api-dotnet-core/Lobby) is appreciated too!
162+
We don't take monetary contributions at the moment.
163+
164+
If you'd like to do more: try things out, ask questions, create GitHub bug reports or feature requests, or upvote existing issues that are important to you.
165+
We welcome PRs, but keep in mind: The worst thing in the world is opening a PR that gets rejected after you've put a lot of effort into it.
166+
So for any non-trivial changes, please open an issue first to discuss your approach and ensure it fits the product vision.
167+
168+
#### Is there anything else I should be aware of?
169+
See [Common Pitfalls](~/usage/common-pitfalls.md).

docs/getting-started/toc.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
# [Installation](install.md)
2+
3+
# [Step By Step](step-by-step.md)
4+
5+
# [FAQ](faq.md)

docs/getting-started/toc.yml

-5
This file was deleted.

docs/request-examples/index.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ These requests have been generated against the "GettingStarted" application and
44

55
All of these requests have been created using out-of-the-box features.
66

7-
_Note that cURL requires "[" and "]" in URLs to be escaped._
7+
> [!NOTE]
8+
> curl requires "[" and "]" in URLs to be escaped.
89
910
# Reading data
1011

docs/usage/caching.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,8 @@ ETag: "356075D903B8FE8D9921201A7E7CD3F9"
5959
"data": [ ... ]
6060
}
6161
```
62-
63-
**Note:** To just poll for changes (without fetching them), send a HEAD request instead:
62+
> [!TIP]
63+
> To just poll for changes (without fetching them), send a HEAD request instead.
6464
6565
```http
6666
HEAD /articles?sort=-lastModifiedAt HTTP/1.1

0 commit comments

Comments
 (0)