diff --git a/Directory.Build.props b/Directory.Build.props
index 2ef7f513f1..6eb8cf3e1f 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -15,6 +15,7 @@
2.4.1
+ 5.10.329.0.14.13.1
diff --git a/benchmarks/BenchmarkResource.cs b/benchmarks/BenchmarkResource.cs
index 6fe5eea7ca..d7dc1bcce6 100644
--- a/benchmarks/BenchmarkResource.cs
+++ b/benchmarks/BenchmarkResource.cs
@@ -1,4 +1,5 @@
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace Benchmarks
{
diff --git a/benchmarks/DependencyFactory.cs b/benchmarks/DependencyFactory.cs
index d291fa77c0..0492a9a185 100644
--- a/benchmarks/DependencyFactory.cs
+++ b/benchmarks/DependencyFactory.cs
@@ -3,7 +3,7 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Internal.Contracts;
using JsonApiDotNetCore.Models;
-using JsonApiDotNetCore.Query;
+using JsonApiDotNetCore.Services.Contract;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
diff --git a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs
index 5524f83e5f..47751425d9 100644
--- a/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs
+++ b/benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs
@@ -8,23 +8,23 @@ namespace Benchmarks.LinkBuilder
public class LinkBuilderGetNamespaceFromPathBenchmarks
{
private const string RequestPath = "/api/some-really-long-namespace-path/resources/current/articles/?some";
- private const string EntityName = "articles";
+ private const string ResourceName = "articles";
private const char PathDelimiter = '/';
[Benchmark]
- public void UsingStringSplit() => GetNamespaceFromPathUsingStringSplit(RequestPath, EntityName);
+ public void UsingStringSplit() => GetNamespaceFromPathUsingStringSplit(RequestPath, ResourceName);
[Benchmark]
- public void UsingReadOnlySpan() => GetNamespaceFromPathUsingReadOnlySpan(RequestPath, EntityName);
+ public void UsingReadOnlySpan() => GetNamespaceFromPathUsingReadOnlySpan(RequestPath, ResourceName);
- public static string GetNamespaceFromPathUsingStringSplit(string path, string entityName)
+ public static string GetNamespaceFromPathUsingStringSplit(string path, string resourceName)
{
StringBuilder namespaceBuilder = new StringBuilder(path.Length);
string[] segments = path.Split('/');
for (int index = 1; index < segments.Length; index++)
{
- if (segments[index] == entityName)
+ if (segments[index] == resourceName)
{
break;
}
@@ -36,22 +36,22 @@ public static string GetNamespaceFromPathUsingStringSplit(string path, string en
return namespaceBuilder.ToString();
}
- public static string GetNamespaceFromPathUsingReadOnlySpan(string path, string entityName)
+ public static string GetNamespaceFromPathUsingReadOnlySpan(string path, string resourceName)
{
- ReadOnlySpan entityNameSpan = entityName.AsSpan();
+ ReadOnlySpan resourceNameSpan = resourceName.AsSpan();
ReadOnlySpan pathSpan = path.AsSpan();
for (int index = 0; index < pathSpan.Length; index++)
{
if (pathSpan[index].Equals(PathDelimiter))
{
- if (pathSpan.Length > index + entityNameSpan.Length)
+ if (pathSpan.Length > index + resourceNameSpan.Length)
{
- ReadOnlySpan possiblePathSegment = pathSpan.Slice(index + 1, entityNameSpan.Length);
+ ReadOnlySpan possiblePathSegment = pathSpan.Slice(index + 1, resourceNameSpan.Length);
- if (entityNameSpan.SequenceEqual(possiblePathSegment))
+ if (resourceNameSpan.SequenceEqual(possiblePathSegment))
{
- int lastCharacterIndex = index + 1 + entityNameSpan.Length;
+ int lastCharacterIndex = index + 1 + resourceNameSpan.Length;
bool isAtEnd = lastCharacterIndex == pathSpan.Length;
bool hasDelimiterAfterSegment = pathSpan.Length >= lastCharacterIndex + 1 && pathSpan[lastCharacterIndex].Equals(PathDelimiter);
diff --git a/benchmarks/Query/QueryParserBenchmarks.cs b/benchmarks/Query/QueryParserBenchmarks.cs
index 79b9caabf1..64f144b9e5 100644
--- a/benchmarks/Query/QueryParserBenchmarks.cs
+++ b/benchmarks/Query/QueryParserBenchmarks.cs
@@ -1,12 +1,13 @@
using System;
using System.Collections.Generic;
+using System.ComponentModel.Design;
using BenchmarkDotNet.Attributes;
using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Internal;
using JsonApiDotNetCore.Internal.Contracts;
-using JsonApiDotNetCore.Managers;
-using JsonApiDotNetCore.Query;
-using JsonApiDotNetCore.QueryParameterServices.Common;
-using JsonApiDotNetCore.Services;
+using JsonApiDotNetCore.Internal.QueryStrings;
+using JsonApiDotNetCore.QueryStrings;
+using JsonApiDotNetCore.RequestServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Extensions.Logging.Abstractions;
@@ -17,56 +18,59 @@ namespace Benchmarks.Query
public class QueryParserBenchmarks
{
private readonly FakeRequestQueryStringAccessor _queryStringAccessor = new FakeRequestQueryStringAccessor();
- private readonly QueryParameterParser _queryParameterParserForSort;
- private readonly QueryParameterParser _queryParameterParserForAll;
+ private readonly QueryStringReader _queryStringReaderForSort;
+ private readonly QueryStringReader _queryStringReaderForAll;
public QueryParserBenchmarks()
{
- IJsonApiOptions options = new JsonApiOptions();
+ IJsonApiOptions options = new JsonApiOptions
+ {
+ EnableLegacyFilterNotation = true
+ };
+
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
-
- var currentRequest = new CurrentRequest();
- currentRequest.SetRequestResource(resourceGraph.GetResourceContext(typeof(BenchmarkResource)));
- IResourceDefinitionProvider resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
+ var currentRequest = new CurrentRequest
+ {
+ PrimaryResource = resourceGraph.GetResourceContext(typeof(BenchmarkResource))
+ };
- _queryParameterParserForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, resourceDefinitionProvider, options, _queryStringAccessor);
- _queryParameterParserForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, resourceDefinitionProvider, options, _queryStringAccessor);
+ _queryStringReaderForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, options, _queryStringAccessor);
+ _queryStringReaderForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, options, _queryStringAccessor);
}
- private static QueryParameterParser CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph,
- CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider,
+ private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph,
+ CurrentRequest currentRequest,
IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
{
- ISortService sortService = new SortService(resourceDefinitionProvider, resourceGraph, currentRequest);
-
- var queryServices = new List
+ var sortReader = new SortQueryStringParameterReader(currentRequest, resourceGraph);
+
+ var readers = new List
{
- sortService
+ sortReader
};
- return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance);
+ return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance);
}
- private static QueryParameterParser CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
- CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider,
- IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
+ private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
+ CurrentRequest currentRequest, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
{
- IIncludeService includeService = new IncludeService(resourceGraph, currentRequest);
- IFilterService filterService = new FilterService(resourceDefinitionProvider, resourceGraph, currentRequest);
- ISortService sortService = new SortService(resourceDefinitionProvider, resourceGraph, currentRequest);
- ISparseFieldsService sparseFieldsService = new SparseFieldsService(resourceGraph, currentRequest);
- IPageService pageService = new PageService(options, resourceGraph, currentRequest);
- IDefaultsService defaultsService = new DefaultsService(options);
- INullsService nullsService = new NullsService(options);
-
- var queryServices = new List
+ var resourceFactory = new ResourceFactory(new ServiceContainer());
+
+ var filterReader = new FilterQueryStringParameterReader(currentRequest, resourceGraph, resourceFactory, options);
+ var sortReader = new SortQueryStringParameterReader(currentRequest, resourceGraph);
+ var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(currentRequest, resourceGraph);
+ var paginationReader = new PaginationQueryStringParameterReader(currentRequest, resourceGraph, options);
+ var defaultsReader = new DefaultsQueryStringParameterReader(options);
+ var nullsReader = new NullsQueryStringParameterReader(options);
+
+ var readers = new List
{
- includeService, filterService, sortService, sparseFieldsService, pageService, defaultsService,
- nullsService
+ filterReader, sortReader, sparseFieldSetReader, paginationReader, defaultsReader, nullsReader
};
- return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance);
+ return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance);
}
[Benchmark]
@@ -75,7 +79,7 @@ public void AscendingSort()
var queryString = $"?sort={BenchmarkResourcePublicNames.NameAttr}";
_queryStringAccessor.SetQueryString(queryString);
- _queryParameterParserForSort.Parse(null);
+ _queryStringReaderForSort.ReadAll(null);
}
[Benchmark]
@@ -84,7 +88,7 @@ public void DescendingSort()
var queryString = $"?sort=-{BenchmarkResourcePublicNames.NameAttr}";
_queryStringAccessor.SetQueryString(queryString);
- _queryParameterParserForSort.Parse(null);
+ _queryStringReaderForSort.ReadAll(null);
}
[Benchmark]
@@ -93,7 +97,7 @@ public void ComplexQuery() => Run(100, () =>
var queryString = $"?filter[{BenchmarkResourcePublicNames.NameAttr}]=abc,eq:abc&sort=-{BenchmarkResourcePublicNames.NameAttr}&include=child&page[size]=1&fields={BenchmarkResourcePublicNames.NameAttr}";
_queryStringAccessor.SetQueryString(queryString);
- _queryParameterParserForAll.Parse(null);
+ _queryStringReaderForAll.ReadAll(null);
});
private void Run(int iterations, Action action) {
diff --git a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
index 0ff4c7fbbd..5908110d56 100644
--- a/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
+++ b/benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs
@@ -39,7 +39,7 @@ public JsonApiDeserializerBenchmarks()
var options = new JsonApiOptions();
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
var targetedFields = new TargetedFields();
- _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
+ _jsonApiDeserializer = new RequestDeserializer(resourceGraph, new ResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
}
[Benchmark]
diff --git a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
index dd1af5aff2..c334e59235 100644
--- a/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
+++ b/benchmarks/Serialization/JsonApiSerializerBenchmarks.cs
@@ -2,8 +2,9 @@
using BenchmarkDotNet.Attributes;
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Internal.Contracts;
-using JsonApiDotNetCore.Managers;
-using JsonApiDotNetCore.Query;
+using JsonApiDotNetCore.Internal.QueryStrings;
+using JsonApiDotNetCore.Queries;
+using JsonApiDotNetCore.RequestServices;
using JsonApiDotNetCore.Serialization;
using JsonApiDotNetCore.Serialization.Server;
using JsonApiDotNetCore.Serialization.Server.Builders;
@@ -14,7 +15,7 @@ namespace Benchmarks.Serialization
[MarkdownExporter]
public class JsonApiSerializerBenchmarks
{
- private static readonly BenchmarkResource Content = new BenchmarkResource
+ private static readonly BenchmarkResource _content = new BenchmarkResource
{
Id = 123,
Name = Guid.NewGuid().ToString()
@@ -40,14 +41,19 @@ public JsonApiSerializerBenchmarks()
private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)
{
- var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
var currentRequest = new CurrentRequest();
- var sparseFieldsService = new SparseFieldsService(resourceGraph, currentRequest);
-
- return new FieldsToSerialize(resourceGraph, sparseFieldsService, resourceDefinitionProvider);
+
+ var constraintProviders = new IQueryConstraintProvider[]
+ {
+ new SparseFieldSetQueryStringParameterReader(currentRequest, resourceGraph)
+ };
+
+ var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
+
+ return new FieldsToSerialize(resourceGraph, constraintProviders, resourceDefinitionProvider);
}
[Benchmark]
- public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content);
+ public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(_content);
}
}
diff --git a/docs/api/index.md b/docs/api/index.md
index c93ca94a89..51c288d491 100644
--- a/docs/api/index.md
+++ b/docs/api/index.md
@@ -4,6 +4,6 @@ This section documents the package API and is generated from the XML source comm
## Common APIs
-- [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.html)
-- [`IResourceGraph`](JsonApiDotNetCore.Internal.Contracts.IResourceGraph.html)
-- [`ResourceDefinition`](JsonApiDotNetCore.Models.ResourceDefinition-1.html)
+- [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.yml)
+- [`IResourceGraph`](JsonApiDotNetCore.Internal.Contracts.IResourceGraph.yml)
+- [`ResourceDefinition`](JsonApiDotNetCore.Models.ResourceDefinition-1.yml)
diff --git a/docs/docfx.json b/docs/docfx.json
index ccc9762111..68351f5471 100644
--- a/docs/docfx.json
+++ b/docs/docfx.json
@@ -25,6 +25,7 @@
"getting-started/**/toc.yml",
"usage/**.md",
"request-examples/**.md",
+ "internals/**.md",
"toc.yml",
"*.md"
]
diff --git a/docs/index.md b/docs/index.md
index cb6a3abad0..c4518c8884 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,10 +14,10 @@ Eliminate CRUD boilerplate and provide the following features across your resour
- Filtering
- Sorting
- Pagination
-- Sparse field selection
+- Sparse fieldset selection
- Relationship inclusion and navigation
-Checkout the [example requests](request-examples) to see the kind of features you will get out of the box.
+Checkout the [example requests](request-examples/index.md) to see the kind of features you will get out of the box.
### 2. Extensibility
diff --git a/docs/internals/index.md b/docs/internals/index.md
new file mode 100644
index 0000000000..7e27842923
--- /dev/null
+++ b/docs/internals/index.md
@@ -0,0 +1,3 @@
+# Internals
+
+The section contains overviews for the inner workings of the JsonApiDotNetCore library.
diff --git a/docs/internals/queries.md b/docs/internals/queries.md
new file mode 100644
index 0000000000..dc3d575217
--- /dev/null
+++ b/docs/internals/queries.md
@@ -0,0 +1,125 @@
+# Processing queries
+
+_since v4.0_
+
+The query pipeline roughly looks like this:
+
+```
+HTTP --[ASP.NET Core]--> QueryString --[JADNC:QueryStringParameterReader]--> QueryExpression[] --[JADNC:ResourceService]--> QueryLayer --[JADNC:Repository]--> IQueryable --[EF Core]--> SQL
+```
+
+Processing a request involves the following steps:
+- `JsonApiMiddleware` collects resource info from routing data for the current request.
+- `JsonApiReader` transforms json request body into objects.
+- `JsonApiController` accepts get/post/patch/delete verb and delegates to service.
+- `IQueryStringParameterReader`s delegate to `QueryParser`s that transform query string text into `QueryExpression` objects.
+ - By using prefix notation in filters, we don't need users to remember operator precedence and associativity rules.
+ - 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 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.
+ `QueryBuilder` depends on `QueryClauseBuilder` implementations that visit the tree nodes, transforming them to `System.Linq.Expression` equivalents.
+ The `IQueryable` expression trees are executed by EF Core, which produces SQL statements out of them.
+- `JsonApiWriter` transforms resource objects into json response.
+
+# Example
+To get a sense of what this all looks like, let's look at an example query string:
+
+```
+/api/v1/blogs?
+ include=owner,articles.revisions.author&
+ filter=has(articles)&
+ sort=count(articles)&
+ page[number]=3&
+ fields=title&
+ filter[articles]=and(not(equals(author.firstName,null)),has(revisions))&
+ sort[articles]=author.lastName&
+ fields[articles]=url&
+ filter[articles.revisions]=and(greaterThan(publishTime,'2001-01-01'),startsWith(author.firstName,'J'))&
+ sort[articles.revisions]=-publishTime,author.lastName&
+ fields[articles.revisions]=publishTime
+```
+
+After parsing, the set of scoped expressions is transformed into the following tree by `QueryLayerComposer`:
+
+```
+QueryLayer
+{
+ Include: owner,articles.revisions
+ Filter: has(articles)
+ Sort: count(articles)
+ Pagination: Page number: 3, size: 5
+ Projection
+ {
+ title
+ id
+ owner: QueryLayer
+ {
+ Sort: id
+ Pagination: Page number: 1, size: 5
+ }
+ articles: QueryLayer
+ {
+ Filter: and(not(equals(author.firstName,null)),has(revisions))
+ Sort: author.lastName
+ Pagination: Page number: 1, size: 5
+ Projection
+ {
+ url
+ id
+ revisions: QueryLayer
+ {
+ Filter: and(greaterThan(publishTime,'2001-01-01'),startsWith(author.firstName,'J'))
+ Sort: -publishTime,author.lastName
+ Pagination: Page number: 1, size: 5
+ Projection
+ {
+ publishTime
+ id
+ }
+ }
+ }
+ }
+ }
+}
+```
+
+Next, the repository translates this into a LINQ query that the following C# code would represent:
+
+```c#
+var query = dbContext.Blogs
+ .Include("Owner")
+ .Include("Articles.Revisions")
+ .Where(blog => blog.Articles.Any())
+ .OrderBy(blog => blog.Articles.Count)
+ .Skip(10)
+ .Take(5)
+ .Select(blog => new Blog
+ {
+ Title = blog.Title,
+ Id = blog.Id,
+ Owner = blog.Owner,
+ Articles = new List(blog.Articles
+ .Where(article => article.Author.FirstName != null && article.Revisions.Any())
+ .OrderBy(article => article.Author.LastName)
+ .Take(5)
+ .Select(article => new Article
+ {
+ Url = article.Url,
+ Id = article.Id,
+ Revisions = new HashSet(article.Revisions
+ .Where(revision => revision.PublishTime > DateTime.Parse("2001-01-01") && revision.Author.FirstName.StartsWith("J"))
+ .OrderByDescending(revision => revision.PublishTime)
+ .ThenBy(revision => revision.Author.LastName)
+ .Take(5)
+ .Select(revision => new Revision
+ {
+ PublishTime = revision.PublishTime,
+ Id = revision.Id
+ }))
+ }))
+ });
+```
diff --git a/docs/internals/toc.md b/docs/internals/toc.md
new file mode 100644
index 0000000000..0533dc5272
--- /dev/null
+++ b/docs/internals/toc.md
@@ -0,0 +1 @@
+# [Queries](queries.md)
diff --git a/docs/toc.yml b/docs/toc.yml
index 0cd11f3c9e..8ed0880347 100644
--- a/docs/toc.yml
+++ b/docs/toc.yml
@@ -14,4 +14,8 @@
#
# - name: Request Examples
# href: request-examples/
-# homepage: request-examples/index.md
\ No newline at end of file
+# homepage: request-examples/index.md
+
+- name: Internals
+ href: internals/
+ homepage: internals/index.md
diff --git a/docs/usage/extensibility/custom-query-formats.md b/docs/usage/extensibility/custom-query-formats.md
index 896e569dab..2653682fe6 100644
--- a/docs/usage/extensibility/custom-query-formats.md
+++ b/docs/usage/extensibility/custom-query-formats.md
@@ -1,9 +1,16 @@
-# Custom Query Formats
+# Custom QueryString parameters
-For information on the default query string parameter formats, see the documentation for each query method.
+For information on the built-in query string parameters, see the documentation for them.
+In order to add parsing of custom query string parameters, you can implement the `IQueryStringParameterReader` interface and inject it.
-In order to customize the query formats, you need to implement the `IQueryParameterParser` interface and inject it.
+```c#
+public class YourQueryStringParameterReader : IQueryStringParameterReader
+{
+ // ...
+}
+```
```c#
-services.AddScoped();
+services.AddScoped();
+services.AddScoped(sp => sp.GetService());
```
diff --git a/docs/usage/extensibility/layer-overview.md b/docs/usage/extensibility/layer-overview.md
index 4efed2c444..458d45b5a1 100644
--- a/docs/usage/extensibility/layer-overview.md
+++ b/docs/usage/extensibility/layer-overview.md
@@ -1,13 +1,13 @@
# Layer Overview
-By default, data retrieval is distributed across 3 layers:
+By default, data retrieval is distributed across three layers:
```
JsonApiController (required)
-+-- DefaultResourceService: IResourceService
++-- JsonApiResourceService : IResourceService
- +-- DefaultResourceRepository: IResourceRepository
+ +-- 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.
@@ -15,7 +15,7 @@ You can use the following as a general rule of thumb for where to put business l
- `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
+- `IResourceRepository`: custom logic that builds on the Entity Framework Core APIs
## Replacing Services
diff --git a/docs/usage/extensibility/repositories.md b/docs/usage/extensibility/repositories.md
index e175f49524..6cae2ac05b 100644
--- a/docs/usage/extensibility/repositories.md
+++ b/docs/usage/extensibility/repositories.md
@@ -1,40 +1,42 @@
-# Entity Repositories
+# Resource Repositories
-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.
+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.
```c#
public void ConfigureServices(IServiceCollection services)
{
- services.AddScoped, AuthorizedArticleRepository>();
+ services.AddScoped, ArticleRepository>();
// ...
}
```
-A sample implementation that performs data authorization might look like this.
+A sample implementation that performs authorization might look like this.
-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.
+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 AuthorizedArticleRepository : DefaultResourceRepository
+public class ArticleRepository : EntityFrameworkCoreRepository
{
private readonly IAuthenticationService _authenticationService;
- public AuthorizedArticleRepository(
+ public ArticleRepository(
IAuthenticationService authenticationService,
ITargetedFields targetedFields,
IDbContextResolver contextResolver,
IResourceGraph resourceGraph,
IGenericServiceFactory genericServiceFactory,
IResourceFactory resourceFactory,
+ IEnumerable constraintProviders,
ILoggerFactory loggerFactory)
- : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory)
+ : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory)
{
_authenticationService = authenticationService;
}
- public override IQueryable Get()
+ public override IQueryable GetAll()
{
return base.Get().Where(article => article.UserId == _authenticationService.UserId);
}
@@ -83,7 +85,7 @@ services.AddScoped();
services.AddScoped();
-public class DbContextARepository : DefaultResourceRepository
+public class DbContextARepository : EntityFrameworkCoreRepository
where TResource : class, IIdentifiable
{
public DbContextARepository(
@@ -92,8 +94,9 @@ public class DbContextARepository : DefaultResourceRepository constraintProviders,
ILoggerFactory loggerFactory)
- : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, loggerFactory)
+ : base(targetedFields, contextResolver, resourceGraph, genericServiceFactory, resourceFactory, constraintProviders, loggerFactory)
{ }
}
diff --git a/docs/usage/extensibility/services.md b/docs/usage/extensibility/services.md
index c4afc56ac8..91c41881e0 100644
--- a/docs/usage/extensibility/services.md
+++ b/docs/usage/extensibility/services.md
@@ -1,45 +1,45 @@
# 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 Core.
-This is also a good place to implement custom business logic.
+This allows you to customize it however you want. 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 DefaultResourceService and override the existing methods.
+
+If you don't need to alter the underlying mechanisms, you can inherit from `JsonApiResourceService` 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.
+A simple example would be to send notifications when a resource gets created.
```c#
-public class TodoItemService : DefaultResourceService
+public class TodoItemService : JsonApiResourceService
{
private readonly INotificationService _notificationService;
public TodoItemService(
- INotificationService notificationService,
- IEnumerable queryParameters,
+ IResourceRepository repository,
+ IQueryLayerComposer queryLayerComposer,
+ IPaginationContext paginationContext,
IJsonApiOptions options,
ILoggerFactory loggerFactory,
- IResourceRepository repository,
- IResourceContextProvider provider,
- IResourceChangeTracker resourceChangeTracker,
+ ICurrentRequest currentRequest,
+ IResourceChangeTracker resourceChangeTracker,
IResourceFactory resourceFactory,
- IResourceHookExecutor hookExecutor)
- : base(queryParameters, options, loggerFactory, repository, provider, resourceChangeTracker, resourceFactory, hookExecutor)
+ IResourceHookExecutor hookExecutor = null)
+ : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, currentRequest,
+ resourceChangeTracker, resourceFactory, hookExecutor)
{
_notificationService = notificationService;
}
- public override async Task CreateAsync(TodoItem entity)
+ public override async Task CreateAsync(TodoItem resource)
{
- // Call the base implementation which uses Entity Framework Core
- var newEntity = await base.CreateAsync(entity);
+ // Call the base implementation
+ var newResource = await base.CreateAsync(resource);
// Custom code
- _notificationService.Notify($"Entity created: {newEntity.Id}");
+ _notificationService.Notify($"Resource created: {newResource.StringId}");
- // Don't forget to return the new entity
- return newEntity;
+ return newResource;
}
}
```
@@ -47,7 +47,7 @@ public class TodoItemService : DefaultResourceService
## 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.
+If you'd like to use another ORM that does not provide what JsonApiResourceService depends upon, you can use a custom `IResourceService` implementation.
```c#
// Startup.cs
@@ -82,7 +82,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.
+In some cases it may be necessary to only expose a few methods on a resource. For this reason, we have created a hierarchy of service interfaces that can be used to get the exact implementation you require.
This interface hierarchy is defined by this tree structure.
@@ -97,10 +97,10 @@ IResourceService
| +-- IGetByIdService
| | GET /{id}
| |
-| +-- IGetRelationshipService
+| +-- IGetSecondaryService
| | GET /{id}/{relationship}
| |
-| +-- IGetRelationshipsService
+| +-- IGetRelationshipService
| GET /{id}/relationships/{relationship}
|
+-- IResourceCommandService
@@ -123,7 +123,7 @@ In order to take advantage of these interfaces you first need to inject the serv
```c#
public class ArticleService : ICreateService, IDeleteService
{
- // ...
+ // ...
}
public class Startup
@@ -156,9 +156,9 @@ public class ArticlesController : BaseJsonApiController
{ }
[HttpPost]
- public override async Task PostAsync([FromBody] Article entity)
+ public override async Task PostAsync([FromBody] Article resource)
{
- return await base.PostAsync(entity);
+ return await base.PostAsync(resource);
}
[HttpDelete("{id}")]
diff --git a/docs/usage/filtering.md b/docs/usage/filtering.md
index 4c0aea22d6..eb3c7a3da8 100644
--- a/docs/usage/filtering.md
+++ b/docs/usage/filtering.md
@@ -1,81 +1,123 @@
# Filtering
+_since v4.0_
+
Resources can be filtered by attributes using the `filter` query string parameter.
By default, all attributes are filterable.
The filtering strategy we have selected, uses the following form.
```
-?filter[attribute]=value
+?filter=expression
```
-For operations other than equality, the query can be prefixed with an operation identifier.
-Examples can be found in the table below.
+Expressions are composed using the following functions:
+
+| Operation | Function | Example |
+|-------------------------------|--------------------|-------------------------------------------------------|
+| Equality | `equals` | `?filter=equals(lastName,'Smith')` |
+| Less than | `lessThan` | `?filter=lessThan(age,'25')` |
+| Less than or equal to | `lessOrEqual` | `?filter=lessOrEqual(lastModified,'2001-01-01')` |
+| Greater than | `greaterThan` | `?filter=greaterThan(duration,'6:12:14')` |
+| Greater than or equal to | `greaterOrEqual` | `?filter=greaterOrEqual(percentage,'33.33')` |
+| Contains text | `contains` | `?filter=contains(description,'cooking')` |
+| Starts with text | `startsWith` | `?filter=startsWith(description,'The')` |
+| Ends with text | `endsWith` | `?filter=endsWith(description,'End')` |
+| Equals one value from set | `any` | `?filter=any(chapter,'Intro','Summary','Conclusion')` |
+| Collection contains items | `has` | `?filter=has(articles)` |
+| Negation | `not` | `?filter=not(equals(lastName,null))` |
+| Conditional logical OR | `or` | `?filter=or(has(orders),has(invoices))` |
+| Conditional logical AND | `and` | `?filter=and(has(orders),has(invoices))` |
+
+Comparison operators compare an attribute against a constant value (between quotes), null or another attribute:
+
+```http
+GET /users?filter=equals(displayName,'Brian O''Connor') HTTP/1.1
+```
+```http
+GET /users?filter=equals(displayName,null) HTTP/1.1
+```
+```http
+GET /users?filter=equals(displayName,lastName) HTTP/1.1
+```
-| Operation | Prefix | Example |
-|-------------------------------|---------------|-------------------------------------------|
-| Equals | `eq` | `?filter[attribute]=eq:value` |
-| Not Equals | `ne` | `?filter[attribute]=ne:value` |
-| Less Than | `lt` | `?filter[attribute]=lt:10` |
-| Greater Than | `gt` | `?filter[attribute]=gt:10` |
-| Less Than Or Equal To | `le` | `?filter[attribute]=le:10` |
-| Greater Than Or Equal To | `ge` | `?filter[attribute]=ge:10` |
-| Like (string comparison) | `like` | `?filter[attribute]=like:value` |
-| In Set | `in` | `?filter[attribute]=in:value1,value2` |
-| Not In Set | `nin` | `?filter[attribute]=nin:value1,value2` |
-| 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.
-The following are equivalent query forms to get articles whose ordinal values are between 1-100.
+Comparison operators can be combined with the `count` function, which acts on HasMany relationships:
```http
-GET /api/articles?filter[ordinal]=gt:1,lt:100 HTTP/1.1
+GET /blogs?filter=lessThan(count(owner.articles),'10') HTTP/1.1
```
```http
-GET /api/articles?filter[ordinal]=gt:1&filter[ordinal]=lt:100 HTTP/1.1
+GET /customers?filter=greaterThan(count(orders),count(invoices)) HTTP/1.1
```
-Aside from filtering on the resource being requested (top-level), filtering on single-depth related resources can be done too.
+When filters are used multiple times on the same resource, they are combined using an OR operator.
+The next request returns all customers that have orders -or- whose last name is Smith.
```http
-GET /api/articles?include=author&filter[title]=like:marketing&filter[author.lastName]=Smith HTTP/1.1
+GET /customers?filter=has(orders)&filter=equals(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:
+Aside from filtering on the resource being requested (which would be blogs in /blogs and articles in /blogs/1/articles),
+filtering on included collections can be done using bracket notation:
```http
-GET /api/blogs/1/articles?filter[title]=like:new HTTP/1.1
+GET /articles?include=author,tags&filter=equals(author.lastName,'Smith')&filter[tags]=contains(label,'tech','design') HTTP/1.1
```
+In the above request, the first filter is applied on the collection of articles, while the second one is applied on the nested collection of tags.
-## Custom Filters
+Putting it all together, you can build quite complex filters, such as:
+
+```http
+GET /blogs?include=owner.articles.revisions&filter=and(or(equals(title,'Technology'),has(owner.articles)),not(equals(owner.lastName,null)))&filter[owner.articles]=equals(caption,'Two')&filter[owner.articles.revisions]=greaterThan(publishTime,'2005-05-05') HTTP/1.1
+```
+
+# Legacy filters
+
+The next section describes how filtering worked in versions prior to v4.0. They are always applied on the set of resources being requested (no nesting).
+Legacy filters use the following form.
+
+```
+?filter[attribute]=value
+```
+
+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 | Equivalent form in v4.0 |
+|-------------------------------|------------------|-------------------------------------------------|-------------------------------------------------------|
+| Equality | `eq` | `?filter[lastName]=eq:Smith` | `?filter=equals(lastName,'Smith')` |
+| Non-equality | `ne` | `?filter[lastName]=ne:Smith` | `?filter=not(equals(lastName,'Smith'))` |
+| Less than | `lt` | `?filter[age]=lt:25` | `?filter=lessThan(age,'25')` |
+| Less than or equal to | `le` | `?filter[lastModified]=le:2001-01-01` | `?filter=lessOrEqual(lastModified,'2001-01-01')` |
+| Greater than | `gt` | `?filter[duration]=gt:6:12:14` | `?filter=greaterThan(duration,'6:12:14')` |
+| Greater than or equal to | `ge` | `?filter[percentage]=ge:33.33` | `?filter=greaterOrEqual(percentage,'33.33')` |
+| Contains text | `like` | `?filter[description]=like:cooking` | `?filter=contains(description,'cooking')` |
+| Equals one value from set | `in` | `?filter[chapter]=in:Intro,Summary,Conclusion` | `?filter=any(chapter,'Intro','Summary','Conclusion')` |
+| Equals none from set | `nin` | `?filter[chapter]=nin:one,two,three` | `?filter=not(any(chapter,'one','two','three'))` |
+| Equal to null | `isnull` | `?filter[lastName]=isnull:` | `?filter=equals(lastName,null)` |
+| Not equal to null | `isnotnull` | `?filter[lastName]=isnotnull:` | `?filter=not(equals(lastName,null))` |
+
+Filters can be combined and will be applied using an OR operator. This used to be AND in versions prior to v4.0.
+
+Attributes to filter on can optionally be prefixed with a HasOne relationship, for example:
-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 `DefaultResourceRepository` shown below
-
-```c#
-public class AuthorRepository : DefaultResourceRepository
-{
- 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);
- }
+```http
+GET /api/articles?include=author&filter[caption]=like:marketing&filter[author.lastName]=Smith HTTP/1.1
+```
+
+Legacy filter notation can still be used in v4.0 by setting `options.EnableLegacyFilterNotation` to `true`.
+If you want to use the new filter notation in that case, prefix the parameter value with `expr:`, for example:
+
+```http
+GET /articles?filter[caption]=tech&filter=expr:equals(caption,'cooking')) HTTP/1.1
```
+
+## Custom Filters
+
+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
+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/including-relationships.md b/docs/usage/including-relationships.md
index e5af295f00..2f061cd00e 100644
--- a/docs/usage/including-relationships.md
+++ b/docs/usage/including-relationships.md
@@ -45,11 +45,11 @@ GET /articles/1?include=comments HTTP/1.1
}
```
-## Deeply Nested Inclusions
+## Nested Inclusions
_since v3.0.0_
-JsonApiDotNetCore also supports deeply nested inclusions.
+JsonApiDotNetCore also supports nested inclusions.
This allows you to include data across relationships by using a period-delimited relationship path, for example:
```http
diff --git a/docs/usage/meta.md b/docs/usage/meta.md
index 890adcb962..830065bfa3 100644
--- a/docs/usage/meta.md
+++ b/docs/usage/meta.md
@@ -1,6 +1,6 @@
# Metadata
-Non-standard metadata can be added to your API responses in 2 ways. Resource and Request meta. In the event of a key collision, the Request Meta will take precendence.
+Non-standard metadata can be added to your API responses in two ways: Resource and Request Meta. In the event of a key collision, the Request Meta will take precendence.
## Resource Meta
diff --git a/docs/usage/options.md b/docs/usage/options.md
index 4f52a93883..fe5751aa60 100644
--- a/docs/usage/options.md
+++ b/docs/usage/options.md
@@ -28,15 +28,15 @@ options.AllowClientGeneratedIds = true;
## Pagination
-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.
+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.
```c#
-options.DefaultPageSize = 25;
-options.MaximumPageSize = 100;
-options.MaximumPageNumber = 50;
-options.IncludeTotalRecordCount = true;
+options.DefaultPageSize = new PageSize(25);
+options.MaximumPageSize = new PageSize(100);
+options.MaximumPageNumber = new PageNumber(50);
+options.IncludeTotalResourceCount = true;
```
## Relative Links
@@ -44,7 +44,7 @@ options.IncludeTotalRecordCount = true;
All links are absolute by default. However, you can configure relative links.
```c#
-options.RelativeLinks = true;
+options.UseRelativeLinks = true;
```
```json
@@ -62,12 +62,21 @@ options.RelativeLinks = true;
}
```
-## Custom Query String Parameters
+## Unknown Query String 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.
+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.
```c#
-options.AllowCustomQueryStringParameters = true;
+options.AllowUnknownQueryStringParameters = true;
+```
+
+## Maximum include depth
+
+To limit the maximum depth of nested includes, use `MaximumIncludeDepth`. This is null by default, which means unconstrained. If set and a request exceeds the limit, an HTTP 400 Bad Request is returned.
+
+```c#
+options.MaximumIncludeDepth = 1;
```
## Custom Serializer Settings
@@ -81,6 +90,8 @@ options.SerializerSettings.Converters.Add(new StringEnumConverter());
options.SerializerSettings.Formatting = Formatting.Indented;
```
+Because we copy resource properties into an intermediate object before serialization, Newtonsoft.Json annotations on properties are ignored.
+
## 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.
@@ -88,6 +99,7 @@ If you would like to use ASP.NET Core ModelState validation into your controller
```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#
diff --git a/docs/usage/pagination.md b/docs/usage/pagination.md
index ca84ac8051..7f06c30988 100644
--- a/docs/usage/pagination.md
+++ b/docs/usage/pagination.md
@@ -1,18 +1,25 @@
# Pagination
-Resources can be paginated. This query 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 - 21).
```http
GET /articles?page[size]=10&page[number]=2 HTTP/1.1
```
-Due to a [limitation](https://github.com/dotnet/efcore/issues/1833) in Entity Framework Core 3.x, paging does **not** work on nested endpoints:
+## Nesting
+
+Pagination can be used on nested endpoints, such as:
```http
GET /blogs/1/articles?page[number]=2 HTTP/1.1
```
+and on included resources, for example:
+
+```http
+GET /api/blogs/1/articles?include=revisions&page[size]=10,revisions:5&page[number]=2,revisions:3 HTTP/1.1
+```
## Configuring Default Behavior
-You can configure the global default behavior as [described previously](~/usage/options.html#pagination).
+You can configure the global default behavior as [described previously](~/usage/options.md#pagination).
diff --git a/docs/usage/resources/attributes.md b/docs/usage/resources/attributes.md
index f5344dd32d..dfbef4aefd 100644
--- a/docs/usage/resources/attributes.md
+++ b/docs/usage/resources/attributes.md
@@ -13,13 +13,15 @@ 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_SerializerSettings
+
+1. By convention, specified in @JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_SerializerSettings
```c#
options.SerializerSettings.ContractResolver = new DefaultContractResolver
{
- NamingStrategy = new CamelCaseNamingStrategy()
+ NamingStrategy = new KebabCaseNamingStrategy() // default: CamelCaseNamingStrategy
};
```
+
2. Individually using the attribute's constructor
```c#
public class Person : Identifiable
@@ -33,7 +35,7 @@ public class Person : Identifiable
_since v4.0_
-Default json:api attribute capabilities are specified by @JsonApiDotNetCore.Configuration.JsonApiOptions.html#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultAttrCapabilities:
+Default json:api attribute capabilities are specified in @JsonApiDotNetCore.Configuration.JsonApiOptions#JsonApiDotNetCore_Configuration_JsonApiOptions_DefaultAttrCapabilities:
```c#
options.DefaultAttrCapabilities = AttrCapabilities.None; // default: All
@@ -41,7 +43,19 @@ options.DefaultAttrCapabilities = AttrCapabilities.None; // default: All
This can be overridden per attribute.
-# Mutability
+### Viewability
+
+Attributes can be marked to allow returning their value in responses. When not allowed and requested using `?fields=`, it results in an HTTP 400 response.
+
+```c#
+public class User : Identifiable
+{
+ [Attr(~AttrCapabilities.AllowView)]
+ public string Password { get; set; }
+}
+```
+
+### Mutability
Attributes can be marked as mutable, which will allow `PATCH` requests to update them. When immutable, an HTTP 422 response is returned.
@@ -53,7 +67,7 @@ public class Person : Identifiable
}
```
-# Filter/Sort-ability
+### Filter/Sort-ability
Attributes can be marked to allow filtering and/or sorting. When not allowed, it results in an HTTP 400 response.
@@ -68,7 +82,7 @@ public class Person : Identifiable
## Complex Attributes
Models may contain complex attributes.
-Serialization of these types is done by Newtonsoft.Json,
+Serialization of these types is done by [Newtonsoft.Json](https://www.newtonsoft.com/json),
so you should use their APIs to specify serialization formats.
You can also use global options to specify `JsonSerializer` configuration.
diff --git a/docs/usage/resources/hooks.md b/docs/usage/resources/hooks.md
index a0c48cbd17..cd08f38fce 100644
--- a/docs/usage/resources/hooks.md
+++ b/docs/usage/resources/hooks.md
@@ -10,21 +10,21 @@ By implementing resource hooks on a `ResourceDefintion`, it is possible to in
* Transformation of the served data
This usage guide covers the following sections
-1. [**Semantics: pipelines, actions and hooks**](#semantics-pipelines-actions-and-hooks)
+1. [**Semantics: pipelines, actions and hooks**](#1-semantics-pipelines-actions-and-hooks)
Understanding the semantics will be helpful in identifying which hooks on `ResourceDefinition` you need to implement for your use-case.
-2. [**Basic usage**](#basic-usage)
+2. [**Basic usage**](#2-basic-usage)
Some examples to get you started.
* [**Getting started: most minimal example**](#getting-started-most-minimal-example)
* [**Logging**](#logging)
* [**Transforming data with OnReturn**](#transforming-data-with-onreturn)
* [**Loading database values**](#loading-database-values)
-3. [**Advanced usage**](#advanced-usage)
+3. [**Advanced usage**](#3-advanced-usage)
Complicated examples that show the advanced features of hooks.
* [**Simple authorization: explicitly affected resources**](#simple-authorization-explicitly-affected-resources)
* [**Advanced authorization: implicitly affected resources**](#advanced-authorization-implicitly-affected-resources)
* [**Synchronizing data across microservices**](#synchronizing-data-across-microservices)
* [**Hooks for many-to-many join tables**](#hooks-for-many-to-many-join-tables)
-5. [**Hook execution overview**](#hook-execution-overview)
+5. [**Hook execution overview**](#4-hook-execution-overview)
A table overview of all pipelines and involved hooks
# 1. Semantics: pipelines, actions and hooks
@@ -72,7 +72,7 @@ the **Delete** pipeline also allows for an `implicit update relationship` actio
### Shared actions
Note that **some actions are shared across pipelines**. For example, both the **Post** and **Patch** pipeline can perform the `update relationship` action on an (already existing) involved resource. Similarly, the **Get** and **GetSingle** pipelines perform the same `read` action.
-For a complete list of actions associated with each pipeline, see the [overview table](#hook-execution-overview).
+For a complete list of actions associated with each pipeline, see the [overview table](#4-hook-execution-overview).
## Hooks
For all actions it is possible to implement **at least one hook** to intercept its execution. These hooks can be implemented by overriding the corresponding virtual implementation on `ResourceDefintion`. (Note that the base implementation is a dummy implementation, which is ignored when firing hooks.)
@@ -459,7 +459,7 @@ public override void BeforeImplicitUpdateRelationship(IAffectedRelationships`
-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.
+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](#4-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 :
diff --git a/docs/usage/resources/relationships.md b/docs/usage/resources/relationships.md
index 4eff452475..7b6813c536 100644
--- a/docs/usage/resources/relationships.md
+++ b/docs/usage/resources/relationships.md
@@ -40,7 +40,7 @@ public class Person : Identifiable
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.
+JsonApiDotNetCore will expose this relationship to the client the same way as any other `HasMany` attribute.
However, under the covers it will use the join type and Entity Framework Core's APIs to get and set the relationship.
```c#
@@ -59,7 +59,7 @@ public class Article : Identifiable
_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.
+Your resource may expose a calculated property, whose value depends on a related entity 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#
diff --git a/docs/usage/resources/resource-definitions.md b/docs/usage/resources/resource-definitions.md
index 16546820aa..d35f2e301f 100644
--- a/docs/usage/resources/resource-definitions.md
+++ b/docs/usage/resources/resource-definitions.md
@@ -3,15 +3,48 @@
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.
-## Runtime Attribute Filtering
+Resource definitions are resolved from the D/I container, so you can inject dependencies in their constructor.
-_since v2.3.4_
+## Customizing query clauses
-There are some cases where you want attributes excluded from your resource response.
-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.
+_since v4.0_
-Using the techniques described below, you can achieve the following request/response behavior:
+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.
+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
+from Entity Framework Core `IQueryable` execution.
+
+### Excluding fields
+
+There are some cases where you want attributes conditionally excluded from your resource response.
+For example, you may accept some sensitive data that should only be exposed to administrators after creation.
+
+Note: to exclude attributes unconditionally, use `Attr[~AttrCapabilities.AllowView]`.
+
+```c#
+public class UserDefinition : ResourceDefinition
+{
+ public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
+ { }
+
+ public override SparseFieldSetExpression OnApplySparseFieldSet(SparseFieldSetExpression existingSparseFieldSet)
+ {
+ if (IsAdministrator)
+ {
+ return existingSparseFieldSet;
+ }
+
+ var resourceContext = ResourceGraph.GetResourceContext();
+ var passwordAttribute = resourceContext.Attributes.Single(a => a.Property.Name == nameof(User.Password));
+
+ return existingSparseFieldSet.Excluding(passwordAttribute);
+ }
+}
+```
+
+Using this technique, you can achieve the following request/response behavior:
```http
POST /users HTTP/1.1
@@ -21,7 +54,7 @@ Content-Type: application/vnd.api+json
"data": {
"type": "users",
"attributes": {
- "account-number": "1234567890",
+ "password": "secret",
"name": "John Doe"
}
}
@@ -44,83 +77,126 @@ Content-Type: application/vnd.api+json
}
```
-### Single Attribute
+## Default sort order
+
+You can define the default sort order if no `sort` query string parameter is provided.
```c#
-public class UserDefinition : ResourceDefinition
+public class AccountDefinition : ResourceDefinition
{
- public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
+ public override SortExpression OnApplySort(SortExpression existingSort)
{
- HideFields(user => user.AccountNumber);
+ if (existingSort != null)
+ {
+ return existingSort;
+ }
+
+ return CreateSortExpressionFromLambda(new PropertySortOrder
+ {
+ (account => account.Name, ListSortDirection.Ascending),
+ (account => account.ModifiedAt, ListSortDirection.Descending)
+ });
}
}
```
-### Multiple Attributes
+## Enforce page size
+
+You may want to enforce paging on large database tables.
```c#
-public class UserDefinition : ResourceDefinition
+public class AccessLogDefinition : ResourceDefinition
{
- public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
+ public override PaginationExpression OnApplyPagination(PaginationExpression existingPagination)
{
- HideFields(user => new {user.AccountNumber, user.Password});
+ var maxPageSize = new PageSize(10);
+
+ if (existingPagination != null)
+ {
+ var pageSize = existingPagination.PageSize?.Value <= maxPageSize.Value ? existingPagination.PageSize : maxPageSize;
+ return new PaginationExpression(existingPagination.PageNumber, pageSize);
+ }
+
+ return new PaginationExpression(PageNumber.ValueOne, _maxPageSize);
}
}
```
-## Default Sort
-
-_since v3.0.0_
+## Exclude soft-deleted resources
-You can define the default sort behavior if no `sort` query is provided.
+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 override PropertySortOrder GetDefaultSortOrder()
+ public override FilterExpression OnApplyFilter(FilterExpression existingFilter)
{
- return new PropertySortOrder
+ var resourceContext = ResourceGraph.GetResourceContext();
+ var isSoftDeletedAttribute = resourceContext.Attributes.Single(a => a.Property.Name == nameof(Account.IsSoftDeleted));
+
+ var isNotSoftDeleted = new ComparisonExpression(ComparisonOperator.Equals,
+ new ResourceFieldChainExpression(isSoftDeletedAttribute), new LiteralConstantExpression(bool.FalseString));
+
+ return existingFilter == null
+ ? (FilterExpression) isNotSoftDeleted
+ : new LogicalExpression(LogicalOperator.And, new[] {isNotSoftDeleted, existingFilter});
+ }
+}
+```
+
+## Block including related resources
+
+```c#
+public class EmployeeDefinition : ResourceDefinition
+{
+ public override IReadOnlyCollection OnApplyIncludes(IReadOnlyCollection existingIncludes)
+ {
+ if (existingIncludes.Any(include => include.Relationship.Property.Name == nameof(Employee.Manager)))
{
- (account => account.LastLoginTime, SortDirection.Descending),
- (account => account.UserName, SortDirection.Ascending)
- };
+ throw new JsonApiException(new Error(HttpStatusCode.BadRequest)
+ {
+ Title = "Including the manager of employees is not permitted."
+ });
+ }
+
+ return existingIncludes;
}
}
```
-## Custom Query Filters
+## Custom query string parameters
_since v3.0.0_
-You can define additional query string 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.
+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.
+
+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
{
- // handles queries like: ?filter[was-active-on]=2018-10-15T01:25:52Z
- public override QueryFilters GetQueryFilters()
+ protected override QueryStringParameterHandlers OnRegisterQueryableHandlersForQueryStringParameters()
{
- return new QueryFilters
+ return new QueryStringParameterHandlers
{
- {
- "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."
- });
- }
- }
+ ["isActive"] = (source, parameterValue) => source
+ .Include(item => item.Children)
+ .Where(item => item.LastUpdateTime > DateTime.Now.AddMonths(-1)),
+ ["isHighRisk"] = FilterByHighRisk
};
}
+
+ private static IQueryable FilterByHighRisk(IQueryable source, StringValues parameterValue)
+ {
+ bool isFilterOnHighRisk = bool.Parse(parameterValue);
+ return isFilterOnHighRisk ? source.Where(item => item.RiskLevel >= 5) : source.Where(item => item.RiskLevel < 5);
+ }
}
```
-## Using ResourceDefinitions Prior to v3
+## Using ResourceDefinitions 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 46805f5523..f6ddc72e37 100644
--- a/docs/usage/routing.md
+++ b/docs/usage/routing.md
@@ -55,5 +55,5 @@ public class MyModelsController : JsonApiController
}
```
-See [this](~/usage/resource-graph.html#public-resource-type-name) for
+See [this](~/usage/resource-graph.md#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 1f542715b6..a6fcb4f771 100644
--- a/docs/usage/sorting.md
+++ b/docs/usage/sorting.md
@@ -1,8 +1,6 @@
# Sorting
-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.
+Resources can be sorted by one or more attributes in ascending or descending order. The default is ascending by ID.
## Ascending
@@ -12,25 +10,47 @@ GET /api/articles?sort=author HTTP/1.1
## Descending
+To sort descending, prepend the attribute with a minus (-) sign.
+
```http
GET /api/articles?sort=-author HTTP/1.1
```
## Multiple attributes
+Multiple attributes are separated by a comma.
+
```http
GET /api/articles?sort=author,-pageCount HTTP/1.1
```
-## Limitations
+## Count
-Sorting currently does **not** work on nested endpoints:
+To sort on the number of nested resources, use the `count` function.
```http
-GET /api/blogs/1/articles?sort=title HTTP/1.1
+GET /api/blogs?sort=count(articles) HTTP/1.1
```
+This sorts the list of blogs by their number of articles.
+
+## Nesting
+
+Sorting can be used on nested endpoints, such as:
+
+```http
+GET /api/blogs/1/articles?sort=caption HTTP/1.1
+```
+
+and on included resources, for example:
+
+```http
+GET /api/blogs/1/articles?include=revisions&sort=caption&sort[revisions]=publishTime HTTP/1.1
+```
+
+This sorts the list of blogs by their captions and included revisions by their publication time.
+
## Default Sort
-See the topic on [Resource Definitions](~/usage/resources/resource-definitions)
-for defining the default sort behavior.
+See the topic on [Resource Definitions](~/usage/resources/resource-definitions.md)
+for overriding the default sort behavior.
diff --git a/docs/usage/sparse-field-selection.md b/docs/usage/sparse-field-selection.md
deleted file mode 100644
index 4d79d3fc7b..0000000000
--- a/docs/usage/sparse-field-selection.md
+++ /dev/null
@@ -1,25 +0,0 @@
-# Sparse Field Selection
-
-As an alternative to returning all attributes from a resource, the `fields` query string 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=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/sparse-fieldset-selection.md b/docs/usage/sparse-fieldset-selection.md
new file mode 100644
index 0000000000..9e7b654136
--- /dev/null
+++ b/docs/usage/sparse-fieldset-selection.md
@@ -0,0 +1,33 @@
+# Sparse Fieldset Selection
+
+As an alternative to returning all attributes from a resource, the `fields` query string parameter can be used to select only a subset.
+This can be used on the resource being requested, as well as nested endpoints and/or included resources.
+
+Top-level example:
+```http
+GET /articles?fields=title,body HTTP/1.1
+```
+
+Nested endpoint example:
+```http
+GET /api/blogs/1/articles?fields=title,body HTTP/1.1
+```
+
+Example for an included HasOne relationship:
+```http
+GET /articles?include=author&fields[author]=name HTTP/1.1
+```
+
+Example for an included HasMany relationship:
+```http
+GET /articles?include=revisions&fields[revisions]=publishTime HTTP/1.1
+```
+
+Example for both top-level and relationship:
+```http
+GET /articles?include=author&fields=title,body&fields[author]=name HTTP/1.1
+```
+
+## Overriding
+
+As a developer, you can force to include and/or exclude specific fields as [described previously](~/usage/resources/resource-definitions.md).
diff --git a/docs/usage/toc.md b/docs/usage/toc.md
index f7a5f5ce1c..ff9476d800 100644
--- a/docs/usage/toc.md
+++ b/docs/usage/toc.md
@@ -8,7 +8,7 @@
# [Filtering](filtering.md)
# [Sorting](sorting.md)
# [Pagination](pagination.md)
-# [Sparse Field Selection](sparse-field-selection.md)
+# [Sparse Fieldset Selection](sparse-fieldset-selection.md)
# [Including Relationships](including-relationships.md)
# [Routing](routing.md)
# [Errors](errors.md)
@@ -17,7 +17,7 @@
# Extensibility
## [Layer Overview](extensibility/layer-overview.md)
## [Controllers](extensibility/controllers.md)
-## [Services](extensibility/services.md)
-## [Repositories](extensibility/repositories.md)
+## [Resource Services](extensibility/services.md)
+## [Resource Repositories](extensibility/repositories.md)
## [Middleware](extensibility/middleware.md)
## [Custom Query Formats](extensibility/custom-query-formats.md)
diff --git a/src/Examples/GettingStarted/Models/Article.cs b/src/Examples/GettingStarted/Models/Article.cs
index b10ede2fb7..3c0226ec2c 100644
--- a/src/Examples/GettingStarted/Models/Article.cs
+++ b/src/Examples/GettingStarted/Models/Article.cs
@@ -1,4 +1,5 @@
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace GettingStarted.Models
{
diff --git a/src/Examples/GettingStarted/Models/Person.cs b/src/Examples/GettingStarted/Models/Person.cs
index afbdc091ba..2bb49bfb2b 100644
--- a/src/Examples/GettingStarted/Models/Person.cs
+++ b/src/Examples/GettingStarted/Models/Person.cs
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace GettingStarted.Models
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs
index 270aeee9bf..4ae287c670 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/ArticlesController.cs
@@ -6,7 +6,6 @@
namespace JsonApiDotNetCoreExample.Controllers
{
- [DisableQuery(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)]
public sealed class ArticlesController : JsonApiController
{
public ArticlesController(
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs
new file mode 100644
index 0000000000..273a62f2b7
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/AuthorsController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Services;
+using JsonApiDotNetCoreExample.Models;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCoreExample.Controllers
+{
+ public sealed class AuthorsController : JsonApiController
+ {
+ public AuthorsController(
+ IJsonApiOptions jsonApiOptions,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(jsonApiOptions, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/ModelsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs
similarity index 72%
rename from src/Examples/JsonApiDotNetCoreExample/Controllers/ModelsController.cs
rename to src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs
index 2c9ce2898a..e98bdad0f9 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/ModelsController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/BlogsController.cs
@@ -6,12 +6,12 @@
namespace JsonApiDotNetCoreExample.Controllers
{
- public sealed class ModelsController : JsonApiController
+ public sealed class BlogsController : JsonApiController
{
- public ModelsController(
+ public BlogsController(
IJsonApiOptions jsonApiOptions,
ILoggerFactory loggerFactory,
- IResourceService resourceService)
+ IResourceService resourceService)
: base(jsonApiOptions, loggerFactory, resourceService)
{ }
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs
new file mode 100644
index 0000000000..5da41f2726
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/CountriesController.cs
@@ -0,0 +1,19 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Services;
+using JsonApiDotNetCoreExample.Models;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCoreExample.Controllers
+{
+ [DisableQuery(StandardQueryStringParameters.Sort | StandardQueryStringParameters.Page)]
+ public sealed class CountriesController : JsonApiController
+ {
+ public CountriesController(
+ IJsonApiOptions jsonApiOptions,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(jsonApiOptions, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs
index 0fe73da0e1..67ec633b6e 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/PassportsController.cs
@@ -28,16 +28,16 @@ public async Task GetAsync(string id)
}
[HttpPatch("{id}")]
- public async Task PatchAsync(string id, [FromBody] Passport entity)
+ public async Task PatchAsync(string id, [FromBody] Passport resource)
{
int idValue = HexadecimalObfuscationCodec.Decode(id);
- return await base.PatchAsync(idValue, entity);
+ return await base.PatchAsync(idValue, resource);
}
[HttpPost]
- public override async Task PostAsync([FromBody] Passport entity)
+ public override async Task PostAsync([FromBody] Passport resource)
{
- return await base.PostAsync(entity);
+ return await base.PostAsync(resource);
}
[HttpDelete("{id}")]
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs
index ccdcb088ca..0c748d385a 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoCollectionsController.cs
@@ -27,15 +27,15 @@ public TodoCollectionsController(
}
[HttpPatch("{id}")]
- public override async Task PatchAsync(Guid id, [FromBody] TodoItemCollection entity)
+ public override async Task PatchAsync(Guid id, [FromBody] TodoItemCollection resource)
{
- if (entity.Name == "PRE-ATTACH-TEST")
+ if (resource.Name == "PRE-ATTACH-TEST")
{
- var targetTodoId = entity.TodoItems.First().Id;
+ var targetTodoId = resource.TodoItems.First().Id;
var todoItemContext = _dbResolver.GetContext().Set();
await todoItemContext.Where(ti => ti.Id == targetTodoId).FirstOrDefaultAsync();
}
- return await base.PatchAsync(id, entity);
+ return await base.PatchAsync(id, resource);
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs
index 1b2865c098..14a02fefae 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsCustomController.cs
@@ -61,8 +61,8 @@ public CustomJsonApiController(
[HttpGet]
public async Task GetAsync()
{
- var entities = await _resourceService.GetAsync();
- return Ok(entities);
+ var resources = await _resourceService.GetAsync();
+ return Ok(resources);
}
[HttpGet("{id}")]
@@ -70,8 +70,8 @@ public async Task GetAsync(TId id)
{
try
{
- var entity = await _resourceService.GetAsync(id);
- return Ok(entity);
+ var resource = await _resourceService.GetAsync(id);
+ return Ok(resource);
}
catch (ResourceNotFoundException)
{
@@ -84,7 +84,7 @@ public async Task GetRelationshipsAsync(TId id, string relationsh
{
try
{
- var relationship = await _resourceService.GetRelationshipsAsync(id, relationshipName);
+ var relationship = await _resourceService.GetRelationshipAsync(id, relationshipName);
return Ok(relationship);
}
catch (ResourceNotFoundException)
@@ -96,34 +96,34 @@ public async Task GetRelationshipsAsync(TId id, string relationsh
[HttpGet("{id}/{relationshipName}")]
public async Task GetRelationshipAsync(TId id, string relationshipName)
{
- var relationship = await _resourceService.GetRelationshipAsync(id, relationshipName);
+ var relationship = await _resourceService.GetSecondaryAsync(id, relationshipName);
return Ok(relationship);
}
[HttpPost]
- public async Task PostAsync([FromBody] T entity)
+ public async Task PostAsync([FromBody] T resource)
{
- if (entity == null)
+ if (resource == null)
return UnprocessableEntity();
- if (_options.AllowClientGeneratedIds && !string.IsNullOrEmpty(entity.StringId))
+ if (_options.AllowClientGeneratedIds && !string.IsNullOrEmpty(resource.StringId))
return Forbidden();
- entity = await _resourceService.CreateAsync(entity);
+ resource = await _resourceService.CreateAsync(resource);
- return Created($"{HttpContext.Request.Path}/{entity.Id}", entity);
+ return Created($"{HttpContext.Request.Path}/{resource.Id}", resource);
}
[HttpPatch("{id}")]
- public async Task PatchAsync(TId id, [FromBody] T entity)
+ public async Task PatchAsync(TId id, [FromBody] T resource)
{
- if (entity == null)
+ if (resource == null)
return UnprocessableEntity();
try
{
- var updatedEntity = await _resourceService.UpdateAsync(id, entity);
- return Ok(updatedEntity);
+ var updated = await _resourceService.UpdateAsync(id, resource);
+ return Ok(updated);
}
catch (ResourceNotFoundException)
{
@@ -134,7 +134,7 @@ public async Task PatchAsync(TId id, [FromBody] T entity)
[HttpPatch("{id}/relationships/{relationshipName}")]
public async Task PatchRelationshipsAsync(TId id, string relationshipName, [FromBody] List relationships)
{
- await _resourceService.UpdateRelationshipsAsync(id, relationshipName, relationships);
+ await _resourceService.UpdateRelationshipAsync(id, relationshipName, relationships);
return Ok();
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs
index 02b0585893..24ed45bc67 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/TodoItemsTestController.cs
@@ -40,15 +40,15 @@ public TodoItemsTestController(
public override async Task GetAsync(int id) => await base.GetAsync(id);
[HttpGet("{id}/relationships/{relationshipName}")]
- public override async Task GetRelationshipsAsync(int id, string relationshipName)
- => await base.GetRelationshipsAsync(id, relationshipName);
-
- [HttpGet("{id}/{relationshipName}")]
public override async Task GetRelationshipAsync(int id, string relationshipName)
=> await base.GetRelationshipAsync(id, relationshipName);
+ [HttpGet("{id}/{relationshipName}")]
+ public override async Task GetSecondaryAsync(int id, string relationshipName)
+ => await base.GetSecondaryAsync(id, relationshipName);
+
[HttpPost]
- public override async Task PostAsync(TodoItem entity)
+ public override async Task PostAsync(TodoItem resource)
{
await Task.Yield();
@@ -59,7 +59,7 @@ public override async Task PostAsync(TodoItem entity)
}
[HttpPatch("{id}")]
- public override async Task PatchAsync(int id, [FromBody] TodoItem entity)
+ public override async Task PatchAsync(int id, [FromBody] TodoItem resource)
{
await Task.Yield();
@@ -67,9 +67,9 @@ public override async Task PatchAsync(int id, [FromBody] TodoItem
}
[HttpPatch("{id}/relationships/{relationshipName}")]
- public override async Task PatchRelationshipsAsync(
+ public override async Task PatchRelationshipAsync(
int id, string relationshipName, [FromBody] object relationships)
- => await base.PatchRelationshipsAsync(id, relationshipName, relationships);
+ => await base.PatchRelationshipAsync(id, relationshipName, relationships);
[HttpDelete("{id}")]
public override async Task DeleteAsync(int id)
diff --git a/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs b/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs
new file mode 100644
index 0000000000..2063710141
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Controllers/VisasController.cs
@@ -0,0 +1,18 @@
+using JsonApiDotNetCore.Configuration;
+using JsonApiDotNetCore.Controllers;
+using JsonApiDotNetCore.Services;
+using JsonApiDotNetCoreExample.Models;
+using Microsoft.Extensions.Logging;
+
+namespace JsonApiDotNetCoreExample.Controllers
+{
+ public sealed class VisasController : JsonApiController
+ {
+ public VisasController(
+ IJsonApiOptions jsonApiOptions,
+ ILoggerFactory loggerFactory,
+ IResourceService resourceService)
+ : base(jsonApiOptions, loggerFactory, resourceService)
+ { }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
index c299c2bdb7..20ba2d0f88 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Data/AppDbContext.cs
@@ -11,6 +11,7 @@ public sealed class AppDbContext : DbContext
public DbSet TodoItems { get; set; }
public DbSet Passports { get; set; }
+ public DbSet Visas { get; set; }
public DbSet People { get; set; }
public DbSet TodoItemCollections { get; set; }
public DbSet KebabCasedModels { get; set; }
@@ -20,6 +21,7 @@ public sealed class AppDbContext : DbContext
public DbSet PersonRoles { get; set; }
public DbSet ArticleTags { get; set; }
public DbSet Tags { get; set; }
+ public DbSet Blogs { get; set; }
public AppDbContext(DbContextOptions options, ISystemClock systemClock) : base(options)
{
@@ -81,6 +83,11 @@ protected override void OnModelCreating(ModelBuilder modelBuilder)
.WithOne(p => p.OneToOneTodoItem)
.HasForeignKey(p => p.OneToOnePersonId);
+ modelBuilder.Entity()
+ .HasOne(p => p.Owner)
+ .WithMany(p => p.TodoCollections)
+ .OnDelete(DeleteBehavior.Cascade);
+
modelBuilder.Entity()
.HasOne(p => p.OneToOneTodoItem)
.WithOne(p => p.OneToOnePerson)
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs
index 94526f898a..3eddbc41e6 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/ArticleDefinition.cs
@@ -14,9 +14,9 @@ public class ArticleDefinition : ResourceDefinition
{
public ArticleDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
- public override IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline)
+ public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline)
{
- if (pipeline == ResourcePipeline.GetSingle && entities.Single().Name == "Classified")
+ if (pipeline == ResourcePipeline.GetSingle && resources.Any(r => r.Caption == "Classified"))
{
throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
{
@@ -24,7 +24,7 @@ public override IEnumerable OnReturn(HashSet entities, Resourc
});
}
- return entities.Where(t => t.Name != "This should not be included");
+ return resources.Where(t => t.Caption != "This should not be included");
}
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs
index 233a4f79bb..a4649807dd 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/LockableDefinition.cs
@@ -13,9 +13,9 @@ public abstract class LockableDefinition : ResourceDefinition where T : cl
{
protected LockableDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
- protected void DisallowLocked(IEnumerable entities)
+ protected void DisallowLocked(IEnumerable resources)
{
- foreach (var e in entities ?? Enumerable.Empty())
+ foreach (var e in resources ?? Enumerable.Empty())
{
if (e.IsLocked)
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/ModelDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/ModelDefinition.cs
deleted file mode 100644
index f471347126..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/ModelDefinition.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-using JsonApiDotNetCore.Internal.Contracts;
-using JsonApiDotNetCore.Models;
-using JsonApiDotNetCoreExample.Models;
-
-namespace JsonApiDotNetCoreExample.Definitions
-{
- public class ModelDefinition : ResourceDefinition
- {
- public ModelDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
- {
- // this allows POST / PATCH requests to set the value of a
- // property, but we don't include this value in the response
- // this might be used if the incoming value gets hashed or
- // encrypted prior to being persisted and this value should
- // never be sent back to the client
- HideFields(model => model.DoNotExpose);
- }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs
index cd4a87a62b..dbe26ddcca 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PassportDefinition.cs
@@ -32,11 +32,11 @@ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary
().ToList().ForEach(kvp => DoesNotTouchLockedPassports(kvp.Value));
}
- private void DoesNotTouchLockedPassports(IEnumerable entities)
+ private void DoesNotTouchLockedPassports(IEnumerable resources)
{
- foreach (var entity in entities ?? Enumerable.Empty())
+ foreach (var passport in resources ?? Enumerable.Empty())
{
- if (entity.IsLocked)
+ if (passport.IsLocked)
{
throw new JsonApiException(new Error(HttpStatusCode.Forbidden)
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs
index c73224ebb9..9364e71505 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/PersonDefinition.cs
@@ -11,15 +11,15 @@ public class PersonDefinition : LockableDefinition, IHasMeta
{
public PersonDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
- public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline)
+ public override IEnumerable BeforeUpdateRelationship(HashSet ids, IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline)
{
- BeforeImplicitUpdateRelationship(entitiesByRelationship, pipeline);
+ BeforeImplicitUpdateRelationship(resourcesByRelationship, pipeline);
return ids;
}
- public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary entitiesByRelationship, ResourcePipeline pipeline)
+ public override void BeforeImplicitUpdateRelationship(IRelationshipsDictionary resourcesByRelationship, ResourcePipeline pipeline)
{
- entitiesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
+ resourcesByRelationship.GetByRelationship().ToList().ForEach(kvp => DisallowLocked(kvp.Value));
}
public Dictionary GetMeta()
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs
index d9d7366437..b1cea0ec72 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Definitions/TagDefinition.cs
@@ -11,14 +11,14 @@ public class TagDefinition : ResourceDefinition
{
public TagDefinition(IResourceGraph resourceGraph) : base(resourceGraph) { }
- public override IEnumerable BeforeCreate(IEntityHashSet affected, ResourcePipeline pipeline)
+ public override IEnumerable BeforeCreate(IResourceHashSet affected, ResourcePipeline pipeline)
{
return base.BeforeCreate(affected, pipeline);
}
- public override IEnumerable OnReturn(HashSet entities, ResourcePipeline pipeline)
+ public override IEnumerable OnReturn(HashSet resources, ResourcePipeline pipeline)
{
- return entities.Where(t => t.Name != "This should not be included");
+ return resources.Where(t => t.Name != "This should not be included");
}
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Definitions/UserDefinition.cs b/src/Examples/JsonApiDotNetCoreExample/Definitions/UserDefinition.cs
deleted file mode 100644
index 5c2ae3fed0..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Definitions/UserDefinition.cs
+++ /dev/null
@@ -1,37 +0,0 @@
-using System.Linq;
-using JsonApiDotNetCore.Internal.Contracts;
-using JsonApiDotNetCore.Internal.Query;
-using JsonApiDotNetCore.Models;
-using JsonApiDotNetCoreExample.Models;
-
-namespace JsonApiDotNetCoreExample.Definitions
-{
- public class UserDefinition : ResourceDefinition
- {
- public UserDefinition(IResourceGraph resourceGraph) : base(resourceGraph)
- {
- HideFields(u => u.Password);
- }
-
- public override QueryFilters GetQueryFilters()
- {
- return new QueryFilters
- {
- { "firstCharacter", FirstCharacterFilter }
- };
- }
-
- private IQueryable FirstCharacterFilter(IQueryable users, FilterQuery filterQuery)
- {
- switch (filterQuery.Operation)
- {
- // In EF core >= 3.0 we need to explicitly evaluate the query first. This could probably be translated
- // into a query by building expression trees.
- case "lt":
- return users.ToList().Where(u => u.Username.First() < filterQuery.Value[0]).AsQueryable();
- default:
- return users.ToList().Where(u => u.Username.First() == filterQuery.Value[0]).AsQueryable();
- }
- }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs
new file mode 100644
index 0000000000..5deccaf48d
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Address.cs
@@ -0,0 +1,17 @@
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
+
+namespace JsonApiDotNetCoreExample.Models
+{
+ public sealed class Address : Identifiable
+ {
+ [Attr]
+ public string Street { get; set; }
+
+ [Attr]
+ public string ZipCode { get; set; }
+
+ [HasOne]
+ public Country Country { get; set; }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
index bde8b8f310..d835965eb1 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Article.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations.Schema;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
@@ -8,11 +9,13 @@ public sealed class Article : Identifiable
{
[Attr]
[IsRequired(AllowEmptyStrings = true)]
- public string Name { get; set; }
+ public string Caption { get; set; }
+
+ [Attr]
+ public string Url { get; set; }
[HasOne]
public Author Author { get; set; }
- public int AuthorId { get; set; }
[NotMapped]
[HasManyThrough(nameof(ArticleTags))]
@@ -23,5 +26,11 @@ public sealed class Article : Identifiable
[HasManyThrough(nameof(IdentifiableArticleTags))]
public ICollection IdentifiableTags { get; set; }
public ICollection IdentifiableArticleTags { get; set; }
+
+ [HasMany]
+ public ICollection Revisions { get; set; }
+
+ [HasOne]
+ public Blog Blog { get; set; }
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs
index 57232e3b34..39d1dca693 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/ArticleTag.cs
@@ -1,6 +1,5 @@
-using System;
using JsonApiDotNetCore.Models;
-using JsonApiDotNetCoreExample.Data;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
@@ -11,11 +10,6 @@ public sealed class ArticleTag
public int TagId { get; set; }
public Tag Tag { get; set; }
-
- public ArticleTag(AppDbContext appDbContext)
- {
- if (appDbContext == null) throw new ArgumentNullException(nameof(appDbContext));
- }
}
public class IdentifiableArticleTag : Identifiable
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
index 7af14d5235..a089730f2b 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Author.cs
@@ -1,16 +1,29 @@
+using System;
using JsonApiDotNetCore.Models;
using System.Collections.Generic;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
public sealed class Author : Identifiable
{
+ [Attr]
+ public string FirstName { get; set; }
+
[Attr]
[IsRequired(AllowEmptyStrings = true)]
- public string Name { get; set; }
+ public string LastName { get; set; }
+
+ [Attr]
+ public DateTime? DateOfBirth { get; set; }
+
+ [Attr]
+ public string BusinessEmail { get; set; }
+
+ [HasOne]
+ public Address LivingAddress { get; set; }
[HasMany]
public IList Articles { get; set; }
}
}
-
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs
new file mode 100644
index 0000000000..15a01e6584
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Blog.cs
@@ -0,0 +1,21 @@
+using System.Collections.Generic;
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
+
+namespace JsonApiDotNetCoreExample.Models
+{
+ public sealed class Blog : Identifiable
+ {
+ [Attr]
+ public string Title { get; set; }
+
+ [Attr]
+ public string CompanyName { get; set; }
+
+ [HasMany]
+ public IList Articles { get; set; }
+
+ [HasOne]
+ public Author Owner { get; set; }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs
index 3e81c1a51e..831b464b44 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Country.cs
@@ -1,8 +1,11 @@
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
+
namespace JsonApiDotNetCoreExample.Models
{
- public class Country
+ public class Country : Identifiable
{
- public int Id { get; set; }
+ [Attr]
public string Name { get; set; }
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs b/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs
index ad36d928f3..85502e17ff 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/KebabCasedModel.cs
@@ -1,4 +1,5 @@
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Model.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Model.cs
deleted file mode 100644
index 977292d0a7..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Model.cs
+++ /dev/null
@@ -1,10 +0,0 @@
-using JsonApiDotNetCore.Models;
-
-namespace JsonApiDotNetCoreExample.Models
-{
- public sealed class Model : Identifiable
- {
- [Attr]
- public string DoNotExpose { get; set; }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
index b9c37f447c..195df3528c 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Passport.cs
@@ -3,6 +3,7 @@
using System.ComponentModel.DataAnnotations.Schema;
using System.Linq;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
using JsonApiDotNetCoreExample.Data;
using Microsoft.AspNetCore.Authentication;
@@ -53,11 +54,7 @@ public string BirthCountryName
get => BirthCountry.Name;
set
{
- if (BirthCountry == null)
- {
- BirthCountry = new Country();
- }
-
+ BirthCountry ??= new Country();
BirthCountry.Name = value;
}
}
@@ -65,7 +62,7 @@ public string BirthCountryName
[EagerLoad]
public Country BirthCountry { get; set; }
- [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowMutate)]
+ [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowChange)]
[NotMapped]
public string GrantedVisaCountries => GrantedVisas == null || !GrantedVisas.Any()
? null
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
index 7c6a171854..979c169d29 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Person.cs
@@ -1,7 +1,8 @@
using System.Collections.Generic;
using System.Linq;
using JsonApiDotNetCore.Models;
-using JsonApiDotNetCore.Models.Links;
+using JsonApiDotNetCore.Models.Annotation;
+using JsonApiDotNetCore.Models.JsonApiDocuments;
namespace JsonApiDotNetCoreExample.Models
{
@@ -53,7 +54,7 @@ public string FirstName
public ISet AssignedTodoItems { get; set; }
[HasMany]
- public HashSet todoCollections { get; set; }
+ public HashSet TodoCollections { get; set; }
[HasOne]
public PersonRole Role { get; set; }
@@ -66,7 +67,7 @@ public string FirstName
public TodoItem StakeHolderTodoItem { get; set; }
public int? StakeHolderTodoItemId { get; set; }
- [HasOne(links: Link.All, canInclude: false)]
+ [HasOne(links: Links.All, canInclude: false)]
public TodoItem UnIncludeableItem { get; set; }
[HasOne]
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs
new file mode 100644
index 0000000000..e475632a36
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Revision.cs
@@ -0,0 +1,18 @@
+using System;
+using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
+
+namespace JsonApiDotNetCoreExample.Models
+{
+ public sealed class Revision : Identifiable
+ {
+ [Attr]
+ public DateTime PublishTime { get; set; }
+
+ [HasOne]
+ public Author Author { get; set; }
+
+ [HasOne]
+ public Article Article { get; set; }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs
index e63df2b9ad..ccf0da0b75 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Tag.cs
@@ -1,7 +1,6 @@
-using System;
using System.ComponentModel.DataAnnotations;
using JsonApiDotNetCore.Models;
-using JsonApiDotNetCoreExample.Data;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
@@ -11,9 +10,14 @@ public class Tag : Identifiable
[RegularExpression(@"^\W$")]
public string Name { get; set; }
- public Tag(AppDbContext appDbContext)
- {
- if (appDbContext == null) throw new ArgumentNullException(nameof(appDbContext));
- }
+ [Attr]
+ public TagColor Color { get; set; }
+ }
+
+ public enum TagColor
+ {
+ Red,
+ Green,
+ Blue
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs b/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs
index 01eda5d7d0..935dcffb29 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/ThrowingResource.cs
@@ -3,6 +3,7 @@
using System.Linq;
using JsonApiDotNetCore.Formatters;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs
index 000e60d238..d19201cc54 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
@@ -38,7 +39,7 @@ public string AlwaysChangingValue
[Attr]
public DateTime? UpdatedDate { get; set; }
- [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowMutate)]
+ [Attr(AttrCapabilities.All & ~AttrCapabilities.AllowChange)]
public string CalculatedValue => "calculated";
[Attr]
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs
index edb6e98692..bd2aed6b10 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/TodoItemCollection.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs
index 89018b997b..79f4c8cc89 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/User.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/User.cs
@@ -1,5 +1,6 @@
using System;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
using JsonApiDotNetCoreExample.Data;
using Microsoft.AspNetCore.Authentication;
@@ -10,9 +11,9 @@ public class User : Identifiable
private readonly ISystemClock _systemClock;
private string _password;
- [Attr] public string Username { get; set; }
+ [Attr] public string UserName { get; set; }
- [Attr]
+ [Attr(AttrCapabilities.AllowChange)]
public string Password
{
get => _password;
diff --git a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs
index a7b31743e2..f67112db0a 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Models/Visa.cs
@@ -1,14 +1,17 @@
using System;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace JsonApiDotNetCoreExample.Models
{
- public class Visa
+ public sealed class Visa : Identifiable
{
- public int Id { get; set; }
-
+ [Attr]
public DateTime ExpiresAt { get; set; }
+ [Attr]
+ public string CountryName => TargetCountry.Name;
+
[EagerLoad]
public Country TargetCountry { get; set; }
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Program.cs b/src/Examples/JsonApiDotNetCoreExample/Program.cs
index f17228e167..8a892fcfe5 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Program.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Program.cs
@@ -1,5 +1,5 @@
-using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Hosting;
namespace JsonApiDotNetCoreExample
{
@@ -7,10 +7,14 @@ public class Program
{
public static void Main(string[] args)
{
- CreateWebHostBuilder(args).Build().Run();
+ CreateHostBuilder(args).Build().Run();
}
- public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
- WebHost.CreateDefaultBuilder(args)
- .UseStartup();
+
+ public static IHostBuilder CreateHostBuilder(string[] args) =>
+ Host.CreateDefaultBuilder(args)
+ .ConfigureWebHostDefaults(webBuilder =>
+ {
+ webBuilder.UseStartup();
+ });
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs
index 3a35f05165..31dba7ef66 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Services/CustomArticleService.cs
@@ -1,37 +1,38 @@
using JsonApiDotNetCore.Configuration;
using JsonApiDotNetCore.Data;
using JsonApiDotNetCore.Hooks;
-using JsonApiDotNetCore.Internal.Contracts;
-using JsonApiDotNetCore.Query;
using JsonApiDotNetCore.Services;
using JsonApiDotNetCoreExample.Models;
using Microsoft.Extensions.Logging;
-using System.Collections.Generic;
using System.Threading.Tasks;
using JsonApiDotNetCore.Internal;
+using JsonApiDotNetCore.Queries;
using JsonApiDotNetCore.RequestServices;
+using JsonApiDotNetCore.RequestServices.Contracts;
namespace JsonApiDotNetCoreExample.Services
{
- public class CustomArticleService : DefaultResourceService
+ public class CustomArticleService : JsonApiResourceService
{
public CustomArticleService(
- IEnumerable queryParameters,
+ IResourceRepository repository,
+ IQueryLayerComposer queryLayerComposer,
+ IPaginationContext paginationContext,
IJsonApiOptions options,
ILoggerFactory loggerFactory,
- IResourceRepository repository,
- IResourceContextProvider provider,
+ ICurrentRequest currentRequest,
IResourceChangeTracker resourceChangeTracker,
IResourceFactory resourceFactory,
IResourceHookExecutor hookExecutor = null)
- : base(queryParameters, options, loggerFactory, repository, provider, resourceChangeTracker, resourceFactory, hookExecutor)
+ : base(repository, queryLayerComposer, paginationContext, options, loggerFactory, currentRequest,
+ resourceChangeTracker, resourceFactory, hookExecutor)
{ }
public override async Task GetAsync(int id)
{
- var newEntity = await base.GetAsync(id);
- newEntity.Name = "None for you Glen Coco";
- return newEntity;
+ var resource = await base.GetAsync(id);
+ resource.Caption = "None for you Glen Coco";
+ return resource;
}
}
}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryParameterService.cs b/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs
similarity index 79%
rename from src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryParameterService.cs
rename to src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs
index e5892ccf3f..a4dcaa50c2 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryParameterService.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Services/SkipCacheQueryStringParameterReader.cs
@@ -1,12 +1,12 @@
using System.Linq;
using JsonApiDotNetCore.Controllers;
using JsonApiDotNetCore.Exceptions;
-using JsonApiDotNetCore.Query;
+using JsonApiDotNetCore.QueryStrings;
using Microsoft.Extensions.Primitives;
namespace JsonApiDotNetCoreExample.Services
{
- public class SkipCacheQueryParameterService : IQueryParameterService
+ public class SkipCacheQueryStringParameterReader : IQueryStringParameterReader
{
private const string _skipCacheParameterName = "skipCache";
@@ -17,12 +17,12 @@ public bool IsEnabled(DisableQueryAttribute disableQueryAttribute)
return !disableQueryAttribute.ParameterNames.Contains(_skipCacheParameterName.ToLowerInvariant());
}
- public bool CanParse(string parameterName)
+ public bool CanRead(string parameterName)
{
return parameterName == _skipCacheParameterName;
}
- public void Parse(string parameterName, StringValues parameterValue)
+ public void Read(string parameterName, StringValues parameterValue)
{
if (!bool.TryParse(parameterValue, out bool skipCache))
{
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs
new file mode 100644
index 0000000000..297f1e8bfc
--- /dev/null
+++ b/src/Examples/JsonApiDotNetCoreExample/Startups/EmptyStartup.cs
@@ -0,0 +1,26 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace JsonApiDotNetCoreExample
+{
+ ///
+ /// Empty startup class, required for integration tests.
+ /// Changes in .NET Core 3 no longer allow Startup class to be defined in test projects. See https://github.com/aspnet/AspNetCore/issues/15373.
+ ///
+ public abstract class EmptyStartup
+ {
+ protected EmptyStartup(IConfiguration configuration)
+ {
+ }
+
+ public virtual void ConfigureServices(IServiceCollection services)
+ {
+ }
+
+ public virtual void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
+ {
+ }
+ }
+}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs
deleted file mode 100644
index 816827345c..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/KebabCaseStartup.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using JsonApiDotNetCore.Configuration;
-using Microsoft.AspNetCore.Hosting;
-using Newtonsoft.Json.Serialization;
-
-namespace JsonApiDotNetCoreExample
-{
- ///
- /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0
- /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373.
- ///
- public sealed class KebabCaseStartup : TestStartup
- {
- public KebabCaseStartup(IWebHostEnvironment env) : base(env)
- {
- }
-
- protected override void ConfigureJsonApiOptions(JsonApiOptions options)
- {
- base.ConfigureJsonApiOptions(options);
-
- ((DefaultContractResolver)options.SerializerSettings.ContractResolver).NamingStrategy = new KebabCaseNamingStrategy();
- }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/MetaStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/MetaStartup.cs
deleted file mode 100644
index 026550950c..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/MetaStartup.cs
+++ /dev/null
@@ -1,32 +0,0 @@
-using Microsoft.AspNetCore.Hosting;
-using Microsoft.Extensions.DependencyInjection;
-using JsonApiDotNetCore.Services;
-using System.Collections.Generic;
-
-namespace JsonApiDotNetCoreExample
-{
- ///
- /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0
- /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373.
- ///
- public sealed class MetaStartup : TestStartup
- {
- public MetaStartup(IWebHostEnvironment env) : base(env) { }
-
- public override void ConfigureServices(IServiceCollection services)
- {
- services.AddScoped();
- base.ConfigureServices(services);
- }
- }
-
- public sealed class MetaService : IRequestMeta
- {
- public Dictionary GetMeta()
- {
- return new Dictionary {
- { "request-meta", "request-meta-value" }
- };
- }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs
deleted file mode 100644
index 9b10947629..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/NoDefaultPageSizeStartup.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using Microsoft.AspNetCore.Hosting;
-using JsonApiDotNetCore.Configuration;
-
-namespace JsonApiDotNetCoreExample
-{
- ///
- /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0
- /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373.
- ///
- public sealed class NoDefaultPageSizeStartup : TestStartup
- {
- public NoDefaultPageSizeStartup(IWebHostEnvironment env) : base(env)
- {
- }
-
- protected override void ConfigureJsonApiOptions(JsonApiOptions options)
- {
- base.ConfigureJsonApiOptions(options);
-
- options.DefaultPageSize = 0;
- }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/NoNamespaceStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/NoNamespaceStartup.cs
deleted file mode 100644
index a6fade4548..0000000000
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/NoNamespaceStartup.cs
+++ /dev/null
@@ -1,23 +0,0 @@
-using JsonApiDotNetCore.Configuration;
-using Microsoft.AspNetCore.Hosting;
-
-namespace JsonApiDotNetCoreExample.Startups
-{
- ///
- /// This should be in JsonApiDotNetCoreExampleTests project but changes in .net core 3.0
- /// do no longer allow that. See https://github.com/aspnet/AspNetCore/issues/15373.
- ///
- public class NoNamespaceStartup : TestStartup
- {
- public NoNamespaceStartup(IWebHostEnvironment env) : base(env)
- {
- }
-
- protected override void ConfigureJsonApiOptions(JsonApiOptions options)
- {
- base.ConfigureJsonApiOptions(options);
-
- options.Namespace = null;
- }
- }
-}
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
index 292dd9019b..0b0d0eba07 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Startups/Startup.cs
@@ -7,45 +7,38 @@
using System;
using JsonApiDotNetCore;
using JsonApiDotNetCore.Configuration;
-using JsonApiDotNetCore.Query;
+using JsonApiDotNetCore.QueryStrings;
using JsonApiDotNetCoreExample.Services;
using Microsoft.AspNetCore.Authentication;
+using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
namespace JsonApiDotNetCoreExample
{
- public class Startup
+ public class Startup : EmptyStartup
{
private readonly string _connectionString;
- public Startup(IWebHostEnvironment env)
+ public Startup(IConfiguration configuration) : base(configuration)
{
- var builder = new ConfigurationBuilder()
- .SetBasePath(env.ContentRootPath)
- .AddJsonFile("appsettings.json", optional: true, reloadOnChange: false)
- .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
- .AddEnvironmentVariables();
- var configuration = builder.Build();
-
string postgresPassword = Environment.GetEnvironmentVariable("PGPASSWORD") ?? "postgres";
_connectionString = configuration["Data:DefaultConnection"].Replace("###", postgresPassword);
}
- public virtual void ConfigureServices(IServiceCollection services)
+ public override void ConfigureServices(IServiceCollection services)
{
ConfigureClock(services);
- services.AddScoped();
- services.AddScoped(sp => sp.GetService());
+ services.AddScoped();
+ services.AddScoped(sp => sp.GetService());
+
+ services.AddDbContext(options =>
+ {
+ options.EnableSensitiveDataLogging();
+ options.UseNpgsql(_connectionString, innerOptions => innerOptions.SetPostgresVersion(new Version(9, 6)));
+ }, ServiceLifetime.Transient);
- services
- .AddDbContext(options =>
- {
- options
- .EnableSensitiveDataLogging()
- .UseNpgsql(_connectionString, innerOptions => innerOptions.SetPostgresVersion(new Version(9,6)));
- }, ServiceLifetime.Transient)
- .AddJsonApi(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly());
+ services.AddJsonApi(ConfigureJsonApiOptions, discovery => discovery.AddCurrentAssembly());
// once all tests have been moved to WebApplicationFactory format we can get rid of this line below
services.AddClientSerialization();
@@ -60,19 +53,20 @@ protected virtual void ConfigureJsonApiOptions(JsonApiOptions options)
{
options.IncludeExceptionStackTraceInErrors = true;
options.Namespace = "api/v1";
- options.DefaultPageSize = 5;
- options.IncludeTotalRecordCount = true;
- options.LoadDatabaseValues = true;
+ options.DefaultPageSize = new PageSize(5);
+ options.IncludeTotalResourceCount = true;
options.ValidateModelState = true;
- options.EnableResourceHooks = true;
+ options.SerializerSettings.Formatting = Formatting.Indented;
options.SerializerSettings.Converters.Add(new StringEnumConverter());
}
- public void Configure(
- IApplicationBuilder app,
- AppDbContext context)
+ public override void Configure(IApplicationBuilder app, IWebHostEnvironment environment)
{
- context.Database.EnsureCreated();
+ using (var scope = app.ApplicationServices.CreateScope())
+ {
+ var appDbContext = scope.ServiceProvider.GetRequiredService();
+ appDbContext.Database.EnsureCreated();
+ }
app.UseRouting();
app.UseJsonApi();
diff --git a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs
index 0d976b7ebd..e1af39084c 100644
--- a/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs
+++ b/src/Examples/JsonApiDotNetCoreExample/Startups/TestStartup.cs
@@ -1,22 +1,25 @@
using System;
using Microsoft.AspNetCore.Authentication;
-using Microsoft.AspNetCore.Hosting;
+using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
namespace JsonApiDotNetCoreExample
{
public class TestStartup : Startup
{
- public TestStartup(IWebHostEnvironment env) : base(env)
+ public TestStartup(IConfiguration configuration) : base(configuration)
{
}
protected override void ConfigureClock(IServiceCollection services)
{
- services.AddSingleton();
+ services.AddSingleton();
}
- private class AlwaysChangingSystemClock : ISystemClock
+ ///
+ /// Advances the clock one second each time the current time is requested.
+ ///
+ private class TickingSystemClock : ISystemClock
{
private DateTimeOffset _utcNow;
@@ -30,12 +33,12 @@ public DateTimeOffset UtcNow
}
}
- public AlwaysChangingSystemClock()
+ public TickingSystemClock()
: this(new DateTimeOffset(new DateTime(2000, 1, 1)))
{
}
- public AlwaysChangingSystemClock(DateTimeOffset utcNow)
+ public TickingSystemClock(DateTimeOffset utcNow)
{
_utcNow = utcNow;
}
diff --git a/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs b/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs
index d2d1b274e6..2c00c8df6d 100644
--- a/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs
+++ b/src/Examples/NoEntityFrameworkExample/Models/WorkItem.cs
@@ -1,5 +1,6 @@
using System;
using JsonApiDotNetCore.Models;
+using JsonApiDotNetCore.Models.Annotation;
namespace NoEntityFrameworkExample.Models
{
diff --git a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs
index 4b6fa24055..8eeae612c7 100644
--- a/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs
+++ b/src/Examples/NoEntityFrameworkExample/Services/WorkItemService.cs
@@ -21,10 +21,10 @@ public WorkItemService(IConfiguration configuration)
_connectionString = configuration["Data:DefaultConnection"].Replace("###", postgresPassword);
}
- public async Task> GetAsync()
+ public async Task> GetAsync()
{
- return await QueryAsync(async connection =>
- await connection.QueryAsync(@"select * from ""WorkItems"""));
+ return (await QueryAsync(async connection =>
+ await connection.QueryAsync(@"select * from ""WorkItems"""))).ToList();
}
public async Task GetAsync(int id)
@@ -35,22 +35,22 @@ public async Task GetAsync(int id)
return query.Single();
}
- public Task