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

loresoft / FluentCommand / 8973726646

06 May 2024 06:14PM UTC coverage: 53.644% (-0.8%) from 54.48%
8973726646

push

github

pwelter34
fix test snapshots

1165 of 2845 branches covered (40.95%)

Branch coverage included in aggregate %.

3686 of 6198 relevant lines covered (59.47%)

697.06 hits per line

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

82.64
/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

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

24
    /// <summary>
25
    /// Initializes a new instance of the <see cref="DataCommand" /> class.
26
    /// </summary>
27
    /// <param name="dataSession">The data session.</param>
28
    /// <param name="transaction">The DbTransaction for this DataCommand.</param>
29
    public DataCommand(IDataSession dataSession, DbTransaction transaction)
444✔
30
    {
31
        _callbacks = new Queue<DataCallback>();
444✔
32
        _dataSession = dataSession ?? throw new ArgumentNullException(nameof(dataSession));
444!
33
        Command = dataSession.Connection.CreateCommand();
444✔
34
        Command.Transaction = transaction;
444✔
35
    }
444✔
36

37
    /// <summary>
38
    /// Gets the underlying <see cref="DbCommand"/> for this <see cref="DataCommand"/>.
39
    /// </summary>
40
    public DbCommand Command { get; }
4,821✔
41

42

43
    /// <summary>
44
    /// Set the data command with the specified SQL statement.
45
    /// </summary>
46
    /// <param name="sql">The SQL statement.</param>
47
    /// <returns>
48
    /// A fluent <see langword="interface" /> to a data command.
49
    /// </returns>
50
    public IDataCommand Sql(string sql)
51
    {
52
        Command.CommandText = sql;
429✔
53
        Command.CommandType = CommandType.Text;
429✔
54
        return this;
429✔
55
    }
56

57
    /// <summary>
58
    /// Set the data command with the specified stored procedure name.
59
    /// </summary>
60
    /// <param name="storedProcedure">Name of the stored procedure.</param>
61
    /// <returns>
62
    /// A fluent <see langword="interface" /> to a data command.
63
    /// </returns>
64
    public IDataCommand StoredProcedure(string storedProcedure)
65
    {
66
        Command.CommandText = storedProcedure;
15✔
67
        Command.CommandType = CommandType.StoredProcedure;
15✔
68
        return this;
15✔
69
    }
70

71

72
    /// <summary>
73
    /// Sets the wait time before terminating the attempt to execute a command and generating an error.
74
    /// </summary>
75
    /// <param name="timeout">The time, in seconds, to wait for the command to execute.</param>
76
    /// <returns>
77
    /// A fluent <see langword="interface" /> to the data command.
78
    /// </returns>
79
    public IDataCommand CommandTimeout(int timeout)
80
    {
81
        Command.CommandTimeout = timeout;
3✔
82
        return this;
3✔
83
    }
84

85

86
    /// <summary>
87
    /// Adds the parameter to the underlying command.
88
    /// </summary>
89
    /// <param name="parameter">The <see cref="DbParameter" /> to add.</param>
90
    /// <returns>
91
    /// A fluent <see langword="interface" /> to the data command.
92
    /// </returns>
93
    /// <exception cref="ArgumentNullException"><paramref name="parameter"/> is null</exception>
94
    public IDataCommand Parameter(DbParameter parameter)
95
    {
96
        if (parameter == null)
915!
97
            throw new ArgumentNullException(nameof(parameter));
×
98

99
        Command.Parameters.Add(parameter);
915✔
100
        return this;
915✔
101
    }
102

103
    /// <summary>
104
    /// Register a return value <paramref name="callback" /> for the specified <paramref name="parameter" />.
105
    /// </summary>
106
    /// <typeparam name="TParameter">The type of the parameter value.</typeparam>
107
    /// <param name="parameter">The <see cref="IDbDataParameter" /> to add.</param>
108
    /// <param name="callback">The callback used to get the out value.</param>
109
    /// <returns>
110
    /// A fluent <see langword="interface" /> to the data command.
111
    /// </returns>
112
    public IDataCommand RegisterCallback<TParameter>(DbParameter parameter, Action<TParameter> callback)
113
    {
114
        var dataCallback = new DataCallback(typeof(TParameter), parameter, callback);
15✔
115
        _callbacks.Enqueue(dataCallback);
15✔
116

117
        return this;
15✔
118
    }
119

120

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

137
        return this;
66✔
138
    }
139

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

154
        return this;
×
155
    }
156

157

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

176
        return this;
×
177
    }
178

179
    /// <summary>
180
    /// Use to pass a state to the <see cref="IDataQueryLogger" />.
181
    /// </summary>
182
    /// <param name="state">The state to pass to the logger.</param>
183
    /// <returns>
184
    /// A fluent <see langword="interface" /> to the data command.
185
    /// </returns>
186
    /// <remarks>
187
    /// Use the state to help control what is logged.
188
    /// </remarks>
189
    public IDataCommand LogState(object state)
190
    {
191
        _logState = state;
×
192
        return this;
×
193
    }
194

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

211
        return QueryFactory(() =>
48✔
212
        {
48✔
213
            var results = new List<TEntity>();
48✔
214

48✔
215
            using var reader = Command.ExecuteReader(commandBehavior);
48✔
216
            while (reader.Read())
3,627✔
217
            {
48✔
218
                var entity = factory(reader);
3,582✔
219
                results.Add(entity);
3,582✔
220
            }
48✔
221

48✔
222
            return results;
45✔
223
        }, true);
93✔
224
    }
225

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

245
        return await QueryFactoryAsync(async (token) =>
99✔
246
        {
99✔
247
            var results = new List<TEntity>();
96✔
248

99✔
249
            using var reader = await Command.ExecuteReaderAsync(commandBehavior, token);
96✔
250
            while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false))
552✔
251
            {
99✔
252
                var entity = factory(reader);
459✔
253
                results.Add(entity);
459✔
254
            }
99✔
255

99✔
256
            return results;
93✔
257

99✔
258
        }, true, cancellationToken).ConfigureAwait(false);
192✔
259
    }
96✔
260

261

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

279
        return QueryFactory(() =>
51✔
280
        {
51✔
281
            using var reader = Command.ExecuteReader(commandBehavior);
45✔
282
            var result = reader.Read()
45!
283
                ? factory(reader)
45✔
284
                : default;
45✔
285

51✔
286
            return result;
45✔
287
        }, true);
96✔
288
    }
289

290
    /// <summary>
291
    /// Executes the query and returns the first row in the result as a <typeparamref name="TEntity" /> object asynchronously.
292
    /// </summary>
293
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
294
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
295
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
296
    /// <param name="cancellationToken">The cancellation instruction.</param>
297
    /// <returns>
298
    /// A instance of <typeparamref name="TEntity" /> if row exists; otherwise null.
299
    /// </returns>
300
    /// <exception cref="System.ArgumentNullException"><paramref name="factory"/> is null</exception>
301
    public async Task<TEntity> QuerySingleAsync<TEntity>(
302
        Func<IDataReader, TEntity> factory,
303
        CommandBehavior commandBehavior = CommandBehavior.SingleResult | CommandBehavior.SingleRow,
304
        CancellationToken cancellationToken = default)
305
    {
306
        if (factory == null)
66!
307
            throw new ArgumentNullException(nameof(factory));
×
308

309
        return await QueryFactoryAsync(async (token) =>
66✔
310
        {
66✔
311
            using var reader = await Command
63✔
312
                .ExecuteReaderAsync(commandBehavior, token)
63✔
313
                .ConfigureAwait(false);
63✔
314

66✔
315
            var result = await reader.ReadAsync(token).ConfigureAwait(false)
63!
316
               ? factory(reader)
63✔
317
               : default;
63✔
318

66✔
319
            return result;
63✔
320
        }, true, cancellationToken).ConfigureAwait(false);
129✔
321
    }
66✔
322

323

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

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

361

362
    /// <summary>
363
    /// Executes the command against the connection and converts the results to a <see cref="DataTable" />.
364
    /// </summary>
365
    /// <returns>
366
    /// A <see cref="DataTable" /> of the results.
367
    /// </returns>
368
    public DataTable QueryTable()
369
    {
370
        return QueryFactory(() =>
9✔
371
        {
9✔
372
            var dataTable = new DataTable();
9✔
373

9✔
374
            using var reader = Command.ExecuteReader();
9✔
375
            dataTable.Load(reader);
9✔
376

9✔
377
            return dataTable;
9✔
378
        }, true);
18✔
379
    }
380

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

9✔
394
            using var reader = await Command.ExecuteReaderAsync(token).ConfigureAwait(false);
9✔
395
            dataTable.Load(reader);
9✔
396

9✔
397
            return dataTable;
9✔
398

9✔
399
        }, true, cancellationToken).ConfigureAwait(false);
18✔
400
    }
9✔
401

402

403
    /// <summary>
404
    /// Executes the command against the connection and sends the resulting <see cref="IDataQuery" /> for reading multiple results sets.
405
    /// </summary>
406
    /// <param name="queryAction">The query action delegate to pass the open <see cref="IDataQuery" /> for reading multiple results.</param>
407
    public void QueryMultiple(Action<IDataQuery> queryAction)
408
    {
409
        if (queryAction == null)
9!
410
            throw new ArgumentNullException(nameof(queryAction));
×
411

412
        QueryFactory(() =>
9✔
413
        {
9✔
414
            using var reader = Command.ExecuteReader();
9✔
415
            var query = new QueryMultipleResult(reader);
9✔
416
            queryAction(query);
9✔
417

9✔
418
            return true;
9✔
419
        }, false);
18✔
420

421
    }
9✔
422

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

435
        await QueryFactoryAsync(async (token) =>
9✔
436
        {
9✔
437
            using var reader = await Command.ExecuteReaderAsync(token).ConfigureAwait(false);
9✔
438
            var query = new QueryMultipleResult(reader);
9✔
439
            await queryAction(query).ConfigureAwait(false);
9✔
440

9✔
441
            return true;
9✔
442
        }, false, cancellationToken).ConfigureAwait(false);
18✔
443
    }
9✔
444

445

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

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

×
473
            return result;
×
474
        }, false, cancellationToken).ConfigureAwait(false);
×
475
    }
×
476

477

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

21✔
492
            return true;
21✔
493
        }, false);
42✔
494
    }
21✔
495

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

27✔
512
            return true;
27✔
513
        }, false, cancellationToken).ConfigureAwait(false);
54✔
514
    }
27✔
515

516

517
    /// <summary>
518
    /// Disposes the managed resources.
519
    /// </summary>
520
    protected override void DisposeManagedResources()
521
    {
522
        Command?.Dispose();
153!
523
    }
153✔
524

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

536
    internal void TriggerCallbacks()
537
    {
538
        if (_callbacks.Count == 0)
423✔
539
            return;
408✔
540

541
        while (_callbacks.Count > 0)
30✔
542
        {
543
            var dataCallback = _callbacks.Dequeue();
15✔
544
            dataCallback.Invoke();
15✔
545
        }
546
    }
15✔
547

548

549
    private TResult QueryFactory<TResult>(Func<TResult> query, bool supportCache)
550
    {
551
        if (query == null)
153!
552
            throw new ArgumentNullException(nameof(query));
×
553

554
        AssertDisposed();
153✔
555

556
        var startingTimestamp = Stopwatch.GetTimestamp();
153✔
557

558
        try
559
        {
560
            var cacheKey = CacheKey<TResult>(supportCache);
153✔
561

562
            var (cacheSuccess, cacheValue) = GetCache<TResult>(cacheKey);
153✔
563
            if (cacheSuccess)
153✔
564
                return cacheValue;
6✔
565

566
            _dataSession.EnsureConnection();
147✔
567

568
            var results = query();
147✔
569

570
            TriggerCallbacks();
144✔
571

572
            SetCache(cacheKey, results);
144✔
573

574
            return results;
144✔
575
        }
576
        catch (Exception ex)
3✔
577
        {
578
            LogCommand(startingTimestamp, ex);
3✔
579
            startingTimestamp = 0;
3✔
580

581
            throw;
3✔
582
        }
583
        finally
584
        {
585
            LogCommand(startingTimestamp);
153✔
586

587
            _dataSession.ReleaseConnection();
153✔
588
            Dispose();
153✔
589
        }
153✔
590
    }
150✔
591

592
    private async Task<TResult> QueryFactoryAsync<TResult>(
593
        Func<CancellationToken, Task<TResult>> query,
594
        bool supportCache,
595
        CancellationToken cancellationToken = default)
596
    {
597
        if (query == null)
288!
598
            throw new ArgumentNullException(nameof(query));
×
599

600
        AssertDisposed();
288✔
601

602
        var startingTimestamp = Stopwatch.GetTimestamp();
288✔
603

604
        try
605
        {
606
            var cacheKey = CacheKey<TResult>(supportCache);
288✔
607

608
            var (cacheSuccess, cacheValue) = await GetCacheAsync<TResult>(cacheKey, cancellationToken).ConfigureAwait(false);
288✔
609
            if (cacheSuccess)
288✔
610
                return cacheValue;
6✔
611

612
            await _dataSession.EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
282✔
613

614
            var results = await query(cancellationToken).ConfigureAwait(false);
282✔
615

616
            TriggerCallbacks();
279✔
617

618
            await SetCacheAsync(cacheKey, results, cancellationToken).ConfigureAwait(false);
279✔
619

620
            return results;
279✔
621
        }
622
        catch (Exception ex)
3✔
623
        {
624
            LogCommand(startingTimestamp, ex);
3✔
625
            startingTimestamp = 0;
3✔
626

627
            throw;
3✔
628
        }
629
        finally
630
        {
631
            LogCommand(startingTimestamp);
288✔
632

633
#if NETCOREAPP3_0_OR_GREATER
634

635
            await _dataSession.ReleaseConnectionAsync();
288✔
636
            await DisposeAsync();
288!
637
#else
638
            _dataSession.ReleaseConnection();
639
            Dispose();
640
#endif
641
        }
642
    }
285✔
643

644

645

646
    private string CacheKey<T>(bool supportCache)
647
    {
648
        if (!supportCache)
441✔
649
            return null;
72✔
650

651
        if (_dataSession.Cache == null)
369✔
652
            return null;
222✔
653

654
        if (_slidingExpiration == null && _absoluteExpiration == null)
147✔
655
            return null;
129✔
656

657
        var commandText = Command.CommandText;
18✔
658
        var commandType = Command.CommandType;
18✔
659
        var typeName = typeof(T).FullName;
18✔
660

661
        var hashCode = HashCode.Seed
18✔
662
            .Combine(commandType)
18✔
663
            .Combine(commandText)
18✔
664
            .Combine(typeName);
18✔
665

666
        foreach (IDbDataParameter parameter in Command.Parameters)
96✔
667
        {
668
            if (parameter.Direction is ParameterDirection.InputOutput or ParameterDirection.Output or ParameterDirection.ReturnValue)
30!
669
                throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
670

671
            hashCode = hashCode
30✔
672
                .Combine(parameter.ParameterName)
30✔
673
                .Combine(parameter.Value)
30✔
674
                .Combine(parameter.DbType);
30✔
675
        }
676

677
        return $"fluent:data:query:{hashCode:X}";
18✔
678
    }
679

680
    private (bool Success, T Value) GetCache<T>(string key)
681
    {
682
        if (_slidingExpiration == null && _absoluteExpiration == null)
153✔
683
            return (false, default);
123✔
684

685
        if (key == null)
30✔
686
            return (false, default);
24✔
687

688
        var cache = _dataSession.Cache;
6✔
689
        if (cache == null)
6!
690
            return (false, default);
×
691

692
        return cache.Get<T>(key);
6✔
693
    }
694

695
    private async Task<(bool Success, T Value)> GetCacheAsync<T>(string key, CancellationToken cancellationToken)
696
    {
697
        if (_slidingExpiration == null && _absoluteExpiration == null)
288✔
698
            return (false, default);
252✔
699

700
        if (key == null)
36✔
701
            return (false, default);
24✔
702

703
        var cache = _dataSession.Cache;
12✔
704
        if (cache == null)
12!
705
            return (false, default);
×
706

707
        return await cache
12✔
708
            .GetAsync<T>(key, cancellationToken)
12✔
709
            .ConfigureAwait(false);
12✔
710
    }
288✔
711

712
    private void SetCache<T>(string key, T value)
713
    {
714
        if (_slidingExpiration == null && _absoluteExpiration == null)
144✔
715
            return;
120✔
716

717
        if (key == null || value == null)
24!
718
            return;
24✔
719

720
        var cache = _dataSession.Cache;
×
721
        if (cache == null)
×
722
            return;
×
723

724
        cache.Set(key, value, _absoluteExpiration, _slidingExpiration);
×
725
    }
×
726

727
    private async Task SetCacheAsync<T>(string key, T value, CancellationToken cancellationToken)
728
    {
729
        if (_slidingExpiration == null && _absoluteExpiration == null)
279✔
730
            return;
249✔
731

732
        if (key == null || value == null)
30✔
733
            return;
24✔
734

735
        var cache = _dataSession.Cache;
6✔
736
        if (cache == null)
6!
737
            return;
×
738

739
        await cache
6✔
740
            .SetAsync(key, value, _absoluteExpiration, _slidingExpiration, cancellationToken)
6✔
741
            .ConfigureAwait(false);
6✔
742
    }
279✔
743

744

745
    private void LogCommand(long startingTimestamp, Exception exception = null)
746
    {
747
        // indicates already logged
748
        if (startingTimestamp == 0)
447✔
749
            return;
6✔
750

751
        var endingTimestamp = Stopwatch.GetTimestamp();
441✔
752

753
#if NET7_0_OR_GREATER
754
        var duration = Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
294✔
755
#else
756
        var duration = new TimeSpan((long)((endingTimestamp - startingTimestamp) * _tickFrequency));
147✔
757
#endif
758

759
        _dataSession.QueryLogger?.LogCommand(Command, duration, exception, _logState);
441!
760
    }
441✔
761

762
    private static readonly double _tickFrequency = (double)TimeSpan.TicksPerSecond / Stopwatch.Frequency;
3✔
763
}
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