Skip to content

Commit 87ec1a6

Browse files
committed
feat(IQueryableExt): allow for filtering on attribute of included rel
1 parent 1cc467d commit 87ec1a6

File tree

1 file changed

+89
-36
lines changed

1 file changed

+89
-36
lines changed

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)));

0 commit comments

Comments
 (0)