Skip to content

Commit 3ef7af6

Browse files
committed
feat(sorting): add sorting
1 parent be460b3 commit 3ef7af6

File tree

8 files changed

+236
-6
lines changed

8 files changed

+236
-6
lines changed

src/JsonApiDotNetCore/Controllers/JsonApiController.cs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System.Linq;
22
using System.Threading.Tasks;
33
using JsonApiDotNetCore.Data;
4+
using JsonApiDotNetCore.Internal.Query;
45
using JsonApiDotNetCore.Models;
56
using JsonApiDotNetCore.Services;
67
using Microsoft.AspNetCore.Mvc;
@@ -10,7 +11,8 @@
1011

1112
namespace JsonApiDotNetCore.Controllers
1213
{
13-
public class JsonApiController<T> : JsonApiController<T, int> where T : class, IIdentifiable<int>
14+
public class JsonApiController<T>
15+
: JsonApiController<T, int> where T : class, IIdentifiable<int>
1416
{
1517
public JsonApiController(
1618
IJsonApiContext jsonApiContext,
@@ -20,7 +22,8 @@ public JsonApiController(
2022
{ }
2123
}
2224

23-
public class JsonApiController<T, TId> : Controller where T : class, IIdentifiable<TId>
25+
public class JsonApiController<T, TId>
26+
: Controller where T : class, IIdentifiable<TId>
2427
{
2528
private readonly IEntityRepository<T, TId> _entities;
2629
private readonly IJsonApiContext _jsonApiContext;
@@ -52,7 +55,9 @@ public virtual IActionResult Get()
5255
{
5356
ApplyContext();
5457

55-
var entities = _entities.Get().ToList();
58+
var entities = _entities.Get();
59+
60+
entities = ApplyQuery(entities);
5661

5762
return Ok(entities);
5863
}
@@ -75,7 +80,8 @@ public virtual async Task<IActionResult> GetRelationshipAsync(TId id, string rel
7580
{
7681
ApplyContext();
7782

78-
relationshipName = _jsonApiContext.ContextGraph.GetRelationshipName<T>(relationshipName);
83+
relationshipName = _jsonApiContext.ContextGraph
84+
.GetRelationshipName<T>(relationshipName);
7985

8086
if (relationshipName == null)
8187
return NotFound();
@@ -85,8 +91,6 @@ public virtual async Task<IActionResult> GetRelationshipAsync(TId id, string rel
8591
if (entity == null)
8692
return NotFound();
8793

88-
_logger?.LogInformation($"Looking up relationship '{relationshipName}' on {entity.GetType().Name}");
89-
9094
var relationship = _jsonApiContext.ContextGraph
9195
.GetRelationship<T>(entity, relationshipName);
9296

@@ -153,5 +157,14 @@ private void ApplyContext()
153157
_jsonApiContext.RequestEntity = _jsonApiContext.ContextGraph.GetContextEntity(typeof(T));
154158
_jsonApiContext.ApplyContext(HttpContext);
155159
}
160+
161+
private IQueryable<T> ApplyQuery(IQueryable<T> entities)
162+
{
163+
if(!HttpContext.Request.Query.Any())
164+
return entities;
165+
166+
return new QuerySet<T>( _jsonApiContext)
167+
.ApplyQuery(entities);
168+
}
156169
}
157170
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
2+
using System.Linq;
3+
using System.Linq.Expressions;
4+
using System.Reflection;
5+
6+
namespace JsonApiDotNetCore.Extensions
7+
{
8+
public static class IQueryableExtensions
9+
{
10+
public static IOrderedQueryable<TSource> OrderBy<TSource>(this IQueryable<TSource> source, string propertyName)
11+
{
12+
// LAMBDA: x => x.[PropertyName]
13+
var parameter = Expression.Parameter(typeof(TSource), "x");
14+
Expression property = Expression.Property(parameter, propertyName);
15+
var lambda = Expression.Lambda(property, parameter);
16+
17+
// REFLECTION: source.OrderBy(x => x.Property)
18+
var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == "OrderBy" && x.GetParameters().Length == 2);
19+
var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), property.Type);
20+
var result = orderByGeneric.Invoke(null, new object[] { source, lambda });
21+
22+
return (IOrderedQueryable<TSource>)result;
23+
}
24+
25+
public static IOrderedQueryable<TSource> OrderByDescending<TSource>(this IQueryable<TSource> source, string propertyName)
26+
{
27+
// LAMBDA: x => x.[PropertyName]
28+
var parameter = Expression.Parameter(typeof(TSource), "x");
29+
Expression property = Expression.Property(parameter, propertyName);
30+
var lambda = Expression.Lambda(property, parameter);
31+
32+
// REFLECTION: source.OrderBy(x => x.Property)
33+
var orderByMethod = typeof(Queryable).GetMethods().First(x => x.Name == "OrderByDescending" && x.GetParameters().Length == 2);
34+
var orderByGeneric = orderByMethod.MakeGenericMethod(typeof(TSource), property.Type);
35+
var result = orderByGeneric.Invoke(null, new object[] { source, lambda });
36+
37+
return (IOrderedQueryable<TSource>)result;
38+
}
39+
}
40+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
namespace JsonApiDotNetCore.Internal.Query
2+
{
3+
public class FilterQuery
4+
{
5+
public FilterQuery(AttrAttribute filteredAttribute, string propertyValue)
6+
{
7+
FilteredAttribute = filteredAttribute;
8+
PropertyValue = propertyValue;
9+
}
10+
11+
public AttrAttribute FilteredAttribute { get; set; }
12+
public string PropertyValue { get; set; }
13+
}
14+
}
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Linq.Expressions;
5+
using System.Reflection;
6+
using JsonApiDotNetCore.Services;
7+
using JsonApiDotNetCore.Extensions;
8+
9+
namespace JsonApiDotNetCore.Internal.Query
10+
{
11+
public class QuerySet<T>
12+
{
13+
IJsonApiContext _jsonApiContext;
14+
15+
public QuerySet(IJsonApiContext jsonApiContext)
16+
{
17+
_jsonApiContext = jsonApiContext;
18+
BuildQuerySet();
19+
}
20+
21+
public FilterQuery Filter { get; set; }
22+
public List<SortQuery> SortParameters { get; set; }
23+
24+
private void BuildQuerySet()
25+
{
26+
foreach (var pair in _jsonApiContext.Query)
27+
{
28+
if (pair.Key.StartsWith("filter"))
29+
{
30+
Filter = ParseFilterQuery(pair.Key, pair.Value);
31+
continue;
32+
}
33+
34+
if (pair.Key.StartsWith("sort"))
35+
{
36+
SortParameters = ParseSortParameters(pair.Value);
37+
}
38+
}
39+
}
40+
41+
private FilterQuery ParseFilterQuery(string key, string value)
42+
{
43+
// expected input = filter[id]=1
44+
var propertyName = key.Split('[', ']')[1];
45+
var attribute = GetAttribute(propertyName);
46+
47+
if(attribute == null)
48+
return null;
49+
50+
return new FilterQuery(attribute, value);
51+
}
52+
53+
// sort=id,name
54+
// sort=-id
55+
private List<SortQuery> ParseSortParameters(string value)
56+
{
57+
var sortParameters = new List<SortQuery>();
58+
value.Split(',').ToList().ForEach(p =>
59+
{
60+
var direction = SortDirection.Ascending;
61+
if (p[0] == '-')
62+
{
63+
direction = SortDirection.Descending;
64+
p = p.Substring(1);
65+
}
66+
67+
var attribute = GetAttribute(p);
68+
69+
sortParameters.Add(new SortQuery(direction, attribute));
70+
});
71+
72+
return sortParameters;
73+
}
74+
75+
private AttrAttribute GetAttribute(string propertyName)
76+
{
77+
return _jsonApiContext.RequestEntity.Attributes
78+
.FirstOrDefault(attr =>
79+
attr.InternalAttributeName.ToLower() == propertyName.ToLower()
80+
);
81+
}
82+
83+
public IQueryable<T> ApplyQuery(IQueryable<T> entities)
84+
{
85+
entities = ApplyFilter(entities);
86+
entities = ApplySort(entities);
87+
return entities;
88+
}
89+
90+
private IQueryable<T> ApplyFilter(IQueryable<T> entities)
91+
{
92+
if(Filter == null)
93+
return entities;
94+
95+
var expression = GetEqualityExpressionForProperty(entities,
96+
Filter.FilteredAttribute.InternalAttributeName, Filter.PropertyValue);
97+
98+
return entities.Where(expression);
99+
}
100+
101+
private IQueryable<T> ApplySort(IQueryable<T> entities)
102+
{
103+
if(SortParameters == null || SortParameters.Count == 0)
104+
return entities;
105+
106+
SortParameters.ForEach(sortParam => {
107+
if(sortParam.Direction == SortDirection.Ascending)
108+
entities = entities.OrderBy(sortParam.SortedAttribute.InternalAttributeName);
109+
else
110+
entities = entities.OrderByDescending(sortParam.SortedAttribute.InternalAttributeName);
111+
});
112+
113+
return entities;
114+
}
115+
116+
private Expression<Func<T, bool>> GetEqualityExpressionForProperty(IQueryable<T> query, string param, object value)
117+
{
118+
var currentType = query.ElementType;
119+
var property = currentType.GetProperty(param);
120+
121+
if (property == null)
122+
throw new ArgumentException($"'{param}' is not a valid property of '{currentType}'");
123+
124+
// convert the incoming value to the target value type
125+
// "1" -> 1
126+
var convertedValue = Convert.ChangeType(value, property.PropertyType);
127+
// {model}
128+
var prm = Expression.Parameter(currentType, "model");
129+
// {model.Id}
130+
var left = Expression.PropertyOrField(prm, property.Name);
131+
// {1}
132+
var right = Expression.Constant(convertedValue, property.PropertyType);
133+
// {model.Id == 1}
134+
var body = Expression.Equal(left, right);
135+
136+
return Expression.Lambda<Func<T, bool>>(body, prm);
137+
}
138+
}
139+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace JsonApiDotNetCore.Internal.Query
2+
{
3+
public enum SortDirection
4+
{
5+
Ascending = 1,
6+
Descending = 2
7+
}
8+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace JsonApiDotNetCore.Internal.Query
2+
{
3+
public class SortQuery
4+
{
5+
public SortQuery(SortDirection direction, AttrAttribute sortedAttribute)
6+
{
7+
Direction = direction;
8+
SortedAttribute = sortedAttribute;
9+
}
10+
public SortDirection Direction { get; set; }
11+
public AttrAttribute SortedAttribute { get; set; }
12+
}
13+
}

src/JsonApiDotNetCore/Services/IJsonApiContext.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,6 @@ public interface IJsonApiContext
99
IContextGraph ContextGraph { get; set; }
1010
ContextEntity RequestEntity { get; set; }
1111
string BasePath { get; set; }
12+
IQueryCollection Query { get; set; }
1213
}
1314
}

src/JsonApiDotNetCore/Services/JsonApiContext.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,13 @@ public JsonApiContext(IContextGraph contextGraph)
1414
public IContextGraph ContextGraph { get; set; }
1515
public ContextEntity RequestEntity { get; set; }
1616
public string BasePath { get; set; }
17+
public IQueryCollection Query { get; set; }
1718

1819
public void ApplyContext(HttpContext context)
1920
{
2021
var linkBuilder = new LinkBuilder(this);
2122
BasePath = linkBuilder.GetBasePath(context, RequestEntity.EntityName);
23+
Query = context.Request.Query;
2224
}
2325
}
2426
}

0 commit comments

Comments
 (0)