-
-
+
+
+
+
+
- The goal of this library is to simplify the development of APIs that leverage the full range of features provided by the JSON:API specification. + The goal of this library is to simplify the development of APIs that leverage the full range of features + provided by the JSON:API specification. You just need to focus on defining the resources and implementing your custom business logic.
We strive to eliminate as much boilerplate as possible by offering out-of-the-box features such as sorting, filtering and pagination.
++ The ultimate goal of this library is to eliminate as much boilerplate as possible by offering out-of-the-box features, such as sorting, filtering, pagination, sparse fieldset selection, and side-loading related resources. +
The following features are supported, from HTTP all the way down to the database
Perform compound filtering using the filter
query string parameter
Order resources on one or multiple attributes using the sort
query string parameter
Order resources on multiple attributes using the sort
query string parameter
Leverage the benefits of paginated resources with the page
query string parameter
Side-load related resources of nested relationships using the include
query string parameter
Configure permissions, such as view/create/change/sort/filter of attributes and relationships
+Configure permissions, such as viewing, creating, modifying, sorting and filtering of attributes and relationships
Validate incoming requests using built-in ASP.NET Core ModelState
validation, which works seamlessly with partial updates
Validate incoming requests using built-in ASP.NET Model Validation, which works seamlessly with partial updates
Use various extensibility points to intercept and run custom code, besides just model annotations
-#nullable enable
+#nullable enable
public class Article : Identifiable<long>
{
@@ -179,26 +220,26 @@ Resource
-
+
Request
- GET /articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields[articles]=title,summary&include=author HTTP/1.1
+ GET /articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields[articles]=title,summary&include=author HTTP/1.1
-
+
-
+
Response
-{
+{
"meta": {
"totalResources": 1
},
"links": {
- "self": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author",
- "first": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author",
- "last": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author"
+ "self": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author",
+ "first": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author",
+ "last": "/articles?filter=contains(summary,'web')&sort=-lastModifiedAt&fields%5Barticles%5D=title,summary&include=author"
},
"data": [
{
@@ -259,17 +300,17 @@ Response
Sponsors
-
-
+
+
-
+
-
-
+
+
-
+
diff --git a/docs/internals/toc.md b/docs/internals/toc.md
deleted file mode 100644
index 0533dc5272..0000000000
--- a/docs/internals/toc.md
+++ /dev/null
@@ -1 +0,0 @@
-# [Queries](queries.md)
diff --git a/docs/internals/toc.yml b/docs/internals/toc.yml
new file mode 100644
index 0000000000..adb35afc58
--- /dev/null
+++ b/docs/internals/toc.yml
@@ -0,0 +1,2 @@
+- name: Queries
+ href: queries.md
diff --git a/docs/request-examples/README.md b/docs/request-examples/README.md
index eb95ea4656..5a2911f5cb 100644
--- a/docs/request-examples/README.md
+++ b/docs/request-examples/README.md
@@ -2,18 +2,20 @@
To update these requests:
-1. Add a PowerShell (.ps1) script prefixed by a number that is used to determine the order the scripts are executed. The script should execute a request and output the response. Example:
-```
-curl -s http://localhost:14141/api/books
-```
+1. Add a PowerShell (`.ps1`) script prefixed by a number that is used to determine the order the scripts are executed.
+ The script should execute a request and output the response. For example:
+ ```
+ curl -s http://localhost:14141/api/books
+ ```
-2. Add the example to `index.md`. Example:
-```
-### Get with relationship
+2. Add the example to `index.md`. For example:
+ ```
+ ### Get with relationship
-[!code-ps[REQUEST](003_GET_Books-including-Author.ps1)]
-[!code-json[RESPONSE](003_GET_Books-including-Author_Response.json)]
-```
+ [!code-ps[REQUEST](003_GET_Books-including-Author.ps1)]
+ [!code-json[RESPONSE](003_GET_Books-including-Author_Response.json)]
+ ```
-3. Run `pwsh ../generate-examples.ps1`
-4. Verify the results by running `pwsh ../build-dev.ps1`
+3. Run `pwsh ../generate-examples.ps1` to execute the request.
+
+4. Run `pwsh ../build-dev.ps1` to view the output on the website.
diff --git a/docs/request-examples/index.md b/docs/request-examples/index.md
index c34b3d713a..89c7043450 100644
--- a/docs/request-examples/index.md
+++ b/docs/request-examples/index.md
@@ -1,13 +1,28 @@
-# Example requests
+# Example projects
+
+Runnable example projects can be found [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples):
+
+- GettingStarted: A simple project with minimal configuration to develop a runnable project in minutes.
+- JsonApiDotNetCoreExample: Showcases commonly-used features, such as resource definitions, atomic operations, and OpenAPI.
+ - OpenApiNSwagClientExample: Uses [NSwag](https://github.com/RicoSuter/NSwag) to generate a typed OpenAPI client.
+ - OpenApiKiotaClientExample: Uses [Kiota](https://learn.microsoft.com/en-us/openapi/kiota/) to generate a typed OpenAPI client.
+- MultiDbContextExample: Shows how to use multiple `DbContext` classes, for connecting to multiple databases.
+- DatabasePerTenantExample: Uses a different database per tenant. See [here](~/usage/advanced/multi-tenancy.md) for using multiple tenants in the same database.
+- NoEntityFrameworkExample: Uses a read-only in-memory repository, instead of a real database.
+- DapperExample: Uses [Dapper](https://github.com/DapperLib/Dapper) to execute SQL queries.
+- ReportsExample: Uses a resource service that returns aggregated data.
-These requests have been generated against the "GettingStarted" application and are updated on every deployment.
+> [!NOTE]
+> The example projects only cover highly-requested features. More advanced use cases can be found [here](~/usage/advanced/index.md).
+
+# Example requests
-All of these requests have been created using out-of-the-box features.
+The following requests are automatically generated against the "GettingStarted" application on every deployment.
> [!NOTE]
> curl requires "[" and "]" in URLs to be escaped.
-# Reading data
+## Reading data
### Get all
@@ -44,7 +59,7 @@ All of these requests have been created using out-of-the-box features.
[!code-ps[REQUEST](007_GET_Books-paginated.ps1)]
[!code-json[RESPONSE](007_GET_Books-paginated_Response.json)]
-# Writing data
+## Writing data
### Create resource
diff --git a/docs/template/public/main.css b/docs/template/public/main.css
new file mode 100644
index 0000000000..a20926d93f
--- /dev/null
+++ b/docs/template/public/main.css
@@ -0,0 +1,6 @@
+/* From https://github.com/dotnet/docfx/discussions/9644 */
+
+body {
+ --bs-link-color-rgb: 66, 184, 131 !important;
+ --bs-link-hover-color-rgb: 64, 180, 128 !important;
+}
diff --git a/docs/template/public/main.js b/docs/template/public/main.js
new file mode 100644
index 0000000000..be4428bed6
--- /dev/null
+++ b/docs/template/public/main.js
@@ -0,0 +1,11 @@
+// From https://github.com/dotnet/docfx/discussions/9644
+
+export default {
+ iconLinks: [
+ {
+ icon: 'github',
+ href: 'https://github.com/json-api-dotnet/JsonApiDotNetCore',
+ title: 'GitHub'
+ }
+ ]
+}
diff --git a/docs/toc.yml b/docs/toc.yml
index e9165998e5..29f786ca4a 100644
--- a/docs/toc.yml
+++ b/docs/toc.yml
@@ -1,17 +1,12 @@
- name: Getting Started
- href: getting-started/
-
+ href: getting-started/index.md
- name: Usage
href: usage/
-
- name: API
href: api/
- homepage: api/index.md
-
+ topicHref: api/index.md
- name: Examples
- href: request-examples/
- homepage: request-examples/index.md
-
+ href: request-examples/index.md
- name: Internals
href: internals/
- homepage: internals/index.md
+ topicHref: internals/index.md
diff --git a/docs/usage/advanced/alternate-routes.md b/docs/usage/advanced/alternate-routes.md
new file mode 100644
index 0000000000..a860a61fa7
--- /dev/null
+++ b/docs/usage/advanced/alternate-routes.md
@@ -0,0 +1,8 @@
+# Alternate Routes
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/CustomRoutes) shows how the default JSON:API routes can be changed.
+
+The classes `TownsController` and `CiviliansController`:
+- Are decorated with `[DisableRoutingConvention]` to turn off the default JSON:API routing convention.
+- Are decorated with the ASP.NET `[Route]` attribute to specify at which route the controller is exposed.
+- Are augmented with non-standard JSON:API action methods, whose `[HttpGet]` attributes specify a custom route.
diff --git a/docs/usage/advanced/archiving.md b/docs/usage/advanced/archiving.md
new file mode 100644
index 0000000000..3892877a52
--- /dev/null
+++ b/docs/usage/advanced/archiving.md
@@ -0,0 +1,14 @@
+# Archiving
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/Archiving) demonstrates how to implement archived resources.
+
+> [!TIP]
+> This scenario is comparable with [Soft Deletion](~/usage/advanced/soft-deletion.md).
+> The difference is that archived resources are accessible to JSON:API clients, whereas soft-deleted resources _never_ are.
+
+- Archived resources can be fetched by ID, but don't show up in searches by default.
+- Resources can only be created in a non-archived state and then archived/unarchived using a PATCH resource request.
+- The archive date is stored in the database, but cannot be modified through JSON:API.
+- To delete a resource, it must be archived first.
+
+This feature is implemented using a custom resource definition. It intercepts write operations and recursively scans incoming filters.
diff --git a/docs/usage/advanced/auth-scopes.md b/docs/usage/advanced/auth-scopes.md
new file mode 100644
index 0000000000..e37cb1b6ae
--- /dev/null
+++ b/docs/usage/advanced/auth-scopes.md
@@ -0,0 +1,10 @@
+# Authorization Scopes
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/Authorization/Scopes) shows how scope-based authorization can be used.
+
+- For simplicity, this code assumes the granted scopes are passed in a plain-text HTTP header. A more realistic use case would be to obtain the scopes from an OAuth token.
+- The HTTP header lists which resource types can be read from and/or written to.
+- An [ASP.NET Action Filter](https://learn.microsoft.com/aspnet/core/mvc/controllers/filters) validates incoming JSON:API resource/relationship requests.
+ - The incoming request path is validated against the permitted read/write permissions per resource type.
+ - The resource types used in query string parameters are validated against the permitted set of resource types.
+- A customized operations controller verifies that all incoming operations are allowed.
diff --git a/docs/usage/advanced/blobs.md b/docs/usage/advanced/blobs.md
new file mode 100644
index 0000000000..d3d4525c66
--- /dev/null
+++ b/docs/usage/advanced/blobs.md
@@ -0,0 +1,9 @@
+# BLOBs
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/Blobs) shows how Binary Large Objects (BLOBs) can be used.
+
+- The `ImageContainer` resource type contains nullable and non-nullable `byte[]` properties.
+- BLOBs are queried and persisted using Entity Framework Core.
+- The BLOB data is returned as a base-64 encoded string in the JSON response.
+
+Blobs are handled automatically; there's no need for custom code.
diff --git a/docs/usage/advanced/composite-keys.md b/docs/usage/advanced/composite-keys.md
new file mode 100644
index 0000000000..768a22a190
--- /dev/null
+++ b/docs/usage/advanced/composite-keys.md
@@ -0,0 +1,8 @@
+# Composite Keys
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/CompositeKeys) shows how database tables with composite keys can be used.
+
+- The `DbContext` configures `Car` to have a composite primary key consisting of the `RegionId` and `LicensePlate` columns.
+- The `Car.Id` property is overridden to provide a unique ID for JSON:API. It is marked with `[NotMapped]`, meaning no `Id` column exists in the database table.
+- The `Engine` and `Dealership` resource types define relationships that generate composite foreign keys in the database.
+- A custom resource repository is used to rewrite IDs from filter/sort query string parameters into `RegionId` and `LicensePlate` lookups.
diff --git a/docs/usage/advanced/content-negotiation.md b/docs/usage/advanced/content-negotiation.md
new file mode 100644
index 0000000000..980b2e0b65
--- /dev/null
+++ b/docs/usage/advanced/content-negotiation.md
@@ -0,0 +1,15 @@
+# Content Negotiation
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation) demonstrates how content negotiation in JSON:API works.
+
+Additionally, the code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/ContentNegotiation/CustomExtensions) provides
+a custom "server-time" JSON:API extension that returns the local or UTC server time in top-level `meta`.
+- This extension can be used in the `Accept` and `Content-Type` HTTP headers.
+- In a request body, the optional `useLocalTime` property in top-level `meta` indicates whether to return the local or UTC time.
+
+This feature is implemented using the following extensibility points:
+
+- At startup, the "server-time" extension is added in `JsonApiOptions`, which permits clients to use it.
+- A custom `JsonApiContentNegotiator` chooses which extensions are active for an incoming request, taking the "server-time" extension into account.
+- A custom `IDocumentAdapter` captures the incoming request body, providing access to the `useLocalTime` property in `meta`.
+- A custom `IResponseMeta` adds the server time to the response, depending on the activated extensions in `IJsonApiRequest` and the captured request body.
diff --git a/docs/usage/advanced/eager-loading.md b/docs/usage/advanced/eager-loading.md
new file mode 100644
index 0000000000..72e401c4f0
--- /dev/null
+++ b/docs/usage/advanced/eager-loading.md
@@ -0,0 +1,12 @@
+# Eager Loading Related Resources
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/EagerLoading) uses the `[EagerLoad]` attribute to facilitate calculated properties that depend on related resources.
+The related resources are fetched from the database, but not returned to the client unless explicitly requested using the `include` query string parameter.
+
+- The `Street` resource type uses `EagerLoad` on its `Buildings` to-many relationship because its `DoorTotalCount` calculated property depends on it.
+- The `Building` resource type uses `EagerLoad` on its `Windows` to-many relationship because its `WindowCount` calculated property depends on it.
+- The `Building` resource type uses `EagerLoad` on its `PrimaryDoor` to-one required relationship because its `PrimaryDoorColor` calculated property depends on it.
+ - Because this is a required relationship, special handling occurs in `Building`, `BuildingRepository`, and `BuildingDefinition`.
+- The `Building` resource type uses `EagerLoad` on its `SecondaryDoor` to-one optional relationship because its `SecondaryDoorColor` calculated property depends on it.
+
+As can be seen from the usages above, a chain of `EagerLoad` attributes can result in fetching a chain of related resources from the database.
diff --git a/docs/usage/advanced/error-handling.md b/docs/usage/advanced/error-handling.md
new file mode 100644
index 0000000000..c53b3f2669
--- /dev/null
+++ b/docs/usage/advanced/error-handling.md
@@ -0,0 +1,13 @@
+# Error Handling
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/ExceptionHandling) shows how to customize error handling.
+
+A user-defined exception, `ConsumerArticleIsNoLongerAvailableException`, is thrown from a resource service to demonstrate handling it.
+Note that this exception can be thrown from anywhere during request execution; a resource service is just used here for simplicity.
+
+To handle the user-defined exception, `AlternateExceptionHandler` inherits from `ExceptionHandler` to:
+- Customize the JSON:API error response by adding a `meta` entry when `ConsumerArticleIsNoLongerAvailableException` is thrown.
+- Indicate that `ConsumerArticleIsNoLongerAvailableException` must be logged at the Warning level.
+
+Additionally, the `ThrowingArticle.Status` property throws an `InvalidOperationException`.
+This triggers the default error handling because `AlternateExceptionHandler` delegates to its base class.
diff --git a/docs/usage/advanced/hosting-iis.md b/docs/usage/advanced/hosting-iis.md
new file mode 100644
index 0000000000..f452adaeec
--- /dev/null
+++ b/docs/usage/advanced/hosting-iis.md
@@ -0,0 +1,7 @@
+# Hosting in Internet Information Services (IIS)
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/HostingInIIS) calls [UsePathBase](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.builder.usepathbaseextensions.usepathbase) to simulate hosting in IIS.
+For details on how `UsePathBase` works, see [Understanding PathBase in ASP.NET Core](https://andrewlock.net/understanding-pathbase-in-aspnetcore/).
+
+- At startup, the line `app.UsePathBase("/iis-application-virtual-directory")` configures ASP.NET to use the base path.
+- `PaintingsController` uses a custom route to demonstrate that both features can be used together.
diff --git a/docs/usage/advanced/id-obfuscation.md b/docs/usage/advanced/id-obfuscation.md
new file mode 100644
index 0000000000..4012238c29
--- /dev/null
+++ b/docs/usage/advanced/id-obfuscation.md
@@ -0,0 +1,16 @@
+# ID Obfuscation
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/IdObfuscation) shows how to use obfuscated IDs.
+They are typically used to prevent clients from guessing primary key values.
+
+All IDs sent by clients are transparently de-obfuscated into internal numeric values before accessing the database.
+Numeric IDs returned from the database are obfuscated before they are sent to the client.
+
+> [!NOTE]
+> An alternate solution is to use GUIDs instead of numeric primary keys in the database.
+
+ID obfuscation is achieved using the following extensibility points:
+
+- For simplicity, `HexadecimalCodec` is used to obfuscate numeric IDs to a hexadecimal format. A more realistic use case would be to use a symmetric crypto algorithm.
+- `ObfuscatedIdentifiable` acts as the base class for resource types, handling the obfuscation and de-obfuscation of IDs.
+- `ObfuscatedIdentifiableController` acts as the base class for controllers. It inherits from `BaseJsonApiController`, changing the `id` parameter in action methods to type `string`.
diff --git a/docs/usage/advanced/index.md b/docs/usage/advanced/index.md
new file mode 100644
index 0000000000..6bf9841dbe
--- /dev/null
+++ b/docs/usage/advanced/index.md
@@ -0,0 +1,19 @@
+# Advanced JSON:API features
+
+This topic goes beyond the basics of what's possible with JsonApiDotNetCore.
+
+Advanced use cases are provided in the form of integration tests [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests).
+This ensures they don't break during development of the framework.
+
+Each directory typically contains:
+
+- A set of resource types.
+- A `DbContext` class to register the resource types.
+- Fakers to generate deterministic test data.
+- Test classes that assert the feature works as expected.
+ - Entities are inserted into a randomly named PostgreSQL database.
+ - An HTTP request is sent.
+ - The returned response is asserted on.
+ - If applicable, the changes are fetched from the database and asserted on.
+
+To run/debug the integration tests, follow the steps in [README.md](https://github.com/json-api-dotnet/JsonApiDotNetCore#build-from-source).
diff --git a/docs/usage/advanced/links.md b/docs/usage/advanced/links.md
new file mode 100644
index 0000000000..d26be87563
--- /dev/null
+++ b/docs/usage/advanced/links.md
@@ -0,0 +1,19 @@
+# Links
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/Links) shows various ways to configure which links are returned, and how they appear in responses.
+
+> [!TIP]
+> By default, absolute links are returned. To return relative links, set [JsonApiOptions.UseRelativeLinks](~/usage/options.md#relative-links) at startup.
+
+> [!TIP]
+> To add a global prefix to all routes, set `JsonApiOptions.Namespace` at startup.
+
+Which links to render can be configured globally in options, then overridden per resource type, and then overridden per relationship.
+
+- The `PhotoLocation` resource type turns off `TopLevelLinks` and `ResourceLinks`, and sets `RelationshipLinks` to `Related`.
+- The `PhotoLocation.Album` relationship turns off all links for this relationship.
+
+The various tests set `JsonApiOptions.Namespace` and `JsonApiOptions.UseRelativeLinks` to verify that the proper links are rendered.
+This can't be set in the tests directly for technical reasons, so they use different `Startup` classes to control this.
+
+Link rendering is fully controlled using attributes on your models. No further code is needed.
diff --git a/docs/usage/advanced/microservices.md b/docs/usage/advanced/microservices.md
new file mode 100644
index 0000000000..88e9cb08b9
--- /dev/null
+++ b/docs/usage/advanced/microservices.md
@@ -0,0 +1,22 @@
+# Microservices
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/Microservices) shows patterns commonly used in microservices architecture:
+
+- [Fire-and-forget](https://microservices.io/patterns/communication-style/messaging.html): Outgoing messages are sent to an external queue, without waiting for their processing to start. While this is the simplest solution, it is not very reliable when errors occur.
+- [Transactional Outbox Pattern](https://microservices.io/patterns/data/transactional-outbox.html): Outgoing messages are saved to a queue table within the same database transaction. A background job (omitted in this example) polls the queue table and sends the messages to an external queue.
+
+> [!TIP]
+> Potential external queue systems you could use are [RabbitMQ](https://www.rabbitmq.com/), [MassTransit](https://masstransit.io/),
+> [Azure Service Bus](https://learn.microsoft.com/en-us/azure/service-bus-messaging/service-bus-messaging-overview) and [Apache Kafka](https://kafka.apache.org/). However, this is beyond the scope of this topic.
+
+The `Messages` directory lists the functional messages that are created from incoming JSON:API requests, which are typically processed by an external system that handles messages from the queue.
+Each message has a unique ID and type, and is versioned to support gradual deployments.
+Example payloads of messages are: user created, user login name changed, user moved to group, group created, group renamed, etc.
+
+The abstract types `MessagingGroupDefinition` and `MessagingUserDefinition` are resource definitions that contain code shared by both patterns. They inspect the incoming request and produce one or more functional messages from it.
+The pattern-specific derived types inject their `DbContext`, which is used to query for additional information when determining what is being changed.
+
+> [!NOTE]
+> Because networks are inherently unreliable, systems that consume messages from an external queue should be [idempotent](https://microservices.io/patterns/communication-style/idempotent-consumer.html).
+> Several years ago, a [prototype](https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1132) was built to make JSON:API idempotent, but it was never finished due to a lack of community interest.
+> Please [open an issue](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/new?labels=enhancement) if idempotency matters to you.
diff --git a/docs/usage/advanced/model-state.md b/docs/usage/advanced/model-state.md
new file mode 100644
index 0000000000..0117cd72e3
--- /dev/null
+++ b/docs/usage/advanced/model-state.md
@@ -0,0 +1,14 @@
+# ASP.NET Model Validation
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/ModelState) shows how to use [ASP.NET Model Validation](https://learn.microsoft.com/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api) attributes.
+
+> [!TIP]
+> See [Atomic Operations](~/usage/advanced/operations.md) for how to implement a custom model validator.
+
+The resource types are decorated with Model Validation attributes, such as `[Required]`, `[RegularExpression]`, `[MinLength]`, and `[Range]`.
+
+Only the fields that appear in a request body (partial POST/PATCH) are validated.
+When validation fails, the source pointer in the response indicates which attribute(s) are invalid.
+
+Model Validation is enabled by default, but can be [turned off in options](~/usage/options.md#modelstate-validation).
+Aside from adding validation attributes to your resource properties, no further code is needed.
diff --git a/docs/usage/advanced/multi-tenancy.md b/docs/usage/advanced/multi-tenancy.md
new file mode 100644
index 0000000000..d6e5b73f62
--- /dev/null
+++ b/docs/usage/advanced/multi-tenancy.md
@@ -0,0 +1,21 @@
+# Multi-tenancy
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/MultiTenancy) shows how to handle multiple tenants in a single database.
+
+> [!TIP]
+> To use a different database per tenant, see [this](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/DatabasePerTenantExample) example instead.
+> Its `DbContext` dynamically sets the connection string per request. This requires the database structure to be identical for all tenants.
+
+The essence of implementing multi-tenancy within a single database is instructing Entity Framework Core to add implicit filters when entities are queried.
+See the usage of `HasQueryFilter` in the `DbContext` class. It injects an `ITenantProvider` to determine the active tenant for the current HTTP request.
+
+> [!NOTE]
+> For simplicity, this example uses a route parameter to indicate the active tenant.
+> Provide your own `ITenantProvider` to determine the tenant from somewhere else, such as the incoming OAuth token.
+
+The generic `MultiTenantResourceService` transparently sets the tenant ID when creating a new resource.
+Furthermore, it performs extra queries to ensure relationship changes apply to the current tenant, and to produce better error messages.
+
+While `MultiTenantResourceService` is used for both resource types, _only_ the `WebShop` resource type implements `IHasTenant`.
+The related resource type `WebProduct` does not. Because the products table has a foreign key to the (tenant-specific) shop it belongs to, it doesn't need a `TenantId` column.
+When a JSON:API request for web products executes, the `HasQueryFilter` in the `DbContext` ensures that only products belonging to the tenant-specific shop are returned.
diff --git a/docs/usage/advanced/operations.md b/docs/usage/advanced/operations.md
new file mode 100644
index 0000000000..aec2b9fe4d
--- /dev/null
+++ b/docs/usage/advanced/operations.md
@@ -0,0 +1,15 @@
+# Atomic Operations
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/AtomicOperations) covers usage of the [Atomic Operations](https://jsonapi.org/ext/atomic/) extension, which enables sending multiple changes in a single request.
+
+- Operations for creating, updating, and deleting resources and relationships are shown.
+- If one of the operations fails, the transaction is rolled back.
+- Local IDs are used to reference resources created in a preceding operation within the same request.
+- A custom controller restricts which operations are allowed, per resource type.
+- The maximum number of operations per request can be configured at startup.
+- For efficiency, operations are validated upfront (before accessing the database). If validation fails, the list of all errors is returned.
+ - Takes [ASP.NET Model Validation](https://learn.microsoft.com/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api) attributes into account.
+ - See `DateMustBeInThePastAttribute` for how to implement a custom model validator.
+- Various interactions with resource definitions are shown.
+
+The Atomic Operations extension is enabled after an operations controller is added to the project. No further code is needed.
diff --git a/docs/usage/advanced/query-string-functions.md b/docs/usage/advanced/query-string-functions.md
new file mode 100644
index 0000000000..214228d654
--- /dev/null
+++ b/docs/usage/advanced/query-string-functions.md
@@ -0,0 +1,23 @@
+# Query String Functions
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/QueryStrings/CustomFunctions) shows how to define custom functions that clients can use in JSON:API query string parameters.
+
+- IsUpperCase: Adds the `isUpperCase` function, which can be used in filters on `string` attributes.
+ - Returns whether the attribute value is uppercase.
+ - Example usage: `GET /blogs/1/posts?filter=and(isUpperCase(caption),not(isUpperCase(url)))`
+- StringLength: Adds the `length` function, which can be used in filters and sorts on `string` attributes.
+ - Returns the number of characters in the attribute value.
+ - Example filter usage: `GET /blogs?filter=greaterThan(length(title),'2')`
+ - Example sort usage: `GET /blogs/1/posts?sort=length(caption),-length(url)`
+- Sum: Adds the `sum` function, which can be used in filters.
+ - Returns the sum of the numeric attribute values in related resources.
+ - Example: `GET /blogPosts?filter=greaterThan(sum(comments,numStars),'4')`
+- TimeOffset: Adds the `timeOffset` function, which can be used in filters on `DateTime` attributes.
+ - Calculates the difference between the attribute value and the current date.
+ - A generic resource definition intercepts all filters, rewriting the usage of `timeOffset` into the equivalent filters on the target attribute.
+ - Example: `GET /reminders?filter=greaterOrEqual(remindsAt,timeOffset('+0:10:00'))`
+
+The basic pattern to implement a custom function is to:
+- Define a custom expression type, which inherits from one of the built-in expression types, such as `FilterExpression` or `FunctionExpression`.
+- Inherit from one of the built-in parsers, such as `FilterParser` or `SortParser`, to convert tokens to your custom expression type. Override the `ParseFilter` or `ParseFunction` method.
+- Inherit from one of the built-in query clause builders, such as `WhereClauseBuilder` or `OrderClauseBuilder`, to produce a LINQ expression for your custom expression type. Override the `DefaultVisit` method.
diff --git a/docs/usage/advanced/resource-injection.md b/docs/usage/advanced/resource-injection.md
new file mode 100644
index 0000000000..c4e82a40fd
--- /dev/null
+++ b/docs/usage/advanced/resource-injection.md
@@ -0,0 +1,11 @@
+# Injecting services in resource types
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/ResourceConstructorInjection) shows how to inject services into resource types.
+
+Because Entity Framework Core doesn't support injecting arbitrary services into entity types (only a few special types), a workaround is used.
+Instead of injecting the desired services directly, the `DbContext` is injected, which injects the desired services and exposes them via properties.
+
+- The `PostOffice` and `GiftCertificate` resource types both inject the `DbContext` in their constructors.
+- The `DbContext` injects `TimeProvider` and exposes it through a property.
+- `GiftCertificate` obtains the `TimeProvider` via the `DbContext` property to calculate the value for its exposed `HasExpired` property, which depends on the current time.
+- `PostOffice` obtains the `TimeProvider` via the `DbContext` property to calculate the value for its exposed `IsOpen` property, which depends on the current time.
diff --git a/docs/usage/advanced/soft-deletion.md b/docs/usage/advanced/soft-deletion.md
new file mode 100644
index 0000000000..cebc18e91c
--- /dev/null
+++ b/docs/usage/advanced/soft-deletion.md
@@ -0,0 +1,15 @@
+# Soft Deletion
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/SoftDeletion) demonstrates how to implement soft deletion of resources.
+
+> [!TIP]
+> This scenario is comparable with [Archiving](~/usage/advanced/archiving.md).
+> The difference is that soft-deleted resources are never accessible by JSON:API clients (despite still being stored in the database), whereas archived resources _are_ accessible.
+
+The essence of implementing soft deletion is instructing Entity Framework Core to add implicit filters when entities are queried.
+See the usage of `HasQueryFilter` in the `DbContext` class.
+
+The `ISoftDeletable` interface provides the `SoftDeletedAt` database column. The `Company` and `Department` resource types implement this interface to indicate they use soft deletion.
+
+The generic `SoftDeletionAwareResourceService` overrides the `DeleteAsync` method to soft-delete a resource instead of truly deleting it, if it implements `ISoftDeletable`.
+Furthermore, it performs extra queries to ensure relationship changes do not reference soft-deleted resources, and to produce better error messages.
diff --git a/docs/usage/advanced/state-machine.md b/docs/usage/advanced/state-machine.md
new file mode 100644
index 0000000000..371300995a
--- /dev/null
+++ b/docs/usage/advanced/state-machine.md
@@ -0,0 +1,11 @@
+# State Transitions in Resource Updates
+
+The code [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody) shows how to validate state transitions when updating a resource.
+
+This feature is implemented using a custom resource definition:
+
+- The `Workflow` resource type contains a `Stage` property of type `WorkflowStage`.
+- The `WorkflowStage` enumeration lists a workflow's possible states.
+- `WorkflowDefinition` contains a hard-coded stage transition table defining the valid transitions. For example, a workflow in stage `InProgress` can be changed to `OnHold` or `Canceled`, but not `Created`.
+ - The `OnPrepareWriteAsync` method is overridden to capture the stage currently stored in the database in the `_previousStage` private field.
+ - The `OnWritingAsync` method is overridden to verify whether the stage change is permitted. It consults the stage transition table to determine whether there's a path from `_previousStage` to the to-be-stored stage, producing an error if there isn't.
diff --git a/docs/usage/advanced/toc.yml b/docs/usage/advanced/toc.yml
new file mode 100644
index 0000000000..9d45cd04b3
--- /dev/null
+++ b/docs/usage/advanced/toc.yml
@@ -0,0 +1,38 @@
+- name: Authorization Scopes
+ href: auth-scopes.md
+- name: BLOBs
+ href: blobs.md
+- name: Microservices
+ href: microservices.md
+- name: Multi-tenancy
+ href: multi-tenancy.md
+- name: Atomic Operations
+ href: operations.md
+- name: Query String Functions
+ href: query-string-functions.md
+- name: Alternate Routes
+ href: alternate-routes.md
+- name: Content Negotiation
+ href: content-negotiation.md
+- name: Error Handling
+ href: error-handling.md
+- name: Hosting in IIS
+ href: hosting-iis.md
+- name: ID Obfuscation
+ href: id-obfuscation.md
+- name: Soft Deletion
+ href: soft-deletion.md
+- name: Archiving
+ href: archiving.md
+- name: ASP.NET Model Validation
+ href: model-state.md
+- name: State Transitions in Resource Updates
+ href: state-machine.md
+- name: Links
+ href: links.md
+- name: Composite Keys
+ href: composite-keys.md
+- name: Eager Loading
+ href: eager-loading.md
+- name: Injecting services in resource types
+ href: resource-injection.md
diff --git a/docs/usage/caching.md b/docs/usage/caching.md
index 537ec70e4b..4243fd8be2 100644
--- a/docs/usage/caching.md
+++ b/docs/usage/caching.md
@@ -2,10 +2,10 @@
_since v4.2_
-GET requests return an [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) HTTP header, which can be used by the client in subsequent requests to save network bandwidth.
+GET requests return an [ETag](https://developer.mozilla.org/docs/Web/HTTP/Headers/ETag) HTTP header, which can be used by the client in subsequent requests to save network bandwidth.
-Be aware that the returned ETag represents the entire response body (a 'resource' in HTTP terminology) for a request URL that includes the query string.
-This is unrelated to JSON:API resources. Therefore, we do not use ETags for optimistic concurrency.
+Be aware that the returned ETag represents the entire response body (a "resource" in HTTP terminology) for the full request URL, including the query string.
+A resource in HTTP is unrelated to a JSON:API resource. Therefore, we do not use ETags for optimistic concurrency.
Getting a list of resources returns an ETag:
@@ -26,7 +26,7 @@ ETag: "7FFF010786E2CE8FC901896E83870E00"
}
```
-The request is later resent using the received ETag. The server data has not changed at this point.
+The request is later resent using the same ETag received earlier. The server data has not changed at this point.
```http
GET /articles?sort=-lastModifiedAt HTTP/1.1
diff --git a/docs/usage/common-pitfalls.md b/docs/usage/common-pitfalls.md
index 7941face82..60162cfd37 100644
--- a/docs/usage/common-pitfalls.md
+++ b/docs/usage/common-pitfalls.md
@@ -50,7 +50,7 @@ Did you notice the missing type of the `LoginAccount.Customer` property? We must
This is only one of the issues you'll run into. Just don't go there.
The right way to model this is by having only `Customer` instead of `WebCustomer` and `AdminCustomer`. And then:
-- Hide the `CreditRating` property for web users using [this](https://www.jsonapi.net/usage/extensibility/resource-definitions.html#excluding-fields) approach.
+- Hide the `CreditRating` property for web users using [this](~/usage/extensibility/resource-definitions.md#excluding-fields) approach.
- Block web users from setting the `CreditRating` property from POST/PATCH resource endpoints by either:
- Detecting if the `CreditRating` property has changed, such as done [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/test/JsonApiDotNetCoreTests/IntegrationTests/InputValidation/RequestBody/WorkflowDefinition.cs).
- Injecting `ITargetedFields`, throwing an error when it contains the `CreditRating` property.
@@ -61,7 +61,7 @@ This paradigm [doesn't work well](https://github.com/json-api-dotnet/JsonApiDotN
So if your API needs to guard invariants such as "the sum of all orders must never exceed 500 dollars", then you're better off with an RPC-style API instead of the REST paradigm that JSON:API follows.
Adding constructors to resource classes that validate incoming parameters before assigning them to properties does not work.
-Entity Framework Core [supports](https://learn.microsoft.com/en-us/ef/core/modeling/constructors#binding-to-mapped-properties) that,
+Entity Framework Core [supports](https://learn.microsoft.com/ef/core/modeling/constructors#binding-to-mapped-properties) that,
but does so via internal implementation details that are inaccessible by JsonApiDotNetCore.
In JsonApiDotNetCore, resources are what DDD calls [anemic models](https://thedomaindrivendesign.io/anemic-model/).
@@ -84,14 +84,21 @@ With stored procedures, you're either going to have a lot of work to do, or you'
Neither sounds very compelling. If stored procedures is what you need, you're better off creating an RPC-style API that doesn't use JsonApiDotNetCore.
#### Do not use `[ApiController]` on JSON:API controllers
-Although recommended by Microsoft for hard-written controllers, the opinionated behavior of [`[ApiController]`](https://learn.microsoft.com/en-us/aspnet/core/web-api/?view=aspnetcore-7.0#apicontroller-attribute) violates the JSON:API specification.
+Although recommended by Microsoft for hard-written controllers, the opinionated behavior of [`[ApiController]`](https://learn.microsoft.com/aspnet/core/web-api/#apicontroller-attribute) violates the JSON:API specification.
Despite JsonApiDotNetCore trying its best to deal with it, the experience won't be as good as leaving it out.
-#### Replace injectable services *after* calling `AddJsonApi()`
-Registering your own services in the IoC container afterwards increases the chances that your replacements will take effect.
-Also, register with `services.AddResourceDefinition/AddResourceService/AddResourceRepository()` instead of `services.AddScoped()`.
+#### Don't use auto-generated controllers with shared models
+
+When model classes are defined in a separate project, the controllers are generated in that project as well, which is probably not what you want.
+For details, see [here](~/usage/extensibility/controllers.md#auto-generated-controllers).
+
+#### Register/override injectable services
+Register your JSON:API resource services, resource definitions and repositories with `services.AddResourceService/AddResourceDefinition/AddResourceRepository()` instead of `services.AddScoped()`.
When using [Auto-discovery](~/usage/resource-graph.md#auto-discovery), you don't need to register these at all.
+> [!NOTE]
+> In older versions of JsonApiDotNetCore, registering your own services in the IoC container *afterwards* increased the chances that your replacements would take effect.
+
#### Never use the Entity Framework Core In-Memory Database Provider
When using this provider, many invalid mappings go unnoticed, leading to strange errors or wrong behavior. A real SQL engine fails to create the schema when mappings are invalid.
If you're in need of a quick setup, use [SQLite](https://www.sqlite.org/). After adding its [NuGet package](https://www.nuget.org/packages/Microsoft.EntityFrameworkCore.Sqlite), it's as simple as:
@@ -100,7 +107,7 @@ If you're in need of a quick setup, use [SQLite](https://www.sqlite.org/). After
builder.Services.AddSqlite("Data Source=temp.db");
```
Which creates `temp.db` on disk. Simply deleting the file gives you a clean slate.
-This is a lot more convenient compared to using [SqlLocalDB](https://learn.microsoft.com/en-us/sql/database-engine/configure-windows/sql-server-express-localdb), which runs a background service that breaks if you delete its underlying storage files.
+This is a lot more convenient compared to using [SqlLocalDB](https://learn.microsoft.com/sql/database-engine/configure-windows/sql-server-express-localdb), which runs a background service that breaks if you delete its underlying storage files.
However, even SQLite does not support all queries produced by Entity Framework Core. You'll get the best (and fastest) experience with [PostgreSQL in a docker container](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/run-docker-postgres.ps1).
@@ -138,6 +145,6 @@ If you need such side effects, it's easiest to inject your `DbContext` in the co
A better way is to inject your `DbContext` in a [Resource Definition](~/usage/extensibility/resource-definitions.md) and apply the changes there.
#### Concurrency tokens (timestamp/rowversion/xmin) won't work
-While we'd love to support such [tokens for optimistic concurrency](https://learn.microsoft.com/en-us/ef/core/saving/concurrency?tabs=data-annotations),
+While we'd love to support such [tokens for optimistic concurrency](https://learn.microsoft.com/ef/core/saving/concurrency),
it turns out that the implementation is far from trivial. We've come a long way, but aren't sure how it should work when relationship endpoints and atomic operations are involved.
If you're interested, we welcome your feedback at https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1119.
diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md
index 7e54d3fb9c..254b305ed9 100644
--- a/docs/usage/extensibility/controllers.md
+++ b/docs/usage/extensibility/controllers.md
@@ -2,9 +2,11 @@
To expose API endpoints, ASP.NET controllers need to be defined.
+## Auto-generated controllers
+
_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:
+Controllers are auto-generated (using [source generators](https://learn.microsoft.com/dotnet/csharp/roslyn-sdk/#source-generators)) when you add `[Resource]` on your model class:
```c#
[Resource] // Generates ArticlesController.g.cs
@@ -14,7 +16,12 @@ public class Article : Identifiable
}
```
-## Resource Access Control
+> [!NOTE]
+> Auto-generated controllers are convenient to get started, but may not work as expected with certain customizations.
+> For example, when model classes are defined in a separate project, the controllers are generated in that project as well, which is probably not what you want.
+> In such cases, it's perfectly fine to use [explicit controllers](#explicit-controllers) instead.
+
+### Resource Access Control
It is often desirable to limit which endpoints are exposed on your controller.
A subset can be specified too:
@@ -52,7 +59,7 @@ DELETE http://localhost:14140/articles/1 HTTP/1.1
}
```
-## Augmenting controllers
+### Augmenting controllers
Auto-generated controllers can easily be augmented because they are partial classes. For example:
@@ -91,9 +98,9 @@ partial class ArticlesController
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
+## Explicit controllers
-In earlier versions of JsonApiDotNetCore, you needed to create controllers that inherit from `JsonApiController`. For example:
+To define your own controller class, inherit from `JsonApiController`. For example:
```c#
public class ArticlesController : JsonApiController
diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md
index 62528893d3..dbbe81699f 100644
--- a/docs/usage/extensibility/middleware.md
+++ b/docs/usage/extensibility/middleware.md
@@ -3,9 +3,9 @@
The default middleware validates incoming `Content-Type` and `Accept` HTTP headers.
Based on routing configuration, it fills `IJsonApiRequest`, an injectable object that contains JSON:API-related information about the request being processed.
-It is possible to replace the built-in middleware components by configuring the IoC container and by configuring `MvcOptions`.
+It is possible to replace the built-in middleware components by configuring the IoC container and by configuring `MvcOptions`.
-## Configuring the IoC container
+## Configuring the IoC container
The following example replaces the internal exception filter with a custom implementation.
diff --git a/docs/usage/extensibility/resource-definitions.md b/docs/usage/extensibility/resource-definitions.md
index cf5400b722..644d43fb75 100644
--- a/docs/usage/extensibility/resource-definitions.md
+++ b/docs/usage/extensibility/resource-definitions.md
@@ -29,7 +29,7 @@ For various reasons (see examples below) you may need to change parts of the que
`JsonApiResourceDefinition` (which is an empty implementation of `IResourceDefinition`) provides overridable methods that pass you the result of query string parameter parsing.
The value returned by you determines what will be used to execute the query.
-An intermediate format (`QueryExpression` and derived types) is used, which enables us to separate JSON:API implementation
+An intermediate format (`QueryExpression` and derived types) is used, which enables us to separate JSON:API implementation
from Entity Framework Core `IQueryable` execution.
### Excluding fields
@@ -220,7 +220,7 @@ You can define additional query string parameters with the LINQ expression that
If the key is present in a query string, the supplied LINQ expression will be added to the database query.
> [!NOTE]
-> This directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of Entity Framework Core operators.
+> This directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of Entity Framework Core operators.
But it only works on primary resource endpoints (for example: /articles, but not on /blogs/1/articles or /blogs?include=articles).
```c#
diff --git a/docs/usage/extensibility/toc.yml b/docs/usage/extensibility/toc.yml
new file mode 100644
index 0000000000..4a32581a60
--- /dev/null
+++ b/docs/usage/extensibility/toc.yml
@@ -0,0 +1,14 @@
+- name: Layer Overview
+ href: layer-overview.md
+- name: Resource Definitions
+ href: resource-definitions.md
+- name: Controllers
+ href: controllers.md
+- name: Resource Services
+ href: services.md
+- name: Resource Repositories
+ href: repositories.md
+- name: Middleware
+ href: middleware.md
+- name: Query Strings
+ href: query-strings.md
diff --git a/docs/usage/faq.md b/docs/usage/faq.md
new file mode 100644
index 0000000000..cbb32c4c00
--- /dev/null
+++ b/docs/usage/faq.md
@@ -0,0 +1,176 @@
+# Frequently Asked Questions
+
+#### Where can I find documentation and examples?
+The [documentation](~/usage/resources/index.md) covers basic features, as well as [advanced use cases](~/usage/advanced/index.md). Several runnable example projects are available [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples).
+
+#### Why don't you use the built-in OpenAPI support in ASP.NET Core?
+The structure of JSON:API request and response bodies differs significantly from the signature of JsonApiDotNetCore controllers.
+JsonApiDotNetCore provides OpenAPI support using [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore), a mature and feature-rich library that is highly extensible.
+The [OpenAPI support in ASP.NET Core](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/openapi/overview) is still very young
+and doesn't provide the level of extensibility needed for JsonApiDotNetCore.
+
+#### What's available to implement a JSON:API client?
+To generate a typed client (specific to the resource types in your project), consider using our [OpenAPI](https://www.jsonapi.net/usage/openapi.html) NuGet package.
+
+If you need a generic client, it depends on the programming language used. There's an overwhelming list of client libraries at https://jsonapi.org/implementations/#client-libraries.
+
+The JSON object model inside JsonApiDotNetCore is tweaked for server-side handling (be tolerant at inputs and strict at outputs).
+While you technically *could* use our `JsonSerializer` converters from a .NET client application with some hacks, we don't recommend doing so.
+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.
+
+#### How can I debug my API project?
+Due to auto-generated controllers, you may find it hard to determine where to put your breakpoints.
+In Visual Studio, controllers are accessible below **Solution Explorer > Project > Dependencies > Analyzers > JsonApiDotNetCore.SourceGenerators**.
+
+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.
+
+Here are some key places in the execution pipeline to set a breakpoint:
+- `JsonApiRoutingConvention.Apply`: Controllers are registered here (executes once at startup)
+- `JsonApiMiddleware.InvokeAsync`: Content negotiation and `IJsonApiRequest` setup
+- `QueryStringReader.ReadAll`: Parses the query string parameters
+- `JsonApiReader.ReadAsync`: Parses the request body
+- `OperationsProcessor.ProcessAsync`: Entry point for handling atomic operations
+- `JsonApiResourceService`: Called by controllers, delegating to the repository layer
+- `EntityFrameworkCoreRepository.ApplyQueryLayer`: Builds the `IQueryable<>` that is offered to Entity Framework Core (which turns it into SQL)
+- `JsonApiWriter.WriteAsync`: Renders the response body
+- `ExceptionHandler.HandleException`: Interception point for thrown exceptions
+
+Aside from debugging, you can get more info by:
+- Including exception stack traces and incoming request bodies in error responses, as well as writing human-readable JSON:
+
+ ```c#
+ // Program.cs
+ builder.Services.AddJsonApi(options =>
+ {
+ options.IncludeExceptionStackTraceInErrors = true;
+ options.IncludeRequestBodyInErrors = true;
+ options.SerializerOptions.WriteIndented = true;
+ });
+ ```
+- Turning on trace logging, or/and logging of executed SQL statements, by adding the following to your `appsettings.Development.json`:
+
+ ```json
+ {
+ "Logging": {
+ "LogLevel": {
+ "Default": "Warning",
+ "Microsoft.EntityFrameworkCore.Database.Command": "Information",
+ "JsonApiDotNetCore": "Trace"
+ }
+ }
+ }
+ ```
+- Activate debug logging of LINQ expressions by adding a NuGet reference to [AgileObjects.ReadableExpressions](https://www.nuget.org/packages/AgileObjects.ReadableExpressions) in your project.
+
+#### What if my JSON:API resources do not exactly match the shape of my database tables?
+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.
+Or it simply fails because it cannot be translated to SQL.
+The good news is that there's an easier solution most of the time: configure Entity Framework Core mappings to do the work.
+
+For example, if your primary key column is named "CustomerId" instead of "Id":
+```c#
+builder.Entity().Property(x => x.Id).HasColumnName("CustomerId");
+```
+
+It certainly pays off to read up on these capabilities at [Creating and Configuring a Model](https://learn.microsoft.com/ef/core/modeling/).
+Another great resource is [Learn Entity Framework Core](https://www.learnentityframeworkcore.com/configuration).
+
+#### Can I share my resource models with .NET Framework projects?
+Yes, you can. Put your model classes in a separate project that only references [JsonApiDotNetCore.Annotations](https://www.nuget.org/packages/JsonApiDotNetCore.Annotations/).
+This package contains just the JSON:API attributes and targets NetStandard 1.0, which makes it flexible to consume.
+At startup, use [Auto-discovery](~/usage/resource-graph.md#auto-discovery) and point it to your shared project.
+
+#### What's the best place to put my custom business/validation logic?
+For basic input validation, use the attributes from [ASP.NET ModelState Validation](https://learn.microsoft.com/aspnet/core/mvc/models/validation?source=recommendations&view=aspnetcore-7.0#built-in-attributes) to get the best experience.
+JsonApiDotNetCore is aware of them and adjusts behavior accordingly. And it produces the best possible error responses.
+
+For non-trivial business rules that require custom code, the place to be is [Resource Definitions](~/usage/extensibility/resource-definitions.md).
+They provide a callback-based model where you can respond to everything going on.
+The great thing is that your callbacks are invoked for various endpoints.
+For example, the filter callback on Author executes at `GET /authors?filter=`, `GET /books/1/authors?filter=` and `GET /books?include=authors?filter[authors]=`.
+Likewise, the callbacks for changing relationships execute for POST/PATCH resource endpoints, as well as POST/PATCH/DELETE relationship endpoints.
+
+#### Can API users send multiple changes in a single request?
+Yes, just activate [atomic operations](~/usage/writing/bulk-batch-operations.md).
+It enables sending multiple changes in a batch request, which are executed in a database transaction.
+If something fails, all changes are rolled back. The error response indicates which operation failed.
+
+#### Is there any way to add `[Authorize(Roles = "...")]` to the generated controllers?
+Sure, this is possible. Simply add the attribute at the class level.
+See the docs on [Augmenting controllers](~/usage/extensibility/controllers.md#augmenting-controllers).
+
+#### How do I expose non-JSON:API endpoints?
+You can add your own controllers that do not derive from `(Base)JsonApiController` or `(Base)JsonApiOperationsController`.
+Whatever you do in those is completely ignored by JsonApiDotNetCore.
+This is useful if you want to add a few RPC-style endpoints or provide binary file uploads/downloads.
+
+A middle-ground approach is to add custom action methods to existing JSON:API controllers.
+While you can route them as you like, they must return JSON:API resources.
+And on error, a JSON:API error response is produced.
+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`.
+
+#### How do I optimize for high scalability and prevent denial of service?
+Fortunately, JsonApiDotNetCore [scales pretty well](https://github.com/json-api-dotnet/PerformanceReports) under high load and/or large database tables.
+It never executes filtering, sorting, or pagination in-memory and tries pretty hard to produce the most efficient query possible.
+There are a few things to keep in mind, though:
+- Prevent users from executing slow queries by locking down [attribute capabilities](~/usage/resources/attributes.md#capabilities) and [relationship capabilities](~/usage/resources/relationships.md#capabilities).
+ Ensure the right database indexes are in place for what you enable.
+- 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).
+- Avoid long-running transactions by tweaking `MaximumOperationsPerRequest` in options.
+- Tell your users to utilize [E-Tags](~/usage/caching.md) to reduce network traffic.
+- Not included in JsonApiDotNetCore: Apply general practices such as rate limiting, load balancing, authentication/authorization, blocking very large URLs/request bodies, etc.
+
+#### Can I offload requests to a background process?
+Yes, that's possible. Override controller methods to return `HTTP 202 Accepted`, with a `Location` HTTP header where users can retrieve the result.
+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.
+From within your background process job handler, reconstruct the request state, execute the appropriate `JsonApiResourceService` method and store the result.
+There's a basic example available at https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1144, which processes a captured query string.
+
+#### What if I want to use something other than Entity Framework Core?
+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.
+Either way, you can use the built-in query string and request body parsing, as well as routing, error handling, and rendering of responses.
+
+Here are some injectable request-scoped types to be aware of:
+- `IJsonApiRequest`: This contains routing information, such as whether a primary, secondary, or relationship endpoint is being accessed.
+- `ITargetedFields`: Lists the attributes and relationships from an incoming POST/PATCH resource request. Any fields missing there should not be stored (partial updates).
+- `IEnumerable`: Provides access to the parsed query string parameters.
+- `IEvaluatedIncludeCache`: This tells the response serializer which related resources to render.
+- `ISparseFieldSetCache`: This tells the response serializer which fields to render in the `attributes` and `relationships` objects.
+
+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, attributes and relationships).
+
+So, back to the topic of where to intercept. It helps to familiarize yourself with the [execution pipeline](~/internals/queries.md).
+Replacing at the service level is the simplest. But it means you'll need to read the parsed query string parameters and invoke
+all resource definition callbacks yourself. And you won't get change detection (HTTP 203 Not Modified).
+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.
+
+You'll get a lot more out of the box if replacing at the repository level instead. You don't need to apply options or analyze query strings.
+And most resource definition callbacks are handled.
+That's because the built-in resource service translates all JSON:API query aspects of the request into a database-agnostic data structure called `QueryLayer`.
+Now the hard part for you becomes reading that data structure and producing data access calls from that.
+If your data store provides a LINQ provider, you can probably reuse [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryableBuilder.cs),
+which drives the translation into [System.Linq.Expressions](https://learn.microsoft.com/dotnet/csharp/programming-guide/concepts/expression-trees/).
+Note however, that it also produces calls to `.Include("")`, which is an Entity Framework Core-specific extension method, so you'll need to
+[prevent that from happening](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/QueryableBuilding/QueryLayerIncludeConverter.cs).
+
+The example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/NoEntityFrameworkExample/Repositories/InMemoryResourceRepository.cs) compiles and executes
+the LINQ query against an in-memory list of resources.
+For [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/master/src/JsonApiDotNetCore.MongoDb/Repositories/MongoRepository.cs), we use the MongoDB LINQ provider.
+If there's no LINQ provider available, the example [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/Examples/DapperExample/Repositories/DapperRepository.cs) may be of help,
+which produces SQL and uses [Dapper](https://github.com/DapperLib/Dapper) for data access.
+
+> [!TIP]
+> [ReadableExpressions](https://github.com/agileobjects/ReadableExpressions) is very helpful in trying to debug LINQ expression trees!
+
+#### I love JsonApiDotNetCore! How can I support the team?
+The best way to express your gratitude is by starring our repository.
+This increases our leverage when asking for bug fixes in dependent projects, such as the .NET runtime and Entity Framework Core.
+You can also [sponsor](https://github.com/sponsors/json-api-dotnet) our project.
+Of course, a simple thank-you message in our [Gitter channel](https://gitter.im/json-api-dotnet-core/Lobby) is appreciated too!
+
+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.
+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.
+So for any non-trivial changes, please open an issue first to discuss your approach and ensure it fits the product vision.
+
+#### Is there anything else I should be aware of?
+See [Common Pitfalls](~/usage/common-pitfalls.md).
diff --git a/docs/usage/meta.md b/docs/usage/meta.md
index a115e25740..674d39413b 100644
--- a/docs/usage/meta.md
+++ b/docs/usage/meta.md
@@ -60,7 +60,7 @@ public class PersonDefinition : JsonApiResourceDefinition
{
return new Dictionary
{
- ["notice"] = "Check our intranet at http://www.example.com/employees/" +
+ ["notice"] = "Check our intranet at https://www.example.com/employees/" +
$"{person.StringId} for personal details."
};
}
@@ -80,7 +80,7 @@ public class PersonDefinition : JsonApiResourceDefinition
...
},
"meta": {
- "notice": "Check our intranet at http://www.example.com/employees/1 for personal details."
+ "notice": "Check our intranet at https://www.example.com/employees/1 for personal details."
}
}
]
diff --git a/docs/usage/openapi-client.md b/docs/usage/openapi-client.md
new file mode 100644
index 0000000000..5dc40ce6fc
--- /dev/null
+++ b/docs/usage/openapi-client.md
@@ -0,0 +1,355 @@
+> [!WARNING]
+> OpenAPI support for JSON:API is currently experimental. The API and the structure of the OpenAPI document may change in future versions.
+
+# OpenAPI clients
+
+After [enabling OpenAPI](~/usage/openapi.md), you can generate a typed JSON:API client for your API in various programming languages.
+
+> [!NOTE]
+> If you prefer a generic JSON:API client instead of a typed one, choose from the existing
+> [client libraries](https://jsonapi.org/implementations/#client-libraries).
+
+The following code generators are supported, though you may try others as well:
+- [NSwag](https://github.com/RicoSuter/NSwag) (v14.1 or higher): Produces clients for C# (requires `Newtonsoft.Json`) and TypeScript
+- [Kiota](https://learn.microsoft.com/openapi/kiota/overview): Produces clients for C#, Go, Java, PHP, Python, Ruby, Swift and TypeScript
+
+# [NSwag](#tab/nswag)
+
+For C# clients, we provide an additional package that provides workarounds for bugs in NSwag and enables using partial POST/PATCH requests.
+
+To add it to your project, run the following command:
+```
+dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag --prerelease
+```
+
+# [Kiota](#tab/kiota)
+
+For C# clients, we provide an additional package that provides workarounds for bugs in Kiota, as well as MSBuild integration.
+
+To add it to your project, run the following command:
+```
+dotnet add package JsonApiDotNetCore.OpenApi.Client.Kiota --prerelease
+```
+
+---
+
+## Getting started
+
+To generate your C# client, follow the steps below.
+
+# [NSwag](#tab/nswag)
+
+### Visual Studio
+
+The easiest way to get started is by using the built-in capabilities of Visual Studio.
+The following steps describe how to generate and use a JSON:API client in C#, combined with our NuGet package.
+
+1. In **Solution Explorer**, right-click your client project, select **Add** > **Service Reference** and choose **OpenAPI**.
+
+1. On the next page, specify the OpenAPI URL to your JSON:API server, for example: `http://localhost:14140/swagger/v1/swagger.json`.
+ Specify `ExampleApiClient` as the class name, optionally provide a namespace and click **Finish**.
+ Visual Studio now downloads your swagger.json and updates your project file.
+ This adds a pre-build step that generates the client code.
+
+ > [!TIP]
+ > To later re-download swagger.json and regenerate the client code,
+ > right-click **Dependencies** > **Manage Connected Services** and click the **Refresh** icon.
+
+1. Run package update now, which fixes incompatibilities and bugs from older versions.
+
+1. Add our client package to your project:
+
+ ```
+ dotnet add package JsonApiDotNetCore.OpenApi.Client.NSwag --prerelease
+ ```
+
+1. Add code that calls one of your JSON:API endpoints.
+
+ ```c#
+ using var httpClient = new HttpClient();
+ var apiClient = new ExampleApiClient(httpClient);
+
+ var getResponse = await apiClient.GetPersonCollectionAsync(new Dictionary
+ {
+ ["filter"] = "has(assignedTodoItems)",
+ ["sort"] = "-lastName",
+ ["page[size]"] = "5"
+ });
+
+ foreach (var person in getResponse.Data)
+ {
+ Console.WriteLine($"Found person {person.Id}: {person.Attributes!.DisplayName}");
+ }
+ ```
+
+1. Extend the demo code to send a partial PATCH request with the help of our package:
+
+ ```c#
+ var updatePersonRequest = new UpdatePersonRequestDocument
+ {
+ Data = new DataInUpdatePersonRequest
+ {
+ Id = "1",
+ // Using TrackChangesFor to send "firstName: null" instead of omitting it.
+ Attributes = new TrackChangesFor(_apiClient)
+ {
+ Initializer =
+ {
+ FirstName = null,
+ LastName = "Doe"
+ }
+ }.Initializer
+ }
+ };
+
+ await ApiResponse.TranslateAsync(async () =>
+ await _apiClient.PatchPersonAsync(updatePersonRequest.Data.Id, updatePersonRequest));
+
+ // The sent request looks like this:
+ // {
+ // "data": {
+ // "type": "people",
+ // "id": "1",
+ // "attributes": {
+ // "firstName": null,
+ // "lastName": "Doe"
+ // }
+ // }
+ // }
+ ```
+
+> [!TIP]
+> The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/OpenApiNSwagClientExample) contains an enhanced version
+> that uses `IHttpClientFactory` for [scalability](https://learn.microsoft.com/dotnet/core/extensions/httpclient-factory) and
+> [resiliency](https://learn.microsoft.com/aspnet/core/fundamentals/http-requests#use-polly-based-handlers) and logs the HTTP requests and responses.
+> Additionally, the example shows how to write the swagger.json file to disk when building the server, which is imported from the client project.
+> This keeps the server and client automatically in sync, which is handy when both are in the same solution.
+
+### Other IDEs
+
+The following section shows what to add to your client project file directly:
+
+```xml
+
+
+
+
+
+
+
+
+ http://localhost:14140/swagger/v1/swagger.json
+ ExampleApiClient
+ %(ClassName).cs
+
+
+```
+
+From here, continue from step 3 in the list of steps for Visual Studio.
+
+# [Kiota](#tab/kiota)
+
+To generate your C# client, first add the Kiota tool to your solution:
+
+```
+dotnet tool install microsoft.openapi.kiota
+```
+
+After adding the `JsonApiDotNetCore.OpenApi.Client.Kiota` package to your project, add a `KiotaReference` element
+to your project file to import your OpenAPI file. For example:
+
+```xml
+
+
+
+ $(MSBuildProjectName).GeneratedCode
+ ExampleApiClient
+ ./GeneratedCode
+ $(JsonApiExtraArguments)
+
+
+
+```
+
+> [!NOTE]
+> The `ExtraArguments` parameter is required for compatibility with JSON:API.
+
+Next, build your project. It runs the kiota command-line tool, which generates files in the `GeneratedCode` subdirectory.
+
+> [!CAUTION]
+> If you're not using ``, at least make sure you're passing the `--backing-store` switch to the command-line tool,
+> which is needed for JSON:API partial POST/PATCH requests to work correctly.
+
+Kiota is pretty young and therefore still rough around the edges. At the time of writing, there are various bugs, for which we have workarounds
+in place. For a full example, see the [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/OpenApiKiotaClientExample).
+
+---
+
+## Configuration
+
+Various switches enable you to tweak the client generation to your needs. See the section below for an overview.
+
+# [NSwag](#tab/nswag)
+
+The `OpenApiReference` element can be customized using various [NSwag-specific MSBuild properties](https://github.com/RicoSuter/NSwag/blob/7d6df3af95081f3f0ed6dee04be8d27faa86f91a/src/NSwag.ApiDescription.Client/NSwag.ApiDescription.Client.props).
+See [the source code](https://github.com/RicoSuter/NSwag/blob/master/src/NSwag.Commands/Commands/CodeGeneration/OpenApiToCSharpClientCommand.cs) for their meaning.
+The `JsonApiDotNetCore.OpenApi.Client.NSwag` package sets various of these for optimal JSON:API support.
+
+> [!NOTE]
+> Earlier versions of NSwag required the use of `` to specify command-line switches directly.
+> This is no longer recommended and may conflict with the new MSBuild properties.
+
+For example, the following section puts the generated code in a namespace, makes the client class internal and generates an interface (handy when writing tests):
+
+```xml
+
+ ExampleProject.GeneratedCode
+ internal
+ true
+
+```
+
+# [Kiota](#tab/kiota)
+
+The available command-line switches for Kiota are described [here](https://learn.microsoft.com/openapi/kiota/using#client-generation).
+
+At the time of writing, Kiota provides [no official integration](https://github.com/microsoft/kiota/issues/3005) with MSBuild.
+Our [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/OpenApiKiotaClientExample) takes a stab at it,
+which seems to work. If you're an MSBuild expert, please help out!
+
+```xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+```
+
+---
+
+## Headers and caching
+
+The use of HTTP headers varies per client generator. To use [ETags for caching](~/usage/caching.md), see the notes below.
+
+# [NSwag](#tab/nswag)
+
+To gain access to HTTP response headers, add the following in a `PropertyGroup` or directly in the `OpenApiReference`:
+
+```
+true
+```
+
+This enables the following code, which is explained below:
+
+```c#
+var getResponse = await ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync());
+string eTag = getResponse.Headers["ETag"].Single();
+Console.WriteLine($"Retrieved {getResponse.Result?.Data.Count ?? 0} people.");
+
+// wait some time...
+
+getResponse = await ApiResponse.TranslateAsync(() => apiClient.GetPersonCollectionAsync(if_None_Match: eTag));
+
+if (getResponse is { StatusCode: (int)HttpStatusCode.NotModified, Result: null })
+{
+ Console.WriteLine("The HTTP response hasn't changed, so no response body was returned.");
+}
+```
+
+The response of the first API call contains both data and an ETag header, which is a fingerprint of the response.
+That ETag gets passed to the second API call. This enables the server to detect if something changed, which optimizes
+network usage: no data is sent back, unless is has changed.
+If you only want to ask whether data has changed without fetching it, use a HEAD request instead.
+
+# [Kiota](#tab/kiota)
+
+Use `HeadersInspectionHandlerOption` to gain access to HTTP response headers. For example:
+
+```c#
+var headerInspector = new HeadersInspectionHandlerOption
+{
+ InspectResponseHeaders = true
+};
+
+var responseDocument = await apiClient.Api.People.GetAsync(configuration => configuration.Options.Add(headerInspector));
+
+string eTag = headerInspector.ResponseHeaders["ETag"].Single();
+```
+
+Due to a [bug in Kiota](https://github.com/microsoft/kiota/issues/4190), a try/catch block is needed additionally to make this work.
+
+For a full example, see the [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/OpenApiKiotaClientExample).
+
+---
+
+## Atomic operations
+
+# [NSwag](#tab/nswag)
+
+[Atomic operations](~/usage/writing/bulk-batch-operations.md) are fully supported.
+The [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/OpenApiNSwagClientExample)
+demonstrates how to use them. It uses local IDs to:
+- Create a new tag
+- Create a new person
+- Update the person to clear an attribute (using `TrackChangesFor`)
+- Create a new todo-item, tagged with the new tag, and owned by the new person
+- Assign the todo-item to the created person
+
+# [Kiota](#tab/kiota)
+
+[Atomic operations](~/usage/writing/bulk-batch-operations.md) are fully supported.
+See the [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/OpenApiKiotaClientExample)
+demonstrates how to use them. It uses local IDs to:
+- Create a new tag
+- Create a new person
+- Update the person to clear an attribute (using built-in backing-store)
+- Create a new todo-item, tagged with the new tag, and owned by the new person
+- Assign the todo-item to the created person
+
+---
+
+## Known limitations
+
+# [NSwag](#tab/nswag)
+
+| Limitation | Workaround | Links |
+| --- | --- | --- |
+| Partial POST/PATCH sends incorrect request | Use `TrackChangesFor` from `JsonApiDotNetCore.OpenApi.Client.NSwag` package | |
+| Exception thrown on successful HTTP status | Use `TranslateAsync` from `JsonApiDotNetCore.OpenApi.Client.NSwag` package | https://github.com/RicoSuter/NSwag/issues/2499 |
+| No `Accept` header sent when only error responses define `Content-Type` | `JsonApiDotNetCore.OpenApi.Swashbuckle` package contains workaround | |
+| Schema type not always inferred with `allOf` | `JsonApiDotNetCore.OpenApi.Swashbuckle` package contains workaround | |
+| Generated code for JSON:API extensions does not compile | `JsonApiDotNetCore.OpenApi.Swashbuckle` package contains workaround | |
+| A project can't contain both JSON:API clients and regular OpenAPI clients | Use separate projects | |
+
+# [Kiota](#tab/kiota)
+
+| Limitation | Workaround | Links |
+| --- | --- | --- |
+| Properties are always nullable | - | https://github.com/microsoft/kiota/issues/3911 |
+| JSON:API query strings are inaccessible | Use `SetQueryStringHttpMessageHandler.CreateScope` from `JsonApiDotNetCore.OpenApi.Client.Kiota` package | https://github.com/microsoft/kiota/issues/3800 |
+| HTTP 304 (Not Modified) is not properly recognized | Catch `ApiException` and inspect the response status code | https://github.com/microsoft/kiota/issues/4190, https://github.com/microsoft/kiota-dotnet/issues/531 |
+| Generator warns about unsupported formats | Use `JsonApiDotNetCore.OpenApi.Client.Kiota` package | https://github.com/microsoft/kiota/issues/4227 |
+| `Stream` response for HEAD request | - | https://github.com/microsoft/kiota/issues/4245 |
+| Unhelpful exception messages | - | https://github.com/microsoft/kiota/issues/4349 |
+| Discriminator properties aren't being set automatically | - | https://github.com/microsoft/kiota/issues/4618 |
+| Discriminator mappings must be repeated in every derived type used in responses | `JsonApiDotNetCore.OpenApi.Swashbuckle` package contains workaround | https://github.com/microsoft/kiota/issues/2432 |
+| `x-abstract` in `openapi.json` is ignored | - | |
+| No MSBuild / IDE support | Use `KiotaReference` from `JsonApiDotNetCore.OpenApi.Client.Kiota` package | https://github.com/microsoft/kiota/issues/3005 |
+| Incorrect nullability in API methods | Use `KiotaReference` from `JsonApiDotNetCore.OpenApi.Client.Kiota` package | https://github.com/microsoft/kiota/issues/3944 |
+| Generated code for JSON:API extensions does not compile | `JsonApiDotNetCore.OpenApi.Swashbuckle` package contains workaround | |
+| Properties are always sent in alphabetic order | - | https://github.com/microsoft/kiota/issues/4680 |
+
+---
diff --git a/docs/usage/openapi-documentation.md b/docs/usage/openapi-documentation.md
new file mode 100644
index 0000000000..6737bd6404
--- /dev/null
+++ b/docs/usage/openapi-documentation.md
@@ -0,0 +1,48 @@
+> [!WARNING]
+> OpenAPI support for JSON:API is currently experimental. The API and the structure of the OpenAPI document may change in future versions.
+
+# OpenAPI documentation
+
+After [enabling OpenAPI](~/usage/openapi.md), you can expose a documentation website with SwaggerUI, Redoc and/or Scalar.
+
+## SwaggerUI
+
+[SwaggerUI](https://swagger.io/tools/swagger-ui/) enables to visualize and interact with the JSON:API endpoints through a web page.
+While it conveniently provides the ability to execute requests, it doesn't show properties of derived types when component schema inheritance is used.
+
+SwaggerUI can be enabled by installing the `Swashbuckle.AspNetCore.SwaggerUI` NuGet package and adding the following to your `Program.cs` file:
+
+```c#
+app.UseSwaggerUI();
+```
+
+Then run your app and open `/swagger` in your browser.
+
+## Redoc
+
+[Redoc](https://github.com/Redocly/redoc) is another popular tool that generates a documentation website from an OpenAPI document.
+It lists the endpoints and their schemas, but doesn't provide the ability to execute requests.
+However, this tool most accurately reflects properties when component schema inheritance is used; choosing a different "type" from the
+dropdown box dynamically adapts the list of schema properties.
+
+The `Swashbuckle.AspNetCore.ReDoc` NuGet package provides integration with Swashbuckle.
+After installing the package, add the following to your `Program.cs` file:
+
+```c#
+app.UseReDoc();
+```
+
+Next, run your app and navigate to `/api-docs` to view the documentation.
+
+## Scalar
+
+[Scalar](https://scalar.com/) is a modern documentation website generator, which includes the ability to execute requests.
+It shows component schemas in a low-level way (not collapsing `allOf` nodes), but does a poor job in handling component schema inheritance.
+
+After installing the `Scalar.AspNetCore` NuGet package, add the following to your `Program.cs` to make it use the OpenAPI document produced by Swashbuckle:
+
+```c#
+app.MapScalarApiReference(options => options.OpenApiRoutePattern = "/swagger/{documentName}/swagger.json");
+```
+
+Then run your app and navigate to `/scalar/v1` to view the documentation.
diff --git a/docs/usage/openapi.md b/docs/usage/openapi.md
new file mode 100644
index 0000000000..83f3ce8a3b
--- /dev/null
+++ b/docs/usage/openapi.md
@@ -0,0 +1,83 @@
+> [!WARNING]
+> OpenAPI support for JSON:API is currently experimental. The API and the structure of the OpenAPI document may change in future versions.
+
+# OpenAPI
+
+Exposing an [OpenAPI document](https://swagger.io/specification/) for your JSON:API endpoints enables to provide a
+[documentation website](https://swagger.io/tools/swagger-ui/) and to generate typed
+[client libraries](https://openapi-generator.tech/docs/generators/) in various languages.
+
+The [JsonApiDotNetCore.OpenApi.Swashbuckle](https://github.com/json-api-dotnet/JsonApiDotNetCore/pkgs/nuget/JsonApiDotNetCore.OpenApi.Swashbuckle) NuGet package
+provides OpenAPI support for JSON:API by integrating with [Swashbuckle](https://github.com/domaindrivendev/Swashbuckle.AspNetCore).
+
+## Getting started
+
+1. Install the `JsonApiDotNetCore.OpenApi.Swashbuckle` NuGet package:
+
+ ```
+ dotnet add package JsonApiDotNetCore.OpenApi.Swashbuckle --prerelease
+ ```
+
+2. Add the JSON:API support to your `Program.cs` file.
+
+ ```c#
+ builder.Services.AddJsonApi();
+
+ // Configure Swashbuckle for JSON:API.
+ builder.Services.AddOpenApiForJsonApi();
+
+ var app = builder.Build();
+
+ app.UseRouting();
+ app.UseJsonApi();
+
+ // Add the Swashbuckle middleware.
+ app.UseSwagger();
+ ```
+
+By default, the OpenAPI document will be available at `http://localhost:/swagger/v1/swagger.json`.
+
+### Customizing the Route Template
+
+Because Swashbuckle doesn't properly implement the ASP.NET Options pattern, you must *not* use its
+[documented way](https://github.com/domaindrivendev/Swashbuckle.AspNetCore?tab=readme-ov-file#change-the-path-for-swagger-json-endpoints)
+to change the route template:
+
+```c#
+// DO NOT USE THIS! INCOMPATIBLE WITH JSON:API!
+app.UseSwagger(options => options.RouteTemplate = "api-docs/{documentName}/swagger.yaml");
+```
+
+Instead, always call `UseSwagger()` *without parameters*. To change the route template, use the code below:
+
+```c#
+builder.Services.Configure(options => options.RouteTemplate = "/api-docs/{documentName}/swagger.yaml");
+```
+
+If you want to inject dependencies to set the route template, use:
+
+```c#
+builder.Services.AddOptions().Configure((options, serviceProvider) =>
+{
+ var webHostEnvironment = serviceProvider.GetRequiredService();
+ string appName = webHostEnvironment.ApplicationName;
+ options.RouteTemplate = $"/api-docs/{{documentName}}/{appName}-swagger.yaml";
+});
+```
+
+## Triple-slash comments
+
+Documentation for JSON:API endpoints is provided out of the box, which shows in SwaggerUI and through IDE IntelliSense in auto-generated clients.
+To also get documentation for your resource classes and their properties, add the following to your project file.
+The `NoWarn` line is optional, which suppresses build warnings for undocumented types and members.
+
+```xml
+
+ True
+ $(NoWarn);1591
+
+```
+
+You can combine this with the documentation that Swagger itself supports, by enabling it as described
+[here](https://github.com/domaindrivendev/Swashbuckle.AspNetCore#include-descriptions-from-xml-comments).
+This adds documentation for additional types, such as triple-slash comments on enums used in your resource models.
diff --git a/docs/usage/options.md b/docs/usage/options.md
index 7607ac8a9e..7e89ff0090 100644
--- a/docs/usage/options.md
+++ b/docs/usage/options.md
@@ -122,7 +122,7 @@ Because we copy resource properties into an intermediate object before serializa
## ModelState Validation
-[ASP.NET ModelState validation](https://docs.microsoft.com/en-us/aspnet/core/mvc/models/validation) can be used to validate incoming request bodies when creating and updating resources. Since v5.0, this is enabled by default.
+[ASP.NET ModelState validation](https://learn.microsoft.com/aspnet/web-api/overview/formats-and-model-binding/model-validation-in-aspnet-web-api) can be used to validate incoming request bodies when creating and updating resources. Since v5.0, this is enabled by default.
When `ValidateModelState` is set to `false`, no model validation is performed.
How nullability affects ModelState validation is described [here](~/usage/resources/nullability.md).
diff --git a/docs/usage/reading/filtering.md b/docs/usage/reading/filtering.md
index 8a568078a0..05c3066644 100644
--- a/docs/usage/reading/filtering.md
+++ b/docs/usage/reading/filtering.md
@@ -60,7 +60,7 @@ The next request returns all customers that have orders -or- whose last name is
GET /customers?filter=has(orders)&filter=equals(lastName,'Smith') HTTP/1.1
```
-Aside from filtering on the resource being requested (which would be blogs in /blogs and articles in /blogs/1/articles),
+Aside from filtering on the resource being requested (which would be blogs in /blogs and articles in /blogs/1/articles),
filtering on to-many relationships can be done using bracket notation:
```http
diff --git a/docs/usage/reading/toc.yml b/docs/usage/reading/toc.yml
new file mode 100644
index 0000000000..aa1ecb6bca
--- /dev/null
+++ b/docs/usage/reading/toc.yml
@@ -0,0 +1,10 @@
+- name: Filtering
+ href: filtering.md
+- name: Sorting
+ href: sorting.md
+- name: Pagination
+ href: pagination.md
+- name: Sparse Fieldset Selection
+ href: sparse-fieldset-selection.md
+- name: Including Related Resources
+ href: including-relationships.md
diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md
index 18a13da907..046daaf7f5 100644
--- a/docs/usage/resource-graph.md
+++ b/docs/usage/resource-graph.md
@@ -14,7 +14,7 @@ There are three ways the resource graph can be created:
2. Specifying an entire DbContext
3. Manually specifying each resource
-It is also possible to combine the three of them at once. Be aware that some configuration might overlap,
+It is also possible to combine the three of them at once. Be aware that some configuration might overlap,
for example one could manually add a resource to the graph which is also auto-discovered. In such a scenario, the configuration
is prioritized by the list above in descending order.
diff --git a/docs/usage/resources/index.md b/docs/usage/resources/index.md
index f8e7d29156..09e0224c57 100644
--- a/docs/usage/resources/index.md
+++ b/docs/usage/resources/index.md
@@ -22,10 +22,9 @@ public class Person : Identifiable
}
```
-If your resource must inherit from another class,
-you can always implement the interface yourself.
-In this example, `ApplicationUser` inherits from `IdentityUser`
-which already contains an Id property of type string.
+If your resource must inherit from another class, you can always implement the interface yourself.
+In this example, `ApplicationUser` inherits from [`IdentityUser`](https://learn.microsoft.com/dotnet/api/microsoft.aspnetcore.identity.entityframeworkcore.identityuser),
+which already contains an `Id` property of type `string`.
```c#
public class ApplicationUser : IdentityUser, IIdentifiable
diff --git a/docs/usage/resources/inheritance.md b/docs/usage/resources/inheritance.md
index 47cf85ca67..56c046ef82 100644
--- a/docs/usage/resources/inheritance.md
+++ b/docs/usage/resources/inheritance.md
@@ -143,7 +143,7 @@ GET /humans HTTP/1.1
}
```
-### Spare fieldsets
+### Sparse fieldsets
If you only want to retrieve the fields from the base type, you can use [sparse fieldsets](~/usage/reading/sparse-fieldset-selection.md).
diff --git a/docs/usage/resources/nullability.md b/docs/usage/resources/nullability.md
index 24b15572fc..875b133a01 100644
--- a/docs/usage/resources/nullability.md
+++ b/docs/usage/resources/nullability.md
@@ -24,7 +24,7 @@ This makes Entity Framework Core generate non-nullable columns. And model errors
# Reference types
-When the [nullable reference types](https://docs.microsoft.com/en-us/dotnet/csharp/nullable-references) (NRT) compiler feature is enabled, it affects both ASP.NET ModelState validation and Entity Framework Core.
+When the [nullable reference types](https://learn.microsoft.com/dotnet/csharp/nullable-references) (NRT) compiler feature is enabled, it affects both ASP.NET ModelState validation and Entity Framework Core.
## NRT turned off
@@ -60,7 +60,7 @@ public sealed class Label : Identifiable
When NRT is turned on, use nullability annotations (?) on attributes and relationships. This makes Entity Framework Core generate non-nullable columns. And model errors are returned when non-nullable fields are omitted.
-The [Entity Framework Core guide on NRT](https://docs.microsoft.com/en-us/ef/core/miscellaneous/nullable-reference-types) recommends to use constructor binding to initialize non-nullable properties, but JsonApiDotNetCore does not support that. For required navigation properties, it suggests to use a non-nullable property with a nullable backing field. JsonApiDotNetCore does not support that either. In both cases, just use the null-forgiving operator (!).
+The [Entity Framework Core guide on NRT](https://learn.microsoft.com/ef/core/miscellaneous/nullable-reference-types) recommends to use constructor binding to initialize non-nullable properties, but JsonApiDotNetCore does not support that. For required navigation properties, it suggests to use a non-nullable property with a nullable backing field. JsonApiDotNetCore does not support that either. In both cases, just use the null-forgiving operator (!).
When ModelState validation is turned on, to-many relationships must be assigned an empty collection. Otherwise an error is returned when they don't occur in the request body.
diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md
index 689b3aa4d2..f318b2ddcd 100644
--- a/docs/usage/resources/relationships.md
+++ b/docs/usage/resources/relationships.md
@@ -1,7 +1,7 @@
# Relationships
A relationship is a named link between two resource types, including a direction.
-They are similar to [navigation properties in Entity Framework Core](https://docs.microsoft.com/en-us/ef/core/modeling/relationships).
+They are similar to [navigation properties in Entity Framework Core](https://learn.microsoft.com/ef/core/modeling/relationships).
Relationships come in two flavors: to-one and to-many.
The left side of a relationship is where the relationship is declared, the right side is the resource type it points to.
@@ -113,7 +113,7 @@ For optional one-to-one relationships, Entity Framework Core uses `DeleteBehavio
This means that Entity Framework Core tries to handle the cascading effects (by sending multiple SQL statements), instead of leaving it up to the database.
Of course that's only going to work when all the related resources are loaded in the change tracker upfront, which is expensive because it requires fetching more data than necessary.
-The reason for this odd default is poor support in SQL Server, as explained [here](https://stackoverflow.com/questions/54326165/ef-core-why-clientsetnull-is-default-ondelete-behavior-for-optional-relations) and [here](https://learn.microsoft.com/en-us/ef/core/saving/cascade-delete#database-cascade-limitations).
+The reason for this odd default is poor support in SQL Server, as explained [here](https://stackoverflow.com/questions/54326165/ef-core-why-clientsetnull-is-default-ondelete-behavior-for-optional-relations) and [here](https://learn.microsoft.com/ef/core/saving/cascade-delete#database-cascade-limitations).
**Our [testing](https://github.com/json-api-dotnet/JsonApiDotNetCore/pull/1205) shows that these limitations don't exist when using PostgreSQL.
Therefore the general advice is to map the delete behavior of optional one-to-one relationships explicitly with `.OnDelete(DeleteBehavior.SetNull)`. This is simpler and more efficient.**
@@ -261,8 +261,8 @@ public class TodoItem : Identifiable
_since v5.1_
-Default JSON:API relationship capabilities are specified in
-@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasOneCapabilities and
+Default JSON:API relationship capabilities are specified in
+@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasOneCapabilities and
@JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultHasManyCapabilities:
```c#
diff --git a/docs/usage/resources/toc.yml b/docs/usage/resources/toc.yml
new file mode 100644
index 0000000000..d4daf205d4
--- /dev/null
+++ b/docs/usage/resources/toc.yml
@@ -0,0 +1,8 @@
+- name: Attributes
+ href: attributes.md
+- name: Relationships
+ href: relationships.md
+- name: Inheritance
+ href: inheritance.md
+- name: Nullability
+ href: nullability.md
diff --git a/docs/usage/routing.md b/docs/usage/routing.md
index e3e021ec23..cb1197e86a 100644
--- a/docs/usage/routing.md
+++ b/docs/usage/routing.md
@@ -86,7 +86,7 @@ public class OrderLineController : JsonApiController
## Advanced usage: custom routing convention
-It is possible to replace the built-in routing convention with a [custom routing convention](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`.
+It is possible to replace the built-in routing convention with a [custom routing convention](https://learn.microsoft.com/aspnet/core/mvc/controllers/application-model#custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`.
```c#
// Program.cs
diff --git a/docs/usage/toc.md b/docs/usage/toc.md
deleted file mode 100644
index 61d3da8de0..0000000000
--- a/docs/usage/toc.md
+++ /dev/null
@@ -1,35 +0,0 @@
-# [Resources](resources/index.md)
-## [Attributes](resources/attributes.md)
-## [Relationships](resources/relationships.md)
-## [Inheritance](resources/inheritance.md)
-## [Nullability](resources/nullability.md)
-
-# Reading data
-## [Filtering](reading/filtering.md)
-## [Sorting](reading/sorting.md)
-## [Pagination](reading/pagination.md)
-## [Sparse Fieldset Selection](reading/sparse-fieldset-selection.md)
-## [Including Relationships](reading/including-relationships.md)
-
-# Writing data
-## [Creating](writing/creating.md)
-## [Updating](writing/updating.md)
-## [Deleting](writing/deleting.md)
-## [Bulk/batch](writing/bulk-batch-operations.md)
-
-# [Resource Graph](resource-graph.md)
-# [Options](options.md)
-# [Routing](routing.md)
-# [Errors](errors.md)
-# [Metadata](meta.md)
-# [Caching](caching.md)
-# [Common Pitfalls](common-pitfalls.md)
-
-# Extensibility
-## [Layer Overview](extensibility/layer-overview.md)
-## [Resource Definitions](extensibility/resource-definitions.md)
-## [Controllers](extensibility/controllers.md)
-## [Resource Services](extensibility/services.md)
-## [Resource Repositories](extensibility/repositories.md)
-## [Middleware](extensibility/middleware.md)
-## [Query Strings](extensibility/query-strings.md)
diff --git a/docs/usage/toc.yml b/docs/usage/toc.yml
new file mode 100644
index 0000000000..f5d60e9a1f
--- /dev/null
+++ b/docs/usage/toc.yml
@@ -0,0 +1,35 @@
+- name: FAQ
+ href: faq.md
+- name: Common Pitfalls
+ href: common-pitfalls.md
+- name: Resources
+ href: resources/toc.yml
+ topicHref: resources/index.md
+- name: Reading data
+ href: reading/toc.yml
+- name: Writing data
+ href: writing/toc.yml
+- name: Resource Graph
+ href: resource-graph.md
+- name: Options
+ href: options.md
+- name: Routing
+ href: routing.md
+- name: Errors
+ href: errors.md
+- name: Metadata
+ href: meta.md
+- name: Caching
+ href: caching.md
+- name: OpenAPI
+ href: openapi.md
+ items:
+ - name: Documentation
+ href: openapi-documentation.md
+ - name: Clients
+ href: openapi-client.md
+- name: Extensibility
+ href: extensibility/toc.yml
+- name: Advanced
+ href: advanced/toc.yml
+ topicHref: advanced/index.md
diff --git a/docs/usage/writing/bulk-batch-operations.md b/docs/usage/writing/bulk-batch-operations.md
index 1ac35fd3fc..c8ba2bf48e 100644
--- a/docs/usage/writing/bulk-batch-operations.md
+++ b/docs/usage/writing/bulk-batch-operations.md
@@ -19,13 +19,24 @@ public sealed class OperationsController : JsonApiOperationsController
{
public OperationsController(IJsonApiOptions options, IResourceGraph resourceGraph,
ILoggerFactory loggerFactory, IOperationsProcessor processor,
- IJsonApiRequest request, ITargetedFields targetedFields)
- : base(options, resourceGraph, loggerFactory, processor, request, targetedFields)
+ IJsonApiRequest request, ITargetedFields targetedFields,
+ IAtomicOperationFilter operationFilter)
+ : base(options, resourceGraph, loggerFactory, processor, request, targetedFields,
+ operationFilter)
{
}
}
```
+> [!IMPORTANT]
+> Since v5.6.0, the set of exposed operations is based on
+> [`GenerateControllerEndpoints` usage](~/usage/extensibility/controllers.md#resource-access-control).
+> Earlier versions always exposed all operations for all resource types.
+> If you're using [explicit controllers](~/usage/extensibility/controllers.md#explicit-controllers),
+> register and implement your own
+> [`IAtomicOperationFilter`](~/api/JsonApiDotNetCore.AtomicOperations.IAtomicOperationFilter.yml)
+> to indicate which operations to expose.
+
You'll need to send the next Content-Type in a POST request for operations:
```
diff --git a/docs/usage/writing/creating.md b/docs/usage/writing/creating.md
index ba0a21d52b..8cc0c03e49 100644
--- a/docs/usage/writing/creating.md
+++ b/docs/usage/writing/creating.md
@@ -16,8 +16,8 @@ POST /articles HTTP/1.1
}
```
-When using client-generated IDs and only attributes from the request have changed, the server returns `204 No Content`.
-Otherwise, the server returns `200 OK`, along with the updated resource and its newly assigned ID.
+When using client-generated IDs and all attributes of the created resource are the same as in the request, the server
+returns `204 No Content`. Otherwise, the server returns `201 Created`, along with the stored attributes and its newly assigned ID.
In both cases, a `Location` header is returned that contains the URL to the new resource.
diff --git a/docs/usage/writing/toc.yml b/docs/usage/writing/toc.yml
new file mode 100644
index 0000000000..db836e548f
--- /dev/null
+++ b/docs/usage/writing/toc.yml
@@ -0,0 +1,8 @@
+- name: Creating
+ href: creating.md
+- name: Updating
+ href: updating.md
+- name: Deleting
+ href: deleting.md
+- name: Bulk/Batch
+ href: bulk-batch-operations.md
diff --git a/docs/usage/writing/updating.md b/docs/usage/writing/updating.md
index ea27e1a220..30e1b4fa7d 100644
--- a/docs/usage/writing/updating.md
+++ b/docs/usage/writing/updating.md
@@ -5,7 +5,7 @@
To modify the attributes of a single resource, send a PATCH request. The next example changes the article caption:
```http
-PATCH /articles HTTP/1.1
+PATCH /articles/1 HTTP/1.1
{
"data": {
diff --git a/inspectcode.ps1 b/inspectcode.ps1
index b379bce1c6..21e96eac67 100644
--- a/inspectcode.ps1
+++ b/inspectcode.ps1
@@ -10,7 +10,7 @@ if ($LastExitCode -ne 0) {
$outputPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.xml')
$resultPath = [System.IO.Path]::Combine([System.IO.Path]::GetTempPath(), 'jetbrains-inspectcode-results.html')
-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
+dotnet jb inspectcode JsonApiDotNetCore.sln --dotnetcoresdk=$(dotnet --version) --build --output="$outputPath" --format="xml" --profile=WarningSeverities.DotSettings --properties:Configuration=Release --properties:RunAnalyzers=false --severity=WARNING --verbosity=WARN -dsl=GlobalAll -dsl=GlobalPerProduct -dsl=SolutionPersonal -dsl=ProjectPersonal
if ($LastExitCode -ne 0) {
throw "Code inspection failed with exit code $LastExitCode"
diff --git a/logo.png b/logo.png
deleted file mode 100644
index 78f1acd521..0000000000
Binary files a/logo.png and /dev/null differ
diff --git a/nuget.config b/nuget.config
new file mode 100644
index 0000000000..16164967c6
--- /dev/null
+++ b/nuget.config
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/package-icon.png b/package-icon.png
new file mode 100644
index 0000000000..f95eb770e8
Binary files /dev/null and b/package-icon.png differ
diff --git a/package-versions.props b/package-versions.props
new file mode 100644
index 0000000000..0f2c3f6e7e
--- /dev/null
+++ b/package-versions.props
@@ -0,0 +1,53 @@
+
+
+
+ 4.1.0
+ 0.4.1
+ 2.14.1
+ 9.0.1
+ 13.0.3
+
+
+ 0.15.*
+ 1.0.*
+ 35.6.*
+ 4.14.*
+ 6.0.*
+ 2.1.*
+ 7.2.*
+ 2.4.*
+ 2.0.*
+ 1.*
+ 9.0.*
+ 9.0.*
+ 14.4.*
+ 13.0.*
+ 4.1.*
+ 2.4.*
+ 9.*-*
+ 9.0.*
+ 17.14.*
+ 2.9.*
+ 3.1.*
+
+
+
+
+ N/A
+
+
+ 9.0.*
+ 9.0.*
+ 9.0.0-*
+
+
+
+
+ 8.0.0
+
+
+ 8.0.*
+ 8.0.*
+ $(EntityFrameworkCoreVersion)
+
+
diff --git a/run-docker-postgres.ps1 b/run-docker-postgres.ps1
index 153b93a846..25b631a7ad 100644
--- a/run-docker-postgres.ps1
+++ b/run-docker-postgres.ps1
@@ -1,12 +1,18 @@
#Requires -Version 7.0
-# This script starts a docker container with postgres database, used for running tests.
+# This script starts a PostgreSQL database in a docker container, which is required for running tests locally.
+# When the -UI switch is passed, pgAdmin (a web-based PostgreSQL management tool) is started in a second container, which lets you query the database.
+# To connect to pgAdmin, open http://localhost:5050 and login with user "admin@admin.com", password "postgres". Use hostname "db" when registering the server.
-docker container stop jsonapi-dotnet-core-testing
+param(
+ [switch] $UI=$False
+)
-docker run --rm --name jsonapi-dotnet-core-testing `
- -e POSTGRES_DB=JsonApiDotNetCoreExample `
- -e POSTGRES_USER=postgres `
- -e POSTGRES_PASSWORD=postgres `
- -p 5432:5432 `
- postgres:15
+docker container stop jsonapi-postgresql-db
+docker container stop jsonapi-postgresql-management
+
+docker run --pull always --rm --detach --name jsonapi-postgresql-db -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:latest -N 500
+
+if ($UI) {
+ docker run --pull always --rm --detach --name jsonapi-postgresql-management --link jsonapi-postgresql-db:db -e PGADMIN_DEFAULT_EMAIL=admin@admin.com -e PGADMIN_DEFAULT_PASSWORD=postgres -p 5050:80 dpage/pgadmin4:latest
+}
diff --git a/src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs b/src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs
new file mode 100644
index 0000000000..11d052da66
--- /dev/null
+++ b/src/Examples/DapperExample/AtomicOperations/AmbientTransaction.cs
@@ -0,0 +1,60 @@
+using System.Data.Common;
+using JsonApiDotNetCore.AtomicOperations;
+
+namespace DapperExample.AtomicOperations;
+
+///
+/// Represents an ADO.NET transaction in a JSON:API atomic:operations request.
+///
+internal sealed class AmbientTransaction : IOperationsTransaction
+{
+ private readonly AmbientTransactionFactory _owner;
+
+ public DbTransaction Current { get; }
+
+ ///
+ public string TransactionId { get; }
+
+ public AmbientTransaction(AmbientTransactionFactory owner, DbTransaction current, Guid transactionId)
+ {
+ ArgumentNullException.ThrowIfNull(owner);
+ ArgumentNullException.ThrowIfNull(current);
+
+ _owner = owner;
+ Current = current;
+ TransactionId = transactionId.ToString();
+ }
+
+ ///
+ public Task BeforeProcessOperationAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task AfterProcessOperationAsync(CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ ///
+ public Task CommitAsync(CancellationToken cancellationToken)
+ {
+ return Current.CommitAsync(cancellationToken);
+ }
+
+ ///
+ public async ValueTask DisposeAsync()
+ {
+ DbConnection? connection = Current.Connection;
+
+ await Current.DisposeAsync();
+
+ if (connection != null)
+ {
+ await connection.DisposeAsync();
+ }
+
+ _owner.Detach(this);
+ }
+}
diff --git a/src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs b/src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs
new file mode 100644
index 0000000000..82790819fe
--- /dev/null
+++ b/src/Examples/DapperExample/AtomicOperations/AmbientTransactionFactory.cs
@@ -0,0 +1,77 @@
+using System.Data.Common;
+using DapperExample.TranslationToSql.DataModel;
+using JsonApiDotNetCore.AtomicOperations;
+using JsonApiDotNetCore.Configuration;
+
+namespace DapperExample.AtomicOperations;
+
+///
+/// Provides transaction support for JSON:API atomic:operation requests using ADO.NET.
+///
+public sealed class AmbientTransactionFactory : IOperationsTransactionFactory
+{
+ private readonly IJsonApiOptions _options;
+ private readonly IDataModelService _dataModelService;
+
+ internal AmbientTransaction? AmbientTransaction { get; private set; }
+
+ public AmbientTransactionFactory(IJsonApiOptions options, IDataModelService dataModelService)
+ {
+ ArgumentNullException.ThrowIfNull(options);
+ ArgumentNullException.ThrowIfNull(dataModelService);
+
+ _options = options;
+ _dataModelService = dataModelService;
+ }
+
+ internal async Task BeginTransactionAsync(CancellationToken cancellationToken)
+ {
+ var instance = (IOperationsTransactionFactory)this;
+
+ IOperationsTransaction transaction = await instance.BeginTransactionAsync(cancellationToken);
+ return (AmbientTransaction)transaction;
+ }
+
+ async Task IOperationsTransactionFactory.BeginTransactionAsync(CancellationToken cancellationToken)
+ {
+ if (AmbientTransaction != null)
+ {
+ throw new InvalidOperationException("Cannot start transaction because another transaction is already active.");
+ }
+
+ DbConnection dbConnection = _dataModelService.CreateConnection();
+
+ try
+ {
+ await dbConnection.OpenAsync(cancellationToken);
+
+ DbTransaction transaction = _options.TransactionIsolationLevel != null
+ ? await dbConnection.BeginTransactionAsync(_options.TransactionIsolationLevel.Value, cancellationToken)
+ : await dbConnection.BeginTransactionAsync(cancellationToken);
+
+ var transactionId = Guid.NewGuid();
+ AmbientTransaction = new AmbientTransaction(this, transaction, transactionId);
+
+ return AmbientTransaction;
+ }
+ catch (DbException)
+ {
+ await dbConnection.DisposeAsync();
+ throw;
+ }
+ }
+
+ internal void Detach(AmbientTransaction ambientTransaction)
+ {
+ ArgumentNullException.ThrowIfNull(ambientTransaction);
+
+ if (AmbientTransaction != null && AmbientTransaction == ambientTransaction)
+ {
+ AmbientTransaction = null;
+ }
+ else
+ {
+ throw new InvalidOperationException("Failed to detach ambient transaction.");
+ }
+ }
+}
diff --git a/src/Examples/DapperExample/Controllers/OperationsController.cs b/src/Examples/DapperExample/Controllers/OperationsController.cs
new file mode 100644
index 0000000000..ed15c6e9a2
--- /dev/null
+++ b/src/Examples/DapperExample/Controllers/OperationsController.cs
@@ -0,0 +1,12 @@
+using JsonApiDotNetCore.AtomicOperations;
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Middleware;
+using JsonApiDotNetCore.Resources;
+
+namespace DapperExample.Controllers;
+
+public sealed class OperationsController(
+ IJsonApiOptions options, IResourceGraph resourceGraph, ILoggerFactory loggerFactory, IOperationsProcessor processor, IJsonApiRequest request,
+ ITargetedFields targetedFields, IAtomicOperationFilter operationFilter)
+ : JsonApiOperationsController(options, resourceGraph, loggerFactory, processor, request, targetedFields, operationFilter);
diff --git a/src/Examples/DapperExample/DapperExample.csproj b/src/Examples/DapperExample/DapperExample.csproj
new file mode 100644
index 0000000000..ed7bd358eb
--- /dev/null
+++ b/src/Examples/DapperExample/DapperExample.csproj
@@ -0,0 +1,21 @@
+
+
+ net9.0;net8.0
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Examples/DapperExample/Data/AppDbContext.cs b/src/Examples/DapperExample/Data/AppDbContext.cs
new file mode 100644
index 0000000000..31f09b277c
--- /dev/null
+++ b/src/Examples/DapperExample/Data/AppDbContext.cs
@@ -0,0 +1,80 @@
+using DapperExample.Models;
+using JetBrains.Annotations;
+using Microsoft.EntityFrameworkCore;
+using Microsoft.EntityFrameworkCore.Metadata;
+
+// @formatter:wrap_chained_method_calls chop_always
+
+namespace DapperExample.Data;
+
+[UsedImplicitly(ImplicitUseTargetFlags.Members)]
+public sealed class AppDbContext : DbContext
+{
+ private readonly IConfiguration _configuration;
+
+ public DbSet TodoItems => Set();
+ public DbSet People => Set();
+ public DbSet LoginAccounts => Set();
+ public DbSet AccountRecoveries => Set();
+ public DbSet Tags => Set();
+ public DbSet RgbColors => Set();
+
+ public AppDbContext(DbContextOptions options, IConfiguration configuration)
+ : base(options)
+ {
+ ArgumentNullException.ThrowIfNull(configuration);
+
+ _configuration = configuration;
+ }
+
+ protected override void OnModelCreating(ModelBuilder builder)
+ {
+ builder.Entity()
+ .HasMany(person => person.AssignedTodoItems)
+ .WithOne(todoItem => todoItem.Assignee);
+
+ builder.Entity()
+ .HasMany(person => person.OwnedTodoItems)
+ .WithOne(todoItem => todoItem.Owner);
+
+ builder.Entity()
+ .HasOne(person => person.Account)
+ .WithOne(loginAccount => loginAccount.Person)
+ .HasForeignKey("AccountId");
+
+ builder.Entity()
+ .HasOne(loginAccount => loginAccount.Recovery)
+ .WithOne(accountRecovery => accountRecovery.Account)
+ .HasForeignKey("RecoveryId");
+
+ builder.Entity()
+ .HasOne(tag => tag.Color)
+ .WithOne(rgbColor => rgbColor.Tag)
+ .HasForeignKey("TagId");
+
+ var databaseProvider = _configuration.GetValue("DatabaseProvider");
+
+ if (databaseProvider != DatabaseProvider.SqlServer)
+ {
+ // In this example project, all cascades happen in the database, but SQL Server doesn't support that very well.
+ AdjustDeleteBehaviorForJsonApi(builder);
+ }
+ }
+
+ private static void AdjustDeleteBehaviorForJsonApi(ModelBuilder builder)
+ {
+ foreach (IMutableForeignKey foreignKey in builder.Model.GetEntityTypes()
+ .SelectMany(entityType => entityType.GetForeignKeys()))
+ {
+ if (foreignKey.DeleteBehavior == DeleteBehavior.ClientSetNull)
+ {
+ foreignKey.DeleteBehavior = DeleteBehavior.SetNull;
+ }
+
+ if (foreignKey.DeleteBehavior == DeleteBehavior.ClientCascade)
+ {
+ foreignKey.DeleteBehavior = DeleteBehavior.Cascade;
+ }
+ }
+ }
+}
diff --git a/src/Examples/DapperExample/Data/RotatingList.cs b/src/Examples/DapperExample/Data/RotatingList.cs
new file mode 100644
index 0000000000..278c34140a
--- /dev/null
+++ b/src/Examples/DapperExample/Data/RotatingList.cs
@@ -0,0 +1,30 @@
+namespace DapperExample.Data;
+
+internal abstract class RotatingList
+{
+ public static RotatingList Create(int count, Func createElement)
+ {
+ List elements = [];
+
+ for (int index = 0; index < count; index++)
+ {
+ T element = createElement(index);
+ elements.Add(element);
+ }
+
+ return new RotatingList(elements);
+ }
+}
+
+internal sealed class RotatingList