Skip to content

Commit 903d57f

Browse files
committed
Move from QueryFilter parameters to a more generic IPosgrestQueryFilter to support constructing new QueryFilters from a LINQ expression.
1 parent e00a2e0 commit 903d57f

File tree

6 files changed

+109
-56
lines changed

6 files changed

+109
-56
lines changed

Postgrest/Interfaces/IPostgrestTable.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ namespace Postgrest.Interfaces
3535
/// </summary>
3636
/// <param name="filters"></param>
3737
/// <returns></returns>
38-
Table<TModel> And(List<QueryFilter> filters);
38+
Table<TModel> And(List<IPostgrestQueryFilter> filters);
3939

4040
/// <summary>
4141
/// Clears currently defined query values.
@@ -166,7 +166,7 @@ Task<ModeledResponse<TModel>> Insert(TModel model, QueryOptions? options = null,
166166
/// </summary>
167167
/// <param name="filter"></param>
168168
/// <returns></returns>
169-
Table<TModel> Not(QueryFilter filter);
169+
Table<TModel> Not(IPostgrestQueryFilter filter);
170170

171171
/// <summary>
172172
/// Adds a NOT filter to the current query args.
@@ -270,7 +270,7 @@ Table<TModel> Not<TCriterion>(Expression<Func<TModel, object>> predicate, Consta
270270
/// </summary>
271271
/// <param name="filters"></param>
272272
/// <returns></returns>
273-
Table<TModel> Or(List<QueryFilter> filters);
273+
Table<TModel> Or(List<IPostgrestQueryFilter> filters);
274274

275275
/// <summary>
276276
/// Adds an ordering to the current query args.

Postgrest/Linq/WhereExpressionVisitor.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ protected override Expression VisitBinary(BinaryExpression node)
4949
var rightVisitor = new WhereExpressionVisitor();
5050
rightVisitor.Visit(node.Right);
5151

52-
Filter = new QueryFilter(op, new List<QueryFilter>() { leftVisitor.Filter!, rightVisitor.Filter! });
52+
Filter = new QueryFilter(op,
53+
new List<IPostgrestQueryFilter> { leftVisitor.Filter!, rightVisitor.Filter! });
5354

5455
return node;
5556
}

Postgrest/QueryFilter.cs

Lines changed: 64 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,58 @@
1-
using System.Collections;
1+
using System;
2+
using System.Collections;
23
using System.Collections.Generic;
3-
using System.ComponentModel;
4+
using System.Globalization;
5+
using System.Linq;
6+
using System.Linq.Expressions;
47
using Newtonsoft.Json;
58
using Postgrest.Exceptions;
69
using Postgrest.Interfaces;
10+
using Postgrest.Linq;
711
using static Postgrest.Constants;
812

913
namespace Postgrest
1014
{
15+
/// <summary>
16+
/// Allow for the expression of a query filter with linq expressions.
17+
/// </summary>
18+
/// <typeparam name="TModel"></typeparam>
19+
/// <typeparam name="TCriterion"></typeparam>
20+
public class QueryFilter<TModel, TCriterion> : IPostgrestQueryFilter
21+
{
22+
/// <inheritdoc />
23+
public object? Criteria { get; }
24+
25+
/// <inheritdoc />
26+
public Operator Op { get; }
27+
28+
/// <inheritdoc />
29+
public string? Property { get; }
30+
31+
/// <summary>
32+
/// Allows the creation of a Query Filter using a LINQ expression.
33+
/// </summary>
34+
/// <param name="predicate"></param>
35+
/// <param name="op"></param>
36+
/// <param name="criterion"></param>
37+
/// <exception cref="ArgumentException"></exception>
38+
public QueryFilter(Expression<Func<TModel, object>> predicate, Operator op, TCriterion? criterion)
39+
{
40+
var visitor = new SelectExpressionVisitor();
41+
visitor.Visit(predicate);
42+
43+
if (visitor.Columns.Count == 0)
44+
throw new ArgumentException("Expected predicate to return a reference to a Model column.");
45+
46+
if (visitor.Columns.Count > 1)
47+
throw new ArgumentException("Only one column should be returned from the predicate.");
48+
49+
var filter = new QueryFilter(visitor.Columns.First(), op, criterion);
50+
51+
Criteria = filter.Criteria;
52+
Op = filter.Op;
53+
Property = filter.Property;
54+
}
55+
}
1156

1257
/// <inheritdoc />
1358
public class QueryFilter : IPostgrestQueryFilter
@@ -26,7 +71,6 @@ public class QueryFilter : IPostgrestQueryFilter
2671
/// <inheritdoc />
2772
public object? Criteria { get; private set; }
2873

29-
3074
/// <summary>
3175
/// Contractor to use single value filtering.
3276
/// </summary>
@@ -53,54 +97,26 @@ public QueryFilter(string property, Operator op, object? criteria)
5397
Op = op;
5498
Criteria = criteria;
5599
break;
56-
default:
57-
throw new PostgrestException("Advanced filters require a constructor with more specific arguments") { Reason = FailureHint.Reason.InvalidArgument };
58-
}
59-
}
60-
61-
/// <summary>
62-
/// Constructor to use multiple values as for filtering.
63-
/// </summary>
64-
/// <param name="property">Column name</param>
65-
/// <param name="op">Operation: In, Contains, ContainedIn, or Overlap</param>
66-
/// <param name="criteria"></param>
67-
public QueryFilter(string property, Operator op, IList criteria)
68-
{
69-
switch (op)
70-
{
71100
case Operator.In:
72101
case Operator.Contains:
73102
case Operator.ContainedIn:
74103
case Operator.Overlap:
104+
if (criteria is IList or IDictionary)
105+
{
75106
Property = property;
76107
Op = op;
77108
Criteria = criteria;
78-
break;
79-
default:
80-
throw new PostgrestException("List constructor must be used with filter that accepts an array of arguments.") { Reason = FailureHint.Reason.InvalidArgument };
81-
}
82109
}
83-
84-
/// <summary>
85-
/// Constructor to use multiple values as for filtering (using a dictionary).
86-
/// </summary>
87-
/// <param name="property">Column name</param>
88-
/// <param name="op">Operation: In, Contains, ContainedIn, or Overlap</param>
89-
/// <param name="criteria"></param>
90-
public QueryFilter(string property, Operator op, IDictionary criteria)
110+
else
91111
{
92-
switch (op)
93-
{
94-
case Operator.In:
95-
case Operator.Contains:
96-
case Operator.ContainedIn:
97-
case Operator.Overlap:
98-
Property = property;
99-
Op = op;
100-
Criteria = criteria;
112+
throw new PostgrestException(
113+
"List or Dictionary must be used supplied as criteria with filters that accept an array of arguments.")
114+
{ Reason = FailureHint.Reason.InvalidArgument };
115+
}
101116
break;
102117
default:
103-
throw new PostgrestException("List constructor must be used with filter that accepts an array of arguments.") { Reason = FailureHint.Reason.InvalidArgument };
118+
throw new PostgrestException("Advanced filters require a constructor with more specific arguments")
119+
{ Reason = FailureHint.Reason.InvalidArgument };
104120
}
105121
}
106122

@@ -123,7 +139,8 @@ public QueryFilter(string property, Operator op, FullTextSearchConfig fullTextSe
123139
Criteria = fullTextSearchConfig;
124140
break;
125141
default:
126-
throw new PostgrestException("Constructor must be called with a full text search operator") { Reason = FailureHint.Reason.InvalidArgument };
142+
throw new PostgrestException("Constructor must be called with a full text search operator")
143+
{ Reason = FailureHint.Reason.InvalidArgument };
127144
}
128145
}
129146

@@ -162,7 +179,7 @@ public QueryFilter(string property, Operator op, IntRange range)
162179
/// </summary>
163180
/// <param name="op">Operation: And, Or</param>
164181
/// <param name="filters"></param>
165-
public QueryFilter(Operator op, List<QueryFilter> filters)
182+
public QueryFilter(Operator op, List<IPostgrestQueryFilter> filters)
166183
{
167184
switch (op)
168185
{
@@ -172,7 +189,8 @@ public QueryFilter(Operator op, List<QueryFilter> filters)
172189
Criteria = filters;
173190
break;
174191
default:
175-
throw new PostgrestException("Constructor can only be used with `or` or `and` filters") { Reason = FailureHint.Reason.InvalidArgument };
192+
throw new PostgrestException("Constructor can only be used with `or` or `and` filters")
193+
{ Reason = FailureHint.Reason.InvalidArgument };
176194
}
177195
}
178196

@@ -181,7 +199,7 @@ public QueryFilter(Operator op, List<QueryFilter> filters)
181199
/// </summary>
182200
/// <param name="op">Operation: Not.</param>
183201
/// <param name="filter"></param>
184-
public QueryFilter(Operator op, QueryFilter filter)
202+
public QueryFilter(Operator op, IPostgrestQueryFilter filter)
185203
{
186204
switch (op)
187205
{
@@ -190,7 +208,8 @@ public QueryFilter(Operator op, QueryFilter filter)
190208
Criteria = filter;
191209
break;
192210
default:
193-
throw new PostgrestException("Constructor can only be used with `not` filter") { Reason = FailureHint.Reason.InvalidArgument };
211+
throw new PostgrestException("Constructor can only be used with `not` filter")
212+
{ Reason = FailureHint.Reason.InvalidArgument };
194213
}
195214
}
196215
}

Postgrest/Table.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ namespace Postgrest
5050

5151
private string? _columnQuery;
5252

53-
private readonly List<QueryFilter> _filters = new();
53+
private readonly List<IPostgrestQueryFilter> _filters = new();
5454
private readonly List<QueryOrderer> _orderers = new();
5555
private readonly List<string> _columns = new();
5656

@@ -169,7 +169,7 @@ public Table<TModel> Filter<TCriterion>(string columnName, Operator op, TCriteri
169169
}
170170

171171
/// <inheritdoc />
172-
public Table<TModel> Not(QueryFilter filter)
172+
public Table<TModel> Not(IPostgrestQueryFilter filter)
173173
{
174174
_filters.Add(new QueryFilter(Operator.Not, filter));
175175
return this;
@@ -237,14 +237,14 @@ public Table<TModel> Not(Expression<Func<TModel, object>> predicate, Operator op
237237

238238

239239
/// <inheritdoc />
240-
public Table<TModel> And(List<QueryFilter> filters)
240+
public Table<TModel> And(List<IPostgrestQueryFilter> filters)
241241
{
242242
_filters.Add(new QueryFilter(Operator.And, filters));
243243
return this;
244244
}
245245

246246
/// <inheritdoc />
247-
public Table<TModel> Or(List<QueryFilter> filters)
247+
public Table<TModel> Or(List<IPostgrestQueryFilter> filters)
248248
{
249249
_filters.Add(new QueryFilter(Operator.Or, filters));
250250
return this;
@@ -767,7 +767,7 @@ public string GenerateUrl()
767767
/// </summary>
768768
/// <param name="filter"></param>
769769
/// <returns></returns>
770-
internal KeyValuePair<string, string> PrepareFilter(QueryFilter filter)
770+
internal KeyValuePair<string, string> PrepareFilter(IPostgrestQueryFilter filter)
771771
{
772772
var asAttribute = filter.Op.GetAttribute<MapToAttribute>();
773773
var strBuilder = new StringBuilder();
@@ -779,7 +779,7 @@ internal KeyValuePair<string, string> PrepareFilter(QueryFilter filter)
779779
{
780780
case Operator.Or:
781781
case Operator.And:
782-
if (filter.Criteria is List<QueryFilter> subFilters)
782+
if (filter.Criteria is List<IPostgrestQueryFilter> subFilters)
783783
{
784784
var list = new List<KeyValuePair<string, string>>();
785785
foreach (var subFilter in subFilters)

PostgrestTests/LinqTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,5 +369,37 @@ public async Task TestLinqNot()
369369

370370
CollectionAssert.AreEqual(supaNotInList, linqNotInList);
371371
}
372+
373+
[TestMethod("Linq: QueryFilter")]
374+
public async Task TestLinqQueryFilter()
375+
{
376+
var client = new Client(BaseUrl);
377+
378+
var filteredResponse = await client.Table<User>()
379+
.Filter(x => x.Username!, Operator.Equals, "supabot")
380+
.Single();
381+
382+
Assert.IsNotNull(filteredResponse);
383+
Assert.AreEqual("supabot", filteredResponse.Username);
384+
385+
var filters = new List<IPostgrestQueryFilter>
386+
{
387+
new QueryFilter<User, List<string>>(x => x.Username!, Operator.In,
388+
new List<string> { "supabot", "kiwicopple" }),
389+
new QueryFilter<User, string>(x => x.Status!, Operator.Equals, "OFFLINE")
390+
};
391+
392+
var orResponse = await client.Table<User>()
393+
.Or(filters)
394+
.Get();
395+
396+
Assert.IsNotNull(orResponse);
397+
398+
foreach (var model in orResponse.Models)
399+
{
400+
Assert.IsTrue(
401+
model.Username == "supabot" || model.Username == "kiwicopple" || model.Status == "OFFLINE");
402+
}
403+
}
372404
}
373405
}

PostgrestTests/db/01-dummy-data.sql

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
VALUES ('supabot', 'ONLINE', '[1,2)'::int4range, 'fat cat'::tsvector),
33
('kiwicopple', 'OFFLINE', '[25,35)'::int4range, 'cat bat'::tsvector),
44
('awailas', 'ONLINE', '[25,35)'::int4range, 'bat rat'::tsvector),
5+
('acupofjose', 'OFFLINE', '[25,35)'::int4range, 'bat rat'::tsvector),
56
('dragarcia', 'ONLINE', '[20,30)'::int4range, 'rat fat'::tsvector);
67

78
INSERT INTO public.channels (slug)
@@ -18,7 +19,7 @@ VALUES ('supabot', 'ONLINE', '[1,2)'::int4range),
1819
('kiwicopple', 'OFFLINE', '[25,35)'::int4range),
1920
('awailas', 'ONLINE', '[25,35)'::int4range),
2021
('dragarcia', 'ONLINE', '[20,30)'::int4range),
21-
('leroyjenkins', 'ONLINE', '[20,40)'::int4range);
22+
('leroyjenkins', 'OFFLINE', '[20,40)'::int4range);
2223

2324
INSERT INTO public.kitchen_sink (id,
2425
string_value,

0 commit comments

Comments
 (0)