diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 87de9d187d..55bb80b740 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -170,11 +170,29 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression break; case FilterOperations.isnotnull: // {model.Id != null} - body = Expression.NotEqual(left, right); + if (left.Type.IsValueType && + !(left.Type.IsGenericType && left.Type.GetGenericTypeDefinition() == typeof(Nullable<>))) + { + var nullableType = typeof(Nullable<>).MakeGenericType(left.Type); + body = Expression.NotEqual(Expression.Convert(left, nullableType), right); + } + else + { + body = Expression.NotEqual(left, right); + } break; case FilterOperations.isnull: // {model.Id == null} - body = Expression.Equal(left, right); + if (left.Type.IsValueType && + !(left.Type.IsGenericType && left.Type.GetGenericTypeDefinition() == typeof(Nullable<>))) + { + var nullableType = typeof(Nullable<>).MakeGenericType(left.Type); + body = Expression.Equal(Expression.Convert(left, nullableType), right); + } + else + { + body = Expression.Equal(left, right); + } break; default: throw new JsonApiException(500, $"Unknown filter operation {operation}"); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 7ce1604683..9989a7f22b 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -175,6 +175,36 @@ public async Task Can_Filter_TodoItems_Using_IsNotNull_Operator() Assert.All(todoItems, t => Assert.NotNull(t.UpdatedDate)); } + [Fact] + public async Task Can_Filter_TodoItems_ByParent_Using_IsNotNull_Operator() + { + // Arrange + var todoItem = _todoItemFaker.Generate(); + todoItem.Assignee = new Person(); + + var otherTodoItem = _todoItemFaker.Generate(); + otherTodoItem.Assignee = null; + + _context.TodoItems.AddRange(new[] { todoItem, otherTodoItem }); + _context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?filter[assignee.id]=isnotnull:"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var todoItems = _fixture.GetService().DeserializeList(body); + + // Assert + Assert.NotEmpty(todoItems); + Assert.All(todoItems, t => Assert.NotNull(t.Assignee)); + } + [Fact] public async Task Can_Filter_TodoItems_Using_IsNull_Operator() { @@ -205,6 +235,36 @@ public async Task Can_Filter_TodoItems_Using_IsNull_Operator() Assert.All(todoItems, t => Assert.Null(t.UpdatedDate)); } + [Fact] + public async Task Can_Filter_TodoItems_ByParent_Using_IsNull_Operator() + { + // Arrange + var todoItem = _todoItemFaker.Generate(); + todoItem.Assignee = null; + + var otherTodoItem = _todoItemFaker.Generate(); + otherTodoItem.Assignee = new Person(); + + _context.TodoItems.AddRange(new[] { todoItem, otherTodoItem }); + _context.SaveChanges(); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?filter[assignee.id]=isnull:"; + var request = new HttpRequestMessage(httpMethod, route); + + // Act + var response = await _fixture.Client.SendAsync(request); + + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + var body = await response.Content.ReadAsStringAsync(); + var todoItems = _fixture.GetService().DeserializeList(body); + + // Assert + Assert.NotEmpty(todoItems); + Assert.All(todoItems, t => Assert.Null(t.Assignee)); + } + [Fact] public async Task Can_Filter_TodoItems_Using_Like_Operator() {