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

loresoft / FluentCommand / 26594173245

28 May 2026 06:28PM UTC coverage: 55.553% (+0.7%) from 54.902%
26594173245

push

github

pwelter34
Move JSON support, add docs and examples

1358 of 3215 branches covered (42.24%)

Branch coverage included in aggregate %.

103 of 234 new or added lines in 9 files covered. (44.02%)

371 existing lines in 26 files now uncovered.

4389 of 7130 relevant lines covered (61.56%)

312.89 hits per line

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

88.8
/src/FluentCommand/DataCommand.cs
1
using System.Data;
2
using System.Data.Common;
3
using System.Diagnostics;
4

5
using FluentCommand.Extensions;
6
using FluentCommand.Internal;
7

8
using HashCode = FluentCommand.Internal.HashCode;
9

10
namespace FluentCommand;
11

12
/// <summary>
13
/// A fluent class to build a data command.
14
/// </summary>
15
public class DataCommand : DisposableBase, IDataCommand
16
{
17
    private readonly Queue<DataCallback> _callbacks;
18
    private readonly IDataSession _dataSession;
19
    private readonly IDataCommandInterceptor[] _commandInterceptors;
20

21
    private TimeSpan? _slidingExpiration;
22
    private DateTimeOffset? _absoluteExpiration;
23
    private object? _logState;
24

25
    /// <summary>
26
    /// Initializes a new instance of the <see cref="DataCommand" /> class.
27
    /// </summary>
28
    /// <param name="dataSession">The data session.</param>
29
    /// <param name="transaction">The DbTransaction for this DataCommand.</param>
30
    /// <param name="commandInterceptors">Pre-filtered command interceptors from the owning session.</param>
31
    /// <param name="commandTimeout">The command timeout in seconds.</param>
32
    public DataCommand(
263✔
33
        IDataSession dataSession,
263✔
34
        DbTransaction? transaction,
263✔
35
        IDataCommandInterceptor[]? commandInterceptors = null,
263✔
36
        int? commandTimeout = null)
263✔
37
    {
38
        _callbacks = new Queue<DataCallback>();
263✔
39
        _dataSession = dataSession ?? throw new ArgumentNullException(nameof(dataSession));
263!
40

41
        Command = dataSession.Connection.CreateCommand();
263✔
42
        Command.Transaction = transaction;
263✔
43
        if (commandTimeout.HasValue)
263✔
44
            Command.CommandTimeout = commandTimeout.Value;
4✔
45

46
        _commandInterceptors = commandInterceptors ?? [];
263!
47
    }
263✔
48

49
    /// <summary>
50
    /// Gets the underlying <see cref="DbCommand"/> for this <see cref="DataCommand"/>.
51
    /// </summary>
52
    public DbCommand Command { get; }
53

54

55
    /// <summary>
56
    /// Set the data command with the specified SQL statement.
57
    /// </summary>
58
    /// <param name="sql">The SQL statement.</param>
59
    /// <returns>
60
    /// A fluent <see langword="interface" /> to a data command.
61
    /// </returns>
62
    public IDataCommand Sql(string sql)
63
    {
64
        Command.CommandText = sql;
250✔
65
        Command.CommandType = CommandType.Text;
250✔
66
        return this;
250✔
67
    }
68

69
    /// <summary>
70
    /// Set the data command with the specified stored procedure name.
71
    /// </summary>
72
    /// <param name="storedProcedure">Name of the stored procedure.</param>
73
    /// <returns>
74
    /// A fluent <see langword="interface" /> to a data command.
75
    /// </returns>
76
    public IDataCommand StoredProcedure(string storedProcedure)
77
    {
78
        Command.CommandText = storedProcedure;
13✔
79
        Command.CommandType = CommandType.StoredProcedure;
13✔
80
        return this;
13✔
81
    }
82

83

84
    /// <summary>
85
    /// Sets the wait time (in seconds) before terminating the attempt to execute the command and generating an error.
86
    /// </summary>
87
    /// <param name="timeout">The time, in seconds, to wait for the command to execute.</param>
88
    /// <returns>
89
    /// A fluent <see langword="interface" /> to the data command.
90
    /// </returns>
91
    public IDataCommand CommandTimeout(int timeout)
92
    {
93
        Command.CommandTimeout = timeout;
2✔
94
        return this;
2✔
95
    }
96

97

98
    /// <summary>
99
    /// Adds the parameter to the underlying command.
100
    /// </summary>
101
    /// <param name="parameter">The <see cref="DbParameter" /> to add.</param>
102
    /// <returns>
103
    /// A fluent <see langword="interface" /> to the data command.
104
    /// </returns>
105
    /// <exception cref="ArgumentNullException"><paramref name="parameter"/> is null</exception>
106
    public IDataCommand Parameter(DbParameter parameter)
107
    {
108
        ArgumentNullException.ThrowIfNull(parameter);
561✔
109

110
        Command.Parameters.Add(parameter);
561✔
111
        return this;
561✔
112
    }
113

114
    /// <summary>
115
    /// Register a return value <paramref name="callback" /> for the specified <paramref name="parameter" />.
116
    /// </summary>
117
    /// <typeparam name="TParameter">The type of the parameter value.</typeparam>
118
    /// <param name="parameter">The <see cref="IDbDataParameter" /> to add.</param>
119
    /// <param name="callback">The callback used to get the out value.</param>
120
    /// <returns>
121
    /// A fluent <see langword="interface" /> to the data command.
122
    /// </returns>
123
    public IDataCommand RegisterCallback<TParameter>(DbParameter parameter, Action<TParameter> callback)
124
    {
125
        var dataCallback = new DataCallback(typeof(TParameter), parameter, callback);
11✔
126
        _callbacks.Enqueue(dataCallback);
11✔
127

128
        return this;
11✔
129
    }
130

131

132
    /// <summary>
133
    /// Uses cache to insert and retrieve cached results for the command with the specified <paramref name="slidingExpiration" />.
134
    /// </summary>
135
    /// <param name="slidingExpiration">
136
    /// A value that indicates whether a cache entry should be evicted if it has not been accessed in a given span of time.
137
    /// </param>
138
    /// <returns>
139
    /// A fluent <see langword="interface" /> to the data command.
140
    /// </returns>
141
    /// <exception cref="InvalidOperationException">A command with Output or Return parameters can not be cached.</exception>
142
    public IDataCommand UseCache(TimeSpan slidingExpiration)
143
    {
144
        _slidingExpiration = slidingExpiration;
22✔
145
        if (_slidingExpiration != null && _callbacks.Count > 0)
22!
UNCOV
146
            throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
147

148
        return this;
22✔
149
    }
150

151
    /// <summary>
152
    /// Uses cache to insert and retrieve cached results for the command with the specified <paramref name="absoluteExpiration" />.
153
    /// </summary>
154
    /// <param name="absoluteExpiration">A value that indicates whether a cache entry should be evicted after a specified duration.</param>
155
    /// <returns>
156
    /// A fluent <see langword="interface" /> to the data command.
157
    /// </returns>
158
    /// <exception cref="InvalidOperationException">A command with Output or Return parameters can not be cached.</exception>
159
    public IDataCommand UseCache(DateTimeOffset absoluteExpiration)
160
    {
UNCOV
161
        _absoluteExpiration = absoluteExpiration;
×
162
        if (_absoluteExpiration != null && _callbacks.Count > 0)
×
163
            throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
164

UNCOV
165
        return this;
×
166
    }
167

168

169
    /// <summary>
170
    /// Expires cached items that have been cached using the current DataCommand.
171
    /// </summary>
172
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
173
    /// <returns>
174
    /// A fluent <see langword="interface" /> to the data command.
175
    /// </returns>
176
    /// <remarks>
177
    /// Cached keys are created using the current DataCommand state.  When any Query operation is
178
    /// executed with a cache policy, the results are cached.  Use this method with the same parameters
179
    /// to expire the cached item.
180
    /// </remarks>
181
    public IDataCommand ExpireCache<TEntity>()
182
    {
UNCOV
183
        string? cacheKey = CacheKey<TEntity>(true);
×
184
        if (_dataSession.Cache != null && cacheKey != null)
×
185
            _dataSession.Cache.Remove(cacheKey);
×
186

UNCOV
187
        return this;
×
188
    }
189

190
    /// <summary>
191
    /// Use to pass a state to the <see cref="IDataQueryLogger" />.
192
    /// </summary>
193
    /// <param name="state">The state to pass to the logger.</param>
194
    /// <returns>
195
    /// A fluent <see langword="interface" /> to the data command.
196
    /// </returns>
197
    /// <remarks>
198
    /// Use the state to help control what is logged.
199
    /// </remarks>
200
    public IDataCommand LogState(object? state)
201
    {
UNCOV
202
        _logState = state;
×
203
        return this;
×
204
    }
205

206
    /// <summary>
207
    /// Executes the command against the connection and converts the results to <typeparamref name="TEntity" /> objects.
208
    /// </summary>
209
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
210
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
211
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
212
    /// <returns>
213
    /// An <see cref="T:System.Collections.Generic.IEnumerable`1" /> of <typeparamref name="TEntity" /> objects.
214
    /// </returns>
215
    public IEnumerable<TEntity> Query<TEntity>(
216
        Func<IDataReader, TEntity> factory,
217
        CommandBehavior commandBehavior = CommandBehavior.SingleResult)
218
    {
219
        ArgumentNullException.ThrowIfNull(factory);
22✔
220

221
        return QueryFactory(() =>
22✔
222
        {
22✔
223
            var results = new List<TEntity>();
22✔
224

22✔
225
            using var reader = Command.ExecuteReader(commandBehavior);
22✔
226
            while (reader.Read())
22✔
227
            {
22✔
228
                var entity = factory(reader);
22✔
229
                results.Add(entity);
22✔
230
            }
22✔
231

22✔
232
            return results;
22✔
233
        }, true);
22✔
234
    }
235

236
    /// <summary>
237
    /// Executes the command against the connection and converts the results to <typeparamref name="TEntity" /> objects asynchronously.
238
    /// </summary>
239
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
240
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
241
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
242
    /// <param name="cancellationToken">The cancellation instruction.</param>
243
    /// <returns>
244
    /// An <see cref="T:System.Collections.Generic.IEnumerable`1" /> of <typeparamref name="TEntity" /> objects.
245
    /// </returns>
246
    /// <exception cref="System.ArgumentNullException"><paramref name="factory"/> is null</exception>
247
    public async Task<IEnumerable<TEntity>> QueryAsync<TEntity>(
248
        Func<IDataReader, TEntity> factory,
249
        CommandBehavior commandBehavior = CommandBehavior.SingleResult,
250
        CancellationToken cancellationToken = default)
251
    {
252
        ArgumentNullException.ThrowIfNull(factory);
35✔
253

254
        return await QueryFactoryAsync(async (token) =>
35✔
255
        {
35✔
256
            var results = new List<TEntity>();
35✔
257

35✔
258
            using var reader = await Command.ExecuteReaderAsync(commandBehavior, token).ConfigureAwait(false);
35✔
259
            while (await reader.ReadAsync(token).ConfigureAwait(false))
35✔
260
            {
35✔
261
                var entity = factory(reader);
35✔
262
                results.Add(entity);
35✔
263
            }
35✔
264

35✔
265
            return results;
35✔
266

35✔
267
        }, true, cancellationToken).ConfigureAwait(false);
35✔
268
    }
34✔
269

270

271
    /// <summary>
272
    /// Executes the query and returns the first row in the result as a <typeparamref name="TEntity" /> object.
273
    /// </summary>
274
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
275
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
276
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
277
    /// <returns>
278
    /// A instance of <typeparamref name="TEntity" /> if row exists; otherwise null.
279
    /// </returns>
280
    /// <exception cref="System.ArgumentNullException"><paramref name="factory"/> is null</exception>
281
    public TEntity? QuerySingle<TEntity>(
282
        Func<IDataReader, TEntity> factory,
283
        CommandBehavior commandBehavior = CommandBehavior.SingleResult | CommandBehavior.SingleRow)
284
    {
285
        ArgumentNullException.ThrowIfNull(factory);
30✔
286

287
        return QueryFactory(() =>
30✔
288
        {
30✔
289
            using var reader = Command.ExecuteReader(commandBehavior);
30✔
290
            return reader.Read()
30✔
291
                ? factory(reader)
30✔
292
                : default;
30✔
293

30✔
294
        }, true);
30✔
295
    }
296

297
    /// <summary>
298
    /// Executes the query and returns the first row in the result as a <typeparamref name="TEntity" /> object asynchronously.
299
    /// </summary>
300
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
301
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
302
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
303
    /// <param name="cancellationToken">The cancellation instruction.</param>
304
    /// <returns>
305
    /// A instance of <typeparamref name="TEntity" /> if row exists; otherwise null.
306
    /// </returns>
307
    /// <exception cref="System.ArgumentNullException"><paramref name="factory"/> is null</exception>
308
    public async Task<TEntity?> QuerySingleAsync<TEntity>(
309
        Func<IDataReader, TEntity> factory,
310
        CommandBehavior commandBehavior = CommandBehavior.SingleResult | CommandBehavior.SingleRow,
311
        CancellationToken cancellationToken = default)
312
    {
313
        ArgumentNullException.ThrowIfNull(factory);
22✔
314

315
        return await QueryFactoryAsync(async (token) =>
22✔
316
        {
22✔
317
            using var reader = await Command
22✔
318
                .ExecuteReaderAsync(commandBehavior, token)
22✔
319
                .ConfigureAwait(false);
22✔
320

22✔
321
            return await reader.ReadAsync(token).ConfigureAwait(false)
22✔
322
               ? factory(reader)
22✔
323
               : default;
22✔
324

22✔
325
        }, true, cancellationToken).ConfigureAwait(false);
22✔
326
    }
22✔
327

328

329
    /// <summary>
330
    /// Executes the query and returns the first column of the first row in the result set returned by the query. All other columns and rows are ignored.
331
    /// </summary>
332
    /// <typeparam name="TValue">The type of the value.</typeparam>
333
    /// <param name="convert">The <see langword="delegate" /> to convert the value..</param>
334
    /// <returns>
335
    /// The value of the first column of the first row in the result set.
336
    /// </returns>
337
    public TValue? QueryValue<TValue>(Func<object?, TValue?>? convert)
338
    {
339
        return QueryFactory(() =>
31✔
340
        {
31✔
341
            var result = Command.ExecuteScalar();
31✔
342
            return ConvertExtensions.ConvertValue(result, convert);
31✔
343
        }, true);
31✔
344
    }
345

346
    /// <summary>
347
    /// Executes the query and returns the first column of the first row in the result set returned by the query asynchronously. All other columns and rows are ignored.
348
    /// </summary>
349
    /// <typeparam name="TValue">The type of the value.</typeparam>
350
    /// <param name="convert">The <see langword="delegate" /> to convert the value..</param>
351
    /// <param name="cancellationToken">The cancellation instruction.</param>
352
    /// <returns>
353
    /// The value of the first column of the first row in the result set.
354
    /// </returns>
355
    public async Task<TValue?> QueryValueAsync<TValue>(
356
        Func<object?, TValue?>? convert,
357
        CancellationToken cancellationToken = default)
358
    {
359
        return await QueryFactoryAsync(async (token) =>
29✔
360
        {
29✔
361
            var result = await Command.ExecuteScalarAsync(token).ConfigureAwait(false);
29✔
362
            return ConvertExtensions.ConvertValue(result, convert);
29✔
363
        }, true, cancellationToken).ConfigureAwait(false);
29✔
364
    }
29✔
365

366

367
    /// <summary>
368
    /// Executes the command against the connection and converts the results to a <see cref="DataTable" />.
369
    /// </summary>
370
    /// <returns>
371
    /// A <see cref="DataTable" /> of the results.
372
    /// </returns>
373
    public DataTable QueryTable()
374
    {
375
        return QueryFactory(() =>
3✔
376
        {
3✔
377
            var dataTable = new DataTable();
3✔
378

3✔
379
            using var reader = Command.ExecuteReader();
3✔
380
            dataTable.Load(reader);
3✔
381

3✔
382
            return dataTable;
3✔
383
        }, true);
3✔
384
    }
385

386
    /// <summary>
387
    /// Executes the command against the connection and converts the results to a <see cref="DataTable" /> asynchronously.
388
    /// </summary>
389
    /// <param name="cancellationToken">The cancellation instruction.</param>
390
    /// <returns>
391
    /// A <see cref="DataTable" /> of the results.
392
    /// </returns>
393
    public async Task<DataTable> QueryTableAsync(CancellationToken cancellationToken = default)
394
    {
395
        return await QueryFactoryAsync(async (token) =>
3✔
396
        {
3✔
397
            var dataTable = new DataTable();
3✔
398

3✔
399
            using var reader = await Command.ExecuteReaderAsync(token).ConfigureAwait(false);
3✔
400
            dataTable.Load(reader);
3✔
401

3✔
402
            return dataTable;
3✔
403

3✔
404
        }, true, cancellationToken).ConfigureAwait(false);
3✔
405
    }
3✔
406

407

408
    /// <summary>
409
    /// Executes the command against the connection and sends the resulting <see cref="IDataQuery" /> for reading multiple results sets.
410
    /// </summary>
411
    /// <param name="queryAction">The query action delegate to pass the open <see cref="IDataQuery" /> for reading multiple results.</param>
412
    public void QueryMultiple(Action<IDataQuery> queryAction)
413
    {
414
        ArgumentNullException.ThrowIfNull(queryAction);
3✔
415

416
        QueryFactory(() =>
3✔
417
        {
3✔
418
            using var reader = Command.ExecuteReader();
3✔
419
            var query = new QueryMultipleResult(reader);
3✔
420
            queryAction(query);
3✔
421

3✔
422
            return true;
3✔
423
        }, false);
3✔
424

425
    }
3✔
426

427
    /// <summary>
428
    /// Executes the command against the connection and sends the resulting <see cref="IDataQueryAsync" /> for reading multiple results sets.
429
    /// </summary>
430
    /// <param name="queryAction">The query action delegate to pass the open <see cref="IDataQueryAsync" /> for reading multiple results.</param>
431
    /// <param name="cancellationToken">The cancellation instruction.</param>
432
    public async Task QueryMultipleAsync(
433
        Func<IDataQueryAsync, Task> queryAction,
434
        CancellationToken cancellationToken = default)
435
    {
436
        ArgumentNullException.ThrowIfNull(queryAction);
3✔
437

438
        await QueryFactoryAsync(async (token) =>
3✔
439
        {
3✔
440
            using var reader = await Command.ExecuteReaderAsync(token).ConfigureAwait(false);
3✔
441
            var query = new QueryMultipleResult(reader);
3✔
442
            await queryAction(query).ConfigureAwait(false);
3✔
443

3✔
444
            return true;
3✔
445
        }, false, cancellationToken).ConfigureAwait(false);
3✔
446
    }
3✔
447

448

449
    /// <summary>
450
    /// Executes the command against a connection.
451
    /// </summary>
452
    /// <returns>
453
    /// The number of rows affected.
454
    /// </returns>
455
    public int Execute()
456
    {
457
        return QueryFactory(() =>
53✔
458
        {
53✔
459
            int result = Command.ExecuteNonQuery();
53✔
460
            return result;
53✔
461
        }, false);
53✔
462
    }
463

464
    /// <summary>
465
    /// Executes the command against a connection asynchronously.
466
    /// </summary>
467
    /// <returns>
468
    /// The number of rows affected.
469
    /// </returns>
470
    public async Task<int> ExecuteAsync(CancellationToken cancellationToken = default)
471
    {
472
        return await QueryFactoryAsync(async (token) =>
1✔
473
        {
1✔
474
            int result = await Command.ExecuteNonQueryAsync(token).ConfigureAwait(false);
1✔
475

1✔
476
            return result;
1✔
477
        }, false, cancellationToken).ConfigureAwait(false);
1✔
478
    }
1✔
479

480

481
    /// <summary>
482
    /// Executes the command against the connection and sends the resulting <see cref="IDataReader" /> to the readAction delegate.
483
    /// </summary>
484
    /// <param name="readAction">The read action delegate to pass the open <see cref="IDataReader" />.</param>
485
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
486
    public void Read(
487
        Action<IDataReader> readAction,
488
        CommandBehavior commandBehavior = CommandBehavior.Default)
489
    {
490
        QueryFactory(() =>
9✔
491
        {
9✔
492
            using var reader = Command.ExecuteReader(commandBehavior);
9✔
493
            readAction(reader);
9✔
494

9✔
495
            return true;
9✔
496
        }, false);
9✔
497
    }
9✔
498

499
    /// <summary>
500
    /// Executes the command against the connection and sends the resulting <see cref="IDataReader" /> to the readAction delegate.
501
    /// </summary>
502
    /// <param name="readAction">The read action delegate to pass the open <see cref="IDataReader" />.</param>
503
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
504
    /// <param name="cancellationToken">The cancellation instruction.</param>
505
    public async Task ReadAsync(
506
        Func<IDataReader, CancellationToken, Task> readAction,
507
        CommandBehavior commandBehavior = CommandBehavior.Default,
508
        CancellationToken cancellationToken = default)
509
    {
510
        await QueryFactoryAsync(async (token) =>
14✔
511
        {
14✔
512
            using var reader = await Command.ExecuteReaderAsync(commandBehavior, token).ConfigureAwait(false);
14✔
513
            await readAction(reader, token).ConfigureAwait(false);
14✔
514

14✔
515
            return true;
14✔
516
        }, false, cancellationToken).ConfigureAwait(false);
14✔
517
    }
14✔
518

519

520
    /// <summary>
521
    /// Disposes the managed resources.
522
    /// </summary>
523
    protected override void DisposeManagedResources()
524
    {
525
        Command?.Dispose();
151!
526
    }
151✔
527

528
#if NETCOREAPP3_0_OR_GREATER
529
    /// <summary>
530
    /// Disposes the managed resources.
531
    /// </summary>
532
    protected override async ValueTask DisposeResourcesAsync()
533
    {
534
        if (Command != null)
107✔
535
            await Command.DisposeAsync();
107✔
536
    }
107✔
537
#endif
538

539
    internal void TriggerCallbacks()
540
    {
541
        if (_callbacks.Count == 0)
252✔
542
            return;
241✔
543

544
        while (_callbacks.Count > 0)
22✔
545
        {
546
            var dataCallback = _callbacks.Dequeue();
11✔
547
            dataCallback.Invoke();
11✔
548
        }
549
    }
11✔
550

551

552
    private TResult QueryFactory<TResult>(Func<TResult> query, bool supportCache)
553
    {
554
        ArgumentNullException.ThrowIfNull(query);
151✔
555

556
        AssertDisposed();
151✔
557

558
        var startingTimestamp = Stopwatch.GetTimestamp();
151✔
559

560
        try
561
        {
562
            var cacheKey = CacheKey<TResult>(supportCache);
151✔
563

564
            var (cacheSuccess, cacheValue) = GetCache<TResult>(cacheKey);
151✔
565
            if (cacheSuccess)
151✔
566
                return cacheValue!;
2✔
567

568
            _dataSession.EnsureConnection();
149✔
569

570
            if (_commandInterceptors.Length > 0)
149✔
571
            {
572
                foreach (var ci in _commandInterceptors)
8✔
573
                    ci.CommandExecuting(Command, _dataSession);
2✔
574
            }
575

576
            var results = query();
149✔
577

578
            TriggerCallbacks();
148✔
579

580
            SetCache(cacheKey, results);
148✔
581

582
            return results;
148✔
583
        }
584
        catch (Exception ex)
1✔
585
        {
586
            LogCommand(startingTimestamp, ex);
1✔
587
            startingTimestamp = 0;
1✔
588

589
            throw;
1✔
590
        }
591
        finally
592
        {
593
            LogCommand(startingTimestamp);
151✔
594

595
            _dataSession.ReleaseConnection();
151✔
596
            Dispose();
151✔
597
        }
151✔
598
    }
150✔
599

600
    private async Task<TResult> QueryFactoryAsync<TResult>(
601
        Func<CancellationToken, Task<TResult>> query,
602
        bool supportCache,
603
        CancellationToken cancellationToken = default)
604
    {
605
        ArgumentNullException.ThrowIfNull(query);
107✔
606

607
        AssertDisposed();
107✔
608

609
        var startingTimestamp = Stopwatch.GetTimestamp();
107✔
610

611
        try
612
        {
613
            var cacheKey = CacheKey<TResult>(supportCache);
107✔
614

615
            var (cacheSuccess, cacheValue) = await GetCacheAsync<TResult>(cacheKey, cancellationToken).ConfigureAwait(false);
107✔
616
            if (cacheSuccess)
107✔
617
                return cacheValue!;
2✔
618

619
            await _dataSession.EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
105✔
620

621
            if (_commandInterceptors.Length > 0)
105✔
622
            {
623
                foreach (var ci in _commandInterceptors)
12✔
624
                    await ci.CommandExecutingAsync(Command, _dataSession, cancellationToken).ConfigureAwait(false);
3✔
625
            }
626

627
            var results = await query(cancellationToken).ConfigureAwait(false);
105✔
628

629
            TriggerCallbacks();
104✔
630

631
            await SetCacheAsync(cacheKey, results, cancellationToken).ConfigureAwait(false);
104✔
632

633
            return results;
104✔
634
        }
635
        catch (Exception ex)
1✔
636
        {
637
            LogCommand(startingTimestamp, ex);
1✔
638
            startingTimestamp = 0;
1✔
639

640
            throw;
1✔
641
        }
642
        finally
643
        {
644
            LogCommand(startingTimestamp);
107✔
645

646
#if NETCOREAPP3_0_OR_GREATER
647

648
            await _dataSession.ReleaseConnectionAsync().ConfigureAwait(false);
107✔
649
            await DisposeAsync().ConfigureAwait(false);
107✔
650
#else
651
            _dataSession.ReleaseConnection();
652
            Dispose();
653
#endif
654
        }
655
    }
106✔
656

657

658

659
    private string? CacheKey<T>(bool supportCache)
660
    {
661
        if (!supportCache)
258✔
662
            return null;
83✔
663

664
        if (_dataSession.Cache == null)
175✔
665
            return null;
104✔
666

667
        if (_slidingExpiration == null && _absoluteExpiration == null)
71✔
668
            return null;
65✔
669

670
        var commandText = Command.CommandText;
6✔
671
        var commandType = Command.CommandType;
6✔
672
        var typeName = typeof(T).FullName!;
6✔
673

674
        var hashCode = HashCode.Seed
6✔
675
            .Combine(commandType)
6✔
676
            .Combine(commandText)
6✔
677
            .Combine(typeName);
6✔
678

679
        foreach (IDbDataParameter parameter in Command.Parameters)
32✔
680
        {
681
            if (parameter.Direction is ParameterDirection.InputOutput or ParameterDirection.Output or ParameterDirection.ReturnValue)
10!
UNCOV
682
                throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
683

684
            hashCode = hashCode
10!
685
                .Combine(parameter.ParameterName)
10✔
686
                .Combine(parameter.Value ?? DBNull.Value)
10✔
687
                .Combine(parameter.DbType);
10✔
688
        }
689

690
        return $"fluent:data:query:{hashCode:X}";
6✔
691
    }
692

693
    private (bool Success, T? Value) GetCache<T>(string? key)
694
    {
695
        if (_slidingExpiration == null && _absoluteExpiration == null)
151✔
696
            return (false, default);
141✔
697

698
        if (key == null)
10✔
699
            return (false, default);
8✔
700

701
        var cache = _dataSession.Cache;
2✔
702
        if (cache == null)
2!
UNCOV
703
            return (false, default);
×
704

705
        return cache.Get<T>(key);
2✔
706
    }
707

708
    private async Task<(bool Success, T? Value)> GetCacheAsync<T>(string? key, CancellationToken cancellationToken)
709
    {
710
        if (_slidingExpiration == null && _absoluteExpiration == null)
107✔
711
            return (false, default);
95✔
712

713
        if (key == null)
12✔
714
            return (false, default);
8✔
715

716
        var cache = _dataSession.Cache;
4✔
717
        if (cache == null)
4!
UNCOV
718
            return (false, default);
×
719

720
        return await cache
4✔
721
            .GetAsync<T>(key, cancellationToken)
4✔
722
            .ConfigureAwait(false);
4✔
723
    }
107✔
724

725
    private void SetCache<T>(string? key, T value)
726
    {
727
        if (_slidingExpiration == null && _absoluteExpiration == null)
148✔
728
            return;
140✔
729

730
        if (key == null || value == null)
8!
731
            return;
8✔
732

UNCOV
733
        var cache = _dataSession.Cache;
×
UNCOV
734
        if (cache == null)
×
UNCOV
735
            return;
×
736

UNCOV
737
        cache.Set(key, value, _absoluteExpiration, _slidingExpiration);
×
UNCOV
738
    }
×
739

740
    private async Task SetCacheAsync<T>(string? key, T value, CancellationToken cancellationToken)
741
    {
742
        if (_slidingExpiration == null && _absoluteExpiration == null)
104✔
743
            return;
94✔
744

745
        if (key == null || value == null)
10✔
746
            return;
8✔
747

748
        var cache = _dataSession.Cache;
2✔
749
        if (cache == null)
2!
UNCOV
750
            return;
×
751

752
        await cache
2✔
753
            .SetAsync(key, value, _absoluteExpiration, _slidingExpiration, cancellationToken)
2✔
754
            .ConfigureAwait(false);
2✔
755
    }
104✔
756

757

758
    private void LogCommand(long startingTimestamp, Exception? exception = null)
759
    {
760
        // indicates already logged
761
        if (startingTimestamp == 0)
260✔
762
            return;
2✔
763

764
        var endingTimestamp = Stopwatch.GetTimestamp();
258✔
765

766
#if NET7_0_OR_GREATER
767
        var duration = Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
258✔
768
#else
769
        var duration = new TimeSpan((long)((endingTimestamp - startingTimestamp) * _tickFrequency));
770
#endif
771

772
        _dataSession.QueryLogger?.LogCommand(Command, duration, exception, _logState);
258✔
773
    }
252✔
774

UNCOV
775
    private static readonly double _tickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
×
776
}
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