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

loresoft / FluentCommand / 23278216331

19 Mar 2026 03:19AM UTC coverage: 57.398% (+0.7%) from 56.658%
23278216331

push

github

pwelter34
Enable nullable and improve source generators

1403 of 3069 branches covered (45.72%)

Branch coverage included in aggregate %.

527 of 907 new or added lines in 58 files covered. (58.1%)

22 existing lines in 10 files now uncovered.

4288 of 6846 relevant lines covered (62.64%)

330.58 hits per line

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

84.91
/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
    public DataCommand(
168✔
32
        IDataSession dataSession,
168✔
33
        DbTransaction? transaction,
168✔
34
        IDataCommandInterceptor[]? commandInterceptors = null)
168✔
35
    {
36
        _callbacks = new Queue<DataCallback>();
168✔
37
        _dataSession = dataSession ?? throw new ArgumentNullException(nameof(dataSession));
168!
38

39
        Command = dataSession.Connection.CreateCommand();
168✔
40
        Command.Transaction = transaction;
168✔
41

42
        _commandInterceptors = commandInterceptors ?? [];
168!
43
    }
168✔
44

45
    /// <summary>
46
    /// Gets the underlying <see cref="DbCommand"/> for this <see cref="DataCommand"/>.
47
    /// </summary>
48
    public DbCommand Command { get; }
1,808✔
49

50

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

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

79

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

93

94
    /// <summary>
95
    /// Adds the parameter to the underlying command.
96
    /// </summary>
97
    /// <param name="parameter">The <see cref="DbParameter" /> to add.</param>
98
    /// <returns>
99
    /// A fluent <see langword="interface" /> to the data command.
100
    /// </returns>
101
    /// <exception cref="ArgumentNullException"><paramref name="parameter"/> is null</exception>
102
    public IDataCommand Parameter(DbParameter parameter)
103
    {
104
        if (parameter == null)
342!
105
            throw new ArgumentNullException(nameof(parameter));
×
106

107
        Command.Parameters.Add(parameter);
342✔
108
        return this;
342✔
109
    }
110

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

125
        return this;
8✔
126
    }
127

128

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

145
        return this;
22✔
146
    }
147

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

162
        return this;
×
163
    }
164

165

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

184
        return this;
×
185
    }
186

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

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

219
        return QueryFactory(() =>
19✔
220
        {
19✔
221
            var results = new List<TEntity>();
19✔
222

19✔
223
            using var reader = Command.ExecuteReader(commandBehavior);
19✔
224
            while (reader.Read())
1,315✔
225
            {
19✔
226
                var entity = factory(reader);
1,297✔
227
                results.Add(entity);
1,297✔
228
            }
19✔
229

19✔
230
            return results;
18✔
231
        }, true);
37✔
232
    }
233

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

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

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

35✔
264
            return results;
33✔
265

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

269

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

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

18✔
294
        }, true);
34✔
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
        if (factory == null)
22!
314
            throw new ArgumentNullException(nameof(factory));
×
315

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

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

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

329

330
    /// <summary>
331
    /// 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.
332
    /// </summary>
333
    /// <typeparam name="TValue">The type of the value.</typeparam>
334
    /// <param name="convert">The <see langword="delegate" /> to convert the value..</param>
335
    /// <returns>
336
    /// The value of the first column of the first row in the result set.
337
    /// </returns>
338
    public TValue? QueryValue<TValue>(Func<object?, TValue?>? convert)
339
    {
340
        return QueryFactory(() =>
8✔
341
        {
8✔
342
            var result = Command.ExecuteScalar();
8✔
343
            return ConvertExtensions.ConvertValue(result, convert);
8✔
344
        }, true);
8✔
345
    }
346

347
    /// <summary>
348
    /// 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.
349
    /// </summary>
350
    /// <typeparam name="TValue">The type of the value.</typeparam>
351
    /// <param name="convert">The <see langword="delegate" /> to convert the value..</param>
352
    /// <param name="cancellationToken">The cancellation instruction.</param>
353
    /// <returns>
354
    /// The value of the first column of the first row in the result set.
355
    /// </returns>
356
    public async Task<TValue?> QueryValueAsync<TValue>(
357
        Func<object?, TValue?>? convert,
358
        CancellationToken cancellationToken = default)
359
    {
360
        return await QueryFactoryAsync(async (token) =>
29✔
361
        {
29✔
362
            var result = await Command.ExecuteScalarAsync(token).ConfigureAwait(false);
29✔
363
            return ConvertExtensions.ConvertValue(result, convert);
29✔
364
        }, true, cancellationToken).ConfigureAwait(false);
58✔
365
    }
29✔
366

367

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

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

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

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

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

3✔
403
            return dataTable;
3✔
404

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

408

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

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

3✔
424
            return true;
3✔
425
        }, false);
6✔
426

427
    }
3✔
428

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

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

3✔
447
            return true;
3✔
448
        }, false, cancellationToken).ConfigureAwait(false);
6✔
449
    }
3✔
450

451

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

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

1✔
479
            return result;
1✔
480
        }, false, cancellationToken).ConfigureAwait(false);
2✔
481
    }
1✔
482

483

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

7✔
498
            return true;
7✔
499
        }, false);
14✔
500
    }
7✔
501

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

10✔
518
            return true;
10✔
519
        }, false, cancellationToken).ConfigureAwait(false);
20✔
520
    }
10✔
521

522

523
    /// <summary>
524
    /// Disposes the managed resources.
525
    /// </summary>
526
    protected override void DisposeManagedResources()
527
    {
528
        Command?.Dispose();
64!
529
    }
64✔
530

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

542
    internal void TriggerCallbacks()
543
    {
544
        if (_callbacks.Count == 0)
161✔
545
            return;
153✔
546

547
        while (_callbacks.Count > 0)
16✔
548
        {
549
            var dataCallback = _callbacks.Dequeue();
8✔
550
            dataCallback.Invoke();
8✔
551
        }
552
    }
8✔
553

554

555
    private TResult QueryFactory<TResult>(Func<TResult> query, bool supportCache)
556
    {
557
        if (query == null)
64!
558
            throw new ArgumentNullException(nameof(query));
×
559

560
        AssertDisposed();
64✔
561

562
        var startingTimestamp = Stopwatch.GetTimestamp();
64✔
563

564
        try
565
        {
566
            var cacheKey = CacheKey<TResult>(supportCache);
64✔
567

568
            var (cacheSuccess, cacheValue) = GetCache<TResult>(cacheKey);
64✔
569
            if (cacheSuccess)
64✔
570
                return cacheValue!;
2✔
571

572
            _dataSession.EnsureConnection();
62✔
573

574
            if (_commandInterceptors.Length > 0)
62✔
575
            {
576
                foreach (var ci in _commandInterceptors)
8✔
577
                    ci.CommandExecuting(Command, _dataSession);
2✔
578
            }
579

580
            var results = query();
62✔
581

582
            TriggerCallbacks();
61✔
583

584
            SetCache(cacheKey, results);
61✔
585

586
            return results;
61✔
587
        }
588
        catch (Exception ex)
1✔
589
        {
590
            LogCommand(startingTimestamp, ex);
1✔
591
            startingTimestamp = 0;
1✔
592

593
            throw;
1✔
594
        }
595
        finally
596
        {
597
            LogCommand(startingTimestamp);
64✔
598

599
            _dataSession.ReleaseConnection();
64✔
600
            Dispose();
64✔
601
        }
64✔
602
    }
63✔
603

604
    private async Task<TResult> QueryFactoryAsync<TResult>(
605
        Func<CancellationToken, Task<TResult>> query,
606
        bool supportCache,
607
        CancellationToken cancellationToken = default)
608
    {
609
        if (query == null)
103!
610
            throw new ArgumentNullException(nameof(query));
×
611

612
        AssertDisposed();
103✔
613

614
        var startingTimestamp = Stopwatch.GetTimestamp();
103✔
615

616
        try
617
        {
618
            var cacheKey = CacheKey<TResult>(supportCache);
103✔
619

620
            var (cacheSuccess, cacheValue) = await GetCacheAsync<TResult>(cacheKey, cancellationToken).ConfigureAwait(false);
103✔
621
            if (cacheSuccess)
103✔
622
                return cacheValue!;
2✔
623

624
            await _dataSession.EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
101✔
625

626
            if (_commandInterceptors.Length > 0)
101✔
627
            {
628
                foreach (var ci in _commandInterceptors)
12✔
629
                    await ci.CommandExecutingAsync(Command, _dataSession, cancellationToken).ConfigureAwait(false);
3✔
630
            }
631

632
            var results = await query(cancellationToken).ConfigureAwait(false);
101✔
633

634
            TriggerCallbacks();
100✔
635

636
            await SetCacheAsync(cacheKey, results, cancellationToken).ConfigureAwait(false);
100✔
637

638
            return results;
100✔
639
        }
640
        catch (Exception ex)
1✔
641
        {
642
            LogCommand(startingTimestamp, ex);
1✔
643
            startingTimestamp = 0;
1✔
644

645
            throw;
1✔
646
        }
647
        finally
648
        {
649
            LogCommand(startingTimestamp);
103✔
650

651
#if NETCOREAPP3_0_OR_GREATER
652

653
            await _dataSession.ReleaseConnectionAsync().ConfigureAwait(false);
103✔
654
            await DisposeAsync().ConfigureAwait(false);
103✔
655
#else
656
            _dataSession.ReleaseConnection();
657
            Dispose();
658
#endif
659
        }
660
    }
102✔
661

662

663

664
    private string? CacheKey<T>(bool supportCache)
665
    {
666
        if (!supportCache)
167✔
667
            return null;
30✔
668

669
        if (_dataSession.Cache == null)
137✔
670
            return null;
79✔
671

672
        if (_slidingExpiration == null && _absoluteExpiration == null)
58✔
673
            return null;
52✔
674

675
        var commandText = Command.CommandText;
6✔
676
        var commandType = Command.CommandType;
6✔
677
        var typeName = typeof(T).FullName!;
6✔
678

679
        var hashCode = HashCode.Seed
6✔
680
            .Combine(commandType)
6✔
681
            .Combine(commandText)
6✔
682
            .Combine(typeName);
6✔
683

684
        foreach (IDbDataParameter parameter in Command.Parameters)
32✔
685
        {
686
            if (parameter.Direction is ParameterDirection.InputOutput or ParameterDirection.Output or ParameterDirection.ReturnValue)
10!
687
                throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
688

689
            hashCode = hashCode
10!
690
                .Combine(parameter.ParameterName)
10✔
691
                .Combine(parameter.Value ?? DBNull.Value)
10✔
692
                .Combine(parameter.DbType);
10✔
693
        }
694

695
        return $"fluent:data:query:{hashCode:X}";
6✔
696
    }
697

698
    private (bool Success, T? Value) GetCache<T>(string? key)
699
    {
700
        if (_slidingExpiration == null && _absoluteExpiration == null)
64✔
701
            return (false, default);
54✔
702

703
        if (key == null)
10✔
704
            return (false, default);
8✔
705

706
        var cache = _dataSession.Cache;
2✔
707
        if (cache == null)
2!
708
            return (false, default);
×
709

710
        return cache.Get<T>(key);
2✔
711
    }
712

713
    private async Task<(bool Success, T? Value)> GetCacheAsync<T>(string? key, CancellationToken cancellationToken)
714
    {
715
        if (_slidingExpiration == null && _absoluteExpiration == null)
103✔
716
            return (false, default);
91✔
717

718
        if (key == null)
12✔
719
            return (false, default);
8✔
720

721
        var cache = _dataSession.Cache;
4✔
722
        if (cache == null)
4!
723
            return (false, default);
×
724

725
        return await cache
4✔
726
            .GetAsync<T>(key, cancellationToken)
4✔
727
            .ConfigureAwait(false);
4✔
728
    }
103✔
729

730
    private void SetCache<T>(string? key, T value)
731
    {
732
        if (_slidingExpiration == null && _absoluteExpiration == null)
61✔
733
            return;
53✔
734

735
        if (key == null || value == null)
8!
736
            return;
8✔
737

738
        var cache = _dataSession.Cache;
×
739
        if (cache == null)
×
740
            return;
×
741

742
        cache.Set(key, value, _absoluteExpiration, _slidingExpiration);
×
743
    }
×
744

745
    private async Task SetCacheAsync<T>(string? key, T value, CancellationToken cancellationToken)
746
    {
747
        if (_slidingExpiration == null && _absoluteExpiration == null)
100✔
748
            return;
90✔
749

750
        if (key == null || value == null)
10✔
751
            return;
8✔
752

753
        var cache = _dataSession.Cache;
2✔
754
        if (cache == null)
2!
755
            return;
×
756

757
        await cache
2✔
758
            .SetAsync(key, value, _absoluteExpiration, _slidingExpiration, cancellationToken)
2✔
759
            .ConfigureAwait(false);
2✔
760
    }
100✔
761

762

763
    private void LogCommand(long startingTimestamp, Exception? exception = null)
764
    {
765
        // indicates already logged
766
        if (startingTimestamp == 0)
169✔
767
            return;
2✔
768

769
        var endingTimestamp = Stopwatch.GetTimestamp();
167✔
770

771
#if NET7_0_OR_GREATER
772
        var duration = Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
167✔
773
#else
774
        var duration = new TimeSpan((long)((endingTimestamp - startingTimestamp) * _tickFrequency));
775
#endif
776

777
        _dataSession.QueryLogger?.LogCommand(Command, duration, exception, _logState);
167✔
778
    }
162✔
779

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