• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

loresoft / FluentCommand / 29205902518

12 Jul 2026 07:29PM UTC coverage: 65.698% (+0.6%) from 65.067%
29205902518

push

github

pwelter34
Add query request models and JSON filter support

1814 of 3552 branches covered (51.07%)

Branch coverage included in aggregate %.

70 of 79 new or added lines in 4 files covered. (88.61%)

5650 of 7809 relevant lines covered (72.35%)

300.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

57.05
/src/FluentCommand/Query/WhereBuilder.cs
1
using System.Collections;
2

3
using FluentCommand.Extensions;
4
using FluentCommand.Query.Generators;
5

6
namespace FluentCommand.Query;
7

8
/// <summary>
9
/// Provides a builder for constructing SQL WHERE clauses with fluent, chainable methods.
10
/// </summary>
11
public class WhereBuilder : WhereBuilder<WhereBuilder>
12
{
13
    /// <summary>
14
    /// Initializes a new instance of the <see cref="WhereBuilder"/> class.
15
    /// </summary>
16
    /// <param name="queryGenerator">The <see cref="IQueryGenerator"/> used to generate SQL expressions.</param>
17
    /// <param name="parameters">The list of <see cref="QueryParameter"/> objects for the query.</param>
18
    /// <param name="logicalOperator">The logical operator (<see cref="LogicalOperators"/>) to combine WHERE expressions. Defaults to <see cref="LogicalOperators.And"/>.</param>
19
    public WhereBuilder(
20
        IQueryGenerator queryGenerator,
21
        List<QueryParameter> parameters,
22
        LogicalOperators logicalOperator = LogicalOperators.And)
23
        : base(queryGenerator, parameters, logicalOperator)
2✔
24
    {
25
    }
2✔
26

27
    /// <inheritdoc />
28
    public override QueryStatement? BuildStatement()
29
    {
30
        var statement = QueryGenerator.BuildWhere(
2✔
31
            whereExpressions: WhereExpressions
2✔
32
        );
2✔
33

34
        if (statement.IsNullOrWhiteSpace())
2!
35
            return null;
×
36

37
        return new QueryStatement(statement, Parameters);
2✔
38
    }
39
}
40

41
/// <summary>
42
/// Provides a generic base class for building SQL WHERE clauses with fluent, chainable methods.
43
/// </summary>
44
/// <typeparam name="TBuilder">The type of the builder for fluent chaining.</typeparam>
45
public abstract class WhereBuilder<TBuilder> : StatementBuilder<TBuilder>
46
    where TBuilder : WhereBuilder<TBuilder>
47
{
48
    /// <summary>
49
    /// Initializes a new instance of the <see cref="WhereBuilder{TBuilder}"/> class.
50
    /// </summary>
51
    /// <param name="queryGenerator">The <see cref="IQueryGenerator"/> used to generate SQL expressions.</param>
52
    /// <param name="parameters">The list of <see cref="QueryParameter"/> objects for the query.</param>
53
    /// <param name="logicalOperator">The logical operator (<see cref="LogicalOperators"/>) to combine WHERE expressions. Defaults to <see cref="LogicalOperators.And"/>.</param>
54
    protected WhereBuilder(IQueryGenerator queryGenerator, List<QueryParameter> parameters, LogicalOperators logicalOperator = LogicalOperators.And)
55
        : base(queryGenerator, parameters)
87✔
56
    {
57
        LogicalOperator = logicalOperator;
87✔
58
    }
87✔
59

60
    /// <summary>
61
    /// Gets the collection of WHERE expressions for the query.
62
    /// </summary>
63
    /// <value>
64
    /// A <see cref="HashSet{WhereExpression}"/> containing the WHERE expressions.
65
    /// </value>
66
    protected HashSet<WhereExpression> WhereExpressions { get; } = new();
67

68
    /// <summary>
69
    /// Gets the logical operator used to combine WHERE expressions.
70
    /// </summary>
71
    /// <value>
72
    /// The <see cref="LogicalOperators"/> value.
73
    /// </value>
74
    protected LogicalOperators LogicalOperator { get; }
75

76
    /// <summary>
77
    /// Adds a WHERE clause for the specified column, value, and operator.
78
    /// </summary>
79
    /// <typeparam name="TValue">The type of the value.</typeparam>
80
    /// <param name="columnName">The name of the column.</param>
81
    /// <param name="parameterValue">The value to compare.</param>
82
    /// <param name="filterOperator">The filter operator (<see cref="FilterOperators"/>). Defaults to <see cref="FilterOperators.Equal"/>.</param>
83
    /// <returns>
84
    /// The same builder instance for method chaining.
85
    /// </returns>
86
    public TBuilder Where<TValue>(
87
       string columnName,
88
       TValue parameterValue,
89
       FilterOperators filterOperator = FilterOperators.Equal)
90
    {
91
        return Where(columnName, parameterValue, null, filterOperator);
7✔
92
    }
93

94
    /// <summary>
95
    /// Adds a WHERE clause for the specified column, value, operator, and table alias.
96
    /// </summary>
97
    /// <typeparam name="TValue">The type of the value.</typeparam>
98
    /// <param name="columnName">The name of the column.</param>
99
    /// <param name="parameterValue">The value to compare.</param>
100
    /// <param name="tableAlias">The table alias, or <c>null</c> if not applicable.</param>
101
    /// <param name="filterOperator">The filter operator (<see cref="FilterOperators"/>). Defaults to <see cref="FilterOperators.Equal"/>.</param>
102
    /// <returns>
103
    /// The same builder instance for method chaining.
104
    /// </returns>
105
    public TBuilder Where<TValue>(
106
        string columnName,
107
        TValue? parameterValue,
108
        string? tableAlias,
109
        FilterOperators filterOperator = FilterOperators.Equal)
110
    {
111
        var parameterName = NextParameter();
55✔
112

113
        WhereExpressions.Add(new WhereExpression(columnName, parameterName, tableAlias, filterOperator));
55✔
114
        Parameters.Add(new QueryParameter(parameterName, parameterValue, typeof(TValue)));
55✔
115

116
        return (TBuilder)this;
55✔
117
    }
118

119
    /// <summary>
120
    /// Adds a WHERE clause for the specified column, value, operator, and table alias.
121
    /// </summary>
122
    /// <param name="columnName">The name of the column.</param>
123
    /// <param name="parameterValue">The value to compare.</param>
124
    /// <param name="tableAlias">The table alias, or <c>null</c> if not applicable.</param>
125
    /// <param name="filterOperator">The filter operator (<see cref="FilterOperators"/>). Defaults to <see cref="FilterOperators.Equal"/>.</param>
126
    /// <returns>
127
    /// The same builder instance for method chaining.
128
    /// </returns>
129
    public TBuilder Where(
130
        string columnName,
131
        object? parameterValue,
132
        string? tableAlias,
133
        FilterOperators filterOperator = FilterOperators.Equal)
134
    {
135
        var parameterName = NextParameter();
4✔
136
        var parameterType = parameterValue?.GetType() ?? typeof(object);
4✔
137

138
        WhereExpressions.Add(new WhereExpression(columnName, parameterName, tableAlias, filterOperator));
4✔
139
        Parameters.Add(new QueryParameter(parameterName, parameterValue, parameterType));
4✔
140

141
        return (TBuilder)this;
4✔
142
    }
143

144
    /// <summary>
145
    /// Adds a WHERE IN clause for the specified column, values, and optional table alias.
146
    /// </summary>
147
    /// <typeparam name="TValue">The type of the values.</typeparam>
148
    /// <param name="columnName">The name of the column.</param>
149
    /// <param name="parameterValues">The collection of values for the IN clause.</param>
150
    /// <param name="tableAlias">The table alias, or <c>null</c> if not applicable.</param>
151
    /// <returns>
152
    /// The same builder instance for method chaining.
153
    /// </returns>
154
    public TBuilder WhereIn<TValue>(
155
        string columnName,
156
        IEnumerable<TValue> parameterValues,
157
        string? tableAlias = null)
158
    {
159
        var parameterNames = new List<string>();
9✔
160
        foreach (var parameterValue in parameterValues)
64✔
161
        {
162
            var parameterName = NextParameter();
23✔
163
            var parameter = new QueryParameter(parameterName, parameterValue, typeof(TValue));
23✔
164

165
            Parameters.Add(parameter);
23✔
166
            parameterNames.Add(parameterName);
23✔
167
        }
168

169
        var whereParameter = parameterNames.ToDelimitedString();
9✔
170

171
        WhereExpressions.Add(new WhereExpression(columnName, whereParameter, tableAlias, FilterOperators.In));
9✔
172

173
        return (TBuilder)this;
9✔
174
    }
175

176
    /// <summary>
177
    /// Adds a WHERE IN clause for the specified column, values, and optional table alias.
178
    /// </summary>
179
    /// <param name="columnName">The name of the column.</param>
180
    /// <param name="parameterValues">The collection of values for the IN clause.</param>
181
    /// <param name="tableAlias">The table alias, or <c>null</c> if not applicable.</param>
182
    /// <returns>
183
    /// The same builder instance for method chaining.
184
    /// </returns>
185
    public TBuilder WhereIn(
186
        string columnName,
187
        IEnumerable parameterValues,
188
        string? tableAlias = null)
189
    {
190
        var parameterNames = new List<string>();
1✔
191
        foreach (var parameterValue in parameterValues)
6✔
192
        {
193
            var parameterName = NextParameter();
2✔
194
            var parameterType = parameterValue?.GetType() ?? typeof(object);
2!
195

196
            var parameter = new QueryParameter(parameterName, parameterValue, parameterType);
2✔
197

198
            Parameters.Add(parameter);
2✔
199
            parameterNames.Add(parameterName);
2✔
200
        }
201

202
        var whereParameter = parameterNames.ToDelimitedString();
1✔
203

204
        WhereExpressions.Add(new WhereExpression(columnName, whereParameter, tableAlias, FilterOperators.In));
1✔
205

206
        return (TBuilder)this;
1✔
207
    }
208

209
    /// <summary>
210
    /// Conditionally adds a WHERE IN clause for the specified column and values if the condition is met.
211
    /// </summary>
212
    /// <typeparam name="TValue">The type of the values.</typeparam>
213
    /// <param name="columnName">The name of the column.</param>
214
    /// <param name="parameterValues">The collection of values for the IN clause.</param>
215
    /// <param name="condition">A function that determines whether to add the clause. If <c>null</c>, the clause is always added.</param>
216
    /// <returns>
217
    /// The same builder instance for method chaining.
218
    /// </returns>
219
    public TBuilder WhereInIf<TValue>(
220
        string columnName,
221
        IEnumerable<TValue> parameterValues,
222
        Func<string, IEnumerable<TValue>, bool>? condition = null)
223
    {
224
        if (condition != null && !condition(columnName, parameterValues))
1!
225
            return (TBuilder)this;
×
226

227
        return WhereIn(columnName, parameterValues);
1✔
228
    }
229

230
    /// <summary>
231
    /// Conditionally adds a WHERE IN clause for the specified column, values, and table alias if the condition is met.
232
    /// </summary>
233
    /// <typeparam name="TValue">The type of the values.</typeparam>
234
    /// <param name="columnName">The name of the column.</param>
235
    /// <param name="parameterValues">The collection of values for the IN clause.</param>
236
    /// <param name="tableAlias">The table alias, or <c>null</c> if not applicable.</param>
237
    /// <param name="condition">A function that determines whether to add the clause. If <c>null</c>, the clause is always added.</param>
238
    /// <returns>
239
    /// The same builder instance for method chaining.
240
    /// </returns>
241
    public TBuilder WhereInIf<TValue>(
242
        string columnName,
243
        IEnumerable<TValue> parameterValues,
244
        string? tableAlias,
245
        Func<string, IEnumerable<TValue>, bool>? condition = null)
246
    {
247
        if (condition != null && !condition(columnName, parameterValues))
×
248
            return (TBuilder)this;
×
249

250
        return WhereIn(columnName, parameterValues, tableAlias);
×
251
    }
252

253
    /// <summary>
254
    /// Conditionally adds a WHERE clause for the specified column, value, and operator if the condition is met.
255
    /// </summary>
256
    /// <typeparam name="TValue">The type of the value.</typeparam>
257
    /// <param name="columnName">The name of the column.</param>
258
    /// <param name="parameterValue">The value to compare.</param>
259
    /// <param name="filterOperator">The filter operator (<see cref="FilterOperators"/>). Defaults to <see cref="FilterOperators.Equal"/>.</param>
260
    /// <param name="condition">A function that determines whether to add the clause. If <c>null</c>, the clause is always added.</param>
261
    /// <returns>
262
    /// The same builder instance for method chaining.
263
    /// </returns>
264
    public TBuilder WhereIf<TValue>(
265
        string columnName,
266
        TValue? parameterValue,
267
        FilterOperators filterOperator = FilterOperators.Equal,
268
        Func<string, TValue?, bool>? condition = null)
269
    {
270
        return WhereIf(columnName, parameterValue, null, filterOperator, condition);
×
271
    }
272

273
    /// <summary>
274
    /// Conditionally adds a WHERE clause for the specified column, value, operator, and table alias if the condition is met.
275
    /// </summary>
276
    /// <typeparam name="TValue">The type of the value.</typeparam>
277
    /// <param name="columnName">The name of the column.</param>
278
    /// <param name="parameterValue">The value to compare.</param>
279
    /// <param name="tableAlias">The table alias, or <c>null</c> if not applicable.</param>
280
    /// <param name="filterOperator">The filter operator (<see cref="FilterOperators"/>). Defaults to <see cref="FilterOperators.Equal"/>.</param>
281
    /// <param name="condition">A function that determines whether to add the clause. If <c>null</c>, the clause is always added.</param>
282
    /// <returns>
283
    /// The same builder instance for method chaining.
284
    /// </returns>
285
    public TBuilder WhereIf<TValue>(
286
        string columnName,
287
        TValue? parameterValue,
288
        string? tableAlias,
289
        FilterOperators filterOperator = FilterOperators.Equal,
290
        Func<string, TValue?, bool>? condition = null)
291
    {
292
        if (condition != null && !condition(columnName, parameterValue))
×
293
            return (TBuilder)this;
×
294

295
        return Where(columnName, parameterValue, tableAlias, filterOperator);
×
296
    }
297

298
    /// <summary>
299
    /// Adds a raw WHERE clause to the query.
300
    /// </summary>
301
    /// <param name="whereClause">The raw SQL WHERE clause.</param>
302
    /// <param name="parameters">The collection of <see cref="QueryParameter"/> objects for the clause, or <c>null</c> if none.</param>
303
    /// <returns>
304
    /// The same builder instance for method chaining.
305
    /// </returns>
306
    /// <exception cref="ArgumentException">Thrown if <paramref name="whereClause"/> is null or empty.</exception>
307
    public TBuilder WhereRaw(
308
        string whereClause,
309
        IEnumerable<QueryParameter>? parameters = null)
310
    {
311
        if (string.IsNullOrWhiteSpace(whereClause))
×
312
            throw new ArgumentException($"'{nameof(whereClause)}' cannot be null or empty.", nameof(whereClause));
×
313

314
        WhereExpressions.Add(new WhereExpression(whereClause, IsRaw: true));
×
315

316
        if (parameters != null)
×
317
            Parameters.AddRange(parameters);
×
318

319
        return (TBuilder)this;
×
320
    }
321

322
    /// <summary>
323
    /// Conditionally adds a raw WHERE clause to the query if the condition is met.
324
    /// </summary>
325
    /// <param name="whereClause">The raw SQL WHERE clause.</param>
326
    /// <param name="parameters">The collection of <see cref="QueryParameter"/> objects for the clause, or <c>null</c> if none.</param>
327
    /// <param name="condition">A function that determines whether to add the clause. If <c>null</c>, the clause is always added.</param>
328
    /// <returns>
329
    /// The same builder instance for method chaining.
330
    /// </returns>
331
    public TBuilder WhereRawIf(
332
        string whereClause,
333
        IEnumerable<QueryParameter>? parameters = null,
334
        Func<string, IEnumerable<QueryParameter>?, bool>? condition = null)
335
    {
336
        if (condition != null && !condition(whereClause, parameters))
×
337
            return (TBuilder)this;
×
338

339
        return WhereRaw(whereClause, parameters);
×
340
    }
341

342
    /// <summary>
343
    /// Adds a logical OR group of WHERE clauses using the specified builder action.
344
    /// </summary>
345
    /// <param name="builder">An action that configures the logical OR group using a <see cref="LogicalBuilder"/>.</param>
346
    /// <returns>
347
    /// The same builder instance for method chaining.
348
    /// </returns>
349
    public TBuilder WhereOr(Action<LogicalBuilder> builder)
350
    {
351
        var innerBuilder = new LogicalBuilder(QueryGenerator, Parameters, LogicalOperators.Or);
×
352
        builder(innerBuilder);
×
353

354
        var statement = innerBuilder.BuildStatement();
×
355

356
        if (statement != null && statement.Statement.HasValue())
×
357
            WhereExpressions.Add(new WhereExpression(statement.Statement, IsRaw: true));
×
358

359
        return (TBuilder)this;
×
360
    }
361

362
    /// <summary>
363
    /// Adds a logical AND group of WHERE clauses using the specified builder action.
364
    /// </summary>
365
    /// <param name="builder">An action that configures the logical AND group using a <see cref="LogicalBuilder"/>.</param>
366
    /// <returns>
367
    /// The same builder instance for method chaining.
368
    /// </returns>
369
    public TBuilder WhereAnd(Action<LogicalBuilder> builder)
370
    {
371
        var innerBuilder = new LogicalBuilder(QueryGenerator, Parameters, LogicalOperators.And);
×
372

373
        builder(innerBuilder);
×
374

375
        var statement = innerBuilder.BuildStatement();
×
376

377
        if (statement != null && statement.Statement.HasValue())
×
378
            WhereExpressions.Add(new WhereExpression(statement.Statement, IsRaw: true));
×
379

380
        return (TBuilder)this;
×
381
    }
382

383
    /// <summary>
384
    /// Adds WHERE clauses from the specified query filter tree.
385
    /// </summary>
386
    /// <param name="filter">The query filter to add, or <c>null</c> to leave the builder unchanged.</param>
387
    /// <returns>
388
    /// The same builder instance for method chaining.
389
    /// </returns>
390
    public TBuilder Where(QueryFilter? filter)
391
    {
392
        if (filter == null)
7!
NEW
393
            return (TBuilder)this;
×
394

395
        // use logical builder to build the filter tree into a SQL statement
396
        if (filter.Filters?.Count > 0)
7✔
397
        {
398
            var innerBuilder = new LogicalBuilder(QueryGenerator, Parameters, filter.Logic ?? LogicalOperators.And);
2✔
399

400
            foreach (var childFilter in filter.Filters)
12✔
401
                innerBuilder.Where(childFilter);
4✔
402

403
            var statement = innerBuilder.BuildStatement();
2✔
404

405
            if (statement?.Statement.HasValue() == true)
2!
406
                WhereExpressions.Add(new WhereExpression(statement.Statement, IsRaw: true));
2✔
407

408
            return (TBuilder)this;
2✔
409
        }
410

411
        // name required for leaf filters
412
        if (filter.Name.IsNullOrWhiteSpace())
5!
NEW
413
            throw new ArgumentException($"'{nameof(filter.Name)}' property cannot be null or empty.", nameof(filter));
×
414

415
        var filterOperator = filter.Operator ?? FilterOperators.Equal;
5✔
416

417
        if (filterOperator == FilterOperators.In && filter.Value is IEnumerable objects && filter.Value is not string)
5✔
418
            return WhereIn(filter.Name, objects, tableAlias: null);
1✔
419
        else
420
            return Where(filter.Name, filter.Value, tableAlias: null, filterOperator);
4✔
421
    }
422
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc