Skip to content

Commit e9dc639

Browse files
author
Bart Koelman
committed
Initial support for compound filters and deeply nested queries
1 parent f81b1b9 commit e9dc639

File tree

414 files changed

+17039
-10144
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

414 files changed

+17039
-10144
lines changed

Directory.Build.props

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<!-- Test Project Dependencies -->
1616
<PropertyGroup>
1717
<XUnitVersion>2.4.1</XUnitVersion>
18+
<FluentAssertionsVersion>5.10.3</FluentAssertionsVersion>
1819
<BogusVersion>29.0.1</BogusVersion>
1920
<MoqVersion>4.13.1</MoqVersion>
2021
</PropertyGroup>

benchmarks/LinkBuilder/LinkBuilderGetNamespaceFromPathBenchmarks.cs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,23 +8,23 @@ namespace Benchmarks.LinkBuilder
88
public class LinkBuilderGetNamespaceFromPathBenchmarks
99
{
1010
private const string RequestPath = "/api/some-really-long-namespace-path/resources/current/articles/?some";
11-
private const string EntityName = "articles";
11+
private const string ResourceName = "articles";
1212
private const char PathDelimiter = '/';
1313

1414
[Benchmark]
15-
public void UsingStringSplit() => GetNamespaceFromPathUsingStringSplit(RequestPath, EntityName);
15+
public void UsingStringSplit() => GetNamespaceFromPathUsingStringSplit(RequestPath, ResourceName);
1616

1717
[Benchmark]
18-
public void UsingReadOnlySpan() => GetNamespaceFromPathUsingReadOnlySpan(RequestPath, EntityName);
18+
public void UsingReadOnlySpan() => GetNamespaceFromPathUsingReadOnlySpan(RequestPath, ResourceName);
1919

20-
public static string GetNamespaceFromPathUsingStringSplit(string path, string entityName)
20+
public static string GetNamespaceFromPathUsingStringSplit(string path, string resourceName)
2121
{
2222
StringBuilder namespaceBuilder = new StringBuilder(path.Length);
2323
string[] segments = path.Split('/');
2424

2525
for (int index = 1; index < segments.Length; index++)
2626
{
27-
if (segments[index] == entityName)
27+
if (segments[index] == resourceName)
2828
{
2929
break;
3030
}
@@ -36,22 +36,22 @@ public static string GetNamespaceFromPathUsingStringSplit(string path, string en
3636
return namespaceBuilder.ToString();
3737
}
3838

39-
public static string GetNamespaceFromPathUsingReadOnlySpan(string path, string entityName)
39+
public static string GetNamespaceFromPathUsingReadOnlySpan(string path, string resourceName)
4040
{
41-
ReadOnlySpan<char> entityNameSpan = entityName.AsSpan();
41+
ReadOnlySpan<char> resourceNameSpan = resourceName.AsSpan();
4242
ReadOnlySpan<char> pathSpan = path.AsSpan();
4343

4444
for (int index = 0; index < pathSpan.Length; index++)
4545
{
4646
if (pathSpan[index].Equals(PathDelimiter))
4747
{
48-
if (pathSpan.Length > index + entityNameSpan.Length)
48+
if (pathSpan.Length > index + resourceNameSpan.Length)
4949
{
50-
ReadOnlySpan<char> possiblePathSegment = pathSpan.Slice(index + 1, entityNameSpan.Length);
50+
ReadOnlySpan<char> possiblePathSegment = pathSpan.Slice(index + 1, resourceNameSpan.Length);
5151

52-
if (entityNameSpan.SequenceEqual(possiblePathSegment))
52+
if (resourceNameSpan.SequenceEqual(possiblePathSegment))
5353
{
54-
int lastCharacterIndex = index + 1 + entityNameSpan.Length;
54+
int lastCharacterIndex = index + 1 + resourceNameSpan.Length;
5555

5656
bool isAtEnd = lastCharacterIndex == pathSpan.Length;
5757
bool hasDelimiterAfterSegment = pathSpan.Length >= lastCharacterIndex + 1 && pathSpan[lastCharacterIndex].Equals(PathDelimiter);

benchmarks/Query/QueryParserBenchmarks.cs

Lines changed: 41 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.ComponentModel.Design;
34
using BenchmarkDotNet.Attributes;
45
using JsonApiDotNetCore.Configuration;
6+
using JsonApiDotNetCore.Internal;
57
using JsonApiDotNetCore.Internal.Contracts;
6-
using JsonApiDotNetCore.Managers;
7-
using JsonApiDotNetCore.Query;
8-
using JsonApiDotNetCore.QueryParameterServices.Common;
9-
using JsonApiDotNetCore.Services;
8+
using JsonApiDotNetCore.Internal.QueryStrings;
9+
using JsonApiDotNetCore.RequestServices;
1010
using Microsoft.AspNetCore.Http;
1111
using Microsoft.AspNetCore.WebUtilities;
1212
using Microsoft.Extensions.Logging.Abstractions;
@@ -17,56 +17,59 @@ namespace Benchmarks.Query
1717
public class QueryParserBenchmarks
1818
{
1919
private readonly FakeRequestQueryStringAccessor _queryStringAccessor = new FakeRequestQueryStringAccessor();
20-
private readonly QueryParameterParser _queryParameterParserForSort;
21-
private readonly QueryParameterParser _queryParameterParserForAll;
20+
private readonly QueryStringReader _queryStringReaderForSort;
21+
private readonly QueryStringReader _queryStringReaderForAll;
2222

2323
public QueryParserBenchmarks()
2424
{
25-
IJsonApiOptions options = new JsonApiOptions();
25+
IJsonApiOptions options = new JsonApiOptions
26+
{
27+
EnableLegacyFilterNotation = true
28+
};
29+
2630
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
27-
28-
var currentRequest = new CurrentRequest();
29-
currentRequest.SetRequestResource(resourceGraph.GetResourceContext(typeof(BenchmarkResource)));
3031

31-
IResourceDefinitionProvider resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
32+
var currentRequest = new CurrentRequest
33+
{
34+
PrimaryResource = resourceGraph.GetResourceContext(typeof(BenchmarkResource))
35+
};
3236

33-
_queryParameterParserForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, resourceDefinitionProvider, options, _queryStringAccessor);
34-
_queryParameterParserForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, resourceDefinitionProvider, options, _queryStringAccessor);
37+
_queryStringReaderForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, options, _queryStringAccessor);
38+
_queryStringReaderForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, options, _queryStringAccessor);
3539
}
3640

37-
private static QueryParameterParser CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph,
38-
CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider,
41+
private static QueryStringReader CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph,
42+
CurrentRequest currentRequest,
3943
IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
4044
{
41-
ISortService sortService = new SortService(resourceDefinitionProvider, resourceGraph, currentRequest);
42-
43-
var queryServices = new List<IQueryParameterService>
45+
var sortReader = new SortQueryStringParameterReader(currentRequest, resourceGraph);
46+
47+
var readers = new List<IQueryStringParameterReader>
4448
{
45-
sortService
49+
sortReader
4650
};
4751

48-
return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance);
52+
return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance);
4953
}
5054

51-
private static QueryParameterParser CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
52-
CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider,
53-
IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
55+
private static QueryStringReader CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
56+
CurrentRequest currentRequest, IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
5457
{
55-
IIncludeService includeService = new IncludeService(resourceGraph, currentRequest);
56-
IFilterService filterService = new FilterService(resourceDefinitionProvider, resourceGraph, currentRequest);
57-
ISortService sortService = new SortService(resourceDefinitionProvider, resourceGraph, currentRequest);
58-
ISparseFieldsService sparseFieldsService = new SparseFieldsService(resourceGraph, currentRequest);
59-
IPageService pageService = new PageService(options, resourceGraph, currentRequest);
60-
IDefaultsService defaultsService = new DefaultsService(options);
61-
INullsService nullsService = new NullsService(options);
62-
63-
var queryServices = new List<IQueryParameterService>
58+
var resourceFactory = new ResourceFactory(new ServiceContainer());
59+
60+
var filterReader = new FilterQueryStringParameterReader(currentRequest, resourceGraph, resourceFactory, options);
61+
var sortReader = new SortQueryStringParameterReader(currentRequest, resourceGraph);
62+
var sparseFieldSetReader = new SparseFieldSetQueryStringParameterReader(currentRequest, resourceGraph);
63+
var paginationReader = new PaginationQueryStringParameterReader(currentRequest, resourceGraph, options);
64+
var defaultsReader = new DefaultsQueryStringParameterReader(options);
65+
var nullsReader = new NullsQueryStringParameterReader(options);
66+
67+
var readers = new List<IQueryStringParameterReader>
6468
{
65-
includeService, filterService, sortService, sparseFieldsService, pageService, defaultsService,
66-
nullsService
69+
filterReader, sortReader, sparseFieldSetReader, paginationReader, defaultsReader, nullsReader
6770
};
6871

69-
return new QueryParameterParser(options, queryStringAccessor, queryServices, NullLoggerFactory.Instance);
72+
return new QueryStringReader(options, queryStringAccessor, readers, NullLoggerFactory.Instance);
7073
}
7174

7275
[Benchmark]
@@ -75,7 +78,7 @@ public void AscendingSort()
7578
var queryString = $"?sort={BenchmarkResourcePublicNames.NameAttr}";
7679

7780
_queryStringAccessor.SetQueryString(queryString);
78-
_queryParameterParserForSort.Parse(null);
81+
_queryStringReaderForSort.ReadAll(null);
7982
}
8083

8184
[Benchmark]
@@ -84,7 +87,7 @@ public void DescendingSort()
8487
var queryString = $"?sort=-{BenchmarkResourcePublicNames.NameAttr}";
8588

8689
_queryStringAccessor.SetQueryString(queryString);
87-
_queryParameterParserForSort.Parse(null);
90+
_queryStringReaderForSort.ReadAll(null);
8891
}
8992

9093
[Benchmark]
@@ -93,7 +96,7 @@ public void ComplexQuery() => Run(100, () =>
9396
var queryString = $"?filter[{BenchmarkResourcePublicNames.NameAttr}]=abc,eq:abc&sort=-{BenchmarkResourcePublicNames.NameAttr}&include=child&page[size]=1&fields={BenchmarkResourcePublicNames.NameAttr}";
9497

9598
_queryStringAccessor.SetQueryString(queryString);
96-
_queryParameterParserForAll.Parse(null);
99+
_queryStringReaderForAll.ReadAll(null);
97100
});
98101

99102
private void Run(int iterations, Action action) {

benchmarks/Serialization/JsonApiDeserializerBenchmarks.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public JsonApiDeserializerBenchmarks()
3939
var options = new JsonApiOptions();
4040
IResourceGraph resourceGraph = DependencyFactory.CreateResourceGraph(options);
4141
var targetedFields = new TargetedFields();
42-
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new DefaultResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
42+
_jsonApiDeserializer = new RequestDeserializer(resourceGraph, new ResourceFactory(new ServiceContainer()), targetedFields, new HttpContextAccessor());
4343
}
4444

4545
[Benchmark]

benchmarks/Serialization/JsonApiSerializerBenchmarks.cs

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
using BenchmarkDotNet.Attributes;
33
using JsonApiDotNetCore.Configuration;
44
using JsonApiDotNetCore.Internal.Contracts;
5-
using JsonApiDotNetCore.Managers;
6-
using JsonApiDotNetCore.Query;
5+
using JsonApiDotNetCore.Internal.Queries;
6+
using JsonApiDotNetCore.Internal.QueryStrings;
7+
using JsonApiDotNetCore.RequestServices;
78
using JsonApiDotNetCore.Serialization;
89
using JsonApiDotNetCore.Serialization.Server;
910
using JsonApiDotNetCore.Serialization.Server.Builders;
@@ -14,7 +15,7 @@ namespace Benchmarks.Serialization
1415
[MarkdownExporter]
1516
public class JsonApiSerializerBenchmarks
1617
{
17-
private static readonly BenchmarkResource Content = new BenchmarkResource
18+
private static readonly BenchmarkResource _content = new BenchmarkResource
1819
{
1920
Id = 123,
2021
Name = Guid.NewGuid().ToString()
@@ -40,14 +41,19 @@ public JsonApiSerializerBenchmarks()
4041

4142
private static FieldsToSerialize CreateFieldsToSerialize(IResourceGraph resourceGraph)
4243
{
43-
var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
4444
var currentRequest = new CurrentRequest();
45-
var sparseFieldsService = new SparseFieldsService(resourceGraph, currentRequest);
46-
47-
return new FieldsToSerialize(resourceGraph, sparseFieldsService, resourceDefinitionProvider);
45+
46+
var constraintProviders = new IQueryConstraintProvider[]
47+
{
48+
new SparseFieldSetQueryStringParameterReader(currentRequest, resourceGraph)
49+
};
50+
51+
var resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
52+
53+
return new FieldsToSerialize(resourceGraph, constraintProviders, resourceDefinitionProvider);
4854
}
4955

5056
[Benchmark]
51-
public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(Content);
57+
public object SerializeSimpleObject() => _jsonApiSerializer.Serialize(_content);
5258
}
5359
}

docs/api/index.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,6 @@ This section documents the package API and is generated from the XML source comm
44

55
## Common APIs
66

7-
- [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.html)
8-
- [`IResourceGraph`](JsonApiDotNetCore.Internal.Contracts.IResourceGraph.html)
9-
- [`ResourceDefinition<T>`](JsonApiDotNetCore.Models.ResourceDefinition-1.html)
7+
- [`JsonApiOptions`](JsonApiDotNetCore.Configuration.JsonApiOptions.yml)
8+
- [`IResourceGraph`](JsonApiDotNetCore.Internal.Contracts.IResourceGraph.yml)
9+
- [`ResourceDefinition<TResource>`](JsonApiDotNetCore.Models.ResourceDefinition-1.yml)

docs/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,10 @@ Eliminate CRUD boilerplate and provide the following features across your resour
1414
- Filtering
1515
- Sorting
1616
- Pagination
17-
- Sparse field selection
17+
- Sparse fieldset selection
1818
- Relationship inclusion and navigation
1919

20-
Checkout the [example requests](request-examples) to see the kind of features you will get out of the box.
20+
Checkout the [example requests](request-examples/index.md) to see the kind of features you will get out of the box.
2121

2222
### 2. Extensibility
2323

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,16 @@
1-
# Custom Query Formats
1+
# Custom QueryString parameters
22

3-
For information on the default query string parameter formats, see the documentation for each query method.
3+
For information on the built-in query string parameters, see the documentation for them.
4+
In order to add parsing of custom query string parameters, you can implement the `IQueryStringParameterReader` interface and inject it.
45

5-
In order to customize the query formats, you need to implement the `IQueryParameterParser` interface and inject it.
6+
```c#
7+
public class YourQueryStringParameterReader : IQueryStringParameterReader
8+
{
9+
// ...
10+
}
11+
```
612

713
```c#
8-
services.AddScoped<IQueryParameterParser, FooQueryParameterParser>();
14+
services.AddScoped<YourQueryStringParameterReader>();
15+
services.AddScoped<IQueryStringParameterReader>(sp => sp.GetService<YourQueryStringParameterReader>());
916
```

docs/usage/extensibility/layer-overview.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
# Layer Overview
22

3-
By default, data retrieval is distributed across 3 layers:
3+
By default, data retrieval is distributed across three layers:
44

55
```
66
JsonApiController (required)
77
8-
+-- DefaultResourceService: IResourceService
8+
+-- JsonApiResourceService : IResourceService
99
10-
+-- DefaultResourceRepository: IResourceRepository
10+
+-- EntityFrameworkCoreRepository : IResourceRepository
1111
```
1212

1313
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.
1414
You can use the following as a general rule of thumb for where to put business logic:
1515

1616
- `Controller`: simple validation logic that should result in the return of specific HTTP status codes, such as model validation
1717
- `IResourceService`: advanced business logic and replacement of data access mechanisms
18-
- `IResourceRepository`: custom logic that builds on the Entity Framework Core APIs, such as Authorization of data
18+
- `IResourceRepository`: custom logic that builds on the Entity Framework Core APIs
1919

2020
## Replacing Services
2121

0 commit comments

Comments
 (0)