diff --git a/README.md b/README.md index 6f8c350398..6e8d91c23e 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,7 @@ identifier): ?filter[attribute]=gt:value ?filter[attribute]=le:value ?filter[attribute]=ge:value +?filter[attribute]=like:value ``` ### Sorting diff --git a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs index 904d2c9fe2..2e2551603c 100644 --- a/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs +++ b/src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs @@ -86,7 +86,7 @@ public static IQueryable Filter(this IQueryable sourc // {1} var right = Expression.Constant(convertedValue, property.PropertyType); - var body = Expression.Equal(left, right); + Expression body; switch (filterQuery.FilterOperation) { case FilterOperations.eq: @@ -109,6 +109,12 @@ public static IQueryable Filter(this IQueryable sourc // {model.Id <= 1} body = Expression.GreaterThanOrEqual(left, right); break; + case FilterOperations.like: + // {model.Id <= 1} + body = Expression.Call(left, "Contains", null, right); + break; + default: + throw new JsonApiException("500", $"Unknown filter operation {filterQuery.FilterOperation}"); } var lambda = Expression.Lambda>(body, parameter); diff --git a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs index cad391073f..cc6166e422 100644 --- a/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs +++ b/src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs @@ -6,6 +6,7 @@ public enum FilterOperations lt = 1, gt = 2, le = 3, - ge = 4 + ge = 4, + like = 5 } } \ No newline at end of file diff --git a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs index ed31fd7969..864251e4a4 100644 --- a/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs +++ b/src/JsonApiDotNetCore/Internal/Query/QuerySet.cs @@ -83,26 +83,29 @@ private FilterQuery ParseFilterOperation(AttrAttribute attribute, string value) if(value.Length < 3) return new FilterQuery(attribute, value, FilterOperations.eq); - var prefix = value.Substring(0, 3); + var operation = value.Split(':'); - if(prefix[2] != ':') + if(operation.Length == 1) return new FilterQuery(attribute, value, FilterOperations.eq); // remove prefix from value - value = value.Substring(3, value.Length - 3); + var prefix = operation[0]; + value = operation[1]; switch(prefix) { - case "eq:": + case "eq": return new FilterQuery(attribute, value, FilterOperations.eq); - case "lt:": + case "lt": return new FilterQuery(attribute, value, FilterOperations.lt); - case "gt:": + case "gt": return new FilterQuery(attribute, value, FilterOperations.gt); - case "le:": + case "le": return new FilterQuery(attribute, value, FilterOperations.le); - case "ge:": + case "ge": return new FilterQuery(attribute, value, FilterOperations.ge); + case "like": + return new FilterQuery(attribute, value, FilterOperations.like); } throw new JsonApiException("400", $"Invalid filter prefix '{prefix}'"); diff --git a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs index 0c137a87bc..a6d234f3cd 100644 --- a/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs +++ b/test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs @@ -124,6 +124,36 @@ public async Task Can_Filter_TodoItems() Assert.Equal(todoItem.Ordinal, todoItemResult.Ordinal); } + [Fact] + public async Task Can_Filter_TodoItems_Using_Like_Operator() + { + // Arrange + var todoItem = _todoItemFaker.Generate(); + todoItem.Ordinal = 999999; + _context.TodoItems.Add(todoItem); + _context.SaveChanges(); + var substring = todoItem.Description.Substring(1, todoItem.Description.Length - 2); + + var httpMethod = new HttpMethod("GET"); + var route = $"/api/v1/todo-items?filter[description]=like:{substring}"; + + var description = new RequestProperties("Filter TodoItems Where Attribute Like", new Dictionary { + { "?filter[...]=", "Filter on attribute" } + }); + + // Act + var response = await _fixture.MakeRequest(description, httpMethod, route); + var body = await response.Content.ReadAsStringAsync(); + var deserializedBody = JsonApiDeSerializer.DeserializeList(body, _jsonApiContext); + + // Assert + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + Assert.NotEmpty(deserializedBody); + + foreach (var todoItemResult in deserializedBody) + Assert.Contains(substring, todoItem.Description); + } + [Fact] public async Task Can_Sort_TodoItems_By_Ordinal_Ascending() {