Skip to content

Commit d9f859d

Browse files
author
Chris Santero
committed
allow sorting by non-reference types
Apparently sorting by value types such as DateTime, int, etc. never worked.
1 parent 4037996 commit d9f859d

2 files changed

Lines changed: 65 additions & 24 deletions

File tree

JSONAPI.Tests/ActionFilters/DefaultSortingTransformerTests.cs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ private class Dummy
2222
public string FirstName { get; set; }
2323

2424
public string LastName { get; set; }
25+
26+
public DateTime BirthDate { get; set; }
2527
}
2628

2729
private IList<Dummy> _fixtures;
@@ -32,15 +34,15 @@ public void SetupFixtures()
3234
{
3335
_fixtures = new List<Dummy>
3436
{
35-
new Dummy { Id = "1", FirstName = "Thomas", LastName = "Paine" },
36-
new Dummy { Id = "2", FirstName = "Samuel", LastName = "Adams" },
37-
new Dummy { Id = "3", FirstName = "George", LastName = "Washington"},
38-
new Dummy { Id = "4", FirstName = "Thomas", LastName = "Jefferson" },
39-
new Dummy { Id = "5", FirstName = "Martha", LastName = "Washington"},
40-
new Dummy { Id = "6", FirstName = "Abraham", LastName = "Lincoln" },
41-
new Dummy { Id = "7", FirstName = "Andrew", LastName = "Jackson" },
42-
new Dummy { Id = "8", FirstName = "Andrew", LastName = "Johnson" },
43-
new Dummy { Id = "9", FirstName = "William", LastName = "Harrison" }
37+
new Dummy {Id = "1", FirstName = "Thomas", LastName = "Paine", BirthDate = new DateTime(1737, 2, 9)},
38+
new Dummy {Id = "2", FirstName = "Samuel", LastName = "Adams", BirthDate = new DateTime(1722, 9, 27)},
39+
new Dummy {Id = "3", FirstName = "George", LastName = "Washington", BirthDate = new DateTime(1732, 2, 22)},
40+
new Dummy {Id = "4", FirstName = "Thomas", LastName = "Jefferson", BirthDate = new DateTime(1743, 4, 13)},
41+
new Dummy {Id = "5", FirstName = "Martha", LastName = "Washington", BirthDate = new DateTime(1731, 6, 13)},
42+
new Dummy {Id = "6", FirstName = "Abraham", LastName = "Lincoln", BirthDate = new DateTime(1809, 2, 12)},
43+
new Dummy {Id = "7", FirstName = "Andrew", LastName = "Jackson", BirthDate = new DateTime(1767, 3, 15)},
44+
new Dummy {Id = "8", FirstName = "Andrew", LastName = "Johnson", BirthDate = new DateTime(1808, 12, 29)},
45+
new Dummy {Id = "9", FirstName = "William", LastName = "Harrison", BirthDate = new DateTime(1773, 2, 9)}
4446
};
4547
_fixturesQuery = _fixtures.AsQueryable();
4648
}
@@ -52,7 +54,7 @@ private DefaultSortingTransformer GetTransformer()
5254
{"Dummy", "Dummies"}
5355
});
5456
var registrar = new ResourceTypeRegistrar(new DefaultNamingConventions(pluralizationService));
55-
var registration = registrar.BuildRegistration(typeof(Dummy));
57+
var registration = registrar.BuildRegistration(typeof (Dummy));
5658
var registry = new ResourceTypeRegistry();
5759
registry.AddRegistration(registration);
5860
return new DefaultSortingTransformer(registry);
@@ -131,13 +133,22 @@ public void Returns_400_if_sort_argument_is_whitespace_descending()
131133
[TestMethod]
132134
public void Returns_400_if_no_property_exists()
133135
{
134-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar", "The attribute \"foobar\" does not exist on type \"dummies\".");
136+
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=foobar",
137+
"The attribute \"foobar\" does not exist on type \"dummies\".");
135138
}
136139

137140
[TestMethod]
138141
public void Returns_400_if_the_same_property_is_specified_more_than_once()
139142
{
140-
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name", "The attribute \"last-name\" was specified more than once.");
143+
RunTransformAndExpectFailure("http://api.example.com/dummies?sort=last-name,last-name",
144+
"The attribute \"last-name\" was specified more than once.");
145+
}
146+
147+
[TestMethod]
148+
public void Can_sort_by_DateTimeOffset()
149+
{
150+
var array = GetArray("http://api.example.com/dummies?sort=birth-date");
151+
array.Should().BeInAscendingOrder(d => d.BirthDate);
141152
}
142153
}
143154
}

JSONAPI/QueryableTransformers/DefaultSortingTransformer.cs

Lines changed: 42 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Linq.Expressions;
55
using System.Net.Http;
66
using System.Reflection;
7-
using JSONAPI.ActionFilters;
87
using JSONAPI.Core;
98
using JSONAPI.Documents.Builders;
109

@@ -43,7 +42,7 @@ public IOrderedQueryable<T> Sort<T>(IQueryable<T> query, HttpRequestMessage requ
4342
sortExpressions = sortParam.Value.Split(',');
4443
}
4544

46-
var selectors = new List<Tuple<bool, Expression<Func<T, object>>>>();
45+
var selectors = new List<ISelector<T>>();
4746
var usedProperties = new Dictionary<PropertyInfo, object>();
4847

4948
var registration = _resourceTypeRegistry.GetRegistrationForType(typeof (T));
@@ -91,24 +90,55 @@ public IOrderedQueryable<T> Sort<T>(IQueryable<T> query, HttpRequestMessage requ
9190
string.Format("The attribute \"{0}\" was specified more than once.", fieldName), "sort");
9291

9392
usedProperties[property] = null;
94-
9593
sortValueExpression = Expression.Property(paramExpr, property);
9694
}
9795

98-
var selector = Expression.Lambda<Func<T, object>>(sortValueExpression, paramExpr);
99-
selectors.Add(Tuple.Create(ascending, selector));
96+
var selector = GetSelector<T>(paramExpr, sortValueExpression, !ascending);
97+
selectors.Add(selector);
10098
}
10199

102100
var firstSelector = selectors.First();
103101

104-
IOrderedQueryable<T> workingQuery =
105-
firstSelector.Item1
106-
? query.OrderBy(firstSelector.Item2)
107-
: query.OrderByDescending(firstSelector.Item2);
102+
IOrderedQueryable<T> workingQuery = firstSelector.ApplyInitially(query);
103+
return selectors.Skip(1).Aggregate(workingQuery, (current, selector) => selector.ApplySubsequently(current));
104+
}
105+
106+
private ISelector<T> GetSelector<T>(ParameterExpression paramExpr, Expression sortValueExpression, bool isDescending)
107+
{
108+
var lambda = Expression.Lambda(sortValueExpression, paramExpr);
109+
var selectorType = typeof (Selector<,>).MakeGenericType(typeof (T), sortValueExpression.Type);
110+
var selector = Activator.CreateInstance(selectorType, isDescending, lambda);
111+
return (ISelector<T>)selector;
112+
}
113+
}
114+
115+
internal interface ISelector<T>
116+
{
117+
IOrderedQueryable<T> ApplyInitially(IQueryable<T> unsortedQuery);
118+
IOrderedQueryable<T> ApplySubsequently(IOrderedQueryable<T> currentQuery);
119+
}
120+
121+
internal class Selector<TResource, TProperty> : ISelector<TResource>
122+
{
123+
private readonly bool _isDescending;
124+
private readonly Expression<Func<TResource, TProperty>> _propertyAccessorExpression;
125+
126+
public Selector(bool isDescending, Expression<Func<TResource, TProperty>> propertyAccessorExpression)
127+
{
128+
_isDescending = isDescending;
129+
_propertyAccessorExpression = propertyAccessorExpression;
130+
}
108131

109-
return selectors.Skip(1).Aggregate(workingQuery,
110-
(current, selector) =>
111-
selector.Item1 ? current.ThenBy(selector.Item2) : current.ThenByDescending(selector.Item2));
132+
public IOrderedQueryable<TResource> ApplyInitially(IQueryable<TResource> unsortedQuery)
133+
{
134+
if (_isDescending) return unsortedQuery.OrderByDescending(_propertyAccessorExpression);
135+
return unsortedQuery.OrderBy(_propertyAccessorExpression);
136+
}
137+
138+
public IOrderedQueryable<TResource> ApplySubsequently(IOrderedQueryable<TResource> currentQuery)
139+
{
140+
if (_isDescending) return currentQuery.ThenByDescending(_propertyAccessorExpression);
141+
return currentQuery.ThenBy(_propertyAccessorExpression);
112142
}
113143
}
114144
}

0 commit comments

Comments
 (0)