Skip to content

Commit 4673653

Browse files
committed
Add filtering
1 parent 48ae63f commit 4673653

File tree

12 files changed

+165
-17
lines changed

12 files changed

+165
-17
lines changed

JsonApiDotNetCore/Data/GenericDataAccessAbstraction.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,10 @@ public object SingleOrDefault(string propertyName, string value)
2727
return InvokeGenericDataAccessMethod("SingleOrDefault", new[] { dbSet, propertyName, value });
2828
}
2929

30-
public object Filter(string propertyName, string value)
30+
public IQueryable Filter(string propertyName, string value)
3131
{
3232
var dbSet = GetDbSet();
33-
return InvokeGenericDataAccessMethod("Where", new[] { dbSet, propertyName, value });
33+
return (IQueryable)InvokeGenericDataAccessMethod("Where", new[] { dbSet, propertyName, value });
3434
}
3535

3636
private object InvokeGenericDataAccessMethod(string methodName, params object[] propertyValues)
@@ -40,7 +40,7 @@ private object InvokeGenericDataAccessMethod(string methodName, params object[]
4040
return genericDataAccessorMethod.Invoke(_dataAccessorInstance, propertyValues);
4141
}
4242

43-
private object GetDbSet()
43+
public object GetDbSet()
4444
{
4545
var dataAccessorGetDbSetMethod = _dataAccessorInstance.GetType().GetMethod("GetDbSet");
4646
var genericGetDbSetMethod = dataAccessorGetDbSetMethod.MakeGenericMethod(_modelType);

JsonApiDotNetCore/Data/ResourceRepository.cs

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
using JsonApiDotNetCore.Extensions;
77
using JsonApiDotNetCore.Routing;
88
using Microsoft.EntityFrameworkCore;
9+
using System.Collections;
910

1011
namespace JsonApiDotNetCore.Data
1112
{
@@ -20,7 +21,15 @@ public ResourceRepository(JsonApiContext context)
2021

2122
public List<object> Get()
2223
{
23-
return (GetDbSetFromContext(_context.Route.BaseRouteDefinition.ContextPropertyName) as IEnumerable<object>)?.ToList();
24+
IQueryable dbSet;
25+
var filter = _context.Route.Query.Filter;
26+
if(filter != null) {
27+
dbSet = FilterEntities(_context.Route.BaseModelType, filter.PropertyName, filter.PropertyValue, null);
28+
}
29+
else {
30+
dbSet = GetDbSet(_context.Route.BaseModelType, null);
31+
}
32+
return ((IEnumerable<object>)dbSet).ToList();
2433
}
2534

2635
public object Get(string id)
@@ -35,28 +44,33 @@ private object GetRelated(string id, RelationalRoute relationalRoute)
3544
return relationalRoute.BaseModelType.GetProperties().FirstOrDefault(pi => pi.Name.ToCamelCase() == relationalRoute.RelationshipName.ToCamelCase())?.GetValue(entity);
3645
}
3746

38-
private IQueryable GetDbSetFromContext(string propName)
47+
private IQueryable GetDbSet(Type modelType, string includedRelationship)
3948
{
4049
var dbContext = _context.DbContext;
41-
return (IQueryable)dbContext.GetType().GetProperties().FirstOrDefault(pI => pI.Name.ToProperCase() == propName.ToProperCase())?.GetValue(dbContext, null);
50+
return (IQueryable)new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).GetDbSet();
4251
}
4352

4453
private object GetEntityById(Type modelType, string id, string includedRelationship)
4554
{
4655
return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).SingleOrDefault("Id", id);
4756
}
4857

58+
private IQueryable FilterEntities(Type modelType, string property, string value, string includedRelationship)
59+
{
60+
return new GenericDataAccessAbstraction(_context.DbContext, modelType, includedRelationship).Filter(property, value);
61+
}
62+
4963
public void Add(object entity)
5064
{
51-
var dbSet = GetDbSetFromContext(_context.Route.BaseRouteDefinition.ContextPropertyName);
65+
var dbSet = GetDbSet(_context.Route.BaseModelType, null);
5266
var dbSetAddMethod = dbSet.GetType().GetMethod("Add");
5367
dbSetAddMethod.Invoke(dbSet, new [] { entity });
5468
}
5569

5670
public void Delete(string id)
5771
{
5872
var entity = Get(id);
59-
var dbSet = GetDbSetFromContext(_context.Route.BaseRouteDefinition.ContextPropertyName);
73+
var dbSet = GetDbSet(_context.Route.BaseModelType, null);
6074
var dbSetAddMethod = dbSet.GetType().GetMethod("Remove");
6175
dbSetAddMethod.Invoke(dbSet, new [] { entity });
6276
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace JsonApiDotNetCore.Routing.Query
2+
{
3+
public class FilterQuery
4+
{
5+
public FilterQuery(string propertyName, string propertyValue)
6+
{
7+
PropertyName = propertyName;
8+
PropertyValue = propertyValue;
9+
}
10+
public string PropertyName { get; set; }
11+
public string PropertyValue { get; set; }
12+
}
13+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using JsonApiDotNetCore.Extensions;
4+
using Microsoft.AspNetCore.Http;
5+
6+
namespace JsonApiDotNetCore.Routing.Query
7+
{
8+
public class QuerySet
9+
{
10+
public QuerySet (IQueryCollection query)
11+
{
12+
BuildQuerySet(query);
13+
}
14+
public FilterQuery Filter { get; set; }
15+
public List<SortParameter> SortParameters { get; set; }
16+
17+
private void BuildQuerySet(IQueryCollection query)
18+
{
19+
foreach (var pair in query)
20+
{
21+
if(pair.Key.StartsWith("filter"))
22+
{
23+
Filter = ParseFilterQuery(pair.Key, pair.Value);
24+
continue;
25+
}
26+
27+
if(pair.Key.StartsWith("sort")){
28+
SortParameters = ParseSortParameters(pair.Value);
29+
}
30+
}
31+
}
32+
33+
private FilterQuery ParseFilterQuery(string key, string value)
34+
{
35+
// expected input = filter[id]=1
36+
var propertyName = key.Split('[', ']')[1].ToProperCase();
37+
return new FilterQuery(propertyName, value);
38+
}
39+
40+
// sort=id,name
41+
// sort=-id
42+
private List<SortParameter> ParseSortParameters(string value)
43+
{
44+
var sortParameters = new List<SortParameter>();
45+
value.Split(',').ToList().ForEach(p => {
46+
var direction = SortDirection.Ascending;
47+
if(p[0] == '-')
48+
{
49+
direction = SortDirection.Descending;
50+
p = p.Substring(1);
51+
}
52+
sortParameters.Add(new SortParameter(direction, p.ToProperCase()));
53+
});
54+
55+
return sortParameters;
56+
}
57+
}
58+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
namespace JsonApiDotNetCore.Routing.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.Routing.Query
2+
{
3+
public class SortParameter
4+
{
5+
public SortParameter(SortDirection direction, string propertyName)
6+
{
7+
Direction = direction;
8+
PropertyName = propertyName;
9+
}
10+
public SortDirection Direction { get; set; }
11+
public string PropertyName { get; set; }
12+
}
13+
}

JsonApiDotNetCore/Routing/RelationalRoute.cs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using System;
2+
using JsonApiDotNetCore.Routing.Query;
23

34
namespace JsonApiDotNetCore.Routing
45
{
56
public class RelationalRoute : Route
67
{
7-
public RelationalRoute(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition, Type relationalType, string relationshipName)
8-
: base(baseModelType, requestMethod, resourceId, baseRouteDefinition)
8+
public RelationalRoute(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition, QuerySet querySet, Type relationalType, string relationshipName)
9+
: base(baseModelType, requestMethod, resourceId, baseRouteDefinition, querySet)
910
{
1011
RelationalType = relationalType;
1112
RelationshipName = relationshipName;

JsonApiDotNetCore/Routing/Route.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
11
using System;
2+
using JsonApiDotNetCore.Routing.Query;
23

34
namespace JsonApiDotNetCore.Routing
45
{
56
public class Route
67
{
7-
public Route(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition)
8+
public Route(Type baseModelType, string requestMethod, string resourceId, RouteDefinition baseRouteDefinition, QuerySet query)
89
{
910
BaseModelType = baseModelType;
1011
RequestMethod = requestMethod;
1112
ResourceId = resourceId;
1213
BaseRouteDefinition = baseRouteDefinition;
14+
Query = query;
1315
}
1416

1517
public Type BaseModelType { get; set; }
1618
public string RequestMethod { get; set; }
1719
public RouteDefinition BaseRouteDefinition { get; set; }
1820
public string ResourceId { get; set; }
21+
public QuerySet Query { get; set; }
1922
}
2023
}

JsonApiDotNetCore/Routing/RouteBuilder.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using JsonApiDotNetCore.Abstractions;
33
using JsonApiDotNetCore.Configuration;
44
using JsonApiDotNetCore.Extensions;
5+
using JsonApiDotNetCore.Routing.Query;
56
using Microsoft.AspNetCore.Http;
67

78
namespace JsonApiDotNetCore.Routing
@@ -21,15 +22,17 @@ public Route BuildFromRequest(HttpRequest request)
2122
{
2223
var remainingPathString = SetBaseRouteDefinition(request.Path);
2324

25+
var querySet = new QuerySet(request.Query);
26+
2427
if (PathStringIsEmpty(remainingPathString))
2528
{ // {baseResource}
26-
return new Route(_baseRouteDefinition.ModelType, request.Method, null, _baseRouteDefinition);
29+
return new Route(_baseRouteDefinition.ModelType, request.Method, null, _baseRouteDefinition, querySet);
2730
}
2831

2932
remainingPathString = SetBaseResourceId(remainingPathString);
3033
if (PathStringIsEmpty(remainingPathString))
3134
{ // {baseResource}/{baseResourceId}
32-
return new Route(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition);
35+
return new Route(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition, querySet);
3336
}
3437

3538
// { baseResource}/{ baseResourceId}/{relatedResourceName}
@@ -41,7 +44,7 @@ public Route BuildFromRequest(HttpRequest request)
4144
}
4245

4346
var relationshipType = GetTypeOfRelatedResource(relatedResource);
44-
return new RelationalRoute(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition, relationshipType, relatedResource);
47+
return new RelationalRoute(_baseRouteDefinition.ModelType, request.Method, _baseResourceId, _baseRouteDefinition, querySet, relationshipType, relatedResource);
4548
}
4649

4750
private bool PathStringIsEmpty(PathString pathString)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using Xunit;
2+
using JsonApiDotNetCore.Routing.Query;
3+
using Microsoft.AspNetCore.Http.Internal;
4+
using System.Collections.Generic;
5+
using Microsoft.Extensions.Primitives;
6+
using System.Linq;
7+
8+
namespace JsonApiDotNetCoreTests.Routing.UnitTests.Query
9+
{
10+
public class QuerySetTests
11+
{
12+
[Fact]
13+
public void QuerySetConstructor_BuildsObject_FromQueryCollection()
14+
{
15+
// arrange
16+
var queries = new Dictionary<string, StringValues>();
17+
queries.Add("filter[id]", "1");
18+
queries.Add("sort", new StringValues(new string[] { "-id", "name" }));
19+
var queryCollection = new QueryCollection(queries);
20+
21+
// act
22+
var querySet = new QuerySet(queryCollection);
23+
24+
// assert
25+
Assert.NotNull(querySet.Filter);
26+
Assert.Equal("Id", querySet.Filter.PropertyName);
27+
Assert.Equal("1", querySet.Filter.PropertyValue);
28+
29+
Assert.NotNull(querySet.SortParameters);
30+
Assert.NotNull(querySet.SortParameters.SingleOrDefault(x=> x.Direction == SortDirection.Descending && x.PropertyName == "Id"));
31+
Assert.NotNull(querySet.SortParameters.SingleOrDefault(x=> x.Direction == SortDirection.Ascending && x.PropertyName == "Name"));
32+
}
33+
}
34+
}

JsonApiDotNetCoreTests/Routing/UnitTests/RouterTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void HandleJsonApiRoute_CallsGetMethod_ForGetRequest()
4646
httpResponseMock.Setup(r => r.Body).Returns(new MemoryStream());
4747
httpContextMock.Setup(c => c.Response).Returns(httpResponseMock.Object);
4848

49-
var route = new Route(null, "GET", null, null);
49+
var route = new Route(null, "GET", null, null, null);
5050

5151
var routeBuilderMock = new Mock<IRouteBuilder>();
5252
routeBuilderMock.Setup(rb => rb.BuildFromRequest(null)).Returns(route);
@@ -79,7 +79,7 @@ public void HandleJsonApiRoute_CallsGetIdMethod_ForGetIdRequest()
7979
httpResponseMock.Setup(r => r.Body).Returns(new MemoryStream());
8080
httpContextMock.Setup(c => c.Response).Returns(httpResponseMock.Object);
8181

82-
var route = new Route(null, "GET", resourceId, null);
82+
var route = new Route(null, "GET", resourceId, null, null);
8383

8484
var routeBuilderMock = new Mock<IRouteBuilder>();
8585
routeBuilderMock.Setup(rb => rb.BuildFromRequest(null)).Returns(route);

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,10 @@ You can access the HttpContext from [IJsonApiContext](https://github.com/Researc
118118
## References
119119
[JsonApi Specification](http://jsonapi.org/)
120120

121-
## Current Assumptions
121+
## Current Entity Requirements
122122

123123
- Using Entity Framework
124124
- All entities in the specified context should have controllers
125125
- All entities are served from the same namespace (i.e. 'api/v1')
126126
- All entities have a primary key "Id" and not "EntityId"
127+
- All entity names are proper case, "Id" not "id"

0 commit comments

Comments
 (0)