Skip to content

Commit 579a340

Browse files
author
Bart Koelman
authored
Includes query string parameters in top-level self link and paging links (#698)
* Includes query string parameters in top-level self link and paging links * Review feedback: rename QueryParameterDiscovery to QueryParameterParser * Review feedback: Make QueryParameterParser use IRequestQueryStringAccessor; make RequestQueryStringAccessor internal and register as singleton
1 parent 041c15b commit 579a340

File tree

10 files changed

+173
-77
lines changed

10 files changed

+173
-77
lines changed

benchmarks/Query/QueryParserBenchmarks.cs

Lines changed: 48 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,19 @@
55
using JsonApiDotNetCore.Internal.Contracts;
66
using JsonApiDotNetCore.Managers;
77
using JsonApiDotNetCore.Query;
8+
using JsonApiDotNetCore.QueryParameterServices.Common;
89
using JsonApiDotNetCore.Services;
910
using Microsoft.AspNetCore.Http;
10-
using Microsoft.Extensions.Primitives;
11+
using Microsoft.AspNetCore.WebUtilities;
1112

1213
namespace Benchmarks.Query
1314
{
1415
[MarkdownExporter, SimpleJob(launchCount: 3, warmupCount: 10, targetCount: 20), MemoryDiagnoser]
1516
public class QueryParserBenchmarks
1617
{
17-
private readonly QueryParameterDiscovery _queryParameterDiscoveryForSort;
18-
private readonly QueryParameterDiscovery _queryParameterDiscoveryForAll;
18+
private readonly FakeRequestQueryStringAccessor _queryStringAccessor = new FakeRequestQueryStringAccessor();
19+
private readonly QueryParameterParser _queryParameterParserForSort;
20+
private readonly QueryParameterParser _queryParameterParserForAll;
1921

2022
public QueryParserBenchmarks()
2123
{
@@ -27,12 +29,13 @@ public QueryParserBenchmarks()
2729

2830
IResourceDefinitionProvider resourceDefinitionProvider = DependencyFactory.CreateResourceDefinitionProvider(resourceGraph);
2931

30-
_queryParameterDiscoveryForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, resourceDefinitionProvider, options);
31-
_queryParameterDiscoveryForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, resourceDefinitionProvider, options);
32+
_queryParameterParserForSort = CreateQueryParameterDiscoveryForSort(resourceGraph, currentRequest, resourceDefinitionProvider, options, _queryStringAccessor);
33+
_queryParameterParserForAll = CreateQueryParameterDiscoveryForAll(resourceGraph, currentRequest, resourceDefinitionProvider, options, _queryStringAccessor);
3234
}
3335

34-
private static QueryParameterDiscovery CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph,
35-
CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider, IJsonApiOptions options)
36+
private static QueryParameterParser CreateQueryParameterDiscoveryForSort(IResourceGraph resourceGraph,
37+
CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider,
38+
IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
3639
{
3740
ISortService sortService = new SortService(resourceDefinitionProvider, resourceGraph, currentRequest);
3841

@@ -41,11 +44,12 @@ private static QueryParameterDiscovery CreateQueryParameterDiscoveryForSort(IRes
4144
sortService
4245
};
4346

44-
return new QueryParameterDiscovery(options, queryServices);
47+
return new QueryParameterParser(options, queryStringAccessor, queryServices);
4548
}
4649

47-
private static QueryParameterDiscovery CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
48-
CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider, IJsonApiOptions options)
50+
private static QueryParameterParser CreateQueryParameterDiscoveryForAll(IResourceGraph resourceGraph,
51+
CurrentRequest currentRequest, IResourceDefinitionProvider resourceDefinitionProvider,
52+
IJsonApiOptions options, FakeRequestQueryStringAccessor queryStringAccessor)
4953
{
5054
IIncludeService includeService = new IncludeService(resourceGraph, currentRequest);
5155
IFilterService filterService = new FilterService(resourceDefinitionProvider, resourceGraph, currentRequest);
@@ -61,40 +65,51 @@ private static QueryParameterDiscovery CreateQueryParameterDiscoveryForAll(IReso
6165
omitNullService
6266
};
6367

64-
return new QueryParameterDiscovery(options, queryServices);
68+
return new QueryParameterParser(options, queryStringAccessor, queryServices);
6569
}
6670

6771
[Benchmark]
68-
public void AscendingSort() => _queryParameterDiscoveryForSort.Parse(new QueryCollection(
69-
new Dictionary<string, StringValues>
70-
{
71-
{"sort", BenchmarkResourcePublicNames.NameAttr}
72-
}
73-
), null);
72+
public void AscendingSort()
73+
{
74+
var queryString = $"?sort={BenchmarkResourcePublicNames.NameAttr}";
75+
76+
_queryStringAccessor.SetQueryString(queryString);
77+
_queryParameterParserForSort.Parse(null);
78+
}
7479

7580
[Benchmark]
76-
public void DescendingSort() => _queryParameterDiscoveryForSort.Parse(new QueryCollection(
77-
new Dictionary<string, StringValues>
78-
{
79-
{"sort", $"-{BenchmarkResourcePublicNames.NameAttr}"}
80-
}
81-
), null);
81+
public void DescendingSort()
82+
{
83+
var queryString = $"?sort=-{BenchmarkResourcePublicNames.NameAttr}";
84+
85+
_queryStringAccessor.SetQueryString(queryString);
86+
_queryParameterParserForSort.Parse(null);
87+
}
8288

8389
[Benchmark]
84-
public void ComplexQuery() => Run(100, () => _queryParameterDiscoveryForAll.Parse(new QueryCollection(
85-
new Dictionary<string, StringValues>
86-
{
87-
{$"filter[{BenchmarkResourcePublicNames.NameAttr}]", new StringValues(new[] {"abc", "eq:abc"})},
88-
{"sort", $"-{BenchmarkResourcePublicNames.NameAttr}"},
89-
{"include", "child"},
90-
{"page[size]", "1"},
91-
{"fields", BenchmarkResourcePublicNames.NameAttr}
92-
}
93-
), null));
90+
public void ComplexQuery() => Run(100, () =>
91+
{
92+
var queryString = $"?filter[{BenchmarkResourcePublicNames.NameAttr}]=abc,eq:abc&sort=-{BenchmarkResourcePublicNames.NameAttr}&include=child&page[size]=1&fields={BenchmarkResourcePublicNames.NameAttr}";
93+
94+
_queryStringAccessor.SetQueryString(queryString);
95+
_queryParameterParserForAll.Parse(null);
96+
});
9497

9598
private void Run(int iterations, Action action) {
9699
for (int i = 0; i < iterations; i++)
97100
action();
98101
}
102+
103+
private sealed class FakeRequestQueryStringAccessor : IRequestQueryStringAccessor
104+
{
105+
public QueryString QueryString { get; private set; }
106+
public IQueryCollection Query { get; private set; }
107+
108+
public void SetQueryString(string queryString)
109+
{
110+
QueryString = new QueryString(queryString);
111+
Query = new QueryCollection(QueryHelpers.ParseQuery(queryString));
112+
}
113+
}
99114
}
100115
}

src/JsonApiDotNetCore/Builders/JsonApiApplicationBuilder.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
using JsonApiDotNetCore.Serialization.Server.Builders;
2121
using JsonApiDotNetCore.Serialization.Server;
2222
using Microsoft.Extensions.DependencyInjection.Extensions;
23+
using JsonApiDotNetCore.QueryParameterServices.Common;
2324

2425
namespace JsonApiDotNetCore.Builders
2526
{
@@ -141,13 +142,15 @@ public void ConfigureServices()
141142
_services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
142143
_services.AddSingleton(resourceGraph);
143144
_services.AddSingleton<IResourceContextProvider>(resourceGraph);
145+
_services.AddSingleton<IRequestQueryStringAccessor, RequestQueryStringAccessor>();
146+
144147
_services.AddScoped<ICurrentRequest, CurrentRequest>();
145148
_services.AddScoped<IScopedServiceProvider, RequestScopedServiceProvider>();
146149
_services.AddScoped<IJsonApiWriter, JsonApiWriter>();
147150
_services.AddScoped<IJsonApiReader, JsonApiReader>();
148151
_services.AddScoped<IGenericServiceFactory, GenericServiceFactory>();
149152
_services.AddScoped(typeof(RepositoryRelationshipUpdateHelper<>));
150-
_services.AddScoped<IQueryParameterDiscovery, QueryParameterDiscovery>();
153+
_services.AddScoped<IQueryParameterParser, QueryParameterParser>();
151154
_services.AddScoped<ITargetedFields, TargetedFields>();
152155
_services.AddScoped<IResourceDefinitionProvider, ResourceDefinitionProvider>();
153156
_services.AddScoped<IFieldsToSerialize, FieldsToSerialize>();

src/JsonApiDotNetCore/Middleware/QueryParameterFilter.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@ namespace JsonApiDotNetCore.Middleware
88
{
99
public sealed class QueryParameterActionFilter : IAsyncActionFilter, IQueryParameterActionFilter
1010
{
11-
private readonly IQueryParameterDiscovery _queryParser;
12-
public QueryParameterActionFilter(IQueryParameterDiscovery queryParser) => _queryParser = queryParser;
11+
private readonly IQueryParameterParser _queryParser;
12+
public QueryParameterActionFilter(IQueryParameterParser queryParser) => _queryParser = queryParser;
1313

1414
public async Task OnActionExecutionAsync(ActionExecutingContext context, ActionExecutionDelegate next)
1515
{
1616
// gets the DisableQueryAttribute if set on the controller that is targeted by the current request.
1717
DisableQueryAttribute disabledQuery = context.Controller.GetType().GetTypeInfo().GetCustomAttribute(typeof(DisableQueryAttribute)) as DisableQueryAttribute;
1818

19-
_queryParser.Parse(context.HttpContext.Request.Query, disabledQuery);
19+
_queryParser.Parse(disabledQuery);
2020
await next();
2121
}
2222
}
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
11
using JsonApiDotNetCore.Controllers;
22
using JsonApiDotNetCore.Query;
3-
using Microsoft.AspNetCore.Http;
43

54
namespace JsonApiDotNetCore.Services
65
{
76
/// <summary>
87
/// Responsible for populating the various service implementations of
98
/// <see cref="IQueryParameterService"/>.
109
/// </summary>
11-
public interface IQueryParameterDiscovery
10+
public interface IQueryParameterParser
1211
{
13-
void Parse(IQueryCollection query, DisableQueryAttribute disabledQuery = null);
12+
void Parse(DisableQueryAttribute disabledQuery = null);
1413
}
1514
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace JsonApiDotNetCore.QueryParameterServices.Common
4+
{
5+
public interface IRequestQueryStringAccessor
6+
{
7+
QueryString QueryString { get; }
8+
IQueryCollection Query { get; }
9+
}
10+
}

src/JsonApiDotNetCore/QueryParameterServices/Common/QueryParameterParser.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,32 +4,34 @@
44
using JsonApiDotNetCore.Controllers;
55
using JsonApiDotNetCore.Internal;
66
using JsonApiDotNetCore.Query;
7-
using Microsoft.AspNetCore.Http;
7+
using JsonApiDotNetCore.QueryParameterServices.Common;
88

99
namespace JsonApiDotNetCore.Services
1010
{
1111
/// <inheritdoc/>
12-
public class QueryParameterDiscovery : IQueryParameterDiscovery
12+
public class QueryParameterParser : IQueryParameterParser
1313
{
1414
private readonly IJsonApiOptions _options;
15+
private readonly IRequestQueryStringAccessor _queryStringAccessor;
1516
private readonly IEnumerable<IQueryParameterService> _queryServices;
1617

17-
public QueryParameterDiscovery(IJsonApiOptions options, IEnumerable<IQueryParameterService> queryServices)
18+
public QueryParameterParser(IJsonApiOptions options, IRequestQueryStringAccessor queryStringAccessor, IEnumerable<IQueryParameterService> queryServices)
1819
{
1920
_options = options;
21+
_queryStringAccessor = queryStringAccessor;
2022
_queryServices = queryServices;
2123
}
2224

2325
/// <summary>
24-
/// For a query parameter in <paramref name="query"/>, calls
26+
/// For a parameter in the query string of the request URL, calls
2527
/// the <see cref="IQueryParameterService.Parse(KeyValuePair{string, Microsoft.Extensions.Primitives.StringValues})"/>
2628
/// method of the corresponding service.
2729
/// </summary>
28-
public virtual void Parse(IQueryCollection query, DisableQueryAttribute disabled)
30+
public virtual void Parse(DisableQueryAttribute disabled)
2931
{
3032
var disabledQuery = disabled?.QueryParams;
3133

32-
foreach (var pair in query)
34+
foreach (var pair in _queryStringAccessor.Query)
3335
{
3436
bool parsed = false;
3537
foreach (var service in _queryServices)
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Microsoft.AspNetCore.Http;
2+
3+
namespace JsonApiDotNetCore.QueryParameterServices.Common
4+
{
5+
internal sealed class RequestQueryStringAccessor : IRequestQueryStringAccessor
6+
{
7+
private readonly IHttpContextAccessor _httpContextAccessor;
8+
9+
public QueryString QueryString => _httpContextAccessor.HttpContext.Request.QueryString;
10+
public IQueryCollection Query => _httpContextAccessor.HttpContext.Request.Query;
11+
12+
public RequestQueryStringAccessor(IHttpContextAccessor httpContextAccessor)
13+
{
14+
_httpContextAccessor = httpContextAccessor;
15+
}
16+
}
17+
}

src/JsonApiDotNetCore/Serialization/Server/Builders/LinkBuilder.cs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
14
using System.Text;
25
using JsonApiDotNetCore.Configuration;
36
using JsonApiDotNetCore.Internal;
@@ -6,25 +9,30 @@
69
using JsonApiDotNetCore.Models;
710
using JsonApiDotNetCore.Models.Links;
811
using JsonApiDotNetCore.Query;
12+
using JsonApiDotNetCore.QueryParameterServices.Common;
13+
using Microsoft.AspNetCore.Http;
914

1015
namespace JsonApiDotNetCore.Serialization.Server.Builders
1116
{
1217
public class LinkBuilder : ILinkBuilder
1318
{
1419
private readonly IResourceContextProvider _provider;
20+
private readonly IRequestQueryStringAccessor _queryStringAccessor;
1521
private readonly ILinksConfiguration _options;
1622
private readonly ICurrentRequest _currentRequest;
1723
private readonly IPageService _pageService;
1824

1925
public LinkBuilder(ILinksConfiguration options,
2026
ICurrentRequest currentRequest,
2127
IPageService pageService,
22-
IResourceContextProvider provider)
28+
IResourceContextProvider provider,
29+
IRequestQueryStringAccessor queryStringAccessor)
2330
{
2431
_options = options;
2532
_currentRequest = currentRequest;
2633
_pageService = pageService;
2734
_provider = provider;
35+
_queryStringAccessor = queryStringAccessor;
2836
}
2937

3038
/// <inheritdoc/>
@@ -101,6 +109,8 @@ private string GetSelfTopLevelLink(ResourceContext resourceContext)
101109
builder.Append(_currentRequest.RequestRelationship.PublicRelationshipName);
102110
}
103111

112+
builder.Append(_queryStringAccessor.QueryString.Value);
113+
104114
return builder.ToString();
105115
}
106116

@@ -111,9 +121,24 @@ private string GetPageLink(ResourceContext resourceContext, int pageOffset, int
111121
pageOffset = -pageOffset;
112122
}
113123

114-
return $"{GetBasePath()}/{resourceContext.ResourceName}?page[size]={pageSize}&page[number]={pageOffset}";
124+
string queryString = BuildQueryString(parameters =>
125+
{
126+
parameters["page[size]"] = pageSize.ToString();
127+
parameters["page[number]"] = pageOffset.ToString();
128+
});
129+
130+
return $"{GetBasePath()}/{resourceContext.ResourceName}" + queryString;
115131
}
116132

133+
private string BuildQueryString(Action<Dictionary<string, string>> updateAction)
134+
{
135+
var parameters = _queryStringAccessor.Query.ToDictionary(pair => pair.Key, pair => pair.Value.ToString());
136+
updateAction(parameters);
137+
string queryString = QueryString.Create(parameters).Value;
138+
139+
queryString = queryString.Replace("%5B", "[").Replace("%5D", "]");
140+
return queryString;
141+
}
117142

118143
/// <inheritdoc/>
119144
public ResourceLinks GetResourceLinks(string resourceName, string id)

0 commit comments

Comments
 (0)