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

loresoft / FluentCommand / 6154463617

12 Sep 2023 02:51AM UTC coverage: 51.125% (-0.04%) from 51.166%
6154463617

push

github

pwelter34
more small fixes

948 of 2392 branches covered (0.0%)

Branch coverage included in aggregate %.

9 of 9 new or added lines in 1 file covered. (100.0%)

2777 of 4894 relevant lines covered (56.74%)

161.44 hits per line

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

80.08
/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)
141✔
30
    {
31
        _callbacks = new Queue<DataCallback>();
141✔
32
        _dataSession = dataSession ?? throw new ArgumentNullException(nameof(dataSession));
141!
33
        Command = dataSession.Connection.CreateCommand();
141✔
34
        Command.Transaction = transaction;
141✔
35
    }
141✔
36

37
    /// <summary>
38
    /// Gets the underlying <see cref="DbCommand"/> for this <see cref="DataCommand"/>.
39
    /// </summary>
40
    public DbCommand Command { get; }
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;
136✔
53
        Command.CommandType = CommandType.Text;
136✔
54
        return this;
136✔
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;
5✔
67
        Command.CommandType = CommandType.StoredProcedure;
5✔
68
        return this;
5✔
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;
1✔
82
        return this;
1✔
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)
305!
97
            throw new ArgumentNullException(nameof(parameter));
×
98

99
        Command.Parameters.Add(parameter);
305✔
100
        return this;
305✔
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);
5✔
115
        _callbacks.Enqueue(dataCallback);
5✔
116

117
        return this;
5✔
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;
22✔
134
        if (_slidingExpiration != null && _callbacks.Count > 0)
22!
135
            throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
136

137
        return this;
22✔
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)
16!
209
            throw new ArgumentNullException(nameof(factory));
×
210

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

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

16✔
222
            return results;
16✔
223
        }, true);
16✔
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)
243
            throw new ArgumentNullException(nameof(factory));
244

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

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

256
            return results;
257

258
        }, true, cancellationToken).ConfigureAwait(false);
259
    }
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)
17!
277
            throw new ArgumentNullException(nameof(factory));
×
278

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

17✔
286
            return result;
17✔
287
        }, true);
17✔
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)
307
            throw new ArgumentNullException(nameof(factory));
308

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

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

319
            return result;
320
        }, true, cancellationToken).ConfigureAwait(false);
321
    }
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(() =>
3✔
335
        {
3✔
336
            var result = Command.ExecuteScalar();
3✔
337
            var value = result.ConvertValue(convert);
3✔
338

3✔
339
            return value;
3✔
340
        }, true);
3✔
341
    }
342

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

361
            return value;
362
        }, true, cancellationToken).ConfigureAwait(false);
363
    }
364

365

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

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

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

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

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

401
            return dataTable;
402

403
        }, true, cancellationToken).ConfigureAwait(false);
404
    }
405

406

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

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

445
            return true;
446
        }, false, cancellationToken).ConfigureAwait(false);
447
    }
448

449

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

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

477
            return result;
478
        }, false, cancellationToken).ConfigureAwait(false);
479
    }
480

481

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

4✔
496
            return true;
4✔
497
        }, false);
4✔
498
    }
4✔
499

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

516
            return true;
517
        }, false, cancellationToken).ConfigureAwait(false);
518
    }
519

520

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

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

540
    internal void TriggerCallbacks()
541
    {
542
        if (_callbacks.Count == 0)
134✔
543
            return;
129✔
544

545
        while (_callbacks.Count > 0)
10✔
546
        {
547
            var dataCallback = _callbacks.Dequeue();
5✔
548
            dataCallback.Invoke();
5✔
549
        }
550
    }
5✔
551

552

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

558
        AssertDisposed();
48✔
559

560
        var startingTimestamp = Stopwatch.GetTimestamp();
48✔
561

562
        try
563
        {
564
            var cacheKey = CacheKey<TResult>(supportCache);
48✔
565

566
            var (cacheSuccess, cacheValue) = GetCache<TResult>(cacheKey);
48✔
567
            if (cacheSuccess)
48✔
568
                return cacheValue;
2✔
569

570
            _dataSession.EnsureConnection();
46✔
571

572
            var results = query();
46✔
573

574
            TriggerCallbacks();
45✔
575

576
            SetCache(cacheKey, results);
45✔
577

578
            return results;
45✔
579
        }
580
        catch (Exception ex)
1✔
581
        {
582
            LogCommand(startingTimestamp, ex);
1✔
583
            startingTimestamp = 0;
1✔
584

585
            throw;
1✔
586
        }
587
        finally
588
        {
589
            LogCommand(startingTimestamp);
48✔
590

591
            _dataSession.ReleaseConnection();
48✔
592
            Dispose();
48✔
593
        }
48✔
594
    }
47✔
595

596
    private async Task<TResult> QueryFactoryAsync<TResult>(
597
        Func<CancellationToken, Task<TResult>> query,
598
        bool supportCache,
599
        CancellationToken cancellationToken = default)
600
    {
601
        if (query == null)
602
            throw new ArgumentNullException(nameof(query));
603

604
        AssertDisposed();
605

606
        var startingTimestamp = Stopwatch.GetTimestamp();
607

608
        try
609
        {
610
            var cacheKey = CacheKey<TResult>(supportCache);
611

612
            var (cacheSuccess, cacheValue) = await GetCacheAsync<TResult>(cacheKey, cancellationToken).ConfigureAwait(false);
613
            if (cacheSuccess)
614
                return cacheValue;
615

616
            await _dataSession.EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
617

618
            var results = await query(cancellationToken).ConfigureAwait(false);
619

620
            TriggerCallbacks();
621

622
            await SetCacheAsync(cacheKey, results, cancellationToken).ConfigureAwait(false);
623

624
            return results;
625
        }
626
        catch (Exception ex)
627
        {
628
            LogCommand(startingTimestamp, ex);
629
            startingTimestamp = 0;
630

631
            throw;
632
        }
633
        finally
634
        {
635
            LogCommand(startingTimestamp);
636

637
#if NETCOREAPP3_0_OR_GREATER
638

639
            await _dataSession.ReleaseConnectionAsync();
640
            await DisposeAsync();
641
#else
642
            _dataSession.ReleaseConnection();
643
            Dispose();
644
#endif
645
        }
646
    }
647

648

649

650
    private string CacheKey<T>(bool supportCache)
651
    {
652
        if (!supportCache)
140✔
653
            return null;
17✔
654

655
        if (_dataSession.Cache == null)
123✔
656
            return null;
74✔
657

658
        if (_slidingExpiration == null && _absoluteExpiration == null)
49✔
659
            return null;
43✔
660

661
        var commandText = Command.CommandText;
6✔
662
        var commandType = Command.CommandType;
6✔
663
        var typeName = typeof(T).FullName;
6✔
664

665
        var hashCode = HashCode.Seed
6✔
666
            .Combine(commandType)
6✔
667
            .Combine(commandText)
6✔
668
            .Combine(typeName);
6✔
669

670
        foreach (IDbDataParameter parameter in Command.Parameters)
32✔
671
        {
672
            if (parameter.Direction is ParameterDirection.InputOutput or ParameterDirection.Output or ParameterDirection.ReturnValue)
10!
673
                throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
674

675
            hashCode = hashCode
10✔
676
                .Combine(parameter.ParameterName)
10✔
677
                .Combine(parameter.Value)
10✔
678
                .Combine(parameter.DbType);
10✔
679
        }
680

681
        return $"fluent:data:query:{hashCode:X}";
6✔
682
    }
683

684
    private (bool Success, T Value) GetCache<T>(string key)
685
    {
686
        if (_slidingExpiration == null && _absoluteExpiration == null)
48✔
687
            return (false, default);
38✔
688

689
        if (key == null)
10✔
690
            return (false, default);
8✔
691

692
        var cache = _dataSession.Cache;
2✔
693
        if (cache == null)
2!
694
            return (false, default);
×
695

696
        return cache.Get<T>(key);
2✔
697
    }
698

699
    private async Task<(bool Success, T Value)> GetCacheAsync<T>(string key, CancellationToken cancellationToken)
700
    {
701
        if (_slidingExpiration == null && _absoluteExpiration == null)
702
            return (false, default);
703

704
        if (key == null)
705
            return (false, default);
706

707
        var cache = _dataSession.Cache;
708
        if (cache == null)
709
            return (false, default);
710

711
        return await cache
712
            .GetAsync<T>(key, cancellationToken)
713
            .ConfigureAwait(false);
714
    }
715

716
    private void SetCache<T>(string key, T value)
717
    {
718
        if (_slidingExpiration == null && _absoluteExpiration == null)
45✔
719
            return;
37✔
720

721
        if (key == null || value == null)
8!
722
            return;
8✔
723

724
        var cache = _dataSession.Cache;
×
725
        if (cache == null)
×
726
            return;
×
727

728
        cache.Set(key, value, _absoluteExpiration, _slidingExpiration);
×
729
    }
×
730

731
    private async Task SetCacheAsync<T>(string key, T value, CancellationToken cancellationToken)
732
    {
733
        if (_slidingExpiration == null && _absoluteExpiration == null)
734
            return;
735

736
        if (key == null || value == null)
737
            return;
738

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

743
        await cache
744
            .SetAsync(key, value, _absoluteExpiration, _slidingExpiration, cancellationToken)
745
            .ConfigureAwait(false);
746
    }
747

748

749
    private void LogCommand(long startingTimestamp, Exception exception = null)
750
    {
751
        // indicates already logged
752
        if (startingTimestamp == 0)
142✔
753
            return;
2✔
754

755
        var endingTimestamp = Stopwatch.GetTimestamp();
140✔
756

757
#if NET7_0_OR_GREATER
758
        var duration = Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
140✔
759
#else
760
        var duration = new TimeSpan((long)((endingTimestamp - startingTimestamp) * _tickFrequency));
761
#endif
762

763
        _dataSession.QueryLogger?.LogCommand(Command, duration, exception, _logState);
140!
764
    }
140✔
765

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