Skip to content

Commit e37d4a3

Browse files
authored
Merge pull request #90 from Research-Institute/feat/filter-on-rel-attrs
Feat/filter on rel attrs
2 parents c4b3361 + 5ade61a commit e37d4a3

File tree

9 files changed

+237
-88
lines changed

9 files changed

+237
-88
lines changed

Diff for: src/JsonApiDotNetCore/Data/DefaultEntityRepository.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ public virtual IQueryable<TEntity> Filter(IQueryable<TEntity> entities, FilterQ
5656
if(filterQuery == null)
5757
return entities;
5858

59-
var attributeFilterQuery = new AttrFilterQuery(_jsonApiContext, filterQuery);
60-
61-
return entities
62-
.Filter(attributeFilterQuery);
59+
if(filterQuery.IsAttributeOfRelationship)
60+
return entities.Filter(new RelatedAttrFilterQuery(_jsonApiContext, filterQuery));
61+
else
62+
return entities.Filter(new AttrFilterQuery(_jsonApiContext, filterQuery));
6363
}
6464

6565
public virtual IQueryable<TEntity> Sort(IQueryable<TEntity> entities, List<SortQuery> sortQueries)

Diff for: src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs

+89-36
Original file line numberDiff line numberDiff line change
@@ -86,36 +86,7 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
8686
// {1}
8787
var right = Expression.Constant(convertedValue, property.PropertyType);
8888

89-
Expression body;
90-
switch (filterQuery.FilterOperation)
91-
{
92-
case FilterOperations.eq:
93-
// {model.Id == 1}
94-
body = Expression.Equal(left, right);
95-
break;
96-
case FilterOperations.lt:
97-
// {model.Id < 1}
98-
body = Expression.LessThan(left, right);
99-
break;
100-
case FilterOperations.gt:
101-
// {model.Id > 1}
102-
body = Expression.GreaterThan(left, right);
103-
break;
104-
case FilterOperations.le:
105-
// {model.Id <= 1}
106-
body = Expression.LessThanOrEqual(left, right);
107-
break;
108-
case FilterOperations.ge:
109-
// {model.Id <= 1}
110-
body = Expression.GreaterThanOrEqual(left, right);
111-
break;
112-
case FilterOperations.like:
113-
// {model.Id <= 1}
114-
body = Expression.Call(left, "Contains", null, right);
115-
break;
116-
default:
117-
throw new JsonApiException("500", $"Unknown filter operation {filterQuery.FilterOperation}");
118-
}
89+
var body = GetFilterExpressionLambda(left, right, filterQuery.FilterOperation);
11990

12091
var lambda = Expression.Lambda<Func<TSource, bool>>(body, parameter);
12192

@@ -126,27 +97,109 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
12697
throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {property.PropertyType.Name}");
12798
}
12899
}
100+
101+
public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> source, RelatedAttrFilterQuery filterQuery)
102+
{
103+
if (filterQuery == null)
104+
return source;
105+
106+
var concreteType = typeof(TSource);
107+
var relation = concreteType.GetProperty(filterQuery.FilteredRelationship.InternalRelationshipName);
108+
if (relation == null)
109+
throw new ArgumentException($"'{filterQuery.FilteredRelationship.InternalRelationshipName}' is not a valid relationship of '{concreteType}'");
110+
111+
var relatedType = filterQuery.FilteredRelationship.Type;
112+
var relatedAttr = relatedType.GetProperty(filterQuery.FilteredAttribute.InternalAttributeName);
113+
if (relatedAttr == null)
114+
throw new ArgumentException($"'{filterQuery.FilteredAttribute.InternalAttributeName}' is not a valid attribute of '{filterQuery.FilteredRelationship.InternalRelationshipName}'");
115+
116+
try
117+
{
118+
// convert the incoming value to the target value type
119+
// "1" -> 1
120+
var convertedValue = TypeHelper.ConvertType(filterQuery.PropertyValue, relatedAttr.PropertyType);
121+
// {model}
122+
var parameter = Expression.Parameter(concreteType, "model");
123+
124+
// {model.Relationship}
125+
var leftRelationship = Expression.PropertyOrField(parameter, relation.Name);
126+
127+
// {model.Relationship.Attr}
128+
var left = Expression.PropertyOrField(leftRelationship, relatedAttr.Name);
129+
130+
// {1}
131+
var right = Expression.Constant(convertedValue, relatedAttr.PropertyType);
132+
133+
var body = GetFilterExpressionLambda(left, right, filterQuery.FilterOperation);
134+
135+
var lambda = Expression.Lambda<Func<TSource, bool>>(body, parameter);
136+
137+
return source.Where(lambda);
138+
}
139+
catch (FormatException)
140+
{
141+
throw new JsonApiException("400", $"Could not cast {filterQuery.PropertyValue} to {relatedAttr.PropertyType.Name}");
142+
}
143+
}
144+
145+
private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation)
146+
{
147+
Expression body;
148+
switch (operation)
149+
{
150+
case FilterOperations.eq:
151+
// {model.Id == 1}
152+
body = Expression.Equal(left, right);
153+
break;
154+
case FilterOperations.lt:
155+
// {model.Id < 1}
156+
body = Expression.LessThan(left, right);
157+
break;
158+
case FilterOperations.gt:
159+
// {model.Id > 1}
160+
body = Expression.GreaterThan(left, right);
161+
break;
162+
case FilterOperations.le:
163+
// {model.Id <= 1}
164+
body = Expression.LessThanOrEqual(left, right);
165+
break;
166+
case FilterOperations.ge:
167+
// {model.Id <= 1}
168+
body = Expression.GreaterThanOrEqual(left, right);
169+
break;
170+
case FilterOperations.like:
171+
// {model.Id <= 1}
172+
body = Expression.Call(left, "Contains", null, right);
173+
break;
174+
default:
175+
throw new JsonApiException("500", $"Unknown filter operation {operation}");
176+
}
177+
178+
return body;
179+
}
180+
181+
129182
public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, IEnumerable<string> columns)
130183
{
131-
if(columns == null || columns.Count() == 0)
184+
if (columns == null || columns.Count() == 0)
132185
return source;
133186

134187
var sourceType = source.ElementType;
135-
188+
136189
var resultType = typeof(TSource);
137190

138191
// {model}
139192
var parameter = Expression.Parameter(sourceType, "model");
140-
193+
141194
var bindings = columns.Select(column => Expression.Bind(
142195
resultType.GetProperty(column), Expression.PropertyOrField(parameter, column)));
143-
196+
144197
// { new Model () { Property = model.Property } }
145198
var body = Expression.MemberInit(Expression.New(resultType), bindings);
146-
199+
147200
// { model => new TodoItem() { Property = model.Property } }
148201
var selector = Expression.Lambda(body, parameter);
149-
202+
150203
return source.Provider.CreateQuery<TSource>(
151204
Expression.Call(typeof(Queryable), "Select", new Type[] { sourceType, resultType },
152205
source.Expression, Expression.Quote(selector)));
+30-42
Original file line numberDiff line numberDiff line change
@@ -1,51 +1,39 @@
1-
using System;
21
using System.Linq;
32
using JsonApiDotNetCore.Models;
43
using JsonApiDotNetCore.Services;
54

65
namespace JsonApiDotNetCore.Internal.Query
76
{
8-
public class AttrFilterQuery
9-
{
10-
private readonly IJsonApiContext _jsonApiContext;
11-
12-
public AttrFilterQuery(
13-
IJsonApiContext jsonApiCopntext,
14-
FilterQuery filterQuery)
15-
{
16-
_jsonApiContext = jsonApiCopntext;
17-
18-
var attribute = GetAttribute(filterQuery.Key);
19-
20-
if (attribute == null)
21-
throw new JsonApiException("400", $"{filterQuery.Key} is not a valid property.");
22-
23-
FilteredAttribute = attribute;
24-
PropertyValue = filterQuery.Value;
25-
FilterOperation = GetFilterOperation(filterQuery.Operation);
26-
}
27-
28-
public AttrAttribute FilteredAttribute { get; set; }
29-
public string PropertyValue { get; set; }
30-
public FilterOperations FilterOperation { get; set; }
31-
32-
private FilterOperations GetFilterOperation(string prefix)
33-
{
34-
if (prefix.Length == 0) return FilterOperations.eq;
35-
36-
FilterOperations opertion;
37-
if (!Enum.TryParse<FilterOperations>(prefix, out opertion))
38-
throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'");
39-
40-
return opertion;
41-
}
42-
43-
private AttrAttribute GetAttribute(string propertyName)
7+
public class AttrFilterQuery : BaseFilterQuery
448
{
45-
return _jsonApiContext.RequestEntity.Attributes
46-
.FirstOrDefault(attr =>
47-
attr.InternalAttributeName.ToLower() == propertyName.ToLower()
48-
);
9+
private readonly IJsonApiContext _jsonApiContext;
10+
11+
public AttrFilterQuery(
12+
IJsonApiContext jsonApiCopntext,
13+
FilterQuery filterQuery)
14+
{
15+
_jsonApiContext = jsonApiCopntext;
16+
17+
var attribute = GetAttribute(filterQuery.Key);
18+
19+
if (attribute == null)
20+
throw new JsonApiException("400", $"{filterQuery.Key} is not a valid property.");
21+
22+
FilteredAttribute = attribute;
23+
PropertyValue = filterQuery.Value;
24+
FilterOperation = GetFilterOperation(filterQuery.Operation);
25+
}
26+
27+
public AttrAttribute FilteredAttribute { get; set; }
28+
public string PropertyValue { get; set; }
29+
public FilterOperations FilterOperation { get; set; }
30+
31+
private AttrAttribute GetAttribute(string propertyName)
32+
{
33+
return _jsonApiContext.RequestEntity.Attributes
34+
.FirstOrDefault(attr =>
35+
attr.InternalAttributeName.ToLower() == propertyName.ToLower()
36+
);
37+
}
4938
}
50-
}
5139
}
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System;
2+
3+
namespace JsonApiDotNetCore.Internal.Query
4+
{
5+
public class BaseFilterQuery
6+
{
7+
protected FilterOperations GetFilterOperation(string prefix)
8+
{
9+
if (prefix.Length == 0) return FilterOperations.eq;
10+
11+
FilterOperations opertion;
12+
if (!Enum.TryParse<FilterOperations>(prefix, out opertion))
13+
throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'");
14+
15+
return opertion;
16+
}
17+
}
18+
}

Diff for: src/JsonApiDotNetCore/Internal/Query/FilterQuery.cs

+1
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,6 @@ public FilterQuery(string key, string value, string operation)
1212
public string Key { get; set; }
1313
public string Value { get; set; }
1414
public string Operation { get; set; }
15+
public bool IsAttributeOfRelationship => Key.Contains(".");
1516
}
1617
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
using System.Linq;
2+
using JsonApiDotNetCore.Models;
3+
using JsonApiDotNetCore.Services;
4+
5+
namespace JsonApiDotNetCore.Internal.Query
6+
{
7+
public class RelatedAttrFilterQuery : BaseFilterQuery
8+
{
9+
private readonly IJsonApiContext _jsonApiContext;
10+
11+
public RelatedAttrFilterQuery(
12+
IJsonApiContext jsonApiCopntext,
13+
FilterQuery filterQuery)
14+
{
15+
_jsonApiContext = jsonApiCopntext;
16+
17+
var relationshipArray = filterQuery.Key.Split('.');
18+
19+
var relationship = GetRelationship(relationshipArray[0]);
20+
if (relationship == null)
21+
throw new JsonApiException("400", $"{relationshipArray[0]} is not a valid relationship.");
22+
23+
var attribute = GetAttribute(relationship, relationshipArray[1]);
24+
if (attribute == null)
25+
throw new JsonApiException("400", $"{relationshipArray[1]} is not a valid attribute on {relationshipArray[0]}.");
26+
27+
FilteredRelationship = relationship;
28+
FilteredAttribute = attribute;
29+
PropertyValue = filterQuery.Value;
30+
FilterOperation = GetFilterOperation(filterQuery.Operation);
31+
}
32+
33+
public AttrAttribute FilteredAttribute { get; set; }
34+
public string PropertyValue { get; set; }
35+
public FilterOperations FilterOperation { get; set; }
36+
public RelationshipAttribute FilteredRelationship { get; private set; }
37+
38+
private RelationshipAttribute GetRelationship(string propertyName)
39+
{
40+
return _jsonApiContext.RequestEntity.Relationships
41+
.FirstOrDefault(r => r.InternalRelationshipName.ToLower() == propertyName.ToLower());
42+
}
43+
44+
private AttrAttribute GetAttribute(RelationshipAttribute relationship, string attribute)
45+
{
46+
var relatedContextExntity = _jsonApiContext.ContextGraph.GetContextEntity(relationship.Type);
47+
return relatedContextExntity.Attributes
48+
.FirstOrDefault(a => a.InternalAttributeName.ToLower() == attribute.ToLower());
49+
}
50+
}
51+
}

Diff for: src/JsonApiDotNetCore/JsonApiDotNetCore.csproj

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<VersionPrefix>1.3.1</VersionPrefix>
3+
<VersionPrefix>1.3.2</VersionPrefix>
44
<TargetFramework>netcoreapp1.0</TargetFramework>
55
<AssemblyName>JsonApiDotNetCore</AssemblyName>
66
<PackageId>JsonApiDotNetCore</PackageId>

Diff for: src/JsonApiDotNetCoreExample/Models/Person.cs

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System.Collections.Generic;
2-
using JsonApiDotNetCore.Internal;
32
using JsonApiDotNetCore.Models;
43
using JsonApiDotNetCore.Services;
54

0 commit comments

Comments
 (0)