diff --git a/README.md b/README.md index fb58acf055..8dd6a6ecfc 100644 --- a/README.md +++ b/README.md @@ -20,16 +20,23 @@ The ultimate goal of this library is to eliminate as much boilerplate as possibl The following steps describe how to create a JSON:API project. +1. Create a new ASP.NET Core Web API project: + + ```bash + dotnet new webapi --no-openapi --use-controllers --name ExampleJsonApi + cd ExampleJsonApi + ``` + 1. Install the JsonApiDotNetCore package, along with your preferred Entity Framework Core provider: + ```bash dotnet add package JsonApiDotNetCore dotnet add package Microsoft.EntityFrameworkCore.Sqlite ``` 1. Declare your entities, annotated with JsonApiDotNetCore attributes: - ```c# - #nullable enable + ```c# [Resource] public class Person : Identifiable { @@ -40,6 +47,7 @@ The following steps describe how to create a JSON:API project. ``` 1. Define your `DbContext`, seeding the database with sample data: + ```c# public class AppDbContext(DbContextOptions options) : DbContext(options) { @@ -70,6 +78,7 @@ The following steps describe how to create a JSON:API project. ``` 1. Configure Entity Framework Core and JsonApiDotNetCore in `Program.cs`: + ```c# var builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContext(); @@ -96,11 +105,13 @@ The following steps describe how to create a JSON:API project. ``` 1. Start your API + ```bash dotnet run ``` 1. Send a GET request to retrieve data: + ```bash GET http://localhost:5000/people?filter=equals(firstName,'John')&include=children HTTP/1.1 ``` @@ -256,7 +267,7 @@ To build the code from this repository locally, run: dotnet build ``` -Running tests locally requires access to a PostgreSQL database. If you have docker installed, this can started via: +Running tests locally requires access to a PostgreSQL database. If you have docker installed, this can be started via: ```bash pwsh run-docker-postgres.ps1 @@ -279,6 +290,6 @@ pwsh Build.ps1 We are very grateful to the sponsors below, who have provided us with a no-cost license for their tools. JetBrains Logo   -Araxis Logo +Araxis Logo Do you like this project? Consider to [sponsor](https://github.com/sponsors/json-api-dotnet), or just reward us by giving our repository a star. diff --git a/docs/api/JsonApiDotNetCore.Controllers.Annotations.NoHttpPatchAttribute.md b/docs/api/JsonApiDotNetCore.Controllers.Annotations.NoHttpPatchAttribute.md new file mode 100644 index 0000000000..b63f67faec --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Controllers.Annotations.NoHttpPatchAttribute.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.Annotations.ResourceAttribute.html#JsonApiDotNetCore_Resources_Annotations_ResourceAttribute_GenerateControllerEndpoints +--- diff --git a/docs/api/JsonApiDotNetCore.Controllers.JsonApiCommandController-1.md b/docs/api/JsonApiDotNetCore.Controllers.JsonApiCommandController-1.md new file mode 100644 index 0000000000..5d980615ff --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Controllers.JsonApiCommandController-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Controllers.JsonApiCommandController-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Controllers.ModelStateViolation.md b/docs/api/JsonApiDotNetCore.Controllers.ModelStateViolation.md new file mode 100644 index 0000000000..9414c98cc4 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Controllers.ModelStateViolation.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Errors.InvalidModelStateException.html +--- diff --git a/docs/api/JsonApiDotNetCore.Diagnostics.CascadingCodeTimer.md b/docs/api/JsonApiDotNetCore.Diagnostics.CascadingCodeTimer.md new file mode 100644 index 0000000000..1b27f4c576 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Diagnostics.CascadingCodeTimer.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Diagnostics.ICodeTimer.html +--- diff --git a/docs/api/JsonApiDotNetCore.Errors.ResourceIdInCreateResourceNotAllowedException.md b/docs/api/JsonApiDotNetCore.Errors.ResourceIdInCreateResourceNotAllowedException.md new file mode 100644 index 0000000000..bd889e5346 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Errors.ResourceIdInCreateResourceNotAllowedException.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Errors.ResourceAlreadyExistsException.html +--- diff --git a/docs/api/JsonApiDotNetCore.Errors.ResourceTypeMismatchException.md b/docs/api/JsonApiDotNetCore.Errors.ResourceTypeMismatchException.md new file mode 100644 index 0000000000..f840a3f3aa --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Errors.ResourceTypeMismatchException.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Errors.InvalidRequestBodyException.html +--- diff --git a/docs/api/JsonApiDotNetCore.Hooks.IResourceHookExecutorFacade.md b/docs/api/JsonApiDotNetCore.Hooks.IResourceHookExecutorFacade.md new file mode 100644 index 0000000000..59094b11cc --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Hooks.IResourceHookExecutorFacade.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.IResourceDefinition-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Hooks.Internal.Discovery.IHooksDiscovery.md b/docs/api/JsonApiDotNetCore.Hooks.Internal.Discovery.IHooksDiscovery.md new file mode 100644 index 0000000000..59094b11cc --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Hooks.Internal.Discovery.IHooksDiscovery.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.IResourceDefinition-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Hooks.Internal.Execution.DiffableResourceHashSet-1.md b/docs/api/JsonApiDotNetCore.Hooks.Internal.Execution.DiffableResourceHashSet-1.md new file mode 100644 index 0000000000..4cf783422b --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Hooks.Internal.Execution.DiffableResourceHashSet-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.JsonApiResourceDefinition-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Hooks.Internal.Execution.IResourceHashSet-1.md b/docs/api/JsonApiDotNetCore.Hooks.Internal.Execution.IResourceHashSet-1.md new file mode 100644 index 0000000000..59094b11cc --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Hooks.Internal.Execution.IResourceHashSet-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.IResourceDefinition-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Hooks.Internal.ICreateHookExecutor.md b/docs/api/JsonApiDotNetCore.Hooks.Internal.ICreateHookExecutor.md new file mode 100644 index 0000000000..dacf9c60b1 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Hooks.Internal.ICreateHookExecutor.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.IResourceDefinition-2.html#JsonApiDotNetCore_Resources_IResourceDefinition_2_OnWritingAsync__0_JsonApiDotNetCore_Middleware_WriteOperationKind_System_Threading_CancellationToken_ +--- diff --git a/docs/api/JsonApiDotNetCore.Hooks.Internal.IUpdateHookExecutor.md b/docs/api/JsonApiDotNetCore.Hooks.Internal.IUpdateHookExecutor.md new file mode 100644 index 0000000000..dacf9c60b1 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Hooks.Internal.IUpdateHookExecutor.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.IResourceDefinition-2.html#JsonApiDotNetCore_Resources_IResourceDefinition_2_OnWritingAsync__0_JsonApiDotNetCore_Middleware_WriteOperationKind_System_Threading_CancellationToken_ +--- diff --git a/docs/api/JsonApiDotNetCore.Middleware.JsonApiExtension.md b/docs/api/JsonApiDotNetCore.Middleware.JsonApiExtension.md new file mode 100644 index 0000000000..5ce9d0e02c --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Middleware.JsonApiExtension.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Middleware.JsonApiMediaTypeExtension.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Expressions.CollectionNotEmptyExpression.md b/docs/api/JsonApiDotNetCore.Queries.Expressions.CollectionNotEmptyExpression.md new file mode 100644 index 0000000000..05c7012a73 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Expressions.CollectionNotEmptyExpression.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.Expressions.HasExpression.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.IEvaluatedIncludeCache.md b/docs/api/JsonApiDotNetCore.Queries.Internal.IEvaluatedIncludeCache.md new file mode 100644 index 0000000000..d990b723a8 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.IEvaluatedIncludeCache.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.IEvaluatedIncludeCache.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.IncludeParser.md b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.IncludeParser.md new file mode 100644 index 0000000000..b7cc547960 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.IncludeParser.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.Parsing.IncludeParser.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.Keywords.md b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.Keywords.md new file mode 100644 index 0000000000..a1a604ffb2 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.Keywords.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.Parsing.Keywords.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryExpressionParser.md b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryExpressionParser.md new file mode 100644 index 0000000000..d3574188c1 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryExpressionParser.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.Parsing.QueryExpressionParser.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryStringParameterScopeParser.md b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryStringParameterScopeParser.md new file mode 100644 index 0000000000..0403d40ebc --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryStringParameterScopeParser.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.Parsing.QueryStringParameterScopeParser.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryTokenizer.md b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryTokenizer.md new file mode 100644 index 0000000000..0cf46bdf57 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.Parsing.QueryTokenizer.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.Parsing.QueryTokenizer.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.LambdaScope.md b/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.LambdaScope.md new file mode 100644 index 0000000000..1884dc786b --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.LambdaScope.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.QueryableBuilding.LambdaScope.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.SelectClauseBuilder.md b/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.SelectClauseBuilder.md new file mode 100644 index 0000000000..005a6b211f --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.SelectClauseBuilder.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.QueryableBuilding.SelectClauseBuilder.html +--- diff --git a/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.WhereClauseBuilder.md b/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.WhereClauseBuilder.md new file mode 100644 index 0000000000..5ff3e97e5d --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Queries.Internal.QueryableBuilding.WhereClauseBuilder.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Queries.QueryableBuilding.WhereClauseBuilder.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.IDefaultsQueryStringParameterReader.md b/docs/api/JsonApiDotNetCore.QueryStrings.IDefaultsQueryStringParameterReader.md new file mode 100644 index 0000000000..d46a266812 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.IDefaultsQueryStringParameterReader.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.IQueryStringParameterReader.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.Internal.DefaultsQueryStringParameterReader.md b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.DefaultsQueryStringParameterReader.md new file mode 100644 index 0000000000..0c6da2ca56 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.DefaultsQueryStringParameterReader.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.QueryStringParameterReader.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.Internal.FilterQueryStringParameterReader.md b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.FilterQueryStringParameterReader.md new file mode 100644 index 0000000000..6456874854 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.FilterQueryStringParameterReader.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.FilterQueryStringParameterReader.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.Internal.IncludeQueryStringParameterReader.md b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.IncludeQueryStringParameterReader.md new file mode 100644 index 0000000000..d8ceb2d5fa --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.IncludeQueryStringParameterReader.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.IncludeQueryStringParameterReader.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.Internal.PaginationQueryStringParameterReader.md b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.PaginationQueryStringParameterReader.md new file mode 100644 index 0000000000..d0fc4348cf --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.PaginationQueryStringParameterReader.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.PaginationQueryStringParameterReader.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.Internal.ResourceDefinitionQueryableParameterReader.md b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.ResourceDefinitionQueryableParameterReader.md new file mode 100644 index 0000000000..ef485e70a7 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.ResourceDefinitionQueryableParameterReader.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.ResourceDefinitionQueryableParameterReader.html +--- diff --git a/docs/api/JsonApiDotNetCore.QueryStrings.Internal.md b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.md new file mode 100644 index 0000000000..9535aea472 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.QueryStrings.Internal.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.QueryStrings.html +--- diff --git a/docs/api/JsonApiDotNetCore.Resources.Internal.md b/docs/api/JsonApiDotNetCore.Resources.Internal.md new file mode 100644 index 0000000000..f547f0925d --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Resources.Internal.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.html +--- diff --git a/docs/api/JsonApiDotNetCore.Resources.ResourceHooksDefinition-1.md b/docs/api/JsonApiDotNetCore.Resources.ResourceHooksDefinition-1.md new file mode 100644 index 0000000000..4cf783422b --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Resources.ResourceHooksDefinition-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Resources.JsonApiResourceDefinition-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.BaseDeserializer.md b/docs/api/JsonApiDotNetCore.Serialization.BaseDeserializer.md new file mode 100644 index 0000000000..e4bd9d0bf0 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.BaseDeserializer.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Request.Adapters.DocumentAdapter.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.BaseSerializer.md b/docs/api/JsonApiDotNetCore.Serialization.BaseSerializer.md new file mode 100644 index 0000000000..af24d07eb0 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.BaseSerializer.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Response.ResponseModelAdapter.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Building.IIncludedResourceObjectBuilder.md b/docs/api/JsonApiDotNetCore.Serialization.Building.IIncludedResourceObjectBuilder.md new file mode 100644 index 0000000000..79a6ec638b --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Building.IIncludedResourceObjectBuilder.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Response.ResponseModelAdapter.html#JsonApiDotNetCore_Serialization_Response_ResponseModelAdapter_ConvertResource_JsonApiDotNetCore_Resources_IIdentifiable_JsonApiDotNetCore_Configuration_ResourceType_JsonApiDotNetCore_Middleware_EndpointKind_ +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Building.IResourceObjectBuilder.md b/docs/api/JsonApiDotNetCore.Serialization.Building.IResourceObjectBuilder.md new file mode 100644 index 0000000000..79a6ec638b --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Building.IResourceObjectBuilder.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Response.ResponseModelAdapter.html#JsonApiDotNetCore_Serialization_Response_ResponseModelAdapter_ConvertResource_JsonApiDotNetCore_Resources_IIdentifiable_JsonApiDotNetCore_Configuration_ResourceType_JsonApiDotNetCore_Middleware_EndpointKind_ +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Building.ResourceObjectBuilderSettings.md b/docs/api/JsonApiDotNetCore.Serialization.Building.ResourceObjectBuilderSettings.md new file mode 100644 index 0000000000..03cbfa162e --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Building.ResourceObjectBuilderSettings.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Configuration.JsonApiOptions.html#JsonApiDotNetCore_Configuration_JsonApiOptions_SerializerOptions +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.ManyResponse-1.md b/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.ManyResponse-1.md new file mode 100644 index 0000000000..2b6744f22c --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.ManyResponse-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: ../usage/openapi-client.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.RequestSerializer.md b/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.RequestSerializer.md new file mode 100644 index 0000000000..2b6744f22c --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.RequestSerializer.md @@ -0,0 +1,3 @@ +--- +redirect_url: ../usage/openapi-client.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.md b/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.md new file mode 100644 index 0000000000..2b6744f22c --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Client.Internal.md @@ -0,0 +1,3 @@ +--- +redirect_url: ../usage/openapi-client.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.IJsonApiDeserializer.md b/docs/api/JsonApiDotNetCore.Serialization.IJsonApiDeserializer.md new file mode 100644 index 0000000000..767e0c94d2 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.IJsonApiDeserializer.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Request.Adapters.IDocumentAdapter.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.IJsonApiWriter.md b/docs/api/JsonApiDotNetCore.Serialization.IJsonApiWriter.md new file mode 100644 index 0000000000..b9bbf20b7c --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.IJsonApiWriter.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Response.IJsonApiWriter.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.Objects.IResourceIdentity.md b/docs/api/JsonApiDotNetCore.Serialization.Objects.IResourceIdentity.md new file mode 100644 index 0000000000..4a3f2ca610 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.Objects.IResourceIdentity.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Serialization.Objects.ResourceIdentity.html +--- diff --git a/docs/api/JsonApiDotNetCore.Serialization.ResponseSerializerFactory.md b/docs/api/JsonApiDotNetCore.Serialization.ResponseSerializerFactory.md new file mode 100644 index 0000000000..03cbfa162e --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Serialization.ResponseSerializerFactory.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Configuration.JsonApiOptions.html#JsonApiDotNetCore_Configuration_JsonApiOptions_SerializerOptions +--- diff --git a/docs/api/JsonApiDotNetCore.Services.IGetAllService-1.md b/docs/api/JsonApiDotNetCore.Services.IGetAllService-1.md new file mode 100644 index 0000000000..36fcd2e43e --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Services.IGetAllService-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Services.IGetAllService-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Services.IRemoveFromRelationshipService-1.md b/docs/api/JsonApiDotNetCore.Services.IRemoveFromRelationshipService-1.md new file mode 100644 index 0000000000..5df240af1c --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Services.IRemoveFromRelationshipService-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Services.IRemoveFromRelationshipService-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Services.IResourceCommandService-1.md b/docs/api/JsonApiDotNetCore.Services.IResourceCommandService-1.md new file mode 100644 index 0000000000..fedee0f018 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Services.IResourceCommandService-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Services.IResourceCommandService-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Services.IResourceQueryService-1.md b/docs/api/JsonApiDotNetCore.Services.IResourceQueryService-1.md new file mode 100644 index 0000000000..0801fc22f9 --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Services.IResourceQueryService-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Services.IResourceQueryService-2.html +--- diff --git a/docs/api/JsonApiDotNetCore.Services.JsonApiResourceService-1.md b/docs/api/JsonApiDotNetCore.Services.JsonApiResourceService-1.md new file mode 100644 index 0000000000..5a2be335cc --- /dev/null +++ b/docs/api/JsonApiDotNetCore.Services.JsonApiResourceService-1.md @@ -0,0 +1,3 @@ +--- +redirect_url: JsonApiDotNetCore.Services.JsonApiResourceService-2.html +--- diff --git a/docs/api/index.md b/docs/api/index.md index 7eb109b9af..8cdc3c745b 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,9 +1,93 @@ -# API +# Public API surface -This section documents the package API and is generated from the XML source comments. +This topic documents the public API, which is generated from the triple-slash XML documentation comments in source code. +Commonly used types are listed in the following sections. -## Common APIs +## Setup -- [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.yml) -- [`IResourceGraph`](JsonApiDotNetCore.Configuration.IResourceGraph.yml) -- [`JsonApiResourceDefinition`](JsonApiDotNetCore.Resources.JsonApiResourceDefinition-2.yml) +- implements +- implements + - + - implements + - and + - + - + - + - + - +- , (OpenAPI) +- +- implements + - + - + +## Query strings + +- implements + - implements + - and + - implements + - implements + - implements + - implements + - implements + - implements + - implements + - implements + - implements + - implements +- + - + - + - + - + - + - + - +- implements + - implements + - implements + - implements + - implements + - implements + +## Request pipeline + +- implements + - + - +- implements + - implements + - + - implements + - implements + - implements + - implements + - implements + - implements +- + - implements +- implements +- implements + - implements +- implements + - + - + +## Serialization + +- implements + - implements + - implements + - implements +- implements + - implements + - implements +- +- +- implements + +## Error handling + +- implements + - implements diff --git a/docs/build-dev.ps1 b/docs/build-dev.ps1 index 6345875fc7..348233253e 100644 --- a/docs/build-dev.ps1 +++ b/docs/build-dev.ps1 @@ -29,12 +29,14 @@ EnsureHttpServerIsInstalled VerifySuccessExitCode if (-Not $NoBuild -Or -Not (Test-Path -Path _site)) { - Remove-Item _site -Recurse -ErrorAction Ignore + Remove-Item _site\* -Recurse -ErrorAction Ignore dotnet build .. --configuration Release VerifySuccessExitCode Invoke-Expression ./generate-examples.ps1 +} else { + Remove-Item _site\* -Recurse -ErrorAction Ignore } dotnet tool restore @@ -58,4 +60,4 @@ Write-Host "Web server started. Press Enter to close." $key = [Console]::ReadKey() Stop-Job -Id $webServerJob.Id -Get-job | Remove-Job +Get-job | Remove-Job -Force diff --git a/docs/docfx.json b/docs/docfx.json index 232d8768eb..25a4aa943d 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -1,4 +1,5 @@ { + "$schema": "https://raw.githubusercontent.com/dotnet/docfx/main/schemas/docfx.schema.json", "metadata": [ { "properties": { @@ -8,30 +9,21 @@ { "files": [ "**/JsonApiDotNetCore.csproj", - "**/JsonApiDotNetCore.Annotations.csproj" + "**/JsonApiDotNetCore.Annotations.csproj", + "**/JsonApiDotNetCore.OpenApi.Swashbuckle.csproj", + "**/JsonApiDotNetCore.OpenApi.Client.NSwag.csproj", + "**/JsonApiDotNetCore.OpenApi.Client.Kiota" ], "src": "../" } ], - "dest": "api", - "disableGitFeatures": false + "output": "api" } ], "build": { "content": [ { - "files": [ - "api/**.yml", - "api/index.md", - "ext/openapi/index.md", - "getting-started/**.md", - "getting-started/**/toc.yml", - "usage/**.md", - "request-examples/**.md", - "internals/**.md", - "toc.yml", - "*.md" - ], + "files": "**.{md|yml}", "exclude": [ "**/README.md" ] @@ -44,25 +36,15 @@ ] } ], - "overwrite": [ - { - "exclude": [ - "obj/**", - "_site/**" - ] - } - ], - "dest": "_site", - "globalMetadataFiles": [], - "fileMetadataFiles": [], + "output": "_site", "template": [ "default", - "modern" + "modern", + "template" ], - "postProcessors": [], - "noLangKeyword": false, - "keepFileLink": false, - "cleanupCacheHistory": false, - "disableGitFeatures": false + "globalMetadata": { + "_appLogoPath": "styles/img/favicon.png", + "_googleAnalyticsTagId": "G-78GTGF1FM2" + } } } diff --git a/docs/getting-started/faq.md b/docs/getting-started/faq.md index 54b4e50d52..c36a09f99d 100644 --- a/docs/getting-started/faq.md +++ b/docs/getting-started/faq.md @@ -1,176 +1,3 @@ -# 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 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? -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/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] -> [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. -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). +--- +redirect_url: ../usage/faq.html +--- diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md new file mode 100644 index 0000000000..0b309e46eb --- /dev/null +++ b/docs/getting-started/index.md @@ -0,0 +1,5 @@ +# Getting Started + +The easiest way to get started is to run the [example project](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/master/src/Examples/GettingStarted). + +Or create your first JsonApiDotNetCore project by following the steps described [here](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/master/README.md#getting-started). diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index bd210e0a76..b09e389c91 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -1,24 +1,3 @@ -# Installation - -Click [here](https://www.nuget.org/packages/JsonApiDotNetCore/) for the latest NuGet version. - -### CLI - -``` -dotnet add package JsonApiDotNetCore -``` - -### Visual Studio - -```powershell -Install-Package JsonApiDotNetCore -``` - -### *.csproj - -```xml - - - - -``` +--- +redirect_url: index.html +--- diff --git a/docs/getting-started/step-by-step.md b/docs/getting-started/step-by-step.md index 57090d2d09..b09e389c91 100644 --- a/docs/getting-started/step-by-step.md +++ b/docs/getting-started/step-by-step.md @@ -1,134 +1,3 @@ -# Step-By-Step Guide to a Running API - -The most basic use case leverages Entity Framework Core. -The shortest path to a running API looks like: - -- Create a new API project -- Install -- Define models -- Define the DbContext -- Add services and middleware -- Seed the database -- Start the API - -This page will walk you through the **simplest** use case. More detailed examples can be found in the detailed usage subsections. - -### Create a new API project - -``` -mkdir MyApi -cd MyApi -dotnet new webapi -``` - -### Install - -``` -dotnet add package JsonApiDotNetCore - -- or - - -Install-Package JsonApiDotNetCore -``` - -### Define models - -Define your domain models such that they implement `IIdentifiable`. -The easiest way to do this is to inherit from `Identifiable`. - -```c# -#nullable enable - -[Resource] -public class Person : Identifiable -{ - [Attr] - public string Name { get; set; } = null!; -} -``` - -### Define the DbContext - -Nothing special here, just an ordinary `DbContext`. - -``` -public class AppDbContext : DbContext -{ - public DbSet People => Set(); - - public AppDbContext(DbContextOptions options) - : base(options) - { - } -} -``` - -### Add services and middleware - -Finally, register the services and middleware by adding them to your Program.cs: - -```c# -WebApplicationBuilder builder = WebApplication.CreateBuilder(args); - -// Add services to the container. - -// Add the Entity Framework Core DbContext like you normally would. -builder.Services.AddDbContext(options => -{ - string connectionString = GetConnectionString(); - - // Use whatever provider you want, this is just an example. - options.UseNpgsql(connectionString); -}); - -// Add JsonApiDotNetCore services. -builder.Services.AddJsonApi(); - -WebApplication app = builder.Build(); - -// Configure the HTTP request pipeline. - -app.UseRouting(); - -// Add JsonApiDotNetCore middleware. -app.UseJsonApi(); - -app.MapControllers(); - -app.Run(); -``` - -### Seed the database - -One way to seed the database is from your Program.cs: - -```c# -await CreateDatabaseAsync(app.Services); - -app.Run(); - -static async Task CreateDatabaseAsync(IServiceProvider serviceProvider) -{ - await using AsyncServiceScope scope = serviceProvider.CreateAsyncScope(); - - var dbContext = scope.ServiceProvider.GetRequiredService(); - await dbContext.Database.EnsureCreatedAsync(); - - if (!dbContext.People.Any()) - { - dbContext.People.Add(new Person - { - Name = "John Doe" - }); - - await dbContext.SaveChangesAsync(); - } -} -``` - -### Start the API - -``` -dotnet run -curl http://localhost:5000/people -``` +--- +redirect_url: index.html +--- diff --git a/docs/getting-started/toc.md b/docs/getting-started/toc.md deleted file mode 100644 index 12f943b7fa..0000000000 --- a/docs/getting-started/toc.md +++ /dev/null @@ -1,5 +0,0 @@ -# [Installation](install.md) - -# [Step By Step](step-by-step.md) - -# [FAQ](faq.md) diff --git a/docs/home/assets/img/araxis-logo.png b/docs/home/assets/img/araxis-logo.png new file mode 100644 index 0000000000..b25ed12ab8 Binary files /dev/null and b/docs/home/assets/img/araxis-logo.png differ diff --git a/docs/home/index.html b/docs/home/index.html index 1593c915dd..a8530ec89a 100644 --- a/docs/home/index.html +++ b/docs/home/index.html @@ -31,6 +31,13 @@ darkModeStyleSheet.disabled = true; } + +
@@ -55,7 +62,7 @@

Includes support for the Atomic Operations extension.

Read more - Getting started + Getting started Contribute on GitHub
@@ -301,9 +308,9 @@

Sponsors

-
+
- Araxis Logo + Araxis Logo
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 614aa4814f..89c7043450 100644 --- a/docs/request-examples/index.md +++ b/docs/request-examples/index.md @@ -1,17 +1,28 @@ ---- -_disableToc: true ---- +# Example projects -# Example requests +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 @@ -48,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/request-examples/toc.md b/docs/request-examples/toc.md deleted file mode 100644 index e69de29bb2..0000000000 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 28d6a6a36e..4243fd8be2 100644 --- a/docs/usage/caching.md +++ b/docs/usage/caching.md @@ -4,8 +4,8 @@ _since v4.2_ 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/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..dacd35889b --- /dev/null +++ b/docs/usage/faq.md @@ -0,0 +1,176 @@ +# 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 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 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/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] +> [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. +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/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/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/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/toc.md b/docs/usage/toc.md deleted file mode 100644 index bdeb0e4958..0000000000 --- a/docs/usage/toc.md +++ /dev/null @@ -1,39 +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) - -# [OpenAPI](openapi.md) -## [Documentation](openapi-documentation.md) -## [Clients](openapi-client.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/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/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiException.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiException.cs index 8b66839e9e..a86118c875 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiException.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiException.cs @@ -5,6 +5,13 @@ namespace JsonApiDotNetCore.OpenApi.Client.NSwag; +/// +/// Replacement for the auto-generated +/// +/// ApiException +/// +/// class from NSwag. +/// [UsedImplicitly(ImplicitUseTargetFlags.Members)] public class ApiException(string message, int statusCode, string? response, IReadOnlyDictionary> headers, Exception? innerException) : Exception($"HTTP {statusCode}: {message}", innerException) @@ -14,6 +21,13 @@ public class ApiException(string message, int statusCode, string? response, IRea public IReadOnlyDictionary> Headers { get; } = headers; } +/// +/// Replacement for the auto-generated +/// +/// ApiException<TResult> +/// +/// class from NSwag. +/// [UsedImplicitly(ImplicitUseTargetFlags.Members)] public sealed class ApiException( string message, int statusCode, string? response, IReadOnlyDictionary> headers, TResult result, Exception? innerException) diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiResponse.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiResponse.cs index 74ee77127f..7d3d7c2a52 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiResponse.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/ApiResponse.cs @@ -3,6 +3,13 @@ namespace JsonApiDotNetCore.OpenApi.Client.NSwag; +/// +/// Replacement for the auto-generated +/// +/// SwaggerResponse +/// +/// class from NSwag. +/// [PublicAPI] public class ApiResponse(int statusCode, IReadOnlyDictionary> headers) { @@ -71,6 +78,13 @@ public static async Task TranslateAsync(Func> ope } } +/// +/// Replacement for the auto-generated +/// +/// SwaggerResponse<TResult> +/// +/// class from NSwag. +/// [PublicAPI] public class ApiResponse(int statusCode, IReadOnlyDictionary> headers, TResult result) : ApiResponse(statusCode, headers) diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs index a3a7e627db..602bacce63 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/BlockedJsonInheritanceConverter.cs @@ -4,6 +4,13 @@ namespace JsonApiDotNetCore.OpenApi.Client.NSwag; // Referenced from liquid template, to ensure the built-in JsonInheritanceConverter from NSwag is never used. +/// +/// Exists to block usage of the default +/// +/// JsonInheritanceConverter +/// +/// from NSwag, which is incompatible with JSON:API. +/// [PublicAPI] public abstract class BlockedJsonInheritanceConverter : JsonConverter { @@ -31,11 +38,17 @@ public override bool CanConvert(Type objectType) return true; } + /// + /// Always throws an . + /// public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer) { throw new InvalidOperationException("JsonInheritanceConverter is incompatible with JSON:API and must not be used."); } + /// + /// Always throws an . + /// public override object ReadJson(JsonReader reader, Type objectType, object? existingValue, JsonSerializer serializer) { throw new InvalidOperationException("JsonInheritanceConverter is incompatible with JSON:API and must not be used."); diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/IJsonApiClient.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/IJsonApiClient.cs deleted file mode 100644 index df6a35d78e..0000000000 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/IJsonApiClient.cs +++ /dev/null @@ -1,41 +0,0 @@ -using System.Linq.Expressions; -using JetBrains.Annotations; - -namespace JsonApiDotNetCore.OpenApi.Client.NSwag; - -[PublicAPI] -public interface IJsonApiClient -{ - /// - /// Ensures correct serialization of JSON:API attributes in the request body of a POST/PATCH request at a resource endpoint. Properties with default - /// values are omitted, unless explicitly included using - /// - /// In JSON:API, an omitted attribute indicates to ignore it, while an attribute that is set to null means to clear it. This poses a problem, - /// because the serializer cannot distinguish between "you have explicitly set this .NET property to its default value" vs "you didn't touch it, so it - /// contains its default value" when converting to JSON. - /// - /// - /// - /// The request document instance for which default values should be omitted. - /// - /// - /// Optional. A list of lambda expressions that indicate which properties to always include in the JSON request body. For example: - /// video.Title, video => video.Summary - /// ]]> - /// - /// - /// The type of the request document. - /// - /// - /// The type of the attributes object inside . - /// - /// - /// An to clear the current registration. For efficient memory usage, it is recommended to wrap calls to this method in a - /// using statement, so the registrations are cleaned up after executing the request. After disposal, the client can be reused without the - /// registrations added earlier. - /// - IDisposable WithPartialAttributeSerialization(TRequestDocument requestDocument, - params Expression>[] alwaysIncludedAttributeSelectors) - where TRequestDocument : class; -} diff --git a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs index 80adb56477..111b894879 100644 --- a/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs +++ b/src/JsonApiDotNetCore.OpenApi.Client.NSwag/NotifyPropertySet.cs @@ -11,7 +11,8 @@ namespace JsonApiDotNetCore.OpenApi.Client.NSwag; /// -/// Implementation of that doesn't detect changes. +/// Implementation of that unconditionally raises the event when a property is +/// assigned. Exists to support JSON:API partial POST/PATCH. /// [PublicAPI] public abstract class NotifyPropertySet : INotifyPropertyChanged