diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 994fc08070..03f74b9e59 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -119,10 +119,10 @@ public static IQueryable Filter(this IQueryable sourc try { - if (filterQuery.FilterOperation == FilterOperations.@in ) + if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) { string[] propertyValues = filterQuery.PropertyValue.Split(','); - var lambdaIn = ArrayContainsPredicate(propertyValues, property.Name); + var lambdaIn = ArrayContainsPredicate(propertyValues, property.Name, filterQuery.FilterOperation); return source.Where(lambdaIn); } @@ -167,10 +167,10 @@ public static IQueryable Filter(this IQueryable sourc try { - if (filterQuery.FilterOperation == FilterOperations.@in) + if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin) { string[] propertyValues = filterQuery.PropertyValue.Split(','); - var lambdaIn = ArrayContainsPredicate(propertyValues, relatedAttr.Name, relation.Name); + var lambdaIn = ArrayContainsPredicate(propertyValues, relatedAttr.Name, filterQuery.FilterOperation, relation.Name); return source.Where(lambdaIn); } @@ -243,7 +243,7 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression return body; } - private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, string relationName = null) + private static Expression> ArrayContainsPredicate(string[] propertyValues, string fieldname, FilterOperations op, string relationName = null) { ParameterExpression entity = Expression.Parameter(typeof(TSource), "entity"); MemberExpression member; @@ -258,8 +258,18 @@ private static Expression> ArrayContainsPredicate(s var method = ContainsMethod.MakeGenericMethod(member.Type); var obj = TypeHelper.ConvertListType(propertyValues, member.Type); - var exprContains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); - return Expression.Lambda>(exprContains, entity); + if (op == FilterOperations.@in) + { + // Where(i => arr.Contains(i.column)) + var contains = Expression.Call(method, new Expression[] { Expression.Constant(obj), member }); + return Expression.Lambda>(contains, entity); + } + else + { + // Where(i => !arr.Contains(i.column)) + var notContains = Expression.Not(Expression.Call(method, new Expression[] { Expression.Constant(obj), member })); + return Expression.Lambda>(notContains, entity); + } } public static IQueryable Select(this IQueryable source, List columns) diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index 88a2da2ee8..554e6f98a4 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -11,5 +11,6 @@ public enum FilterOperations like = 5, ne = 6, @in = 7, // prefix with @ to use keyword + nin = 8 } } diff --git a/src/JsonApiDotNetCore/Services/QueryParser.cs b/src/JsonApiDotNetCore/Services/QueryParser.cs index 7e17352815..0a6d4f2c16 100644 --- a/src/JsonApiDotNetCore/Services/QueryParser.cs +++ b/src/JsonApiDotNetCore/Services/QueryParser.cs @@ -86,7 +86,8 @@ protected virtual List ParseFilterQuery(string key, string value) // InArray case string op = GetFilterOperation(value); - if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase)) + if (string.Equals(op, FilterOperations.@in.ToString(), StringComparison.OrdinalIgnoreCase) + || string.Equals(op, FilterOperations.nin.ToString(), StringComparison.OrdinalIgnoreCase)) { (var operation, var filterValue) = ParseFilterOperation(value); queries.Add(new FilterQuery(propertyName, filterValue, op)); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs index 9e154f9b47..591dfa4c7f 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/Spec/AttributeFilterTests.cs @@ -209,5 +209,45 @@ public async Task Can_Filter_On_Related_In_Array_Values() Assert.Contains(item.Attributes["first-name"], ownerFirstNames); } + + [Fact] + public async Task Can_Filter_On_Not_In_Array_Values() + { + // arrange + var context = _fixture.GetService(); + var todoItems = _todoItemFaker.Generate(5); + var guids = new List(); + var notInGuids = new List(); + foreach (var item in todoItems) + { + context.TodoItems.Add(item); + // Exclude 2 items + if (guids.Count < (todoItems.Count() - 2)) + guids.Add(item.GuidProperty); + else + notInGuids.Add(item.GuidProperty); + } + context.SaveChanges(); + + var totalCount = context.TodoItems.Count(); + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?page[size]={totalCount}&filter[guid-property]=nin:{string.Join(",", notInGuids)}"; + var request = new HttpRequestMessage(httpMethod, route); + + // act + var response = await _fixture.Client.SendAsync(request); + var body = await response.Content.ReadAsStringAsync(); + var deserializedTodoItems = _fixture + .GetService() + .DeserializeList(body); + + // assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.Equal(totalCount - notInGuids.Count(), deserializedTodoItems.Count()); + foreach (var item in deserializedTodoItems) + { + Assert.DoesNotContain(item.GuidProperty, notInGuids); + } + } } }