Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit c1bf919

Browse files
Merge pull request #387 from NullVoxPopuli/additional-filter-operations
Additional filter operations: isnull and isnotnull
2 parents df664ab + edd0f22 commit c1bf919

File tree

5 files changed

+133
-4
lines changed

5 files changed

+133
-4
lines changed

‎README.md‎

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,39 @@ public class Startup
7575
}
7676
}
7777
```
78+
79+
### Development
80+
81+
Restore all nuget packages with:
82+
83+
```bash
84+
dotnet restore
85+
```
86+
87+
#### Testing
88+
89+
Running tests locally requires access to a postgresql database.
90+
If you have docker installed, this can be propped up via:
91+
92+
```bash
93+
docker run --rm --name jsonapi-dotnet-core-testing \
94+
-e POSTGRES_DB=JsonApiDotNetCoreExample \
95+
-e POSTGRES_USER=postgres \
96+
-e POSTGRES_PASSWORD=postgres \
97+
-p 5432:5432 \
98+
postgres
99+
```
100+
101+
And then to run the tests:
102+
103+
```bash
104+
dotnet test
105+
```
106+
107+
#### Cleaning
108+
109+
Sometimes the compiled files can be dirty / corrupt from other branches / failed builds.
110+
111+
```bash
112+
dotnet clean
113+
```

‎src/Examples/JsonApiDotNetCoreExample/Models/TodoItem.cs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ public TodoItem()
2424

2525
[Attr("achieved-date", isFilterable: false, isSortable: false)]
2626
public DateTime? AchievedDate { get; set; }
27+
28+
29+
[Attr("updated-date")]
30+
public DateTime? UpdatedDate { get; set; }
31+
32+
2733

2834
public int? OwnerId { get; set; }
2935
public int? AssigneeId { get; set; }

‎src/JsonApiDotNetCore/Extensions/IQueryableExtensions.cs‎

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -113,19 +113,32 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
113113

114114
var concreteType = typeof(TSource);
115115
var property = concreteType.GetProperty(filterQuery.FilteredAttribute.InternalAttributeName);
116+
var op = filterQuery.FilterOperation;
116117

117118
if (property == null)
118119
throw new ArgumentException($"'{filterQuery.FilteredAttribute.InternalAttributeName}' is not a valid property of '{concreteType}'");
119120

120121
try
121122
{
122-
if (filterQuery.FilterOperation == FilterOperations.@in || filterQuery.FilterOperation == FilterOperations.nin)
123+
if (op == FilterOperations.@in || op == FilterOperations.nin)
123124
{
124125
string[] propertyValues = filterQuery.PropertyValue.Split(',');
125-
var lambdaIn = ArrayContainsPredicate<TSource>(propertyValues, property.Name, filterQuery.FilterOperation);
126+
var lambdaIn = ArrayContainsPredicate<TSource>(propertyValues, property.Name, op);
126127

127128
return source.Where(lambdaIn);
128129
}
130+
else if (op == FilterOperations.isnotnull || op == FilterOperations.isnull) {
131+
// {model}
132+
var parameter = Expression.Parameter(concreteType, "model");
133+
// {model.Id}
134+
var left = Expression.PropertyOrField(parameter, property.Name);
135+
var right = Expression.Constant(null);
136+
137+
var body = GetFilterExpressionLambda(left, right, op);
138+
var lambda = Expression.Lambda<Func<TSource, bool>>(body, parameter);
139+
140+
return source.Where(lambda);
141+
}
129142
else
130143
{ // convert the incoming value to the target value type
131144
// "1" -> 1
@@ -137,7 +150,7 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
137150
// {1}
138151
var right = Expression.Constant(convertedValue, property.PropertyType);
139152

140-
var body = GetFilterExpressionLambda(left, right, filterQuery.FilterOperation);
153+
var body = GetFilterExpressionLambda(left, right, op);
141154

142155
var lambda = Expression.Lambda<Func<TSource, bool>>(body, parameter);
143156

@@ -204,6 +217,9 @@ public static IQueryable<TSource> Filter<TSource>(this IQueryable<TSource> sourc
204217
}
205218
}
206219

220+
private static bool IsNullable(Type type) => type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
221+
222+
207223
private static Expression GetFilterExpressionLambda(Expression left, Expression right, FilterOperations operation)
208224
{
209225
Expression body;
@@ -236,6 +252,14 @@ private static Expression GetFilterExpressionLambda(Expression left, Expression
236252
case FilterOperations.ne:
237253
body = Expression.NotEqual(left, right);
238254
break;
255+
case FilterOperations.isnotnull:
256+
// {model.Id != null}
257+
body = Expression.NotEqual(left, right);
258+
break;
259+
case FilterOperations.isnull:
260+
// {model.Id == null}
261+
body = Expression.Equal(left, right);
262+
break;
239263
default:
240264
throw new JsonApiException(500, $"Unknown filter operation {operation}");
241265
}

‎src/JsonApiDotNetCore/Internal/Query/FilterOperations.cs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ public enum FilterOperations
1111
like = 5,
1212
ne = 6,
1313
@in = 7, // prefix with @ to use keyword
14-
nin = 8
14+
nin = 8,
15+
isnull = 9,
16+
isnotnull = 10
1517
}
1618
}

‎test/JsonApiDotNetCoreExampleTests/Acceptance/TodoItemsControllerTests.cs‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
using System;
12
using System.Collections.Generic;
23
using System.Linq;
34
using System.Net;
@@ -90,6 +91,66 @@ public async Task Can_Filter_TodoItems()
9091
Assert.Equal(todoItem.Ordinal, todoItemResult.Ordinal);
9192
}
9293

94+
[Fact]
95+
public async Task Can_Filter_TodoItems_Using_IsNotNull_Operator()
96+
{
97+
// Arrange
98+
var todoItem = _todoItemFaker.Generate();
99+
todoItem.UpdatedDate = new DateTime();
100+
101+
var otherTodoItem = _todoItemFaker.Generate();
102+
otherTodoItem.UpdatedDate = null;
103+
104+
_context.TodoItems.AddRange(new[] { todoItem, otherTodoItem });
105+
_context.SaveChanges();
106+
107+
var httpMethod = new HttpMethod("GET");
108+
var route = $"/api/v1/todo-items?filter[updated-date]=isnotnull:";
109+
var request = new HttpRequestMessage(httpMethod, route);
110+
111+
// Act
112+
var response = await _fixture.Client.SendAsync(request);
113+
114+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
115+
116+
var body = await response.Content.ReadAsStringAsync();
117+
var todoItems = _fixture.GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
118+
119+
// Assert
120+
Assert.NotEmpty(todoItems);
121+
Assert.All(todoItems, t => Assert.NotNull(t.UpdatedDate));
122+
}
123+
124+
[Fact]
125+
public async Task Can_Filter_TodoItems_Using_IsNull_Operator()
126+
{
127+
// Arrange
128+
var todoItem = _todoItemFaker.Generate();
129+
todoItem.UpdatedDate = null;
130+
131+
var otherTodoItem = _todoItemFaker.Generate();
132+
otherTodoItem.UpdatedDate = new DateTime();
133+
134+
_context.TodoItems.AddRange(new[] { todoItem, otherTodoItem });
135+
_context.SaveChanges();
136+
137+
var httpMethod = new HttpMethod("GET");
138+
var route = $"/api/v1/todo-items?filter[updated-date]=isnull:";
139+
var request = new HttpRequestMessage(httpMethod, route);
140+
141+
// Act
142+
var response = await _fixture.Client.SendAsync(request);
143+
144+
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
145+
146+
var body = await response.Content.ReadAsStringAsync();
147+
var todoItems = _fixture.GetService<IJsonApiDeSerializer>().DeserializeList<TodoItem>(body);
148+
149+
// Assert
150+
Assert.NotEmpty(todoItems);
151+
Assert.All(todoItems, t => Assert.Null(t.UpdatedDate));
152+
}
153+
93154
[Fact]
94155
public async Task Can_Filter_TodoItems_Using_Like_Operator()
95156
{

0 commit comments

Comments
(0)

AltStyle によって変換されたページ (->オリジナル) /