Skip to content

Commit 74667eb

Browse files
authored
Merge pull request #361 from milosloub/master
Not In Filter
2 parents 13404ab + b515283 commit 74667eb

File tree

4 files changed

+60
-8
lines changed

4 files changed

+60
-8
lines changed

src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs

+17-7
Original file line numberDiff line numberDiff line change
@@ -119,10 +119,10 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
119119

120120
try
121121
{
122-
if (filterQuery.FilterOperation == FilterOperations.@in )
122+
if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin)
123123
{
124124
string[] propertyValues = filterQuery.PropertyValue.Split(',');
125-
var lambdaIn = ArrayContainsPredicate<TSource>(propertyValues, property.Name);
125+
var lambdaIn = ArrayContainsPredicate<TSource>(propertyValues, property.Name, filterQuery.FilterOperation);
126126

127127
return source.Where(lambdaIn);
128128
}
@@ -167,10 +167,10 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
167167

168168
try
169169
{
170-
if (filterQuery.FilterOperation == FilterOperations.@in)
170+
if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin)
171171
{
172172
string[] propertyValues = filterQuery.PropertyValue.Split(',');
173-
var lambdaIn = ArrayContainsPredicate<TSource>(propertyValues, relatedAttr.Name, relation.Name);
173+
var lambdaIn = ArrayContainsPredicate<TSource>(propertyValues, relatedAttr.Name, filterQuery.FilterOperation, relation.Name);
174174

175175
return source.Where(lambdaIn);
176176
}
@@ -243,7 +243,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression
243243
return body;
244244
}
245245

246-
private static Expression<Func<TSource, bool>> ArrayContainsPredicate<TSource>(string[] propertyValues, string fieldname, string relationName = null)
246+
private static Expression<Func<TSource, bool>> ArrayContainsPredicate<TSource>(string[] propertyValues, string fieldname, FilterOperations op, string relationName = null)
247247
{
248248
ParameterExpression entity = Expression.Parameter(typeof(TSource), "entity");
249249
MemberExpression member;
@@ -258,8 +258,18 @@ private static Expression<Func<TSource, bool>> ArrayContainsPredicate<TSource>(s
258258
var method = ContainsMethod.MakeGenericMethod(member.Type);
259259
var obj = TypeHelper.ConvertListType(propertyValues, member.Type);
260260

261-
var exprContains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member });
262-
return Expression.Lambda<Func<TSource, bool>>(exprContains, entity);
261+
if (op == FilterOperations.@in)
262+
{
263+
// Where(i => arr.Contains(i.column))
264+
var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member });
265+
return Expression.Lambda<Func<TSource, bool>>(contains, entity);
266+
}
267+
else
268+
{
269+
// Where(i => !arr.Contains(i.column))
270+
var notContains = Expression.Not(Expression.Call(method, new Expression[] { Expression.Constant(obj), member }));
271+
return Expression.Lambda<Func<TSource, bool>>(notContains, entity);
272+
}
263273
}
264274

265275
public static IQueryable<TSource> Select<TSource>(this IQueryable<TSource> source, List<string> columns)

src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs

+1
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ public enum FilterOperations
1111
like = 5,
1212
ne = 6,
1313
@in = 7, // prefix with @ to use keyword
14+
nin = 8
1415
}
1516
}

src/JsonApiDotNetCore/Services/QueryParser.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@ protected virtual List<FilterQuery> ParseFilterQuery(string key, string value)
8686

8787
// InArray case
8888
string op = GetFilterOperation(value);
89-
if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase))
89+
if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase)
90+
|| string.Equals(op, FilterOperations.nin.ToString(), StringComparison.OrdinalIgnoreCase))
9091
{
9192
(var operation, var filterValue) = ParseFilterOperation(value);
9293
queries.Add(new FilterQuery(propertyName, filterValue, op));

test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs

+40
Original file line numberDiff line numberDiff line change
@@ -209,5 +209,45 @@ public async Task Can_Filter_On_Related_In_Array_Values()
209209
Assert.Contains(item.Attributes["first-name"], ownerFirstNames);
210210

211211
}
212+
213+
[Fact]
214+
public async Task Can_Filter_On_Not_In_Array_Values()
215+
{
216+
// arrange
217+
var context = _fixture.GetService<AppDbContext>();
218+
var todoItems = _todoItemFaker.Generate(5);
219+
var guids = new List<Guid>();
220+
var notInGuids = new List<Guid>();
221+
foreach (var item in todoItems)
222+
{
223+
context.TodoItems.Add(item);
224+
// Exclude 2 items
225+
if (guids.Count < (todoItems.Count() - 2))
226+
guids.Add(item.GuidProperty);
227+
else
228+
notInGuids.Add(item.GuidProperty);
229+
}
230+
context.SaveChanges();
231+
232+
var totalCount = context.TodoItems.Count();
233+
var httpMethod = new HttpMethod("GET");
234+
var route = $"/api/v1/todo-items?page[size]={totalCount}&filter[guid-property]=nin:{string.Join(",", notInGuids)}";
235+
var request = new HttpRequestMessage(httpMethod, route);
236+
237+
// act
238+
var response = await _fixture.Client.SendAsync(request);
239+
var body = await response.Content.ReadAsStringAsync();
240+
var deserializedTodoItems = _fixture
241+
.GetService<IJsonApiDeSerializer>()
242+
.DeserializeList<TodoItem>(body);
243+
244+
// assert
245+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
246+
Assert.Equal(totalCount - notInGuids.Count(), deserializedTodoItems.Count());
247+
foreach (var item in deserializedTodoItems)
248+
{
249+
Assert.DoesNotContain(item.GuidProperty, notInGuids);
250+
}
251+
}
212252
}
213253
}

0 commit comments

Comments
 (0)