diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index f8589a73a7..c4b7aeba5e 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -25,6 +25,12 @@ "commands": [ "reportgenerator" ] + }, + "docfx": { + "version": "2.60.2", + "commands": [ + "docfx" + ] } } } diff --git a/appveyor.yml b/appveyor.yml index 81ba53020c..ff9191da7c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -45,17 +45,13 @@ for: # https://dotnet.github.io/docfx/tutorial/docfx_getting_started.html git checkout $env:APPVEYOR_REPO_BRANCH -q } - choco install docfx -y - if ($lastexitcode -ne 0) { - throw "docfx install failed with exit code $lastexitcode." - } after_build: - pwsh: | CD ./docs & ./generate-examples.ps1 - & docfx docfx.json - if ($lastexitcode -ne 0) { - throw "docfx build failed with exit code $lastexitcode." + & dotnet docfx docfx.json + if ($LastExitCode -ne 0) { + throw "docfx failed with exit code $LastExitCode." } # https://www.appveyor.com/docs/how-to/git-push/ diff --git a/docs/README.md b/docs/README.md index bd33197f00..af8b89537f 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,17 +1,9 @@ # Intro -Documentation for JsonApiDotNetCore is produced using [DocFX](https://dotnet.github.io/docfx/) from several files in this directory. +Documentation for JsonApiDotNetCore is produced using [docfx](https://dotnet.github.io/docfx/) from several files in this directory. In addition, the example request/response pairs are generated by executing `curl` commands against the GettingStarted project. # Installation -Run the following commands once to setup your system: - -``` -choco install docfx -y -``` - -``` -npm install -g httpserver -``` +You need to have 'npm' installed. Download Node.js from https://nodejs.org/. # Running The next command regenerates the documentation website and opens it in your default browser: diff --git a/docs/build-dev.ps1 b/docs/build-dev.ps1 index a5ee0ff947..5212429b7d 100644 --- a/docs/build-dev.ps1 +++ b/docs/build-dev.ps1 @@ -1,17 +1,48 @@ -# This script assumes that you have already installed docfx and httpserver. -# If that's not the case, run the next commands: -# choco install docfx -y -# npm install -g httpserver +#Requires -Version 7.0 -Remove-Item _site -Recurse -ErrorAction Ignore +# This script builds the documentation website, starts a web server and opens the site in your browser. Intended for local development. -dotnet build .. --configuration Release -Invoke-Expression ./generate-examples.ps1 +param( + # Specify -NoBuild to skip code build and examples generation. This runs faster, so handy when only editing Markdown files. + [switch] $NoBuild=$False +) -docfx ./docfx.json -Copy-Item home/*.html _site/ -Copy-Item home/*.ico _site/ -Copy-Item -Recurse home/assets/* _site/styles/ +function VerifySuccessExitCode { + if ($LastExitCode -ne 0) { + throw "Command failed with exit code $LastExitCode." + } +} + +function EnsureHttpServerIsInstalled { + if ((Get-Command "npm" -ErrorAction SilentlyContinue) -eq $null) { + throw "Unable to find npm in your PATH. please install Node.js first." + } + + npm list --depth 1 --global httpserver >$null + + if ($LastExitCode -eq 1) { + npm install -g httpserver + } +} + +EnsureHttpServerIsInstalled +VerifySuccessExitCode + +if (-Not $NoBuild -Or -Not (Test-Path -Path _site)) { + Remove-Item _site -Recurse -ErrorAction Ignore + + dotnet build .. --configuration Release + VerifySuccessExitCode + + Invoke-Expression ./generate-examples.ps1 +} + +dotnet docfx ./docfx.json +VerifySuccessExitCode + +Copy-Item -Force home/*.html _site/ +Copy-Item -Force home/*.ico _site/ +Copy-Item -Force -Recurse home/assets/* _site/styles/ cd _site $webServerJob = httpserver & diff --git a/docs/generate-examples.ps1 b/docs/generate-examples.ps1 index 6f7f7dc574..468b8447ac 100644 --- a/docs/generate-examples.ps1 +++ b/docs/generate-examples.ps1 @@ -8,7 +8,7 @@ function Get-WebServer-ProcessId { $processId = $(lsof -ti:14141) } elseif ($IsWindows) { - $processId = $(Get-NetTCPConnection -LocalPort 14141 -ErrorAction SilentlyContinue).OwningProcess + $processId = $(Get-NetTCPConnection -LocalPort 14141 -ErrorAction SilentlyContinue).OwningProcess?[0] } else { throw [System.Exception] "Unsupported operating system." @@ -22,7 +22,11 @@ function Kill-WebServer { if ($processId -ne $null) { Write-Output "Stopping web server" - Get-Process -Id $processId | Stop-Process + Get-Process -Id $processId | Stop-Process -ErrorVariable stopErrorMessage + + if ($stopErrorMessage) { + throw "Failed to stop web server: $stopErrorMessage" + } } } @@ -40,18 +44,21 @@ function Start-WebServer { Kill-WebServer Start-WebServer -Remove-Item -Force -Path .\request-examples\*.json +try { + Remove-Item -Force -Path .\request-examples\*.json -$scriptFiles = Get-ChildItem .\request-examples\*.ps1 -foreach ($scriptFile in $scriptFiles) { - $jsonFileName = [System.IO.Path]::GetFileNameWithoutExtension($scriptFile.Name) + "_Response.json" + $scriptFiles = Get-ChildItem .\request-examples\*.ps1 + foreach ($scriptFile in $scriptFiles) { + $jsonFileName = [System.IO.Path]::GetFileNameWithoutExtension($scriptFile.Name) + "_Response.json" - Write-Output "Writing file: $jsonFileName" - & $scriptFile.FullName > .\request-examples\$jsonFileName + Write-Output "Writing file: $jsonFileName" + & $scriptFile.FullName > .\request-examples\$jsonFileName - if ($LastExitCode -ne 0) { - throw [System.Exception] "Example request from '$($scriptFile.Name)' failed with exit code $LastExitCode." + if ($LastExitCode -ne 0) { + throw [System.Exception] "Example request from '$($scriptFile.Name)' failed with exit code $LastExitCode." + } } } - -Kill-WebServer +finally { + Kill-WebServer +} diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md new file mode 100644 index 0000000000..2acde9d7a8 --- /dev/null +++ b/docs/getting-started/faq.md @@ -0,0 +1,169 @@ +# Frequently Asked Questions + +#### Where can I find documentation and examples? +While the [documentation](~/usage/resources/index.md) covers basic features and a few runnable example projects are available [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples), +many more advanced use cases are available as integration tests [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/test/JsonApiDotNetCoreTests/IntegrationTests), so be sure to check them out! + +#### Why can't I use OpenAPI? +Due to the mismatch between the JSON:API structure and the shape of ASP.NET controller methods, this does not work out of the box. +This is high on our agenda and we're steadily making progress, but it's quite complex and far from complete. +See [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1046) for the current status, which includes instructions on trying out the latest build. + +#### What's available to implement a JSON:API 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 it. +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. + +In the long term, we'd like to solve this through OpenAPI, which enables the generation of a (statically typed) client library in various languages. + +#### 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 verbose logging 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": "Verbose" + } + } + } + ``` + +#### 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/en-us/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/en-us/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, which you need to populate. +- `ISparseFieldSetCache`: This tells the response serializer which fields to render in the attributes and relationship objects. You need to populate this as well. + +You may also want to inject the singletons `IJsonApiOptions` (which contains settings such as default page size) and `IResourceGraph` (the JSON:API model of resources and relationships). + +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, analyze query strings or populate caches for the serializer. +And most resource definition callbacks are handled. +That's because the built-in resource service translates all JSON:API 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 may reuse most of [QueryableBuilder](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/QueryableBuilder.cs), +which drives the translation into [System.Linq.Expressions](https://learn.microsoft.com/en-us/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 likely need to prevent that from happening. +We use this for accessing [MongoDB](https://github.com/json-api-dotnet/JsonApiDotNetCore.MongoDb/blob/674889e037334e3f376550178ce12d0842d7560c/src/JsonApiDotNetCore.MongoDb/Queries/Internal/QueryableBuilding/MongoQueryableBuilder.cs). + +> [!TIP] +> [ExpressionTreeVisualizer](https://github.com/zspitz/ExpressionTreeVisualizer) 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. +Of course, a simple thank-you message in our [Gitter channel](https://gitter.im/json-api-dotnet-core/Lobby) is appreciated too! +We don't take monetary contributions at the moment. + +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/getting-started/toc.md b/docs/getting-started/toc.md new file mode 100644 index 0000000000..12f943b7fa --- /dev/null +++ b/docs/getting-started/toc.md @@ -0,0 +1,5 @@ +# [Installation](install.md) + +# [Step By Step](step-by-step.md) + +# [FAQ](faq.md) diff --git a/docs/getting-started/toc.yml b/docs/getting-started/toc.yml deleted file mode 100644 index 4a2a008591..0000000000 --- a/docs/getting-started/toc.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: Installation - href: install.md - -- name: Step By Step - href: step-by-step.md \ No newline at end of file diff --git a/docs/request-examples/index.md b/docs/request-examples/index.md index 4d82e95854..c34b3d713a 100644 --- a/docs/request-examples/index.md +++ b/docs/request-examples/index.md @@ -4,7 +4,8 @@ These requests have been generated against the "GettingStarted" application and All of these requests have been created using out-of-the-box features. -_Note that cURL requires "[" and "]" in URLs to be escaped._ +> [!NOTE] +> curl requires "[" and "]" in URLs to be escaped. # Reading data diff --git a/docs/usage/caching.md b/docs/usage/caching.md index d5f644997b..537ec70e4b 100644 --- a/docs/usage/caching.md +++ b/docs/usage/caching.md @@ -59,8 +59,8 @@ ETag: "356075D903B8FE8D9921201A7E7CD3F9" "data": [ ... ] } ``` - -**Note:** To just poll for changes (without fetching them), send a HEAD request instead: +> [!TIP] +> To just poll for changes (without fetching them), send a HEAD request instead. ```http HEAD /articles?sort=-lastModifiedAt HTTP/1.1 diff --git a/docs/usage/common-pitfalls.md b/docs/usage/common-pitfalls.md new file mode 100644 index 0000000000..7941face82 --- /dev/null +++ b/docs/usage/common-pitfalls.md @@ -0,0 +1,143 @@ +# Common Pitfalls + +This section lists various problems we've seen users run into over the years when using JsonApiDotNetCore. +See also [Frequently Asked Questions](~/getting-started/faq.md). + +#### JSON:API resources are not DTOs or ViewModels +This is a common misconception. +Similar to a database model, which consists of tables and foreign keys, JSON:API defines resources that are connected via relationships. +You're opening up a can of worms when trying to model a single table to multiple JSON:API resources. + +This is best clarified using an example. Let's assume we're building a public website and an admin portal, both using the same API. +The API uses the database tables "Customers" and "LoginAccounts", having a one-to-one relationship between them. + +Now let's try to define the resource classes: +```c# +[Table("Customers")] +public sealed class WebCustomer : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [HasOne] + public LoginAccount? Account { get; set; } +} + +[Table("Customers")] +public sealed class AdminCustomer : Identifiable +{ + [Attr] + public string Name { get; set; } = null!; + + [Attr] + public string? CreditRating { get; set; } + + [HasOne] + public LoginAccount? Account { get; set; } +} + +[Table("LoginAccounts")] +public sealed class LoginAccount : Identifiable +{ + [Attr] + public string EmailAddress { get; set; } = null!; + + [HasOne] + public ??? Customer { get; set; } +} +``` +Did you notice the missing type of the `LoginAccount.Customer` property? We must choose between `WebCustomer` or `AdminCustomer`, but neither is correct. +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. +- 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. + +#### JSON:API resources are not DDD domain entities +In [Domain-driven design](https://martinfowler.com/bliki/DomainDrivenDesign.html), it's considered best practice to implement business rules inside entities, with changes being controlled through an aggregate root. +This paradigm [doesn't work well](https://github.com/json-api-dotnet/JsonApiDotNetCore/issues/1092#issuecomment-932749676) with JSON:API, because each resource can be changed in isolation. +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, +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/). +Validation and business rules are typically implemented in [Resource Definitions](~/usage/extensibility/resource-definitions.md). + +#### Model relationships instead of foreign key attributes +It may be tempting to expose numeric resource attributes such as `customerId`, `orderId`, etc. You're better off using relationships instead, because they give you +the richness of JSON:API. For example, it enables users to include related resources in a single request, apply filters over related resources and use dedicated endpoints for updating relationships. +As an API developer, you'll benefit from rich input validation and fine-grained control for setting what's permitted when users access relationships. + +#### Model relationships instead of complex (JSON) attributes +Similar to the above, returning a complex object takes away all the relationship features of JSON:API. Users can't filter inside a complex object. Or update +a nested value, without risking accidentally overwriting another unrelated nested value from a concurrent request. Basically, there's no partial PATCH to prevent that. + +#### Stay away from stored procedures +There are [many reasons](https://stackoverflow.com/questions/1761601/is-the-usage-of-stored-procedures-a-bad-practice/9483781#9483781) to not use stored procedures. +But with JSON:API, there's an additional concern. Due to its dynamic nature of filtering, sorting, pagination, sparse fieldsets, and including related resources, +the number of required stored procedures to support all that either explodes, or you'll end up with one extremely complex stored proceduce to handle it all. +With stored procedures, you're either going to have a lot of work to do, or you'll end up with an API that has very limited capabilities. +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. +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()`. +When using [Auto-discovery](~/usage/resource-graph.md#auto-discovery), you don't need to register these at all. + +#### 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: +```c# +// Program.cs +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. + +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). + +#### One-to-one relationships require custom Entity Framework Core mappings +Entity Framework Core has great conventions and sane mapping defaults. But two of them are problematic for JSON:API: identifying foreign keys and default delete behavior. +See [here](~/usage/resources/relationships.md#one-to-one-relationships-in-entity-framework-core) for how to get it right. + +#### Prefer model attributes over fluent mappings +Validation attributes such as `[Required]` are detected by ASP.NET ModelState validation, Entity Framework Core, OpenAPI, and JsonApiDotNetCore. +When using a Fluent API instead, the other frameworks cannot know about it, resulting in a less streamlined experience. + +#### Validation of `[Required]` value types doesn't work +This is a limitation of ASP.NET ModelState validation. For example: +```c# +[Required] public int Age { get; set; } +``` +won't cause a validation error when sending `0` or omitting it entirely in the request body. +This limitation does not apply to reference types. +The workaround is to make it nullable: +```c# +[Required] public int? Age { get; set; } +``` +Entity Framework Core recognizes this and generates a non-nullable column. + +#### Don't change resource property values from POST/PATCH controller methods +It simply won't work. Without going into details, this has to do with JSON:API partial POST/PATCH. +Use [Resource Definition](~/usage/extensibility/resource-definitions.md) callback methods to apply such changes from code. + +#### You can't mix up pipeline methods +For example, you can't call `service.UpdateAsync()` from `controller.GetAsync()`, or call `service.SetRelationshipAsync()` from `controller.PatchAsync()`. +The reason is that various ambient injectable objects are in play, used to track what's going on during the request pipeline internally. +And they won't match up with the current endpoint when switching to a different pipeline halfway during a request. + +If you need such side effects, it's easiest to inject your `DbContext` in the controller, directly apply the changes on it and save. +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), +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/layer-overview.md b/docs/usage/extensibility/layer-overview.md index 2fe99e2fbd..9233508179 100644 --- a/docs/usage/extensibility/layer-overview.md +++ b/docs/usage/extensibility/layer-overview.md @@ -23,8 +23,6 @@ on your needs, you may want to replace other parts by deriving from the built-in ## Replacing injected services -**Note:** If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), then resource services, repositories and resource definitions will be automatically registered for you. - Replacing built-in services is done on a per-resource basis and can be done at startup. For convenience, extension methods are provided to register layers on all their implemented interfaces. @@ -37,3 +35,6 @@ builder.Services.AddResourceDefinition(); builder.Services.AddScoped(); builder.Services.AddScoped(); ``` + +> [!TIP] +> If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), then resource services, repositories and resource definitions will be automatically registered for you. diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md index 102406e71b..733634bf33 100644 --- a/docs/usage/extensibility/repositories.md +++ b/docs/usage/extensibility/repositories.md @@ -13,13 +13,14 @@ builder.Services.AddScoped, ArticleReposi In v4.0 we introduced an extension method that you can use to register a resource repository on all of its JsonApiDotNetCore interfaces. This is helpful when you implement (a subset of) the resource interfaces and want to register them all in one go. -**Note:** If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), this happens automatically. - ```c# // Program.cs builder.Services.AddResourceRepository(); ``` +> [!TIP] +> If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), then resource services, repositories and resource definitions will be automatically registered for you. + A sample implementation that performs authorization might look like this. All of the methods in EntityFrameworkCoreRepository will use the `GetAll()` method to get the `DbSet`, so this is a good method to apply filters such as user or tenant authorization. diff --git a/docs/usage/extensibility/resource-definitions.md b/docs/usage/extensibility/resource-definitions.md index 6bc16b869e..cf5400b722 100644 --- a/docs/usage/extensibility/resource-definitions.md +++ b/docs/usage/extensibility/resource-definitions.md @@ -7,19 +7,19 @@ They are resolved from the dependency injection container, so you can inject dep In v4.2 we introduced an extension method that you can use to register your resource definition. -**Note:** If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), this happens automatically. - ```c# // Program.cs builder.Services.AddResourceDefinition(); ``` -**Note:** Prior to the introduction of auto-discovery (in v3), you needed to register the -resource definition on the container yourself: +> [!TIP] +> If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), then resource services, repositories and resource definitions will be automatically registered for you. -```c# -builder.Services.AddScoped, ArticleDefinition>(); -``` +> [!NOTE] +> Prior to the introduction of auto-discovery (in v3), you needed to register the resource definition on the container yourself: +> ```c# +> builder.Services.AddScoped, ArticleDefinition>(); +> ``` ## Customizing queries @@ -37,7 +37,8 @@ from Entity Framework Core `IQueryable` execution. There are some cases where you want attributes or relationships conditionally excluded from your resource response. For example, you may accept some sensitive data that should only be exposed to administrators after creation. -**Note:** to exclude fields unconditionally, [attribute capabilities](~/usage/resources/attributes.md#capabilities) and [relationship capabilities](~/usage/resources/relationships.md#capabilities) can be used instead. +> [!NOTE] +> To exclude fields unconditionally, [attribute capabilities](~/usage/resources/attributes.md#capabilities) and [relationship capabilities](~/usage/resources/relationships.md#capabilities) can be used instead. ```c# public class UserDefinition : JsonApiResourceDefinition @@ -218,7 +219,8 @@ _since v3_ You can define additional query string parameters with the LINQ expression that should be used. 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. +> [!NOTE] +> 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/services.md b/docs/usage/extensibility/services.md index bc3dd5bff8..6cdea8b783 100644 --- a/docs/usage/extensibility/services.md +++ b/docs/usage/extensibility/services.md @@ -135,13 +135,14 @@ builder.Services.AddScoped, ArticleService>(); In v3.0 we introduced an extension method that you can use to register a resource service on all of its JsonApiDotNetCore interfaces. This is helpful when you implement (a subset of) the resource interfaces and want to register them all in one go. -**Note:** If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), this happens automatically. - ```c# // Program.cs builder.Services.AddResourceService(); ``` +> [!TIP] +> If you're using [auto-discovery](~/usage/resource-graph.md#auto-discovery), then resource services, repositories and resource definitions will be automatically registered for you. + Then on your model, pass in the set of endpoints to expose (the ones that you've registered services for): ```c# diff --git a/docs/usage/options.md b/docs/usage/options.md index 83d535bce4..919642c5c8 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -24,7 +24,11 @@ options.AllowClientGeneratedIds = true; The default page size used for all resources can be overridden in options (10 by default). To disable paging, set it to `null`. The maximum page size and number allowed from client requests can be set too (unconstrained by default). -You can also include the total number of resources in each response. Note that when using this feature, it does add some query overhead since we have to also request the total number of resources. + +You can also include the total number of resources in each response. + +> [!NOTE] +> Including the total number of resources adds some overhead, because the count is fetched in a separate query. ```c# options.DefaultPageSize = new PageSize(25); diff --git a/docs/usage/reading/filtering.md b/docs/usage/reading/filtering.md index b99a14c3b2..8a568078a0 100644 --- a/docs/usage/reading/filtering.md +++ b/docs/usage/reading/filtering.md @@ -69,7 +69,8 @@ GET /articles?include=author,tags&filter=equals(author.lastName,'Smith')&filter[ In the above request, the first filter is applied on the collection of articles, while the second one is applied on the nested collection of tags. -Note this does **not** hide articles without any matching tags! Use the `has` function with a filter condition (see below) to accomplish that. +> [!WARNING] +> The request above does **not** hide articles without any matching tags! Use the `has` function with a filter condition (see below) to accomplish that. Putting it all together, you can build quite complex filters, such as: diff --git a/docs/usage/reading/sparse-fieldset-selection.md b/docs/usage/reading/sparse-fieldset-selection.md index 5c08bc6ae4..6491cb050b 100644 --- a/docs/usage/reading/sparse-fieldset-selection.md +++ b/docs/usage/reading/sparse-fieldset-selection.md @@ -36,7 +36,8 @@ Example for both top-level and relationship: GET /articles?include=author&fields[articles]=title,body,author&fields[authors]=name HTTP/1.1 ``` -Note that in the last example, the `author` relationship is also added to the `articles` fieldset, so that the relationship from article to author is returned. +> [!NOTE] +> In the last example, the `author` relationship is also added to the `articles` fieldset, so that the relationship from article to author is returned. When omitted, you'll get the included resources returned, but without full resource linkage (as described [here](https://jsonapi.org/examples/#sparse-fieldsets)). ## Overriding diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 4010cbea5f..5e3195ca0b 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -3,7 +3,8 @@ The `ResourceGraph` is a map of all the JSON:API resources and their relationships that your API serves. It is built at app startup and available as a singleton through Dependency Injection. -**Note:** Prior to v4 this was called the `ContextGraph`. +> [!NOTE] +> Prior to v4, this was called the `ContextGraph`. ## Constructing The Graph diff --git a/docs/usage/resources/index.md b/docs/usage/resources/index.md index 552b3886fa..f8e7d29156 100644 --- a/docs/usage/resources/index.md +++ b/docs/usage/resources/index.md @@ -8,7 +8,8 @@ public class Person : Identifiable } ``` -**Note:** Earlier versions of JsonApiDotNetCore allowed a short-hand notation when `TId` is of type `int`. This was removed in v5. +> [!NOTE] +> Earlier versions of JsonApiDotNetCore allowed a short-hand notation when `TId` is of type `int`. This was removed in v5. If you need to attach annotations or attributes on the `Id` property, you can override the virtual property. diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 1787bbd8ac..689b3aa4d2 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -277,7 +277,8 @@ This can be overridden per relationship. Indicates whether the relationship can be returned in responses. When not allowed and requested using `?fields[]=`, it results in an HTTP 400 response. Otherwise, the relationship (and its related resources, when included) are silently omitted. -Note that this setting does not affect retrieving the related resources directly. +> [!WARNING] +> This setting does not affect retrieving the related resources directly. ```c# #nullable enable diff --git a/docs/usage/toc.md b/docs/usage/toc.md index c30a2b0f37..c23d8f7308 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -24,6 +24,8 @@ # [Metadata](meta.md) # [Caching](caching.md) +# [Common Pitfalls](common-pitfalls.md) + # Extensibility ## [Layer Overview](extensibility/layer-overview.md) ## [Resource Definitions](extensibility/resource-definitions.md) diff --git a/docs/usage/writing/creating.md b/docs/usage/writing/creating.md index 4cbe42602e..ba0a21d52b 100644 --- a/docs/usage/writing/creating.md +++ b/docs/usage/writing/creating.md @@ -71,4 +71,6 @@ POST /articles?include=owner&fields[people]=firstName HTTP/1.1 } ``` -After the resource has been created on the server, it is re-fetched from the database using the specified query string parameters and returned to the client. +> [!NOTE] +> After the resource has been created on the server, it is re-fetched from the database using the specified query string parameters and returned to the client. +> However, the used query string parameters only have an effect when `200 OK` is returned. diff --git a/docs/usage/writing/updating.md b/docs/usage/writing/updating.md index 132d487cfe..01d0740cba 100644 --- a/docs/usage/writing/updating.md +++ b/docs/usage/writing/updating.md @@ -21,12 +21,14 @@ POST /articles HTTP/1.1 This preserves the values of all other unsent attributes and is called a *partial patch*. When only the attributes that were sent in the request have changed, the server returns `204 No Content`. -But if additional attributes have changed (for example, by a database trigger that refreshes the last-modified date) the server returns `200 OK`, along with all attributes of the updated resource. +But if additional attributes have changed (for example, by a database trigger or [resource definition](~/usage/extensibility/resource-definitions.md) that refreshes the last-modified date) the server returns `200 OK`, along with all attributes of the updated resource. ## Updating resource relationships Besides its attributes, the relationships of a resource can be changed using a PATCH request too. -Note that all resources being assigned in a relationship must already exist. + +> [!NOTE] +> All resources being assigned in a relationship must already exist. When updating a HasMany relationship, the existing set is replaced by the new set. See below on how to add/remove resources. @@ -65,7 +67,8 @@ PATCH /articles/1 HTTP/1.1 A HasOne relationship can be cleared by setting `data` to `null`, while a HasMany relationship can be cleared by setting it to an empty array. -By combining the examples above, both attributes and relationships can be updated using a single PATCH request. +> [!TIP] +> By combining the examples above, both attributes and relationships can be updated using a single PATCH request. ## Response body @@ -79,8 +82,9 @@ PATCH /articles/1?include=owner&fields[people]=firstName HTTP/1.1 } ``` -After the resource has been updated on the server, it is re-fetched from the database using the specified query string parameters and returned to the client. -Note this only has an effect when `200 OK` is returned. +> [!NOTE] +> After the resource has been updated on the server, it is re-fetched from the database using the specified query string parameters and returned to the client. +> However, the used query string parameters only have an effect when `200 OK` is returned. # Updating relationships diff --git a/inspectcode.ps1 b/inspectcode.ps1 index 16dccfd373..b379bce1c6 100644 --- a/inspectcode.ps1 +++ b/inspectcode.ps1 @@ -4,16 +4,16 @@ dotnet tool restore -if ($LASTEXITCODE -ne 0) { - throw "Tool restore failed with exit code $LASTEXITCODE" +if ($LastExitCode -ne 0) { + throw "Tool restore failed with exit code $LastExitCode" } $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 -if ($LASTEXITCODE -ne 0) { - throw "Code inspection failed with exit code $LASTEXITCODE" +if ($LastExitCode -ne 0) { + throw "Code inspection failed with exit code $LastExitCode" } [xml]$xml = Get-Content "$outputPath"