diff --git a/README.md b/README.md index a547f360f9..80e30488ac 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ See the [examples](https://github.com/json-api-dotnet/JsonApiDotNetCore/tree/mas ## Installation And Usage -See [the documentation](https://json-api-dotnet.github.io/#/) for detailed usage. +See [the documentation](https://json-api-dotnet.github.io/#/) for detailed usage. ### Models @@ -56,7 +56,7 @@ public class ArticlesController : JsonApiController
public ArticlesController( IJsonApiOptions jsonApiOptions, IResourceService
resourceService, - ILoggerFactory loggerFactory) + ILoggerFactory loggerFactory) : base(jsonApiOptions, resourceService, loggerFactory) { } } @@ -65,7 +65,7 @@ public class ArticlesController : JsonApiController
### Middleware ```csharp -public class Startup +public class Startup { public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddJsonApi(); @@ -89,7 +89,7 @@ dotnet restore #### Testing -Running tests locally requires access to a PostgreSQL database. If you have docker installed, this can be propped up via: +Running tests locally requires access to a PostgreSQL database. If you have docker installed, this can be propped up via: ```bash docker run --rm --name jsonapi-dotnet-core-testing -e POSTGRES_DB=JsonApiDotNetCoreExample -e POSTGRES_USER=postgres -e POSTGRES_PASSWORD=postgres -p 5432:5432 postgres:12.0 diff --git a/docs/README.md b/docs/README.md index 9f51f25945..ee240324c8 100644 --- a/docs/README.md +++ b/docs/README.md @@ -3,4 +3,4 @@ ``` ./generate.sh docfx ./docfx.json --serve -``` \ No newline at end of file +``` diff --git a/docs/api/index.md b/docs/api/index.md index a3cdf039eb..c93ca94a89 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -5,6 +5,5 @@ This section documents the package API and is generated from the XML source comm ## Common APIs - [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.html) -- [`IResourceGraph`](JsonApiDotNetCore.Internal.IResourceGraph.html) +- [`IResourceGraph`](JsonApiDotNetCore.Internal.Contracts.IResourceGraph.html) - [`ResourceDefinition`](JsonApiDotNetCore.Models.ResourceDefinition-1.html) -- [`IQueryAccessor`](JsonApiDotNetCore.Services.IQueryAccessor.html) \ No newline at end of file diff --git a/docs/generators/index.md b/docs/generators/index.md deleted file mode 100644 index 130d3d3636..0000000000 --- a/docs/generators/index.md +++ /dev/null @@ -1,9 +0,0 @@ -## Installing - -... - -## Creating a project template - -``` -dotnet new jadnc -``` \ No newline at end of file diff --git a/docs/getting-started/install.md b/docs/getting-started/install.md index 6f3b614b9e..7243d15a67 100644 --- a/docs/getting-started/install.md +++ b/docs/getting-started/install.md @@ -16,19 +16,3 @@ Install-Package JsonApiDotnetCore ``` - -## Pre-Release Packages - -Occasionally we will release experimental features as pre-release packages on our -MyGet feed. You can download these by adding [the pacakge feed](https://www.myget.org/feed/Details/research-institute) to your nuget configuration. - -These releases are deployed from the `develop` branch directly. - -```xml - - - - - - -``` \ No newline at end of file diff --git a/docs/getting-started/step-by-step.md b/docs/getting-started/step-by-step.md index f42e5dcafd..732b15924e 100644 --- a/docs/getting-started/step-by-step.md +++ b/docs/getting-started/step-by-step.md @@ -1,6 +1,6 @@ # Step-By-Step Guide to a Running API -The most basic use case leverages Entity Framework. +The most basic use case leverages Entity Framework Core. The shortest path to a running API looks like: - Create a new web app @@ -33,20 +33,20 @@ Install-Package JsonApiDotnetCore ``` ### Define Models - + Define your domain models such that they implement `IIdentifiable`. -The easiest way to do this is to inherit `Identifiable` +The easiest way to do this is to inherit from `Identifiable` ```c# public class Person : Identifiable -{ +{ [Attr("name")] public string Name { get; set; } } ``` ### Define DbContext - + Nothing special here, just an ordinary `DbContext` ``` @@ -54,24 +54,24 @@ public class AppDbContext : DbContext { public AppDbContext(DbContextOptions options) : base(options) { } - + public DbSet People { get; set; } } ``` ### Define Controllers - -You need to create controllers that inherit from `JsonApiController` or `JsonApiController` -where `TEntity` is the model that inherits from `Identifiable` + +You need to create controllers that inherit from `JsonApiController` or `JsonApiController` +where `T` is the model that inherits from `Identifiable` ```c# public class PeopleController : JsonApiController { public PeopleController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(jsonApiOptions, loggerFactory, resourceService) { } } ``` @@ -81,24 +81,26 @@ public class PeopleController : JsonApiController Finally, add the services by adding the following to your Startup.ConfigureServices: ```c# -public IServiceProvider ConfigureServices(IServiceCollection services) +// This method gets called by the runtime. Use this method to add services to the container. +public void ConfigureServices(IServiceCollection services) { - // add the db context like you normally would + // Add the Entity Framework Core DbContext like you normally would services.AddDbContext(options => - { // use whatever provider you want, this is just an example + { + // Use whatever provider you want, this is just an example options.UseNpgsql(GetDbConnectionString()); - }, ServiceLifetime.Transient); + }); - // add jsonapi dotnet core + // Add JsonApiDotNetCore services.AddJsonApi(); - // ... } ``` -Add the middleware to the Startup.Configure method. Note that under the hood, -this will call `app.UseMvc()` so there is no need to add that as well. +Add the middleware to the Startup.Configure method. Note that under the hood, +this will call `app.UseRouting()` and `app.UseEndpoints(...)` so there is no need to add that as well. ```c# +// This method gets called by the runtime. Use this method to configure the HTTP request pipeline. public void Configure(IApplicationBuilder app) { app.UseJsonApi(); @@ -110,19 +112,19 @@ public void Configure(IApplicationBuilder app) One way to seed the database is in your Configure method: ```c# -public void Configure( - IApplicationBuilder app, - AppDbContext context) +public void Configure(IApplicationBuilder app, AppDbContext context) { context.Database.EnsureCreated(); - if(context.People.Any() == false) + + if (!context.People.Any()) { - context.People.Add(new Person { + context.People.Add(new Person + { Name = "John Doe" }); context.SaveChanges(); } - // ... + app.UseJsonApi(); } ``` diff --git a/docs/index.md b/docs/index.md index 15ba7f65a4..6efb89274f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -6,10 +6,10 @@ A [{ json:api }](https://jsonapi.org) web application framework for .Net Core. ### 1. Eliminate Boilerplate -The goal of this package is to facility the development of json:api applications that leverage the full range +The goal of this package is to facility the development of json:api applications that leverage the full range of features provided by the specification. -Eliminate CRUD boilerplate and provide the following features across all your resource endpoints: +Eliminate CRUD boilerplate and provide the following features across all your resource endpoints: - Relationship inclusion and navigation - Filtering diff --git a/docs/request-examples/000-CREATE_Person.sh b/docs/request-examples/000-CREATE_Person.sh index bc5847f293..89392b31fb 100755 --- a/docs/request-examples/000-CREATE_Person.sh +++ b/docs/request-examples/000-CREATE_Person.sh @@ -1,5 +1,4 @@ curl -vs http://localhost:5001/api/people \ - -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { diff --git a/docs/request-examples/001-CREATE_Article.sh b/docs/request-examples/001-CREATE_Article.sh index 361915e177..236bb5b975 100755 --- a/docs/request-examples/001-CREATE_Article.sh +++ b/docs/request-examples/001-CREATE_Article.sh @@ -1,5 +1,4 @@ curl -vs http://localhost:5001/api/articles \ - -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { diff --git a/docs/request-examples/002-GET_Articles.sh b/docs/request-examples/002-GET_Articles.sh index 722677a748..46a2c0daa8 100755 --- a/docs/request-examples/002-GET_Articles.sh +++ b/docs/request-examples/002-GET_Articles.sh @@ -1,2 +1 @@ -curl -vs http://localhost:5001/api/articles \ - -H "Accept: application/vnd.api+json" \ No newline at end of file +curl -vs http://localhost:5001/api/articles diff --git a/docs/request-examples/003-GET_Article.sh b/docs/request-examples/003-GET_Article.sh index 5da8265236..2411cbac9f 100755 --- a/docs/request-examples/003-GET_Article.sh +++ b/docs/request-examples/003-GET_Article.sh @@ -1,2 +1 @@ -curl -vs http://localhost:5001/api/articles/1 \ - -H "Accept: application/vnd.api+json" \ No newline at end of file +curl -vs http://localhost:5001/api/articles/1 diff --git a/docs/request-examples/004-GET_Articles_With_Authors.sh b/docs/request-examples/004-GET_Articles_With_Authors.sh index 8ad3021292..dbddb068cb 100755 --- a/docs/request-examples/004-GET_Articles_With_Authors.sh +++ b/docs/request-examples/004-GET_Articles_With_Authors.sh @@ -1,2 +1 @@ -curl -vs http://localhost:5001/api/articles?include=author \ - -H "Accept: application/vnd.api+json" \ No newline at end of file +curl -vs http://localhost:5001/api/articles?include=author diff --git a/docs/request-examples/005-PATCH_Article.sh b/docs/request-examples/005-PATCH_Article.sh index f4d222d0e0..84d062f920 100755 --- a/docs/request-examples/005-PATCH_Article.sh +++ b/docs/request-examples/005-PATCH_Article.sh @@ -1,5 +1,4 @@ -curl -vs http://localhost:5001/api/people/1 \ - -H "Accept: application/vnd.api+json" \ +curl -vs http://localhost:5001/api/people/1 \ -H "Content-Type: application/vnd.api+json" \ -X PATCH \ -d '{ diff --git a/docs/request-examples/006-DELETE_Article.sh b/docs/request-examples/006-DELETE_Article.sh index 8d25a49418..70b237212a 100755 --- a/docs/request-examples/006-DELETE_Article.sh +++ b/docs/request-examples/006-DELETE_Article.sh @@ -1,3 +1,2 @@ curl -vs http://localhost:5001/api/articles/1 \ - -H "Accept: application/vnd.api+json" \ -X DELETE \ No newline at end of file diff --git a/docs/request-examples/007-__SEED__.sh b/docs/request-examples/007-__SEED__.sh index 185da440e8..32ee24b491 100755 --- a/docs/request-examples/007-__SEED__.sh +++ b/docs/request-examples/007-__SEED__.sh @@ -1,5 +1,4 @@ curl -vs http://localhost:5001/api/people \ - -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { @@ -11,7 +10,6 @@ curl -vs http://localhost:5001/api/people \ }' curl -vs http://localhost:5001/api/articles \ - -H "Accept: application/vnd.api+json" \ -H "Content-Type: application/vnd.api+json" \ -d '{ "data": { diff --git a/docs/request-examples/008-GET_Articles_With_Filter_Eq.sh b/docs/request-examples/008-GET_Articles_With_Filter_Eq.sh index 6616e4bf08..a5f3b8de1d 100755 --- a/docs/request-examples/008-GET_Articles_With_Filter_Eq.sh +++ b/docs/request-examples/008-GET_Articles_With_Filter_Eq.sh @@ -1,2 +1 @@ -curl -vs http://localhost:5001/api/articles?filter%5Btitle%5D=Moby \ - -H "Accept: application/vnd.api+json" +curl -vs http://localhost:5001/api/articles?filter%5Btitle%5D=Moby diff --git a/docs/request-examples/009-GET_Articles_With_Filter_Like.sh b/docs/request-examples/009-GET_Articles_With_Filter_Like.sh index 7ba25f0fb3..09eae2e692 100755 --- a/docs/request-examples/009-GET_Articles_With_Filter_Like.sh +++ b/docs/request-examples/009-GET_Articles_With_Filter_Like.sh @@ -1,2 +1 @@ -curl -vs http://localhost:5001/api/people?filter%5Bname%5D=like:Al \ - -H "Accept: application/vnd.api+json" \ No newline at end of file +curl -vs http://localhost:5001/api/people?filter%5Bname%5D=like:Al diff --git a/docs/request-examples/README.md b/docs/request-examples/README.md index 39111b3579..809dac86c4 100644 --- a/docs/request-examples/README.md +++ b/docs/request-examples/README.md @@ -4,8 +4,7 @@ To update these requests: 1. Add a bash (.sh) file prefixed by a number that is used to determine the order the scripts are executed. The bash script should execute a request and output the response. Example: ``` -curl -vs http://localhost:5001/api/articles \ - -H "Accept: application/vnd.api+json" +curl -vs http://localhost:5001/api/articles ``` 2. Add the example to `index.md`. Example: diff --git a/docs/usage/errors.md b/docs/usage/errors.md index da88e93668..79ae35af6c 100644 --- a/docs/usage/errors.md +++ b/docs/usage/errors.md @@ -1,67 +1,25 @@ # Errors -By default, errors will only contain the properties defined by the `Error` class. -However, you can create your own by inheriting from Error and either throwing it in a `JsonApiException` or returning the error from your controller. +Errors returned will contain only the properties that are set on the `Error` class. Custom fields can be added through `Error.Meta`. +You can create a custom error by throwing a `JsonApiException` (which accepts an `Error` instance), or returning an `Error` instance from an `ActionResult` in a controller. +Please keep in mind that json:api requires Title to be a generic message, while Detail should contain information about the specific problem occurence. +From a controller method: ```c# -public class CustomError : Error +return Conflict(new Error(HttpStatusCode.Conflict) { - public CustomError(int status, string title, string detail, string myProp) - : base(status, title, detail) - { - MyCustomProperty = myProp; - } - - public string MyCustomProperty { get; set; } -} + Title = "Target resource was modified by another user.", + Detail = $"User {userName} changed the {resourceField} field on the {resourceName} resource." +}); ``` -If you throw a `JsonApiException` that is unhandled, the middleware will properly serialize it and return it as a json:api error. - +From other code: ```c# -public void MyMethod() +throw new JsonApiException(new Error(HttpStatusCode.Conflict) { - var error = new CustomError(507, "title", "detail", "custom"); - throw new JsonApiException(error); -} + Title = "Target resource was modified by another user.", + Detail = $"User {userName} changed the {resourceField} field on the {resourceName} resource." +}); ``` -You can use the `IActionResult Error(Error error)` method to return a single error message, or you can use the `IActionResult Errors(ErrorCollection errors)` method to return a collection of errors from your controller. - -```c# -[HttpPost] -public override async Task PostAsync([FromBody] MyEntity entity) -{ - if(_db.IsFull) - return Error(new CustomError("507", "Database is full.", "Theres no more room.", "Sorry.")); - - if(model.Validations.IsValid == false) - return Errors(model.Validations.GetErrors()); -} -``` - -## Example: Including Links - -This example demonstrates one way you can include links with your error payloads. - -This example assumes that there is a support documentation site that provides additional information based on the HTTP Status Code. - -```c# -public class LinkableError : Error -{ - public LinkableError(int status, string title) - : base(status, title) - { } - - public ErrorLink Links => "https://example.com/errors/" + Status; -} - -var error = new LinkableError(401, "You're not allowed to do that."); -throw new JsonApiException(error); -``` - - - - - - +In both cases, the middleware will properly serialize it and return it as a json:api error. diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md index 1802e20217..b6e4e8d686 100644 --- a/docs/usage/extensibility/controllers.md +++ b/docs/usage/extensibility/controllers.md @@ -1,19 +1,19 @@ # Controllers -You need to create controllers that inherit from `JsonApiController` +You need to create controllers that inherit from `JsonApiController` ```c# public class ArticlesController : JsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService, - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService
resourceService) + : base(jsonApiOptions, loggerFactory, resourceService) { } } ``` - + ## Non-Integer Type Keys If your model is using a type other than int for the primary key, you must explicitly declare it in the controller and service generic type definitions. @@ -23,15 +23,15 @@ public class ArticlesController : JsonApiController //---------------------------------------------------------- ^^^^ { public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService resourceService, + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService resourceService) //----------------------- ^^^^ - ILoggerFactory loggerFactory) - : base(jsonApiContext, resourceService, loggerFactory) + : base(jsonApiOptions, loggerFactory, resourceService) { } } ``` - + ## Resource Access Control It is often desirable to limit what methods are exposed on your controller. The first way, you can do this is to simply inherit from `BaseJsonApiController` and explicitly declare what methods are available. @@ -44,31 +44,36 @@ This approach is ok, but introduces some boilerplate that can easily be avoided. public class ArticlesController : BaseJsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService
resourceService) + : base(jsonApiOptions, loggerFactory, resourceService) { } [HttpGet] - public override async Task GetAsync() - => await base.GetAsync(); + public override async Task GetAsync() + { + return await base.GetAsync(); + } [HttpGet("{id}")] - public override async Task GetAsync(TId id) - => await base.GetAsync(id); + public override async Task GetAsync(int id) + { + return await base.GetAsync(id); + } } ``` - + ## Using ActionFilterAttributes -The next option is to use the ActionFilterAttributes that ship with the library. The available attributes are: +The next option is to use the ActionFilter attributes that ship with the library. The available attributes are: - `NoHttpPost`: disallow POST requests - `NoHttpPatch`: disallow PATCH requests - `NoHttpDelete`: disallow DELETE requests - `HttpReadOnly`: all of the above -Not only does this reduce boilerplate, but it also provides a more meaningful HTTP response code. +Not only does this reduce boilerplate, but it also provides a more meaningful HTTP response code. An attempt to use one blacklisted methods will result in a HTTP 405 Method Not Allowed response. ```c# @@ -76,32 +81,36 @@ An attempt to use one blacklisted methods will result in a HTTP 405 Method Not A public class ArticlesController : BaseJsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - IResourceService
resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService
resourceService) + : base(jsonApiOptions, loggerFactory, resourceService) { } } ``` - + ## Implicit Access By Service Injection -Finally, you can control the allowed methods by supplying only the available service implementations. In some cases, resources may be an aggregation of entities or a view on top of the underlying entities. In these cases, there may not be a writable IResourceService implementation. In these cases, simply inject the implementation that is available. +Finally, you can control the allowed methods by supplying only the available service implementations. In some cases, resources may be an aggregation of entities or a view on top of the underlying entities. In these cases, there may not be a writable IResourceService implementation, so simply inject the implementation that is available. -As with the ActionFilterAttributes, if a service implementation is not available to service a request, HTTP 405 Method Not Allowed will be returned. +As with the ActionFilter attributes, if a service implementation is not available to service a request, HTTP 405 Method Not Allowed will be returned. For more information about resource injection, see the next section titled Resource Services. ```c# -public class ReportsController : BaseJsonApiController +public class ReportsController : BaseJsonApiController { public ReportsController( - IJsonApiContext jsonApiContext, - IGetAllService getAll) - : base(jsonApiContext, getAll: getAll) + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(jsonApiOptions, loggerFactory, resourceService) { } [HttpGet] - public override async Task GetAsync() - => await base.GetAsync(); + public override async Task GetAsync() + { + return await base.GetAsync(); + } } ``` diff --git a/docs/usage/extensibility/custom-query-formats.md b/docs/usage/extensibility/custom-query-formats.md index 39e9026572..73d02fe159 100644 --- a/docs/usage/extensibility/custom-query-formats.md +++ b/docs/usage/extensibility/custom-query-formats.md @@ -2,8 +2,8 @@ For information on the default query parameter formats, see the documentation for each query method. -In order to customize the query formats, you need to implement the `IQueryParser` interface and inject it. +In order to customize the query formats, you need to implement the `IQueryParameterParser` interface and inject it. ```c# -services.AddScoped(); -``` \ No newline at end of file +services.AddScoped(); +``` diff --git a/docs/usage/extensibility/layer-overview.md b/docs/usage/extensibility/layer-overview.md index fe19477183..fc8d981e3d 100644 --- a/docs/usage/extensibility/layer-overview.md +++ b/docs/usage/extensibility/layer-overview.md @@ -5,23 +5,23 @@ By default, data retrieval is distributed across 3 layers: ``` JsonApiController (required) -└── EntityResourceService: IResourceService ++-- DefaultResourceService: IResourceService - └── DefaultEntityRepository: IEntityRepository + +-- DefaultResourceRepository: IResourceRepository ``` -Customization can be done at any of these layers. However, it is recommended that you make your customizations at the service or the repository layer when possible to keep the controllers free of unnecessary logic. +Customization can be done at any of these layers. However, it is recommended that you make your customizations at the service or the repository layer when possible to keep the controllers free of unnecessary logic. You can use the following as a general rule of thumb for where to put business logic: -- `Controller`: simple validation logic that should result in the return of specific HTTP status codes such as model validation -- `IResourceService`: advanced BL and replacement of data access mechanisms -- `IEntityRepository`: custom logic that builds on the EF APIs, such as Authorization of data +- `Controller`: simple validation logic that should result in the return of specific HTTP status codes, such as model validation +- `IResourceService`: advanced business logic and replacement of data access mechanisms +- `IResourceRepository`: custom logic that builds on the Entity Framework Core APIs, such as Authorization of data ## Replacing Services **Note:** If you are using auto-discovery, services will be automatically registered for you. -Replacing services is done on a per resource basis and can be done through simple DI in your Startup.cs file. +Replacing services is done on a per-resource basis and can be done through simple DI in your Startup.cs file. In v3.0.0 we introduced an extenion method that you should use to register services. This method handles some of the common issues @@ -29,7 +29,7 @@ users have had with service registration. ```c# // Startup.cs -public IServiceProvider ConfigureServices(IServiceCollection services) +public void ConfigureServices(IServiceCollection services) { // custom service services.AddResourceService(); @@ -43,14 +43,14 @@ Prior to v3.0.0 you could do it like so: ```c# // Startup.cs -public IServiceProvider ConfigureServices(IServiceCollection services) +public void ConfigureServices(IServiceCollection services) { // custom service - services.AddScoped, FooService>(); + services.AddScoped, FooService>(); // custom repository - services.AddScoped, FooService>(); + services.AddScoped, FooService>(); // ... } -``` \ No newline at end of file +``` diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md index 65e5cedda1..15e4854b60 100644 --- a/docs/usage/extensibility/middleware.md +++ b/docs/usage/extensibility/middleware.md @@ -5,8 +5,9 @@ Add the following to your Startup.ConfigureServices method. Replace AppDbContext services.AddJsonApi(); ``` -Add the middleware to the Startup.Configure method. Note that under the hood, this will call app.UseMvc() so there is no need to add that as well. +Add the middleware to the Startup.Configure method. Note that under the hood, +this will call `app.UseRouting()` and `app.UseEndpoints(...)` so there is no need to add that as well. ```c3 app.UseJsonApi(); -``` \ No newline at end of file +``` diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md index bd94820159..e175f49524 100644 --- a/docs/usage/extensibility/repositories.md +++ b/docs/usage/extensibility/repositories.md @@ -1,52 +1,53 @@ # Entity Repositories -If you want to use EF, but need additional data access logic (such as authorization), you can implement custom methods for accessing the data by creating an implementation of IEntityRepository. If you only need minor changes you can override the methods defined in DefaultEntityRepository. +If you want to use Entity Framework Core, but need additional data access logic (such as authorization), you can implement custom methods for accessing the data by creating an implementation of IResourceRepository. If you only need minor changes you can override the methods defined in DefaultResourceRepository. -The repository should then be add to the service collection in Startup.cs. +The repository should then be added to the service collection in Startup.cs. ```c# -public IServiceProvider ConfigureServices(IServiceCollection services) +public void ConfigureServices(IServiceCollection services) { - services.AddScoped, AuthorizedArticleRepository>(); + services.AddScoped, AuthorizedArticleRepository>(); // ... } ``` A sample implementation that performs data authorization might look like this. -All of the methods in the DefaultEntityRepository will use the Get() method to get the DbSet so this is a good method to apply scoped filters such as user or tenant authorization. +All of the methods in the DefaultResourceRepository will use the Get() method to get the DbSet, so this is a good method to apply scoped filters such as user or tenant authorization. ```c# -public class AuthorizedArticleRepository - : DefaultEntityRepository
+public class AuthorizedArticleRepository : DefaultResourceRepository
{ private readonly IAuthenticationService _authenticationService; public AuthorizedArticleRepository( - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, + IAuthenticationService authenticationService, + ITargetedFields targetedFields, IDbContextResolver contextResolver, - IAuthenticationService authenticationService) - : base(loggerFactory, jsonApiContext, contextResolver) + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory, + IResourceFactory resourceFactory, + ILoggerFactory loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory) { _authenticationService = authenticationService; } - public override IQueryable Get() - => base.Get() - .Where(e => - e.UserId == _authenticationService.UserId - ); + public override IQueryable
Get() + { + return base.Get().Where(article => article.UserId == _authenticationService.UserId); + } } ``` ## Multiple DbContexts -If you need to use multiple EF DbContext, first add each DbContext to the ContextGraphBuilder. +If you need to use multiple Entity Framework Core DbContexts, first add each DbContext to the ResourceGraphBuilder. Then, create an implementation of IDbContextResolver for each context. -Register each of the new IDbContextResolver implementations in the Startup. +Register each of the new IDbContextResolver implementations in Startup.cs. You can then create a general repository for each context and inject it per resource type. This example shows a single DbContextARepository for all entities that are members of DbContextA. @@ -54,15 +55,12 @@ Then inject the repository for the correct entity, in this case Foo is a member ```c# // Startup.cs -services.AddJsonApi(options => { - options.BuildContextGraph((builder) => - { - // Add both contexts using the builder - builder.AddDbContext(); - builder.AddDbContext(); - }); -}, mvcBuilder); - +services.AddJsonApi(resources: builder => +{ + // Add both contexts using the builder + builder.AddDbContext(); + builder.AddDbContext(); +}); public class DbContextAResolver : IDbContextResolver { @@ -73,7 +71,10 @@ public class DbContextAResolver : IDbContextResolver _context = context; } - public DbContext GetContext() => _context; + public DbContext GetContext() + { + return _context; + } } @@ -82,18 +83,21 @@ services.AddScoped(); services.AddScoped(); -public class DbContextARepository -: DefaultEntityRepository where TEntity : class, IIdentifiable +public class DbContextARepository : DefaultResourceRepository + where TResource : class, IIdentifiable { - public DbContextARepository( - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext, - DbContextAResolver contextResolver) - : base(loggerFactory, jsonApiContext, contextResolver) - { } + public DbContextARepository( + ITargetedFields targetedFields, + DbContextAResolver contextResolver, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory, + IResourceFactory resourceFactory, + ILoggerFactory loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory) + { } } // Startup.cs -services.AddScoped, DbContextARepository>(); -``` \ No newline at end of file +services.AddScoped, DbContextARepository>(); +``` diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md index e14fb7c773..b7c169bf6e 100644 --- a/docs/usage/extensibility/services.md +++ b/docs/usage/extensibility/services.md @@ -1,47 +1,52 @@ # Resource Services -The `IResourceService` acts as a service layer between the controller and the data access layer. -This allows you to customize it however you want and not be dependent upon Entity Framework. +The `IResourceService` acts as a service layer between the controller and the data access layer. +This allows you to customize it however you want and not be dependent upon Entity Framework Core. This is also a good place to implement custom business logic. ## Supplementing Default Behavior -If you don't need to alter the actual persistence mechanism, you can inherit from the EntityResourceService and override the existing methods. +If you don't need to alter the actual persistence mechanism, you can inherit from the DefaultResourceService and override the existing methods. In simple cases, you can also just wrap the base implementation with your custom logic. A simple example would be to send notifications when an entity gets created. ```c# -public class TodoItemService : EntityResourceService +public class TodoItemService : DefaultResourceService { private readonly INotificationService _notificationService; public TodoItemService( - IJsonApiContext jsonApiContext, - IEntityRepository repository, + INotificationService notificationService, + IEnumerable queryParameters, + IJsonApiOptions options, ILoggerFactory loggerFactory, - INotificationService notificationService) - : base(jsonApiContext, repository, loggerFactory) + IResourceRepository repository, + IResourceContextProvider provider, + IResourceChangeTracker resourceChangeTracker, + IResourceFactory resourceFactory, + IResourceHookExecutor hookExecutor) + : base(queryParameters, options, loggerFactory, repository, provider, resourceChangeTracker, resourceFactory, hookExecutor) { _notificationService = notificationService; } public override async Task CreateAsync(TodoItem entity) { - // call the base implementation which uses Entity Framework + // Call the base implementation which uses Entity Framework Core var newEntity = await base.CreateAsync(entity); - // custom code - _notificationService.Notify($"Entity created: { newEntity.Id }"); + // Custom code + _notificationService.Notify($"Entity created: {newEntity.Id}"); - // don't forget to return the new entity + // Don't forget to return the new entity return newEntity; } } ``` - -## Not Using Entity Framework? -As previously discussed, this library uses Entity Framework by default. +## Not Using Entity Framework Core? + +As previously discussed, this library uses Entity Framework Core by default. If you'd like to use another ORM that does not implement `IQueryable`, you can use a custom `IResourceService` implementation. ```c# @@ -51,7 +56,7 @@ public void ConfigureServices(IServiceCollection services) // add the service override for MyModel services.AddScoped, MyModelService>(); - // add your own DAO + // add your own Data Access Object services.AddScoped(); // ... } @@ -62,9 +67,9 @@ public class MyModelService : IResourceService private readonly IMyModelDao _dao; public MyModelService(IMyModelDao dao) - { + { _dao = dao; - } + } public Task> GetAsync() { @@ -74,7 +79,7 @@ public class MyModelService : IResourceService // ... } ``` - + ## Limited Requirements In some cases it may be necessary to only expose a few methods on the resource. For this reason, we have created a hierarchy of service interfaces that can be used to get the exact implementation you require. @@ -84,76 +89,82 @@ This interface hierarchy is defined by this tree structure. ``` IResourceService | -├── IResourceQueryService ++-- IResourceQueryService | | -│ ├── IGetAllService -│ │ GET / + +-- IGetAllService + GET / | | -│ ├── IGetByIdService + +-- IGetByIdService | | GET /{id} | | -│ ├── IGetRelationshipService + +-- IGetRelationshipService | | GET /{id}/{relationship} | | -│ └── IGetRelationshipsService + +-- IGetRelationshipsService | GET /{id}/relationships/{relationship} | -└── IResourceCommandService ++-- IResourceCommandService | - ├── ICreateService + +-- ICreateService | POST / | - ├── IDeleteService + +-- IDeleteService | DELETE /{id} | - ├── IUpdateService + +-- IUpdateService | PATCH /{id} | - └── IUpdateRelationshipService + +-- IUpdateRelationshipService PATCH /{id}/relationships/{relationship} ``` - + In order to take advantage of these interfaces you first need to inject the service for each implemented interface. ```c# -public class ArticleService : ICreateService
, IDeleteService
+public class ArticleService : ICreateService
, IDeleteService
{ // ... } -public class Startup +public class Startup { - public IServiceProvider ConfigureServices(IServiceCollection services) + public void ConfigureServices(IServiceCollection services) { services.AddScoped, ArticleService>(); services.AddScoped, ArticleService>(); } } ``` - + Other dependency injection frameworks such as Autofac can be used to simplify this syntax. ```c# builder.RegisterType().AsImplementedInterfaces(); ``` - -Then in the controller, you should inherit the base controller and pass the services into the named, optional base parameters: + +Then in the controller, you should inherit from the base controller and pass the services into the named, optional base parameters: ```c# -public class ArticlesController : BaseJsonApiController
+public class ArticlesController : BaseJsonApiController
{ public ArticlesController( - IJsonApiContext jsonApiContext, - ICreateService
create, - IDeleteService
delete - ) : base(jsonApiContext, create: create, delete: delete) { } + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + ICreateService create, + IDeleteService delete) + : base(jsonApiOptions, loggerFactory, create: create, delete: delete) + { } [HttpPost] - public override async Task PostAsync([FromBody] Article entity) - => await base.PostAsync(entity); + public override async Task PostAsync([FromBody] Article entity) + { + return await base.PostAsync(entity); + } [HttpDelete("{id}")] - public override async TaskDeleteAsync(int id) - => await base.DeleteAsync(id); + public override async TaskDeleteAsync(int id) + { + return await base.DeleteAsync(id); + } } -``` \ No newline at end of file +``` diff --git a/docs/usage/filtering.md b/docs/usage/filtering.md index 74f264d54b..9ae0893ac9 100644 --- a/docs/usage/filtering.md +++ b/docs/usage/filtering.md @@ -1,7 +1,7 @@ # Filtering -Resources can be filtered by attributes using the `filter` query parameter. -By default, all attributes are filterable. +Resources can be filtered by attributes using the `filter` query parameter. +By default, all attributes are filterable. The filtering strategy we have selected, uses the following form. ``` @@ -11,8 +11,8 @@ The filtering strategy we have selected, uses the following form. For operations other than equality, the query can be prefixed with an operation identifier. Examples can be found in the table below. -| Operation | Prefix | Example | -|-------------------------------|---------------|------------------------------------------| +| Operation | Prefix | Example | +|-------------------------------|---------------|-------------------------------------------| | Equals | `eq` | `?filter[attribute]=eq:value` | | Not Equals | `ne` | `?filter[attribute]=ne:value` | | Less Than | `lt` | `?filter[attribute]=lt:10` | @@ -25,46 +25,57 @@ Examples can be found in the table below. | Is Null | `isnull` | `?filter[attribute]=isnull:` | | Is Not Null | `isnotnull` | `?filter[attribute]=isnotnull:` | -Filters can be combined and will be applied using an AND operator. +Filters can be combined and will be applied using an AND operator. The following are equivalent query forms to get articles whose ordinal values are between 1-100. ```http GET /api/articles?filter[ordinal]=gt:1,lt:100 HTTP/1.1 -Accept: application/vnd.api+json ``` ```http GET /api/articles?filter[ordinal]=gt:1&filter[ordinal]=lt:100 HTTP/1.1 -Accept: application/vnd.api+json ``` +Aside from filtering on the resource being requested (top-level), filtering on single-depth related resources that are being included can be done too. + +```http +GET /api/articles?include=author&filter[title]=like:marketing&filter[author.lastName]=Smith HTTP/1.1 +``` + +Due to a [limitation](https://github.com/dotnet/efcore/issues/1833) in Entity Framework Core 3.x, filtering does **not** work on nested endpoints: + +```http +GET /api/blogs/1/articles?filter[title]=like:new HTTP/1.1 +``` + + ## Custom Filters There are two ways you can add custom filters: 1. Creating a `ResourceDefinition` as [described previously](~/usage/resources/resource-definitions.html#custom-query-filters) -2. Overriding the `DefaultEntityRepository` shown below +2. Overriding the `DefaultResourceRepository` shown below ```c# -public class AuthorRepository : DefaultEntityRepository +public class AuthorRepository : DefaultResourceRepository { - public AuthorRepository( - AppDbContext context, - ILoggerFactory loggerFactory, - IJsonApiContext jsonApiContext) - : base(context, loggerFactory, jsonApiContext) - { } - - public override IQueryable Filter( - IQueryable authors, - FilterQuery filterQuery) - // if the filter key is "query" (filter[query]), - // find Authors with matching first or last names - // for all other filter keys, use the base method - => filter.Attribute.Is("query") - ? authors.Where(a => - a.First.Contains(filter.Value) - || a.Last.Contains(filter.Value)) - : base.Filter(authors, filter); -} -``` + public AuthorRepository( + ITargetedFields targetedFields, + IDbContextResolver contextResolver, + IResourceGraph resourceGraph, + IGenericServiceFactory genericServiceFactory, + IResourceFactory resourceFactory, + ILoggerFactory loggerFactory) + : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory) + { } + public override IQueryable Filter(IQueryable authors, FilterQueryContext filterQueryContext) + { + // If the filter key is "name" (filter[name]), find authors with matching first or last names. + // For all other filter keys, use the base method. + return filterQueryContext.Attribute.Is("name") + ? authors.Where(author => + author.FirstName.Contains(filterQueryContext.Value) || + author.LastName.Contains(filterQueryContext.Value)) + : base.Filter(authors, filterQueryContext); + } +``` diff --git a/docs/usage/including-relationships.md b/docs/usage/including-relationships.md index e407a3262b..e5af295f00 100644 --- a/docs/usage/including-relationships.md +++ b/docs/usage/including-relationships.md @@ -1,11 +1,10 @@ # Including Relationships -JADNC supports [request include params](http://jsonapi.org/format/#fetching-includes) out of the box, -for side loading related resources. +JsonApiDotNetCore supports [request include params](http://jsonapi.org/format/#fetching-includes) out of the box, +for side-loading related resources. ```http GET /articles/1?include=comments HTTP/1.1 -Accept: application/vnd.api+json { "data": { @@ -34,7 +33,7 @@ Accept: application/vnd.api+json "attributes": { "body": "First!" } - }, + }, { "type": "comments", "id": "12", @@ -50,6 +49,21 @@ Accept: application/vnd.api+json _since v3.0.0_ -JsonApiDotNetCore also supports deeply nested inclusions. -This allows you to include data across relationships by using a period delimited -relationship path such as comments.author. \ No newline at end of file +JsonApiDotNetCore also supports deeply nested inclusions. +This allows you to include data across relationships by using a period-delimited relationship path, for example: + +```http +GET /api/articles?include=author.livingAddress.country +``` + +which is equivalent to: + +```http +GET /api/articles?include=author&include=author.livingAddress&include=author.livingAddress.country +``` + +This can be used on nested endpoints too: + +```http +GET /api/blogs/1/articles?include=author.livingAddress.country +``` diff --git a/docs/usage/index.md b/docs/usage/index.md index fc56ab5147..8f04b05adb 100644 --- a/docs/usage/index.md +++ b/docs/usage/index.md @@ -1 +1 @@ -# Usage \ No newline at end of file +# Usage diff --git a/docs/usage/meta.md b/docs/usage/meta.md index 2a582b59cd..890adcb962 100644 --- a/docs/usage/meta.md +++ b/docs/usage/meta.md @@ -9,17 +9,20 @@ Resource Meta is metadata defined on the resource itself by implementing the `IH ```c# public class Person : Identifiable, IHasMeta { - public Dictionary GetMeta(IJsonApiContext context) - => new Dictionary { - { "copyright", "Copyright 2018 Example Corp." }, - { "authors", new string[] { "Jared Nance" } } + public Dictionary GetMeta() + { + return new Dictionary + { + {"copyright", "Copyright 2018 Example Corp."}, + {"authors", new[] {"John Doe"}} }; + } } ``` ## Request Meta -Request Meta can be added by injecting a service that implements `IRequestMeta`. +Request Meta can be added by injecting a service that implements `IRequestMeta`. This is useful if you need access to other injected services to build the meta object. ```c# @@ -29,20 +32,23 @@ public class RequestMetaService : IRequestMeta // ... } - public Dictionary GetMeta(IJsonApiContext context) - => return new Dictionary { - { "copyright", "Copyright 2018 Example Corp." }, - { "authors", new string[] { "Jared Nance" } } - }; + public Dictionary GetMeta() + { + return new Dictionary + { + {"copyright", "Copyright 2018 Example Corp."}, + {"authors", new string[] {"John Doe"}} + }; + } } ``` ```json { "meta": { - "copyright": "Copyright 2015 Example Corp.", + "copyright": "Copyright 2018 Example Corp.", "authors": [ - "Jared Nance" + "John Doe" ] }, "data": { diff --git a/docs/usage/options.md b/docs/usage/options.md index 8716a27ab2..eefe172e66 100644 --- a/docs/usage/options.md +++ b/docs/usage/options.md @@ -3,35 +3,39 @@ Configuration can be applied when adding the services to the DI container. ```c# -public IServiceProvider ConfigureServices(IServiceCollection services) { - services.AddJsonApi(options => { - // configure the options here - }); +public class Startup +{ + // This method gets called by the runtime. Use this method to add services to the container. + public void ConfigureServices(IServiceCollection services) + { + services.AddJsonApi(options => + { + // configure the options here + }); + } } ``` ## Client Generated Ids -By default, the server will respond with a 403 Forbidden HTTP Status Code if a POST request is received with a client generated id. +By default, the server will respond with a 403 Forbidden HTTP Status Code if a POST request is received with a client-generated ID. However, this can be allowed by setting the AllowClientGeneratedIds flag in the options ```c# -services.AddJsonApi(options => { - options.AllowClientGeneratedIds = true; -}); +options.AllowClientGeneratedIds = true; ``` ## Pagination -If you would like pagination implemented for all resources, you can specify a default page size. - +The default page size used for all resources can be overridden in options (10 by default). To disable paging, set it to 0. +The maximum page size and maximum page number allowed from client requests can be set too (unconstrained by default). You can also include the total number of records in each request. Note that when using this feature, it does add some query overhead since we have to also request the total number of records. ```c# -services.AddJsonApi(options => { - options.DefaultPageSize = 10; -}); +options.DefaultPageSize = 25; +options.MaximumPageSize = 100; +options.MaximumPageNumber = 50; ``` ## Relative Links @@ -39,9 +43,7 @@ services.AddJsonApi(options => { All links are absolute by default. However, you can configure relative links. ```c# -services.AddJsonApi(options => { - options.RelativeLinks = true; -}); +options.RelativeLinks = true; ``` ```json @@ -59,14 +61,12 @@ services.AddJsonApi(options => { } ``` -## Custom Query Parameters +## Custom Query String Parameters -If you would like to use custom query params (parameters not reserved by the json:api specification), you can set `AllowCustomQueryParameters = true`. The default behavior is to return an HTTP 400 Bad Request for unknown query parameters. +If you would like to use custom query string parameters (parameters not reserved by the json:api specification), you can set `AllowCustomQueryStringParameters = true`. The default behavior is to return an HTTP 400 Bad Request for unknown query string parameters. ```c# -services.AddJsonApi(options => { - options.AllowCustomQueryParameters = true; -}); +options.AllowCustomQueryStringParameters = true; ``` ## Custom Serializer Settings @@ -75,16 +75,16 @@ We use Newtonsoft.Json for all serialization needs. If you want to change the default serializer settings, you can: ```c# -options.SerializerSettings.NullValueHandling = NullValueHandling.Ignore; -options.SerializerSettings.ContractResolver = new DasherizedResolver(); +options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; +options.SerializerSettings.Converters.Add(new StringEnumConverter()); +options.SerializerSettings.Formatting = Formatting.Indented; ``` ## Enable ModelState Validation -If you would like to use ModelState validation into your controllers when creating / updating resources you set `ValidateModelState = true`. By default, no model validation is performed. +If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState = true`. By default, no model validation is performed. ```c# -services.AddJsonApi(options => { - options.ValidateModelState = true; -}); +options.ValidateModelState = true; ``` + diff --git a/docs/usage/pagination.md b/docs/usage/pagination.md index 56c3255eed..ca84ac8051 100644 --- a/docs/usage/pagination.md +++ b/docs/usage/pagination.md @@ -4,9 +4,15 @@ Resources can be paginated. This query would fetch the second page of 10 article ```http GET /articles?page[size]=10&page[number]=2 HTTP/1.1 -Accept: application/vnd.api+json ``` +Due to a [limitation](https://github.com/dotnet/efcore/issues/1833) in Entity Framework Core 3.x, paging does **not** work on nested endpoints: + +```http +GET /blogs/1/articles?page[number]=2 HTTP/1.1 +``` + + ## Configuring Default Behavior You can configure the global default behavior as [described previously](~/usage/options.html#pagination). diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md index 4b300b3a6d..529969d146 100644 --- a/docs/usage/resource-graph.md +++ b/docs/usage/resource-graph.md @@ -16,12 +16,12 @@ There are three ways the resource graph can be created: ### Auto-Discovery -Auto-discovery refers to process of reflecting on an assembly and +Auto-discovery refers to the process of reflecting on an assembly and detecting all of the json:api resources and services. The following command will build the resource graph using all `IIdentifiable` -implementations. It also injects service layer overrides which we will -cover in a later section. You can enable auto-discovery for the +implementations. It also injects resource definitions and service layer overrides which we will +cover in a later section. You can enable auto-discovery for the current assembly by adding the following to your `Startup` class. ```c# @@ -29,13 +29,12 @@ current assembly by adding the following to your `Startup` class. public void ConfigureServices(IServiceCollection services) { services.AddJsonApi( - options => { /* ... */ }, - mvcBuilder, + options => { /* ... */ }, discovery => discovery.AddCurrentAssembly()); } ``` -### Entity Framework DbContext +### Entity Framework Core DbContext If you are using Entity Framework Core as your ORM, you can add an entire `DbContext` with one line. @@ -47,6 +46,18 @@ public void ConfigureServices(IServiceCollection services) } ``` +Be aware that the previous command does not inject resource definitions and service layer overrides. You can combine it with auto-discovery to register them. + +```c# +// Startup.cs +public void ConfigureServices(IServiceCollection services) +{ + services.AddJsonApi( + options => { /* ... */ }, + discovery => discovery.AddCurrentAssembly()); +} +``` + ### Manual Specification You can also manually construct the graph. @@ -55,19 +66,16 @@ You can also manually construct the graph. // Startup.cs public void ConfigureServices(IServiceCollection services) { - var mvcBuilder = services.AddMvc(); - - services.AddJsonApi(options => { - options.BuildResourceGraph((builder) => { - builder.AddResource(); - }); - }, mvcBuilder); + services.AddJsonApi(resources: builder => + { + builder.AddResource(); + }); } ``` ### Public Resource Type Name -The public resource type name for is determined by the following criteria (in order of priority): +The public resource type name is determined by the following criteria (in order of priority): 1. The model is decorated with a `ResourceAttribute` ```c# @@ -75,14 +83,8 @@ The public resource type name for is determined by the following criteria (in or public class MyModel : Identifiable { /* ... */ } ``` -2. The `DbSet` is decorated with a `ResourceAttribute`. Note that this only applies if the graph was created from the DbContext (i.e. `services.AddJsonApi()`) -```c# -[Resource("my-models")] -public DbSet MyModel { get; set; } -``` - -3. The configured naming convention (by default this is kebab-case). +2. The configured naming convention (by default this is camel-case). ```c# -// this will be registered as "my-models" +// this will be registered as "myModels" public class MyModel : Identifiable { /* ... */ } ``` diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md index 8f86624254..f5344dd32d 100644 --- a/docs/usage/resources/attributes.md +++ b/docs/usage/resources/attributes.md @@ -13,11 +13,13 @@ public class Person : Identifiable ## Public name There are two ways the public attribute name is determined: -1. By convention, specified by @JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_ResourceNameFormatter +1. By convention, specified by @JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_SerializerSettings ```c# -options.ResourceNameFormatter = new DefaultResourceNameFormatter(); +options.SerializerSettings.ContractResolver = new DefaultContractResolver +{ + NamingStrategy = new CamelCaseNamingStrategy() +}; ``` - 2. Individually using the attribute's constructor ```c# public class Person : Identifiable @@ -27,28 +29,38 @@ public class Person : Identifiable } ``` -## Immutability +## Capabilities + +_since v4.0_ + +Default json:api attribute capabilities are specified by @JsonApiDotNetCore.Configuration.JsonApiOptions.html#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultAttrCapabilities: + +```c# +options.DefaultAttrCapabilities = AttrCapabilities.None; // default: All +``` + +This can be overridden per attribute. -Attributes can be marked as immutable which will prevent `PATCH` requests from updating them. +# Mutability + +Attributes can be marked as mutable, which will allow `PATCH` requests to update them. When immutable, an HTTP 422 response is returned. ```c# public class Person : Identifiable { - [Attr(immutable: true)] + [Attr(AttrCapabilities.AllowMutate)] public string FirstName { get; set; } } ``` -## Filter|Sort-ability +# Filter/Sort-ability -All attributes are filterable and sortable by default. -You can disable this by setting `IsFiterable` and `IsSortable` to `false `. -Requests to filter or sort these attributes will receive an HTTP 400 response. +Attributes can be marked to allow filtering and/or sorting. When not allowed, it results in an HTTP 400 response. ```c# public class Person : Identifiable { - [Attr(isFilterable: false, isSortable: false)] + [Attr(AttrCapabilities.AllowSort | AttrCapabilities.AllowFilter)] public string FirstName { get; set; } } ``` @@ -58,7 +70,7 @@ public class Person : Identifiable Models may contain complex attributes. Serialization of these types is done by Newtonsoft.Json, so you should use their APIs to specify serialization formats. -You can also use global options to specify the `JsonSerializer` that gets used. +You can also use global options to specify `JsonSerializer` configuration. ```c# public class Foo : Identifiable @@ -74,9 +86,9 @@ public class Bar } ``` -If you need your complex attributes persisted as a -JSON string in your database, but you need access to it as a concrete type, you can define two members on your resource. -The first member is the concrete type that you will directly interact with in your application. We can use the `NotMapped` attribute to prevent Entity Framework from mapping it to the database. The second is the raw JSON property that will be persisted to the database. How you use these members should determine which one is responsible for serialization. In this example, we only serialize and deserialize at the time of persistence +If you need your complex attributes persisted as a +JSON string in your database, but you need access to it as a concrete type, you can define two members on your resource. +The first member is the concrete type that you will directly interact with in your application. You can use the `NotMapped` attribute to prevent Entity Framework Core from mapping it to the database. The second is the raw JSON property that will be persisted to the database. How you use these members should determine which one is responsible for serialization. In this example, we only serialize and deserialize at the time of persistence and retrieval. ```c# @@ -85,15 +97,18 @@ public class Foo : Identifiable [Attr, NotMapped] public Bar Bar { get; set; } - public string BarJson - { - get => (Bar == null) - ? "{}" - : JsonConvert.SerializeObject(Bar); - - set => Bar = string.IsNullOrWhiteSpace(value) + public string BarJson + { + get + { + return Bar == null ? "{}" : JsonConvert.SerializeObject(Bar); + } + set + { + Bar = string.IsNullOrWhiteSpace(value) ? null - : JsonConvert.DeserializeObject(value); - }; + : JsonConvert.DeserializeObject(value); + } + } } -``` \ No newline at end of file +``` diff --git a/docs/usage/resources/discovery.md b/docs/usage/resources/discovery.md deleted file mode 100644 index 0596d53fe5..0000000000 --- a/docs/usage/resources/discovery.md +++ /dev/null @@ -1 +0,0 @@ -# Auto-Discovery \ No newline at end of file diff --git a/docs/usage/resources/hooks.md b/docs/usage/resources/hooks.md index 8e3f0e9ae8..a0c48cbd17 100644 --- a/docs/usage/resources/hooks.md +++ b/docs/usage/resources/hooks.md @@ -6,7 +6,7 @@ This section covers the usage of **Resource Hooks**, which is a feature of`Resou By implementing resource hooks on a `ResourceDefintion`, it is possible to intercept the execution of the **Resource Service Layer** (RSL) in various ways. This enables the developer to conveniently define business logic without having to override the RSL. It can be used to implement e.g. * Authorization * [Event-based synchronisation between microservices](https://docs.microsoft.com/en-us/dotnet/standard/microservices-architecture/multi-container-microservice-net-applications/integration-event-based-microservice-communications) -* Logging +* Logging * Transformation of the served data This usage guide covers the following sections @@ -33,16 +33,16 @@ Understanding the semantics will be helpful in identifying which hooks on `Resou The different execution flows within the RSL that may be intercepted can be identified as **pipelines**. Examples of such pipelines are * **Post**: creation of a resource (triggered by the endpoint `POST /my-resource`). * **PostBulk**: creation of multiple resources (triggered by the endpoint `POST /bulk/my-resource`). - * *NB: hooks are not yet supported with bulk operations.* + * *NB: hooks are not yet supported with bulk operations.* * **Get**: reading a resource (triggered by the endpoint `GET /my-resource`). * **GetSingle**: reading a single resource (triggered by the endpoint `GET /my-resource/1`). See the [ResourcePipeline](https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/feat/%23477/src/JsonApiDotNetCore/Hooks/Execution/ResourcePipelineEnum.cs) enum for a full list of available pipelines. ## Actions -Each pipeline is associated with a set of **actions** that work on resources and their relationships. These actions reflect the associated database operations that is performed by JsonApiDotNetCore (in the Repository Layer). Typically, the RSL will execute some service-layer-related code, then invoke the Repository Layer which will perform these actions, after which the execution returns to the RSL. +Each pipeline is associated with a set of **actions** that work on resources and their relationships. These actions reflect the associated database operations that is performed by JsonApiDotNetCore (in the Repository Layer). Typically, the RSL will execute some service-layer-related code, then invoke the Repository Layer which will perform these actions, after which the execution returns to the RSL. -Note that some actions are shared across different pipelines, and note that most pipelines perform multiple actions. There are two types of actions: **main resource actions** and **nested resource actions**. +Note that some actions are shared across different pipelines, and note that most pipelines perform multiple actions. There are two types of actions: **main resource actions** and **nested resource actions**. ### Main resource actions Most actions are trivial in the context of the pipeline where they're executed from. They may be recognised as the familiar *CRUD* operations of an API. These actions are: @@ -66,7 +66,7 @@ These actions are called **nested resource actions** of a particular pipeline be **The `update relationship` action** [As per the Json:Api specification](https://jsonapi.org/format/#crud-creating](https://jsonapi.org/format/#crud-creating), the **Post** pipeline also allows for an `update relationship` action on an already existing resource. For example, when creating an `Article` it is possible to simultaneously relate it to an existing `Person` by setting its author. In this case, the `update relationship` action is a nested action that will work on that `Person`. -**The `implicit update relationship` action** +**The `implicit update relationship` action** the **Delete** pipeline also allows for an `implicit update relationship` action on an already existing resource. For example, for an `Article` that its author property assigned to a particular `Person`, the relationship between them is destroyed when this article is deleted. This update is "implicit" in the sense that no explicit information about that `Person` was provided in the request that triggered this pipeline. An `implicit update relationship` action is therefore performed on that `Person`. See [this section](#advanced-authorization-implicitly-affected-resources) for a more detailed. ### Shared actions @@ -127,7 +127,7 @@ public class ArticleResource : ResourceDefinition
} ``` ## Logging -This example shows how some actions can be logged on the level of API users. +This example shows how some actions can be logged on the level of API users. First consider the following scoped service which creates a logger bound to a particular user and request. ```c# @@ -140,7 +140,7 @@ public class UserActionsLogger : IUserActionsLogger IUserService userService) { var userId = userService.GetUser().Id; - Instance = loggerFactory.CreateLogger($"[request: {Guid.NewGuid()}" + Instance = loggerFactory.CreateLogger($"[request: {Guid.NewGuid()}" + "user: {userId}]"); } } @@ -191,7 +191,7 @@ public class PersonResource : ResourceDefinition { if (pipeline == ResourcePipeline.Delete) { - _userLogger.Log(LogLevel.Information, $"Deleted the {relationship.PublicRelationshipName}" + + _userLogger.Log(LogLevel.Information, $"Deleted the {relationship.PublicRelationshipName}" + "relationship to Article for person '{p.FirstName + p.LastName}' with {p.Id}"); } } @@ -240,14 +240,14 @@ public class PersonResource : ResourceDefinition Note that not only anonymous people will be excluded when directly performing a `GET /people`, but also when included through relationships, like `GET /articles?include=author,reviewers`. Simultaneously, `if` condition that checks for `ResourcePipeline.Get` in the `PersonResource` ensures we still get expected responses from the API when eg. creating a person with `WantsPrivacy` set to true. ## Loading database values -When a hook is executed for a particular resource, JsonApiDotNetCore can load the corresponding database values and provide them in the hooks. This can be useful for eg. +When a hook is executed for a particular resource, JsonApiDotNetCore can load the corresponding database values and provide them in the hooks. This can be useful for eg. * having a diff between a previous and new state of a resource (for example when updating a resource) * performing authorization rules based on the property of a resource. - -For example, consider a scenario in with the following two requirements: + +For example, consider a scenario in with the following two requirements: * We need to log all updates on resources revealing their old and new value. * We need to check if the property `IsLocked` is set is `true`, and if so, cancel the operation. - + Consider an `Article` with title *Hello there* and API user trying to update the the title of this article to *Bye bye*. The above requirements could be implemented as follows ```c# public class ArticleResource : ResourceDefinition
@@ -257,14 +257,14 @@ public class ArticleResource : ResourceDefinition
public constructor ArticleResource(ILogger logger, IJsonApiContext context) { _logger = logger; - _context = context; - } + _context = context; + } public override IEnumerable
BeforeUpdate(IResourceDiff
entityDiff, ResourcePipeline pipeline) { // PropertyGetter is a helper class that takes care of accessing the values on an instance of Article using reflection. var getter = new PropertyGetter
(); - + // ResourceDiff is a class that is like a list that contains ResourceDiffPair elements foreach (ResourceDiffPair
affected in entityDiff) { @@ -282,7 +282,7 @@ public class ArticleResource : ResourceDefinition
} } // You must return IEnumerable
from this hook. - // This means that you could reduce the set of entities that is + // This means that you could reduce the set of entities that is // affected by this request, eg. by entityDiff.Entities.Where( ... ); entityDiff.Entities; } @@ -314,11 +314,11 @@ public class ArticleResource : ResourceDefinition
{ .... } - - [LoadDatabaseValues(false)] + + [LoadDatabaseValues(false)] public override IEnumerable BeforeUpdateRelationships(HashSet ids, IAffectedRelationships
resourcesByRelationship, ResourcePipeline pipeline) { - // the entities stored in the IAffectedRelationships
instance + // the entities stored in the IAffectedRelationships
instance // are plain resource identifier objects when LoadDatabaseValues is turned off, // or objects loaded from the database when LoadDatabaseValues is turned on. .... @@ -344,7 +344,7 @@ Resource hooks can be used to easily implement authorization in your application This can be achieved as follows: ```c# public class PersonResource : ResourceDefinition -{ +{ private readonly _IAuthorizationHelper _auth; public constructor PersonResource(IAuthorizationHelper auth) { @@ -354,12 +354,12 @@ public class PersonResource : ResourceDefinition public override IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline) { - if (!_auth.CanSeeSecretPeople()) + if (!_auth.CanSeeSecretPeople()) { - if (pipeline == ResourcePipeline.GetSingle) + if (pipeline == ResourcePipeline.GetSingle) { throw new JsonApiException(403, "Forbidden to view this person", new UnauthorizedAccessException()); - } + } entities = entities.Where( p => !p.IsSecret) } return entities; @@ -385,7 +385,7 @@ Apart from this, we also wish to verify permissions for the resources that are * 3. Is the API user allowed to update `Bob`? 4. Is the API user allowed to update `Old Article`? -This authorization requirement can be fulfilled as follows. +This authorization requirement can be fulfilled as follows. For checking the permissions for the explicitly affected resources, `New Article` and `Alice`, we may implement the `BeforeUpdate` hook for `Article`: ```c# @@ -409,7 +409,7 @@ public override IEnumerable
BeforeUpdate(IResourceDiff
entityD and the `BeforeUpdateRelationship` hook for `Person`: ```c# -public override IEnumerable BeforeUpdateRelationship(HashSet ids, IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) +public override IEnumerable BeforeUpdateRelationship(HashSet ids, IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) { var updatedOwnerships = resourcesByRelationship.GetByRelationship
(); if (updatedOwnerships.Any()) @@ -420,7 +420,7 @@ public override IEnumerable BeforeUpdateRelationship(HashSet ids throw new JsonApiException(403, "Forbidden to update relationship of this person", new UnauthorizedAccessException()); } } - return ids; + return ids; } ``` @@ -439,7 +439,7 @@ public override void BeforeImplicitUpdateRelationship(IAffectedRelationships resourcesByRelationship, ResourcePipeline pipeline) { @@ -455,10 +455,10 @@ public override void BeforeImplicitUpdateRelationship(IAffectedRelationships` +If you want to use Resource Hooks without Entity Framework Core, there are several things that you need to consider that need to be met. For any resource that you want to use hooks for: +1. The corresponding resource repository must fully implement `IResourceReadRepository` 2. If you are using custom services, you will be responsible for injecting the `IResourceHookExecutor` service into your services and call the appropriate methods. See the [hook execution overview](#hook-execution-overview) to determine which hook should be fired in which scenario. If you are required to use the `BeforeImplicitUpdateRelationship` hook (see previous example), there is an additional requirement. For this hook, given a particular relationship, JsonApiDotNetCore needs to be able to resolve the inverse relationship. For example: if `Article` has one author (a `Person`), then it needs to be able to resolve the `RelationshipAttribute` that corresponds to the inverse relationship for the `author` property. There are two approaches : @@ -469,14 +469,14 @@ public class Article : Identifiable { ... [HasOne("author", inverseNavigationProperty: "OwnerOfArticle")] - public virtual Person Author { get; set; } + public Person Author { get; set; } ... } public class Person : Identifiable { ... [HasOne("article")] - public virtual Article OwnerOfArticle { get; set; } + public Article OwnerOfArticle { get; set; } ... } ``` @@ -488,10 +488,10 @@ public class CustomInverseRelationshipsResolver : IInverseRelationships { // the implementation of this method depends completely // the data access layer you're using. - // It should set the RelationshipAttribute.InverseRelationship property + // It should set the RelationshipAttribute.InverseRelationship property // for all (relevant) relationships. // To have an idea of how to implement this method, see the InverseRelationships class - // in the source code of JADNC: + // in the source code of JsonApiDotNetCore: // https://github.com/json-api-dotnet/JsonApiDotNetCore/blob/59a93590ac4f05c9c246eca9459b49e331250805/src/JsonApiDotNetCore/Internal/InverseRelationships.cs } } @@ -561,7 +561,7 @@ public class ArticleTag If we then eg. implement the `AfterRead` and `OnReturn` hook for `Article` and `Tag`, and perform a `GET /articles?include=tags` request, we may expect the following order of execution: 1. Article AfterRead -2. Tag AfterRead +2. Tag AfterRead 3. Article OnReturn 4. Tag OnReturn @@ -594,7 +594,7 @@ public class ArticleTagWithLinkDateResource : ResourceDefinition Pipeline @@ -623,47 +623,47 @@ This table below shows the involved hooks per pipeline. BeforeRead read AfterRead - ✅ + [x] GetSingle BeforeRead AfterRead - ✅ + [x] GetRelationship BeforeRead AfterRead - ✅ + [x] Post BeforeCreate create
update relationship AfterCreate - ✅ + [x] Patch BeforeUpdate
BeforeUpdateRelationship
BeforeImplicitUpdateRelationship update
update relationship
implicit update relationship AfterUpdate
AfterUpdateRelationship - ✅ + [x] PatchRelationship BeforeUpdate
BeforeUpdateRelationship update
update relationship
implicit update relationship AfterUpdate
AfterUpdateRelationship - ❌ + [ ] Delete BeforeDelete delete
implicit update relationship AfterDelete - ❌ + [ ] BulkPost diff --git a/docs/usage/resources/index.md b/docs/usage/resources/index.md index e8dece398a..f6d056034a 100644 --- a/docs/usage/resources/index.md +++ b/docs/usage/resources/index.md @@ -1,6 +1,6 @@ # Resources -At a minimum, resources must implement `IIdentifiable` where `TId` is the type of the primary key. The easiest way to do this is to inherit `Identifiable`. +At a minimum, resources must implement `IIdentifiable` where `TId` is the type of the primary key. The easiest way to do this is to inherit from `Identifiable`. ```c# public class Person : Identifiable @@ -19,21 +19,21 @@ public class Person : Identifiable { } ``` -If you need to hang annotations or attributes on the `Id` property, +If you need to hang annotations or attributes on the `Id` property, you can override the virtual property. ```c# public class Person : Identifiable -{ +{ [Key] [Column("person_id")] public override int Id { get; set; } } ``` -If your resource must inherit from another class, -you can always implement the interface yourself. -In this example, `ApplicationUser` inherits `IdentityUser` +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. ```c# @@ -42,4 +42,4 @@ public class ApplicationUser : IdentityUser, IIdentifiable [NotMapped] public string StringId { get => Id; set => Id = value; } } -``` \ No newline at end of file +``` diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md index 8eb98c17b4..4eff452475 100644 --- a/docs/usage/resources/relationships.md +++ b/docs/usage/resources/relationships.md @@ -1,11 +1,11 @@ # Relationships -In order for navigation properties to be identified in the model, +In order for navigation properties to be identified in the model, they should be labeled with the appropriate attribute (either `HasOne`, `HasMany` or `HasManyThrough`). ## HasOne -Dependent relationships should contain a property in the form `{RelationshipName}Id`. +Dependent relationships should contain a property in the form `{RelationshipName}Id`. For example, a TodoItem may have an Owner and so the Id attribute should be OwnerId. ```c# @@ -15,7 +15,7 @@ public class TodoItem : Identifiable public string Description { get; set; } [HasOne("owner")] - public virtual Person Owner { get; set; } + public Person Owner { get; set; } public int OwnerId { get; set; } } ``` @@ -32,25 +32,55 @@ public class Person : Identifiable public string FirstName { get; set; } [HasMany("todo-items")] - public virtual List TodoItems { get; set; } + public ICollection TodoItems { get; set; } } ``` ## HasManyThrough -Currently EntityFrameworkCore [does not support](https://github.com/aspnet/EntityFrameworkCore/issues/1368) Many-to-Many relationships without a join entity. -For this reason, we have decided to fill this gap by allowing applications to declare a relationships as `HasManyThrough`. +Currently, Entity Framework Core [does not support](https://github.com/aspnet/EntityFrameworkCore/issues/1368) many-to-many relationships without a join entity. +For this reason, we have decided to fill this gap by allowing applications to declare a relationship as `HasManyThrough`. JsonApiDotNetCore will expose this attribute to the client the same way as any other `HasMany` attribute. -However, under the covers it will use the join type and EntityFramework's APIs to get and set the relationship. +However, under the covers it will use the join type and Entity Framework Core's APIs to get and set the relationship. ```c# public class Article : Identifiable { - [NotMapped] // ← tells EF to ignore this property - [HasManyThrough(nameof(ArticleTags))] // ← tells JADNC to use this as an alias to ArticleTags.Tags - public List Tags { get; set; } + [NotMapped] // tells Entity Framework Core to ignore this property + [HasManyThrough(nameof(ArticleTags))] // tells JsonApiDotNetCore to use this as an alias to ArticleTags.Tags + public ICollection Tags { get; set; } - // this is the EF join relationship - public List ArticleTags { get; set; } + // this is the Entity Framework Core join relationship + public ICollection ArticleTags { get; set; } +} +``` + +# Eager loading + +_since v4.0_ + +Your entity may contain a calculated property, whose value depends on a navigation property that is not exposed as a json:api resource. +So for the calculated property to be evaluated correctly, the related entity must always be retrieved. You can achieve that using `EagerLoad`, for example: + +```c# +public class ShippingAddress : Identifiable +{ + [Attr] + public string Street { get; set; } + + [Attr] + public string CountryName + { + get { return Country.DisplayName; } + } + + [EagerLoad] // not exposed as resource, but adds .Include("Country") to the query + public Country Country { get; set; } +} + +public class Country +{ + public string IsoCode { get; set; } + public string DisplayName { get; set; } } ``` diff --git a/docs/usage/resources/resource-definitions.md b/docs/usage/resources/resource-definitions.md index 6810c97227..d103630d8e 100644 --- a/docs/usage/resources/resource-definitions.md +++ b/docs/usage/resources/resource-definitions.md @@ -11,12 +11,11 @@ There are some cases where you want attributes excluded from your resource respo For example, you may accept some form data that shouldn't be exposed after creation. This kind of data may get hashed in the database and should never be exposed to the client. -Using the techniques described below, you can achieve the following reques/response behavior: +Using the techniques described below, you can achieve the following request/response behavior: ```http POST /users HTTP/1.1 Content-Type: application/vnd.api+json -Accept: application/vnd.api+json { "data": { @@ -48,38 +47,24 @@ Content-Type: application/vnd.api+json ### Single Attribute ```c# -public class ModelResource : ResourceDefinition +public class UserDefinition : ResourceDefinition { - protected override List OutputAttrs() - => Remove(m => m.AccountNumber); + public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + { + HideFields(user => user.AccountNumber); + } } ``` ### Multiple Attributes ```c# -public class ModelResource : ResourceDefinition -{ - protected override List OutputAttrs() - => Remove(m => new { m.AccountNumber, m.Password }); -} -``` - -### Derived ResourceDefinitions - -If you want to inherit from a different `ResourceDefinition`, these attributes can be composed like so: - -```c# -public class BaseResource : ResourceDefinition -{ - protected override List OutputAttrs() - => Remove(m => m.TenantId); -} - -public class AccountResource : ResourceDefinition +public class UserDefinition : ResourceDefinition { - protected override List OutputAttrs() - => Remove(m => m.AccountNumber, from: base.OutputAttrs()); + public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph) + { + HideFields(user => new {user.AccountNumber, user.Password}); + } } ``` @@ -90,13 +75,16 @@ _since v3.0.0_ You can define the default sort behavior if no `sort` query is provided. ```c# -public class AccountResource : ResourceDefinition +public class AccountDefinition : ResourceDefinition { - protected override PropertySortOrder GetDefaultSortOrder() - => new PropertySortOrder { - (t => t.Prop, SortDirection.Ascending), - (t => t.Prop2, SortDirection.Descending), + public override PropertySortOrder GetDefaultSortOrder() + { + return new PropertySortOrder + { + (account => account.LastLoginTime, SortDirection.Descending), + (account => account.UserName, SortDirection.Ascending) }; + } } ``` @@ -108,16 +96,27 @@ You can define additional query parameters and the query that should be used. If the key is present in a filter request, the supplied query will be used rather than the default behavior. ```c# -public class ItemResource : ResourceDefinition +public class ItemDefinition : ResourceDefinition { // handles queries like: ?filter[was-active-on]=2018-10-15T01:25:52Z public override QueryFilters GetQueryFilters() - => new QueryFilters { - { "was-active-on", (items, filter) => DateTime.TryParse(filter.Value, out dateValue) - ? items.Where(i => i.Expired == null || dateValue < i.Expired) - : throw new JsonApiException(400, $"'{filter.Value}' is not a valid date.") + { + return new QueryFilters + { + { + "was-active-on", (items, filter) => + { + return DateTime.TryParse(filter.Value, out DateTime timeValue) + ? items.Where(item => item.ExpireTime == null || timeValue < item.ExpireTime) + : throw new JsonApiException(new Error(HttpStatusCode.BadRequest) + { + Title = "Invalid filter value", + Detail = $"'{filter.Value}' is not a valid date." + }); + } } }; + } } ``` diff --git a/docs/usage/routing.md b/docs/usage/routing.md index d11b45ac43..fae3ffeb93 100644 --- a/docs/usage/routing.md +++ b/docs/usage/routing.md @@ -1,57 +1,59 @@ # Routing -By default the library will configure routes for each controller. -Based on the recommendations outlined in the JSONAPI spec, routes are hyphenated. +By default the library will configure routes for each controller. +Based on the recommendations outlined in the json:api spec, routes are camel-cased. ```http -GET /api/compound-models HTTP/1.1 -Accept: application/vnd.api+json +GET /api/compoundModels HTTP/1.1 ``` ## Namespacing and Versioning URLs -You can add a namespace to the URL by specifying it in ConfigureServices +You can add a namespace to all URLs by specifying it in ConfigureServices ```c# -public IServiceProvider ConfigureServices(IServiceCollection services) { +public void ConfigureServices(IServiceCollection services) +{ services.AddJsonApi( - opt => opt.Namespace = "api/v1"); + options => options.Namespace = "api/v1"); } ``` +Which results in URLs like: https://yourdomain.com/api/v1/people ## Disable Convention -You can disable the dasherized convention and specify your own template by using the `DisableRoutingConvention` Attribute. +You can disable the default casing convention and specify your own template by using the `DisableRoutingConvention` attribute. ```c# [Route("[controller]")] [DisableRoutingConvention] -public class CamelCasedModelsController : JsonApiController { +public class CamelCasedModelsController : JsonApiController +{ public CamelCasedModelsController( - IJsonApiContext jsonApiContext, - IResourceService resourceService) - : base(jsonApiContext, resourceService) + IJsonApiOptions jsonApiOptions, + ILoggerFactory loggerFactory, + IResourceService resourceService) + : base(jsonApiOptions, loggerFactory, resourceService) { } } ``` -It is important to note that your routes must still end with the model name in the same format as the resource name. This is so that we can build accurate resource links in the json:api document. For example, if you define a resource as MyModels the controller route must match. +It is important to note that your routes must still end with the model name in the same format as the resource name. This is so that we can build accurate resource links in the json:api document. For example, if you define a resource as MyModels, the controller route must match. ```c# -public IServiceProvider ConfigureServices(IServiceCollection services) { - services.AddJsonApi(options => { - options.BuildContextGraph((builder) => { - builder.AddResource("myModels"); // camelCased - }); - }); +public void ConfigureServices(IServiceCollection services) +{ + services.AddJsonApi(resources: builder => + builder.AddResource("my-models")); // kebab-cased } // controller definition -[Route("api/myModels"), DisableRoutingConvention] -public class MyModelsController : JsonApiController { +[Route("api/my-models"), DisableRoutingConvention] +public class MyModelsController : JsonApiController +{ //... } ``` -See [this](~/usage/resource-graph.html#public-resource-type-name) for -more information on how the resource name is determined. \ No newline at end of file +See [this](~/usage/resource-graph.html#public-resource-type-name) for +more information on how the resource name is determined. diff --git a/docs/usage/sorting.md b/docs/usage/sorting.md index e8d5c83bae..1f542715b6 100644 --- a/docs/usage/sorting.md +++ b/docs/usage/sorting.md @@ -1,21 +1,33 @@ # Sorting -Resources can be sorted by an attribute. -The default sort order is ascending. +Resources can be sorted by one or more attributes. +The default sort order is ascending. To sort descending, prepend the sort key with a minus (-) sign. ## Ascending ```http GET /api/articles?sort=author HTTP/1.1 -Accept: application/vnd.api+json ``` ## Descending ```http GET /api/articles?sort=-author HTTP/1.1 -Accept: application/vnd.api+json +``` + +## Multiple attributes + +```http +GET /api/articles?sort=author,-pageCount HTTP/1.1 +``` + +## Limitations + +Sorting currently does **not** work on nested endpoints: + +```http +GET /api/blogs/1/articles?sort=title HTTP/1.1 ``` ## Default Sort diff --git a/docs/usage/sparse-field-selection.md b/docs/usage/sparse-field-selection.md index 04a0ecd146..a2aa7c3f38 100644 --- a/docs/usage/sparse-field-selection.md +++ b/docs/usage/sparse-field-selection.md @@ -1,8 +1,25 @@ # Sparse Field Selection -We currently support top-level field selection. What this means is you can restrict which fields are returned by a query using the fields query parameter, but this does not yet apply to included relationships. +As an alternative to returning all attributes from a resource, the fields query parameter can be used to select only a subset. +This can be used on the resource being requested (top-level), as well as on single-depth related resources that are being included. +Top-level example: ```http -GET /articles?fields[articles]=title,body HTTP/1.1 -Accept: application/vnd.api+json -``` \ No newline at end of file +GET /articles?fields=title,body HTTP/1.1 +``` + +Example for an included relationship: +```http +GET /articles?include=author&fields[author]=name HTTP/1.1 +``` + +Example for both top-level and relationship: +```http +GET /articles?fields=title,body&include=author&fields[author]=name HTTP/1.1 +``` + +Field selection currently does **not** work on nested endpoints: + +```http +GET /api/blogs/1/articles?fields=title,body HTTP/1.1 +``` diff --git a/docs/usage/toc.md b/docs/usage/toc.md index 0e03d73662..aee63a8b7d 100644 --- a/docs/usage/toc.md +++ b/docs/usage/toc.md @@ -20,4 +20,4 @@ ## [Services](extensibility/services.md) ## [Repositories](extensibility/repositories.md) ## [Middleware](extensibility/middleware.md) -## [Custom Query Formats](extensibility/custom-query-formats.md) \ No newline at end of file +## [Custom Query Formats](extensibility/custom-query-formats.md) diff --git a/src/Examples/GettingStarted/README.md b/src/Examples/GettingStarted/README.md index d2c91c6d6a..04d0f70048 100644 --- a/src/Examples/GettingStarted/README.md +++ b/src/Examples/GettingStarted/README.md @@ -11,4 +11,4 @@ For further documentation and implementation of a JsonApiDotnetCore Application Repository: https://github.com/json-api-dotnet/JsonApiDotNetCore -Documentation: https://json-api-dotnet.github.io/ \ No newline at end of file +Documentation: https://json-api-dotnet.github.io/ diff --git a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs index 07a71edaf2..c5028060e0 100644 --- a/src/JsonApiDotNetCore/Models/ResourceDefinition.cs +++ b/src/JsonApiDotNetCore/Models/ResourceDefinition.cs @@ -130,14 +130,14 @@ public virtual void BeforeImplicitUpdateRelationship(IRelationshipsDictionary, FilterQuery, IQueryable>> { } /// - /// Define a the default sort order if no sort key is provided. + /// Define the default sort order if no sort key is provided. /// /// /// A list of properties and the direction they should be sorted. /// /// /// - /// protected override PropertySortOrder GetDefaultSortOrder() + /// public override PropertySortOrder GetDefaultSortOrder() /// => new PropertySortOrder { /// (t => t.Prop1, SortDirection.Ascending), /// (t => t.Prop2, SortDirection.Descending), diff --git a/wiki/v4/content/query-parameter-services.md b/wiki/v4/content/query-parameter-services.md index 28aba58f51..73bdd84f55 100644 --- a/wiki/v4/content/query-parameter-services.md +++ b/wiki/v4/content/query-parameter-services.md @@ -9,15 +9,15 @@ This article describes Below is a list of the query parameters that are supported. Each supported query parameter has it's own dedicated service. -| Query Parameter Service | Occurence in URL | Domain | -|-------------------------|--------------------------------|-------------------------------------------------------| -| `IFilterService` | `?filter[article.title]=title` | filtering the resultset | -| `IIncludeService` | `?include=article.author` | including related data | -| `IPageService` | `?page[size]=10&page[number]=3` | pagination of the resultset | -| `ISortService` | `?sort=-title` | sorting the resultset | -| `ISparseFieldsService` | `?fields[article]=title,summary` | sparse field selection | -| `IOmitDefaultService` | `?omitDefault=true` | omitting default values from the serialization result, eg `guid-value": "00000000-0000-0000-0000-000000000000"` | -| `IOmitNullService` | `?omitNull=false` | omitting null values from the serialization result | +| Query Parameter Service | Occurence in URL | Domain | +|-------------------------|----------------------------------|-------------------------------------------------------| +| `IFilterService` | `?filter[article.title]=title` | filtering the resultset | +| `IIncludeService` | `?include=article.author` | including related data | +| `IPageService` | `?page[size]=10&page[number]=3` | pagination of the resultset | +| `ISortService` | `?sort=-title` | sorting the resultset | +| `ISparseFieldsService` | `?fields[article]=title,summary` | sparse field selection | +| `IDefaultsService` | `?default=false` | omitting default values from the serialization result, eg `guid-value": "00000000-0000-0000-0000-000000000000"` | +| `INullsService` | `?nulls=true` | omitting null values from the serialization result | These services are responsible for parsing the value from the URL by gathering relevant (meta)data and performing validations as required by JsonApiDotNetCore down the pipeline. For example, the `IIncludeService` is responsible for checking if `article.author` is a valid relationship chain, and pre-processes the chain into a `List` so that the rest of the framework can process it easier.