diff --git a/Directory.Build.props b/Directory.Build.props
index 9aaa62aba4..0710d72cd6 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -16,7 +16,7 @@
2.4.1
5.10.3
- 29.0.1
- 4.13.1
+ 31.0.3
+ 4.14.6
diff --git a/benchmarks/Benchmarks.csproj b/benchmarks/Benchmarks.csproj
index 49761638ed..31ddcf4eaa 100644
--- a/benchmarks/Benchmarks.csproj
+++ b/benchmarks/Benchmarks.csproj
@@ -9,7 +9,7 @@
-
+
diff --git a/docs/getting-started/step-by-step.md b/docs/getting-started/step-by-step.md
index ba59a29838..3924601dcf 100644
--- a/docs/getting-started/step-by-step.md
+++ b/docs/getting-started/step-by-step.md
@@ -62,7 +62,7 @@ public class AppDbContext : DbContext
### Define Controllers
You need to create controllers that inherit from `JsonApiController` or `JsonApiController`
-where `T` is the model that inherits from `Identifiable`
+where `TResource` is the model that inherits from `Identifiable`
```c#
public class PeopleController : JsonApiController
diff --git a/docs/internals/queries.md b/docs/internals/queries.md
index dc3d575217..f87e818072 100644
--- a/docs/internals/queries.md
+++ b/docs/internals/queries.md
@@ -17,7 +17,7 @@ Processing a request involves the following steps:
- These validated expressions contain direct references to attributes and relationships.
- The readers also implement `IQueryConstraintProvider`, which exposes expressions through `ExpressionInScope` objects.
- `QueryLayerComposer` (used from `JsonApiResourceService`) collects all query constraints.
- - It combines them with default options and `ResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
+ - It combines them with default options and `IResourceDefinition` overrides and composes a tree of `QueryLayer` objects.
- It lifts the tree for nested endpoints like /blogs/1/articles and rewrites includes.
- `JsonApiResourceService` contains no more usage of `IQueryable`.
- `EntityFrameworkCoreRepository` delegates to `QueryableBuilder` to transform the `QueryLayer` tree into `IQueryable` expression trees.
diff --git a/docs/usage/errors.md b/docs/usage/errors.md
index 79ae35af6c..452250eb5c 100644
--- a/docs/usage/errors.md
+++ b/docs/usage/errors.md
@@ -23,3 +23,70 @@ throw new JsonApiException(new Error(HttpStatusCode.Conflict)
```
In both cases, the middleware will properly serialize it and return it as a json:api error.
+
+# Exception handling
+
+The translation of user-defined exceptions to error responses can be customized by registering your own handler.
+This handler is also the place to choose the log level and message, based on the exception type.
+
+```c#
+public class ProductOutOfStockException : Exception
+{
+ public int ProductId { get; }
+
+ public ProductOutOfStockException(int productId)
+ {
+ ProductId = productId;
+ }
+}
+
+public class CustomExceptionHandler : ExceptionHandler
+{
+ public CustomExceptionHandler(ILoggerFactory loggerFactory, IJsonApiOptions options)
+ : base(loggerFactory, options)
+ {
+ }
+
+ protected override LogLevel GetLogLevel(Exception exception)
+ {
+ if (exception is ProductOutOfStockException)
+ {
+ return LogLevel.Information;
+ }
+
+ return base.GetLogLevel(exception);
+ }
+
+ protected override string GetLogMessage(Exception exception)
+ {
+ if (exception is ProductOutOfStockException productOutOfStock)
+ {
+ return $"Product {productOutOfStock.ProductId} is currently unavailable.";
+ }
+
+ return base.GetLogMessage(exception);
+ }
+
+ protected override ErrorDocument CreateErrorDocument(Exception exception)
+ {
+ if (exception is ProductOutOfStockException productOutOfStock)
+ {
+ return new ErrorDocument(new Error(HttpStatusCode.Conflict)
+ {
+ Title = "Product is temporarily available.",
+ Detail = $"Product {productOutOfStock.ProductId} cannot be ordered at the moment."
+ });
+ }
+
+ return base.CreateErrorDocument(exception);
+ }
+}
+
+public class Startup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddScoped();
+ }
+}
+```
diff --git a/docs/usage/extensibility/controllers.md b/docs/usage/extensibility/controllers.md
index 82e98796db..1252b1c84e 100644
--- a/docs/usage/extensibility/controllers.md
+++ b/docs/usage/extensibility/controllers.md
@@ -16,7 +16,7 @@ public class ArticlesController : JsonApiController
## 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.
+If your model is using a type other than `int` for the primary key, you must explicitly declare it in the controller/service/repository definitions.
```c#
public class ArticlesController : JsonApiController
@@ -34,7 +34,7 @@ public class ArticlesController : JsonApiController
## 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.
+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.
In this example, if a client attempts to do anything other than GET a resource, an HTTP 404 Not Found response will be returned since no other methods are exposed.
diff --git a/docs/usage/extensibility/layer-overview.md b/docs/usage/extensibility/layer-overview.md
index 458d45b5a1..6f3b622e07 100644
--- a/docs/usage/extensibility/layer-overview.md
+++ b/docs/usage/extensibility/layer-overview.md
@@ -10,7 +10,7 @@ JsonApiController (required)
+-- EntityFrameworkCoreRepository : 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
@@ -19,36 +19,15 @@ You can use the following as a general rule of thumb for where to put business l
## Replacing Services
-**Note:** If you are using auto-discovery, services will be automatically registered for you.
+**Note:** If you are using auto-discovery, resource services and repositories 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.
-
-In v3.0.0 we introduced an extenion method that you should use to
-register services. This method handles some of the common issues
-users have had with service registration.
+Replacing services and repositories is done on a per-resource basis and can be done through dependency injection in your Startup.cs file.
```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
- // custom service
- services.AddResourceService();
-
- // custom repository
- services.AddScoped();
-}
-```
-
-Prior to v3.0.0 you could do it like so:
-
-```c#
-// Startup.cs
-public void ConfigureServices(IServiceCollection services)
-{
- // custom service
- services.AddScoped, FooService>();
-
- // custom repository
- services.AddScoped, FooRepository>();
+ services.AddScoped();
+ services.AddScoped>();
}
```
diff --git a/docs/usage/extensibility/middleware.md b/docs/usage/extensibility/middleware.md
index 2b1c0bce2e..9a95296754 100644
--- a/docs/usage/extensibility/middleware.md
+++ b/docs/usage/extensibility/middleware.md
@@ -14,27 +14,37 @@ services.AddService()
The following example replaces all internal filters with a custom filter.
```c#
-/// In Startup.ConfigureServices
-services.AddSingleton();
-
-var builder = services.AddMvcCore();
-services.AddJsonApi(mvcBuilder: builder);
-
-// Ensure this call is placed after the AddJsonApi call.
-builder.AddMvcOptions(mvcOptions =>
+public class Startup
{
- _postConfigureMvcOptions?.Invoke(mvcOptions);
-});
-
-/// In Startup.Configure
-app.UseJsonApi();
-
-// Ensure this call is placed before the UseEndpoints call.
-_postConfigureMvcOptions = mvcOptions =>
-{
- mvcOptions.Filters.Clear();
- mvcOptions.Filters.Insert(0, app.ApplicationServices.GetService());
-};
-
-app.UseEndpoints(endpoints => endpoints.MapControllers());
+ private Action _postConfigureMvcOptions;
+
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddSingleton();
+
+ var builder = services.AddMvcCore();
+ services.AddJsonApi(mvcBuilder: builder);
+
+ // Ensure this call is placed after the AddJsonApi call.
+ builder.AddMvcOptions(mvcOptions =>
+ {
+ _postConfigureMvcOptions.Invoke(mvcOptions);
+ });
+ }
+
+ public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
+ {
+ // Ensure this call is placed before the UseEndpoints call.
+ _postConfigureMvcOptions = mvcOptions =>
+ {
+ mvcOptions.Filters.Clear();
+ mvcOptions.Filters.Insert(0,
+ app.ApplicationServices.GetService());
+ };
+
+ app.UseRouting();
+ app.UseJsonApi();
+ app.UseEndpoints(endpoints => endpoints.MapControllers());
+ }
+}
```
diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md
index d4e9c27f09..eb59c81d7c 100644
--- a/docs/usage/extensibility/repositories.md
+++ b/docs/usage/extensibility/repositories.md
@@ -1,7 +1,7 @@
# Resource Repositories
-If you want to use a data access technology other than Entity Framework Core, you can create an implementation of IResourceRepository.
-If you only need minor changes you can override the methods defined in EntityFrameworkCoreRepository.
+If you want to use a data access technology other than Entity Framework Core, you can create an implementation of `IResourceRepository`.
+If you only need minor changes you can override the methods defined in `EntityFrameworkCoreRepository`.
The repository should then be added to the service collection in Startup.cs.
@@ -9,13 +9,12 @@ The repository should then be added to the service collection in Startup.cs.
public void ConfigureServices(IServiceCollection services)
{
services.AddScoped, ArticleRepository>();
- // ...
}
```
A sample implementation that performs authorization might look like this.
-All of the methods in EntityFrameworkCoreRepository will use the GetAll() method to get the DbSet, so this is a good method to apply filters such as user or tenant authorization.
+All of the methods in EntityFrameworkCoreRepository will use the `GetAll()` method to get the `DbSet`, so this is a good method to apply filters such as user or tenant authorization.
```c#
public class ArticleRepository : EntityFrameworkCoreRepository
@@ -31,7 +30,8 @@ public class ArticleRepository : EntityFrameworkCoreRepository
IResourceFactory resourceFactory,
IEnumerable constraintProviders,
ILoggerFactory loggerFactory)
- : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory)
+ : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory,
+ resourceFactory, constraintProviders, loggerFactory)
{
_authenticationService = authenticationService;
}
diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md
index 98fbf3fc85..c99911df42 100644
--- a/docs/usage/extensibility/services.md
+++ b/docs/usage/extensibility/services.md
@@ -16,17 +16,17 @@ public class TodoItemService : JsonApiResourceService
private readonly INotificationService _notificationService;
public TodoItemService(
- IResourceRepository repository,
+ IResourceRepository repository,
IQueryLayerComposer queryLayerComposer,
IPaginationContext paginationContext,
IJsonApiOptions options,
ILoggerFactory loggerFactory,
IJsonApiRequest request,
- IResourceChangeTracker resourceChangeTracker,
+ IResourceChangeTracker resourceChangeTracker,
IResourceFactory resourceFactory,
IResourceHookExecutor hookExecutor = null)
- : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, request,
- resourceChangeTracker, resourceFactory, hookExecutor)
+ : base(repository, queryLayerComposer, paginationContext, options, loggerFactory,
+ request, resourceChangeTracker, resourceFactory, hookExecutor)
{
_notificationService = notificationService;
}
@@ -53,30 +53,27 @@ If you'd like to use another ORM that does not provide what JsonApiResourceServi
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
- // add the service override for MyModel
- services.AddScoped, MyModelService>();
+ // add the service override for Product
+ services.AddScoped, ProductService>();
// add your own Data Access Object
- services.AddScoped();
- // ...
+ services.AddScoped();
}
-// MyModelService.cs
-public class MyModelService : IResourceService
+// ProductService.cs
+public class ProductService : IResourceService
{
- private readonly IMyModelDao _dao;
+ private readonly IProductDao _dao;
- public MyModelService(IMyModelDao dao)
+ public ProductService(IProductDao dao)
{
_dao = dao;
}
- public Task> GetAsync()
+ public Task> GetAsync()
{
- return await _dao.GetModelAsync();
+ return await _dao.GetProductsAsync();
}
-
- // ...
}
```
@@ -136,10 +133,17 @@ public class Startup
}
```
-Other dependency injection frameworks such as Autofac can be used to simplify this syntax.
+In v3.0 we introduced an extension method that you can use to register a resource service on all of its JsonApiDotNetCore interfaces.
+This is helpful when you implement a subset of the resource interfaces and want to register them all in one go.
```c#
-builder.RegisterType().AsImplementedInterfaces();
+public class Startup
+{
+ public void ConfigureServices(IServiceCollection services)
+ {
+ services.AddResourceService();
+ }
+}
```
Then in the controller, you should inherit from the base controller and pass the services into the named, optional base parameters:
diff --git a/docs/usage/filtering.md b/docs/usage/filtering.md
index eb3c7a3da8..dbf6d81c7f 100644
--- a/docs/usage/filtering.md
+++ b/docs/usage/filtering.md
@@ -116,8 +116,8 @@ GET /articles?filter[caption]=tech&filter=expr:equals(caption,'cooking')) HTTP/1
There are multiple ways you can add custom filters:
-1. Creating a `ResourceDefinition` using `OnApplyFilter` (see [here](~/usage/resources/resource-definitions.md#exclude-soft-deleted-resources)) and inject `IRequestQueryStringAccessor`, which works at all depths, but filter operations are constrained to what `FilterExpression` provides
-2. Creating a `ResourceDefinition` using `OnRegisterQueryableHandlersForQueryStringParameters` as [described previously](~/usage/resources/resource-definitions.md#custom-query-string-parameters), which enables the full range of `IQueryable` functionality, but only works on primary endpoints
+1. Implementing `IResourceDefinition.OnApplyFilter` (see [here](~/usage/resources/resource-definitions.md#exclude-soft-deleted-resources)) and inject `IRequestQueryStringAccessor`, which works at all depths, but filter operations are constrained to what `FilterExpression` provides
+2. Implementing `IResourceDefinition.OnRegisterQueryableHandlersForQueryStringParameters` as [described previously](~/usage/resources/resource-definitions.md#custom-query-string-parameters), which enables the full range of `IQueryable` functionality, but only works on primary endpoints
3. Add an implementation of `IQueryConstraintProvider` to supply additional `FilterExpression`s, which are combined with existing filters using AND operator
4. Override `EntityFrameworkCoreRepository.ApplyQueryLayer` to adapt the `IQueryable` expression just before execution
5. Take a deep dive and plug into reader/parser/tokenizer/visitor/builder for adding additional general-purpose filter operators
diff --git a/docs/usage/options.md b/docs/usage/options.md
index 6bb09865b7..1ee93a13de 100644
--- a/docs/usage/options.md
+++ b/docs/usage/options.md
@@ -1,6 +1,6 @@
# Global Options
-Configuration can be applied when adding the services to the DI container.
+Configuration can be applied when adding the services to the dependency injection container.
```c#
public class Startup
@@ -20,7 +20,7 @@ public class Startup
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
+However, this can be allowed by setting the AllowClientGeneratedIds flag in the options:
```c#
options.AllowClientGeneratedIds = true;
@@ -30,7 +30,7 @@ options.AllowClientGeneratedIds = true;
The default page size used for all resources can be overridden in options (10 by default). To disable paging, set it to `null`.
The maximum page size and number allowed from client requests can be set too (unconstrained by default).
-You can also include the total number of resources in each request. Note that when using this feature, it does add some query overhead since we have to also request the total number of resources.
+You can also include the total number of resources in each response. Note that when using this feature, it does add some query overhead since we have to also request the total number of resources.
```c#
options.DefaultPageSize = new PageSize(25);
@@ -64,8 +64,7 @@ options.UseRelativeLinks = true;
## Unknown Query String Parameters
-If you would like to use unknown query string parameters (parameters not reserved by the json:api specification or registered using ResourceDefinitions), you can set `AllowUnknownQueryStringParameters = true`.
-When set, an HTTP 400 Bad Request is returned for unknown query string parameters.
+If you would like to allow unknown query string parameters (parameters not reserved by the json:api specification or registered using resource definitions), you can set `AllowUnknownQueryStringParameters = true`. When set to `false` (the default), an HTTP 400 Bad Request is returned for unknown query string parameters.
```c#
options.AllowUnknownQueryStringParameters = true;
@@ -81,7 +80,7 @@ options.MaximumIncludeDepth = 1;
## Custom Serializer Settings
-We use Newtonsoft.Json for all serialization needs.
+We use [Newtonsoft.Json](https://www.newtonsoft.com/json) for all serialization needs.
If you want to change the default serializer settings, you can:
```c#
@@ -92,7 +91,10 @@ options.SerializerSettings.Formatting = Formatting.Indented;
The default naming convention (as used in the routes and public resources names) is also determined here, and can be changed (default is camel-case):
```c#
-options.SerializerSettings.ContractResolver = new DefaultContractResolver { NamingStrategy = new KebabCaseNamingStrategy() };
+options.SerializerSettings.ContractResolver = new DefaultContractResolver
+{
+ NamingStrategy = new KebabCaseNamingStrategy()
+};
```
Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored.
@@ -100,18 +102,18 @@ Because we copy resource properties into an intermediate object before serializa
## Enable ModelState Validation
-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.
+If you would like to use ASP.NET Core ModelState validation into your controllers when creating / updating resources, set `ValidateModelState` to `true`. By default, no model validation is performed.
```c#
options.ValidateModelState = true;
```
-You will need to use the JsonApiDotNetCore 'IsRequiredAttribute' instead of the built-in 'RequiredAttribute' because it contains modifications to enable partial patching.
-
```c#
public class Person : Identifiable
{
- [IsRequired(AllowEmptyStrings = true)]
+ [Attr]
+ [Required]
+ [MinLength(3)]
public string FirstName { get; set; }
}
```
diff --git a/docs/usage/pagination.md b/docs/usage/pagination.md
index 7f06c30988..7ab92e2dec 100644
--- a/docs/usage/pagination.md
+++ b/docs/usage/pagination.md
@@ -1,6 +1,6 @@
# Pagination
-Resources can be paginated. This request would fetch the second page of 10 articles (articles 11 - 21).
+Resources can be paginated. This request would fetch the second page of 10 articles (articles 11 - 20).
```http
GET /articles?page[size]=10&page[number]=2 HTTP/1.1
diff --git a/docs/usage/resource-graph.md b/docs/usage/resource-graph.md
index e66c6dce17..8dd32eb794 100644
--- a/docs/usage/resource-graph.md
+++ b/docs/usage/resource-graph.md
@@ -23,18 +23,14 @@ is prioritized by the list above in descending order.
Auto-discovery refers to the process of reflecting on an assembly and
detecting all of the json:api resources, resource definitions, resource services and repositories.
-The following command will build the resource graph using all `IIdentifiable`
-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.
+The following command builds the resource graph using all `IIdentifiable` implementations and registers the services mentioned.
+You can enable auto-discovery for the current assembly by adding the following to your `Startup` class.
```c#
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
- services.AddJsonApi(
- options => { /* ... */ },
- discovery => discovery.AddCurrentAssembly());
+ services.AddJsonApi(discovery => discovery.AddCurrentAssembly());
}
```
@@ -56,9 +52,7 @@ Be aware that this does not register resource definitions, resource services and
// Startup.cs
public void ConfigureServices(IServiceCollection services)
{
- services.AddJsonApi(
- options => { /* ... */ },
- discovery => discovery.AddCurrentAssembly());
+ services.AddJsonApi(discovery => discovery.AddCurrentAssembly());
}
```
@@ -92,13 +86,13 @@ services.AddJsonApi(resources: builder =>
2. The model is decorated with a `ResourceAttribute`
```c#
[Resource("myResources")]
-public class MyModel : Identifiable { /* ... */ }
+public class MyModel : Identifiable { }
```
3. The configured naming convention (by default this is camel-case).
```c#
// this will be registered as "myModels"
-public class MyModel : Identifiable { /* ... */ }
+public class MyModel : Identifiable { }
```
The default naming convention can be changed in [options](~/usage/options.md#custom-serializer-settings).
diff --git a/docs/usage/resources/resource-definitions.md b/docs/usage/resources/resource-definitions.md
index ad92a64c1b..a209ea2df4 100644
--- a/docs/usage/resources/resource-definitions.md
+++ b/docs/usage/resources/resource-definitions.md
@@ -1,16 +1,16 @@
# Resource Definitions
In order to improve the developer experience, we have introduced a type that makes
-common modifications to the default API behavior easier. `ResourceDefinition` was first introduced in v2.3.4.
+common modifications to the default API behavior easier. Resource definitions were first introduced in v2.3.4.
-Resource definitions are resolved from the D/I container, so you can inject dependencies in their constructor.
+Resource definitions are resolved from the dependency injection container, so you can inject dependencies in their constructor.
## Customizing query clauses
_since v4.0_
For various reasons (see examples below) you may need to change parts of the query, depending on resource type.
-`ResourceDefinition` provides overridable methods that pass you the result of query string parameter parsing.
+`JsonApiResourceDefinition` (which is an empty implementation of `IResourceDefinition`) provides overridable methods that pass you the result of query string parameter parsing.
The value returned by you determines what will be used to execute the query.
An intermediate format (`QueryExpression` and derived types) is used, which enables us to separate json:api implementation
@@ -24,7 +24,7 @@ For example, you may accept some sensitive data that should only be exposed to a
Note: to exclude attributes unconditionally, use `[Attr(Capabilities = ~AttrCapabilities.AllowView)]`.
```c#
-public class UserDefinition : ResourceDefinition
+public class UserDefinition : JsonApiResourceDefinition
{
public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
{ }
@@ -82,7 +82,7 @@ Content-Type: application/vnd.api+json
You can define the default sort order if no `sort` query string parameter is provided.
```c#
-public class AccountDefinition : ResourceDefinition
+public class AccountDefinition : JsonApiResourceDefinition
{
public override SortExpression OnApplySort(SortExpression existingSort)
{
@@ -102,10 +102,10 @@ public class AccountDefinition : ResourceDefinition
## Enforce page size
-You may want to enforce paging on large database tables.
+You may want to enforce pagination on large database tables.
```c#
-public class AccessLogDefinition : ResourceDefinition
+public class AccessLogDefinition : JsonApiResourceDefinition
{
public override PaginationExpression OnApplyPagination(PaginationExpression existingPagination)
{
@@ -127,7 +127,7 @@ public class AccessLogDefinition : ResourceDefinition
Soft-deletion sets `IsSoftDeleted` to `true` instead of actually deleting the record, so you may want to always filter them out.
```c#
-public class AccountDefinition : ResourceDefinition
+public class AccountDefinition : JsonApiResourceDefinition
{
public override FilterExpression OnApplyFilter(FilterExpression existingFilter)
{
@@ -147,7 +147,7 @@ public class AccountDefinition : ResourceDefinition
## Block including related resources
```c#
-public class EmployeeDefinition : ResourceDefinition
+public class EmployeeDefinition : JsonApiResourceDefinition
{
public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes)
{
@@ -168,18 +168,18 @@ public class EmployeeDefinition : ResourceDefinition
_since v3.0.0_
-You can define additional query string parameters with the query expression that should be used.
-If the key is present in a query string, the supplied query will be executed before the default behavior.
+You can define additional query string parameters with the LINQ expression that should be used.
+If the key is present in a query string, the supplied LINQ expression will be added to the database query.
Note this directly influences the Entity Framework Core `IQueryable`. As opposed to using `OnApplyFilter`, this enables the full range of EF Core functionality.
But it only works on primary resource endpoints (for example: /articles, but not on /blogs/1/articles or /blogs?include=articles).
```c#
-public class ItemDefinition : ResourceDefinition-
+public class ItemDefinition : JsonApiResourceDefinition
-
{
protected override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters()
{
- return new QueryStringParameterHandlers
+ return new QueryStringParameterHandlers
-
{
["isActive"] = (source, parameterValue) => source
.Include(item => item.Children)
@@ -196,7 +196,7 @@ public class ItemDefinition : ResourceDefinition
-
}
```
-## Using ResourceDefinitions prior to v3
+## Using Resource Definitions prior to v3
Prior to the introduction of auto-discovery, you needed to register the
`ResourceDefinition` on the container yourself:
diff --git a/docs/usage/routing.md b/docs/usage/routing.md
index 2ef38335c8..454d256bc9 100644
--- a/docs/usage/routing.md
+++ b/docs/usage/routing.md
@@ -17,9 +17,16 @@ Which results in URLs like: https://yourdomain.com/api/v1/people
The library will configure routes for all controllers in your project. By default, routes are camel-cased. This is based on the [recommendations](https://jsonapi.org/recommendations/) outlined in the json:api spec.
```c#
-public class OrderLine : Identifiable { }
+public class OrderLine : Identifiable { }
-public class OrderLineController : JsonApiController { /* .... */ }
+public class OrderLineController : JsonApiController
+{
+ public OrderLineController(IJsonApiOptions options, ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ {
+ }
+}
```
```http
@@ -32,7 +39,7 @@ The public name of the resource ([which can be customized](~/usage/resource-grap
If a controller does not inherit from `JsonApiController`, the [configured naming convention](~/usage/options.md#custom-serializer-settings) is applied to the name of the controller.
```c#
-public class OrderLineController : ControllerBase { /* .... */ }
+public class OrderLineController : ControllerBase { }
```
```http
GET /orderLines HTTP/1.1
@@ -43,13 +50,20 @@ GET /orderLines HTTP/1.1
It is possible to bypass the default routing convention for a controller.
```c#
[Route("v1/custom/route/orderLines"), DisableRoutingConvention]
-public class OrderLineController : JsonApiController { /* ... */ }
+public class OrderLineController : JsonApiController
+{
+ public OrderLineController(IJsonApiOptions options, ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(options, loggerFactory, resourceService)
+ {
+ }
+}
```
It is required to match your custom url with the public name of the associated resource.
## Advanced Usage: Custom Routing Convention
-It is possible to replace the built-in routing convention with a [custom routing convention]](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`.
+It is possible to replace the built-in routing convention with a [custom routing convention](https://docs.microsoft.com/en-us/aspnet/core/mvc/controllers/application-model?view=aspnetcore-3.1#sample-custom-routing-convention) by registering an implementation of `IJsonApiRoutingConvention`.
```c#
public void ConfigureServices(IServiceCollection services)
{
diff --git a/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj b/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj
index c4db919042..b28d7a0c5a 100644
--- a/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj
+++ b/src/Examples/MultiDbContextExample/MultiDbContextExample.csproj
@@ -1,6 +1,6 @@
- netcoreapp3.1
+ $(NetCoreAppVersion)
diff --git a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj
index 0bf022bbdd..e1b85e00cc 100644
--- a/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj
+++ b/src/Examples/NoEntityFrameworkExample/NoEntityFrameworkExample.csproj
@@ -8,7 +8,7 @@
-
+
diff --git a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
index 092743c5cb..24253becd3 100644
--- a/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
+++ b/src/JsonApiDotNetCore/JsonApiDotNetCore.csproj
@@ -22,7 +22,7 @@
-
+
diff --git a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs
index 3d45eafdb0..e9a51c4092 100644
--- a/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs
+++ b/src/JsonApiDotNetCore/Queries/Internal/QueryableBuilding/SelectClauseBuilder.cs
@@ -5,6 +5,7 @@
using System.Reflection;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Queries.Expressions;
+using JsonApiDotNetCore.Repositories;
using JsonApiDotNetCore.Resources;
using JsonApiDotNetCore.Resources.Annotations;
using Microsoft.EntityFrameworkCore;
@@ -173,21 +174,31 @@ private Expression CreateCollectionInitializer(LambdaScope lambdaScope, Property
Expression layerExpression = builder.ApplyQuery(layer);
- Type enumerableOfElementType = typeof(IEnumerable<>).MakeGenericType(elementType);
- Type typedCollection = TypeHelper.ToConcreteCollectionType(collectionProperty.PropertyType);
+ // Earlier versions of EF Core 3.x failed to understand `query.ToHashSet()`, so we emit `new HashSet(query)` instead.
+ // Interestingly, EF Core 5 RC1 fails to understand `new HashSet(query)`, so we emit `query.ToHashSet()` instead.
+ // https://github.com/dotnet/efcore/issues/22902
- ConstructorInfo typedCollectionConstructor = typedCollection.GetConstructor(new[]
+ if (EntityFrameworkCoreSupport.Version.Major < 5)
{
- enumerableOfElementType
- });
+ Type enumerableOfElementType = typeof(IEnumerable<>).MakeGenericType(elementType);
+ Type typedCollection = TypeHelper.ToConcreteCollectionType(collectionProperty.PropertyType);
- if (typedCollectionConstructor == null)
- {
- throw new Exception(
- $"Constructor on '{typedCollection.Name}' that accepts '{enumerableOfElementType.Name}' not found.");
+ ConstructorInfo typedCollectionConstructor = typedCollection.GetConstructor(new[]
+ {
+ enumerableOfElementType
+ });
+
+ if (typedCollectionConstructor == null)
+ {
+ throw new Exception(
+ $"Constructor on '{typedCollection.Name}' that accepts '{enumerableOfElementType.Name}' not found.");
+ }
+
+ return Expression.New(typedCollectionConstructor, layerExpression);
}
- return Expression.New(typedCollectionConstructor, layerExpression);
+ string operationName = TypeHelper.TypeCanContainHashSet(collectionProperty.PropertyType) ? "ToHashSet" : "ToList";
+ return CopyCollectionExtensionMethodCall(layerExpression, operationName, elementType);
}
private static Expression TestForNull(Expression expressionToTest, Expression ifFalseExpression)
@@ -196,6 +207,14 @@ private static Expression TestForNull(Expression expressionToTest, Expression if
return Expression.Condition(equalsNull, Expression.Convert(_nullConstant, expressionToTest.Type), ifFalseExpression);
}
+ private static Expression CopyCollectionExtensionMethodCall(Expression source, string operationName, Type elementType)
+ {
+ return Expression.Call(typeof(Enumerable), operationName, new[]
+ {
+ elementType
+ }, source);
+ }
+
private Expression SelectExtensionMethodCall(Expression source, Type elementType, Expression selectorBody)
{
return Expression.Call(_extensionType, "Select", new[]
diff --git a/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreSupport.cs b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreSupport.cs
new file mode 100644
index 0000000000..5f4110cbe5
--- /dev/null
+++ b/src/JsonApiDotNetCore/Repositories/EntityFrameworkCoreSupport.cs
@@ -0,0 +1,10 @@
+using System;
+using Microsoft.EntityFrameworkCore;
+
+namespace JsonApiDotNetCore.Repositories
+{
+ internal static class EntityFrameworkCoreSupport
+ {
+ public static Version Version { get; } = typeof(DbContext).Assembly.GetName().Version;
+ }
+}
diff --git a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs
index 4a9ed64bfe..5405fd21e9 100644
--- a/src/JsonApiDotNetCore/Resources/ResourceFactory.cs
+++ b/src/JsonApiDotNetCore/Resources/ResourceFactory.cs
@@ -3,6 +3,7 @@
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
+using JsonApiDotNetCore.Repositories;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
@@ -74,7 +75,7 @@ public NewExpression CreateNewExpression(Type resourceType)
object constructorArgument =
ActivatorUtilities.GetServiceOrCreateInstance(_serviceProvider, constructorParameter.ParameterType);
- var argumentExpression = typeof(DbContext).Assembly.GetName().Version.Major >= 5
+ var argumentExpression = EntityFrameworkCoreSupport.Version.Major >= 5
// Workaround for https://github.com/dotnet/efcore/issues/20502 to not fail on injected DbContext in EF Core 5.
? CreateTupleAccessExpressionForConstant(constructorArgument, constructorArgument.GetType())
: Expression.Constant(constructorArgument);
diff --git a/src/JsonApiDotNetCore/TypeHelper.cs b/src/JsonApiDotNetCore/TypeHelper.cs
index 348612bd5e..2a0b802957 100644
--- a/src/JsonApiDotNetCore/TypeHelper.cs
+++ b/src/JsonApiDotNetCore/TypeHelper.cs
@@ -11,6 +11,11 @@ namespace JsonApiDotNetCore
{
internal static class TypeHelper
{
+ private static readonly Type[] _hashSetCompatibleCollectionTypes =
+ {
+ typeof(HashSet<>), typeof(ICollection<>), typeof(ISet<>), typeof(IEnumerable<>), typeof(IReadOnlyCollection<>)
+ };
+
public static object ConvertType(object value, Type type)
{
if (type == null)
@@ -236,6 +241,21 @@ public static Type ToConcreteCollectionType(Type collectionType)
return collectionType;
}
+ ///
+ /// Indicates whether a instance can be assigned to the specified type,
+ /// for example IList{Article} -> false or ISet{Article} -> true.
+ ///
+ public static bool TypeCanContainHashSet(Type collectionType)
+ {
+ if (collectionType.IsGenericType)
+ {
+ var openCollectionType = collectionType.GetGenericTypeDefinition();
+ return _hashSetCompatibleCollectionTypes.Contains(openCollectionType);
+ }
+
+ return false;
+ }
+
///
/// Gets the type (Guid or int) of the Id of a type that implements IIdentifiable
///
diff --git a/test/UnitTests/DbSetMock.cs b/test/UnitTests/DbSetMock.cs
deleted file mode 100644
index 2921a8247b..0000000000
--- a/test/UnitTests/DbSetMock.cs
+++ /dev/null
@@ -1,135 +0,0 @@
-using System.Collections.Generic;
-using System.Linq;
-using System.Linq.Expressions;
-using System.Threading;
-using System.Threading.Tasks;
-using Microsoft.EntityFrameworkCore;
-using Microsoft.EntityFrameworkCore.Query.Internal;
-using Moq;
-
-public static class DbSetMock
-{
- public static Mock> Create(params T[] elements) where T : class
- {
- return new List(elements).AsDbSetMock();
- }
-}
-
-public static class ListExtensions
-{
- public static Mock> AsDbSetMock(this List list) where T : class
- {
- IQueryable queryableList = list.AsQueryable();
- Mock> dbSetMock = new Mock>();
-
- dbSetMock.As>().Setup(x => x.Expression).Returns(queryableList.Expression);
- dbSetMock.As>().Setup(x => x.ElementType).Returns(queryableList.ElementType);
- dbSetMock.As>().Setup(x => x.GetEnumerator()).Returns(queryableList.GetEnumerator());
-
- var toReturn = new TestAsyncEnumerator(queryableList.GetEnumerator());
-
-
- dbSetMock.As>()
- .Setup(m => m.GetAsyncEnumerator(It.IsAny()))
- .Returns(toReturn);
- dbSetMock.As>().Setup(m => m.Provider).Returns(new TestAsyncQueryProvider(queryableList.Provider));
- return dbSetMock;
- }
-}
-
-internal sealed class TestAsyncQueryProvider : IAsyncQueryProvider
-{
- private readonly IQueryProvider _inner;
-
- internal TestAsyncQueryProvider(IQueryProvider inner)
- {
- _inner = inner;
- }
-
- public IQueryable CreateQuery(Expression expression)
- {
- return new TestAsyncEnumerable(expression);
- }
-
- public IQueryable CreateQuery(Expression expression)
- {
- return new TestAsyncEnumerable(expression);
- }
-
- public object Execute(Expression expression)
- {
- return _inner.Execute(expression);
- }
-
- public TResult Execute(Expression expression)
- {
- return _inner.Execute(expression);
- }
-
- public IAsyncEnumerable ExecuteAsync(Expression expression)
- {
- return new TestAsyncEnumerable(expression);
- }
-
- public Task ExecuteAsync(Expression expression, CancellationToken cancellationToken)
- {
- return Task.FromResult(Execute(expression));
- }
-
- TResult IAsyncQueryProvider.ExecuteAsync(Expression expression, CancellationToken cancellationToken)
- {
-
- return Execute(expression);
- }
-}
-
-internal sealed class TestAsyncEnumerable : EnumerableQuery, IAsyncEnumerable, IQueryable
-{
- public TestAsyncEnumerable(Expression expression)
- : base(expression)
- { }
-
- public IAsyncEnumerator GetEnumerator()
- {
- return new TestAsyncEnumerator(this.AsEnumerable().GetEnumerator());
- }
-
- public IAsyncEnumerator GetAsyncEnumerator(CancellationToken cancellationToken = default)
- {
- throw new System.NotImplementedException();
- }
-
- IQueryProvider IQueryable.Provider => new TestAsyncQueryProvider(this);
-}
-
-internal sealed class TestAsyncEnumerator : IAsyncEnumerator
-{
- private readonly IEnumerator _inner;
-
- public TestAsyncEnumerator(IEnumerator inner)
- {
- _inner = inner;
- }
-
- public void Dispose()
- {
- _inner.Dispose();
- }
-
- public T Current => _inner.Current;
-
- public Task MoveNext(CancellationToken cancellationToken)
- {
- return Task.FromResult(_inner.MoveNext());
- }
-
- public ValueTask MoveNextAsync()
- {
- throw new System.NotImplementedException();
- }
-
- public ValueTask DisposeAsync()
- {
- throw new System.NotImplementedException();
- }
-}