Skip to content

Commit d6afafd

Browse files
authored
refactor(query-set): move logic into IQueryParser to improve extensibility
* chore(csproj): bump package version * refactor(query-set): move logic into IQueryParser to improve extensibility closes #186
1 parent 5bf0e52 commit d6afafd

File tree

9 files changed

+310
-280
lines changed

9 files changed

+310
-280
lines changed

src/JsonApiDotNetCore/Configuration/JsonApiOptions.cs

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,8 @@ public class JsonApiOptions
1616
public bool AllowClientGeneratedIds { get; set; }
1717
public IContextGraph ContextGraph { get; set; }
1818
public bool RelativeLinks { get; set; }
19-
20-
/// <summary>
21-
/// This flag is experimental and could be perceived as a violation
22-
/// of the v1 spec. However, we have decided that this is a real
23-
/// requirement for users of this library and a gap in the specification.
24-
/// It will likely be removed when the spec is updated to support this
25-
/// requirement.
26-
/// </summary>
2719
public bool AllowCustomQueryParameters { get; set; }
20+
2821
[Obsolete("JsonContract resolver can now be set on SerializerSettings.")]
2922
public IContractResolver JsonContractResolver
3023
{
@@ -38,8 +31,7 @@ public IContractResolver JsonContractResolver
3831
};
3932
internal IContextGraphBuilder ContextGraphBuilder { get; } = new ContextGraphBuilder();
4033

41-
public void BuildContextGraph<TContext>(Action<IContextGraphBuilder> builder)
42-
where TContext : DbContext
34+
public void BuildContextGraph<TContext>(Action<IContextGraphBuilder> builder) where TContext : DbContext
4335
{
4436
BuildContextGraph(builder);
4537

src/JsonApiDotNetCore/Extensions/IServiceCollectionExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ public static void AddJsonApiInternals(
110110
services.AddScoped<IGenericProcessorFactory, GenericProcessorFactory>();
111111
services.AddScoped(typeof(GenericProcessor<>));
112112
services.AddScoped<IQueryAccessor, QueryAccessor>();
113+
services.AddScoped<IQueryParser, QueryParser>();
114+
services.AddScoped<IControllerContext, Services.ControllerContext>();
113115
}
114116

115117
public static void SerializeAsJsonApi(this MvcOptions options, JsonApiOptions jsonApiOptions)
Lines changed: 0 additions & 180 deletions
Original file line numberDiff line numberDiff line change
@@ -1,193 +1,13 @@
1-
using System;
21
using System.Collections.Generic;
3-
using System.Linq;
4-
using JsonApiDotNetCore.Extensions;
5-
using JsonApiDotNetCore.Services;
6-
using Microsoft.AspNetCore.Http;
7-
using JsonApiDotNetCore.Models;
8-
using JsonApiDotNetCore.Controllers;
92

103
namespace JsonApiDotNetCore.Internal.Query
114
{
125
public class QuerySet
136
{
14-
private readonly IJsonApiContext _jsonApiContext;
15-
16-
public QuerySet(
17-
IJsonApiContext jsonApiContext,
18-
IQueryCollection query)
19-
{
20-
_jsonApiContext = jsonApiContext;
21-
BuildQuerySet(query);
22-
}
23-
247
public List<FilterQuery> Filters { get; set; } = new List<FilterQuery>();
258
public PageQuery PageQuery { get; set; } = new PageQuery();
269
public List<SortQuery> SortParameters { get; set; } = new List<SortQuery>();
2710
public List<string> IncludedRelationships { get; set; } = new List<string>();
2811
public List<string> Fields { get; set; } = new List<string>();
29-
30-
private void BuildQuerySet(IQueryCollection query)
31-
{
32-
var disabledQueries = _jsonApiContext.GetControllerAttribute<DisableQueryAttribute>()?.QueryParams ?? QueryParams.None;
33-
34-
foreach (var pair in query)
35-
{
36-
if (pair.Key.StartsWith("filter"))
37-
{
38-
if (disabledQueries.HasFlag(QueryParams.Filter) == false)
39-
Filters.AddRange(ParseFilterQuery(pair.Key, pair.Value));
40-
continue;
41-
}
42-
43-
if (pair.Key.StartsWith("sort"))
44-
{
45-
if (disabledQueries.HasFlag(QueryParams.Sort) == false)
46-
SortParameters = ParseSortParameters(pair.Value);
47-
continue;
48-
}
49-
50-
if (pair.Key.StartsWith("include"))
51-
{
52-
if (disabledQueries.HasFlag(QueryParams.Include) == false)
53-
IncludedRelationships = ParseIncludedRelationships(pair.Value);
54-
continue;
55-
}
56-
57-
if (pair.Key.StartsWith("page"))
58-
{
59-
if (disabledQueries.HasFlag(QueryParams.Page) == false)
60-
PageQuery = ParsePageQuery(pair.Key, pair.Value);
61-
continue;
62-
}
63-
64-
if (pair.Key.StartsWith("fields"))
65-
{
66-
if (disabledQueries.HasFlag(QueryParams.Fields) == false)
67-
Fields = ParseFieldsQuery(pair.Key, pair.Value);
68-
continue;
69-
}
70-
71-
if (_jsonApiContext.Options.AllowCustomQueryParameters == false)
72-
throw new JsonApiException(400, $"{pair} is not a valid query.");
73-
}
74-
}
75-
76-
private List<FilterQuery> ParseFilterQuery(string key, string value)
77-
{
78-
// expected input = filter[id]=1
79-
// expected input = filter[id]=eq:1
80-
var queries = new List<FilterQuery>();
81-
82-
var propertyName = key.Split('[', ']')[1].ToProperCase();
83-
84-
var values = value.Split(',');
85-
foreach (var val in values)
86-
{
87-
(var operation, var filterValue) = ParseFilterOperation(val);
88-
queries.Add(new FilterQuery(propertyName, filterValue, operation));
89-
}
90-
91-
return queries;
92-
}
93-
94-
private (string operation, string value) ParseFilterOperation(string value)
95-
{
96-
if (value.Length < 3)
97-
return (string.Empty, value);
98-
99-
var operation = value.Split(':');
100-
101-
if (operation.Length == 1)
102-
return (string.Empty, value);
103-
104-
// remove prefix from value
105-
if (Enum.TryParse(operation[0], out FilterOperations op) == false)
106-
return (string.Empty, value);
107-
108-
var prefix = operation[0];
109-
value = string.Join(":", operation.Skip(1));
110-
111-
return (prefix, value);
112-
}
113-
114-
private PageQuery ParsePageQuery(string key, string value)
115-
{
116-
// expected input = page[size]=10
117-
// page[number]=1
118-
PageQuery = PageQuery ?? new PageQuery();
119-
120-
var propertyName = key.Split('[', ']')[1];
121-
122-
if (propertyName == "size")
123-
PageQuery.PageSize = Convert.ToInt32(value);
124-
else if (propertyName == "number")
125-
PageQuery.PageOffset = Convert.ToInt32(value);
126-
127-
return PageQuery;
128-
}
129-
130-
// sort=id,name
131-
// sort=-id
132-
private List<SortQuery> ParseSortParameters(string value)
133-
{
134-
var sortParameters = new List<SortQuery>();
135-
value.Split(',').ToList().ForEach(p =>
136-
{
137-
var direction = SortDirection.Ascending;
138-
if (p[0] == '-')
139-
{
140-
direction = SortDirection.Descending;
141-
p = p.Substring(1);
142-
}
143-
144-
var attribute = GetAttribute(p.ToProperCase());
145-
146-
sortParameters.Add(new SortQuery(direction, attribute));
147-
});
148-
149-
return sortParameters;
150-
}
151-
152-
private List<string> ParseIncludedRelationships(string value)
153-
{
154-
if (value.Contains("."))
155-
throw new JsonApiException(400, "Deeply nested relationships are not supported");
156-
157-
return value
158-
.Split(',')
159-
.ToList();
160-
}
161-
162-
private List<string> ParseFieldsQuery(string key, string value)
163-
{
164-
// expected: fields[TYPE]=prop1,prop2
165-
var typeName = key.Split('[', ']')[1];
166-
167-
var includedFields = new List<string> { "Id" };
168-
169-
if (typeName != _jsonApiContext.RequestEntity.EntityName)
170-
return includedFields;
171-
172-
var fields = value.Split(',');
173-
foreach (var field in fields)
174-
{
175-
var internalAttrName = _jsonApiContext.RequestEntity
176-
.Attributes
177-
.SingleOrDefault(attr => attr.PublicAttributeName == field)
178-
.InternalAttributeName;
179-
includedFields.Add(internalAttrName);
180-
}
181-
182-
return includedFields;
183-
}
184-
185-
private AttrAttribute GetAttribute(string propertyName)
186-
{
187-
return _jsonApiContext.RequestEntity.Attributes
188-
.FirstOrDefault(attr =>
189-
attr.InternalAttributeName.ToLower() == propertyName.ToLower()
190-
);
191-
}
19212
}
19313
}

src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>2.1.8</VersionPrefix>
3+
<VersionPrefix>2.1.9</VersionPrefix>
44
<TargetFrameworks>netstandard1.6</TargetFrameworks>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Reflection;
3+
using JsonApiDotNetCore.Internal;
4+
5+
namespace JsonApiDotNetCore.Services
6+
{
7+
public interface IControllerContext
8+
{
9+
Type ControllerType { get; set; }
10+
ContextEntity RequestEntity { get; set; }
11+
TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute;
12+
}
13+
14+
public class ControllerContext : IControllerContext
15+
{
16+
public Type ControllerType { get; set; }
17+
public ContextEntity RequestEntity { get; set; }
18+
19+
public TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute
20+
{
21+
var attribute = ControllerType.GetTypeInfo().GetCustomAttribute(typeof(TAttribute));
22+
return attribute == null ? null : (TAttribute)attribute;
23+
}
24+
}
25+
}

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4-
using System.Reflection;
54
using JsonApiDotNetCore.Builders;
65
using JsonApiDotNetCore.Configuration;
76
using JsonApiDotNetCore.Data;
@@ -17,26 +16,33 @@ public class JsonApiContext : IJsonApiContext
1716
{
1817
private readonly IHttpContextAccessor _httpContextAccessor;
1918
private readonly IDbContextResolver _contextResolver;
19+
private readonly IQueryParser _queryParser;
20+
private readonly IControllerContext _controllerContext;
2021

2122
public JsonApiContext(
2223
IDbContextResolver contextResolver,
2324
IContextGraph contextGraph,
2425
IHttpContextAccessor httpContextAccessor,
2526
JsonApiOptions options,
2627
IMetaBuilder metaBuilder,
27-
IGenericProcessorFactory genericProcessorFactory)
28+
IGenericProcessorFactory genericProcessorFactory,
29+
IQueryParser queryParser,
30+
IControllerContext controllerContext)
2831
{
2932
_contextResolver = contextResolver;
3033
ContextGraph = contextGraph;
3134
_httpContextAccessor = httpContextAccessor;
3235
Options = options;
3336
MetaBuilder = metaBuilder;
3437
GenericProcessorFactory = genericProcessorFactory;
38+
_queryParser = queryParser;
39+
_controllerContext = controllerContext;
3540
}
3641

3742
public JsonApiOptions Options { get; set; }
3843
public IContextGraph ContextGraph { get; set; }
39-
public ContextEntity RequestEntity { get; set; }
44+
[Obsolete("Use the proxied member IControllerContext.RequestEntity instead.")]
45+
public ContextEntity RequestEntity { get => _controllerContext.RequestEntity; set => _controllerContext.RequestEntity = value; }
4046
public string BasePath { get; set; }
4147
public QuerySet QuerySet { get; set; }
4248
public bool IsRelationshipData { get; set; }
@@ -54,21 +60,20 @@ public IJsonApiContext ApplyContext<T>(object controller)
5460
if (controller == null)
5561
throw new JsonApiException(500, $"Cannot ApplyContext from null controller for type {typeof(T)}");
5662

57-
ControllerType = controller.GetType();
63+
_controllerContext.ControllerType = controller.GetType();
64+
_controllerContext.RequestEntity = ContextGraph.GetContextEntity(typeof(T));
5865

5966
var context = _httpContextAccessor.HttpContext;
6067
var path = context.Request.Path.Value.Split('/');
6168

62-
RequestEntity = ContextGraph.GetContextEntity(typeof(T));
63-
6469
if (context.Request.Query.Any())
6570
{
66-
QuerySet = new QuerySet(this, context.Request.Query);
71+
QuerySet = _queryParser.Parse(context.Request.Query);
6772
IncludedRelationships = QuerySet.IncludedRelationships;
6873
}
6974

7075
var linkBuilder = new LinkBuilder(this);
71-
BasePath = linkBuilder.GetBasePath(context, RequestEntity.EntityName);
76+
BasePath = linkBuilder.GetBasePath(context, _controllerContext.RequestEntity.EntityName);
7277
PageManager = GetPageManager();
7378
IsRelationshipPath = path[path.Length - 2] == "relationships";
7479
return this;
@@ -91,10 +96,8 @@ private PageManager GetPageManager()
9196
};
9297
}
9398

99+
[Obsolete("Use the proxied method IControllerContext.GetControllerAttribute instead.")]
94100
public TAttribute GetControllerAttribute<TAttribute>() where TAttribute : Attribute
95-
{
96-
var attribute = ControllerType.GetTypeInfo().GetCustomAttribute(typeof(TAttribute));
97-
return attribute == null ? null : (TAttribute)attribute;
98-
}
101+
=> _controllerContext.GetControllerAttribute<TAttribute>();
99102
}
100103
}

0 commit comments

Comments
 (0)