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

loresoft / FluentCommand / 22970781217

11 Mar 2026 07:32PM UTC coverage: 56.585% (+0.2%) from 56.372%
22970781217

push

github

pwelter34
Add ParameterJson overloads

1277 of 2823 branches covered (45.24%)

Branch coverage included in aggregate %.

0 of 4 new or added lines in 1 file covered. (0.0%)

64 existing lines in 4 files now uncovered.

3892 of 6312 relevant lines covered (61.66%)

367.61 hits per line

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

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

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

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

45
    /// <summary>
46
    /// Gets the underlying <see cref="DbCommand"/> for this <see cref="DataCommand"/>.
47
    /// </summary>
48
    public DbCommand Command { get; }
1,720✔
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;
152✔
61
        Command.CommandType = CommandType.Text;
152✔
62
        return this;
152✔
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;
5✔
75
        Command.CommandType = CommandType.StoredProcedure;
5✔
76
        return this;
5✔
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)
331!
UNCOV
105
            throw new ArgumentNullException(nameof(parameter));
×
106

107
        Command.Parameters.Add(parameter);
331✔
108
        return this;
331✔
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);
5✔
123
        _callbacks.Enqueue(dataCallback);
5✔
124

125
        return this;
5✔
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!
UNCOV
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
    {
UNCOV
158
        _absoluteExpiration = absoluteExpiration;
×
UNCOV
159
        if (_absoluteExpiration != null && _callbacks.Count > 0)
×
UNCOV
160
            throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
161

UNCOV
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
    {
UNCOV
180
        string cacheKey = CacheKey<TEntity>(true);
×
UNCOV
181
        if (_dataSession.Cache != null && cacheKey != null)
×
UNCOV
182
            _dataSession.Cache.Remove(cacheKey);
×
183

UNCOV
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
    {
UNCOV
199
        _logState = state;
×
UNCOV
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)
16!
UNCOV
217
            throw new ArgumentNullException(nameof(factory));
×
218

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

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

16✔
230
            return results;
15✔
231
        }, true);
31✔
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!
UNCOV
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);
34✔
258
            while (await reader.ReadAsync(cancellationToken).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)
17!
UNCOV
285
            throw new ArgumentNullException(nameof(factory));
×
286

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

17✔
294
            return result;
15✔
295
        }, true);
32✔
296
    }
297

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

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

22✔
323
            var result = await reader.ReadAsync(token).ConfigureAwait(false)
21!
324
               ? factory(reader)
21✔
325
               : default;
21✔
326

22✔
327
            return result;
21✔
328
        }, true, cancellationToken).ConfigureAwait(false);
43✔
329
    }
22✔
330

331

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

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

369

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

3✔
382
            using var reader = Command.ExecuteReader();
3✔
383
            dataTable.Load(reader);
3✔
384

3✔
385
            return dataTable;
3✔
386
        }, true);
6✔
387
    }
388

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

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

3✔
405
            return dataTable;
3✔
406

3✔
407
        }, true, cancellationToken).ConfigureAwait(false);
6✔
408
    }
3✔
409

410

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

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

3✔
426
            return true;
3✔
427
        }, false);
6✔
428

429
    }
3✔
430

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

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

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

453

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

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

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

485

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

7✔
500
            return true;
7✔
501
        }, false);
14✔
502
    }
7✔
503

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

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

524

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

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

544
    internal void TriggerCallbacks()
545
    {
546
        if (_callbacks.Count == 0)
150✔
547
            return;
145✔
548

549
        while (_callbacks.Count > 0)
10✔
550
        {
551
            var dataCallback = _callbacks.Dequeue();
5✔
552
            dataCallback.Invoke();
5✔
553
        }
554
    }
5✔
555

556

557
    private TResult QueryFactory<TResult>(Func<TResult> query, bool supportCache)
558
    {
559
        if (query == null)
53!
UNCOV
560
            throw new ArgumentNullException(nameof(query));
×
561

562
        AssertDisposed();
53✔
563

564
        var startingTimestamp = Stopwatch.GetTimestamp();
53✔
565

566
        try
567
        {
568
            var cacheKey = CacheKey<TResult>(supportCache);
53✔
569

570
            var (cacheSuccess, cacheValue) = GetCache<TResult>(cacheKey);
53✔
571
            if (cacheSuccess)
53✔
572
                return cacheValue;
2✔
573

574
            _dataSession.EnsureConnection();
51✔
575

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

582
            var results = query();
51✔
583

584
            TriggerCallbacks();
50✔
585

586
            SetCache(cacheKey, results);
50✔
587

588
            return results;
50✔
589
        }
590
        catch (Exception ex)
1✔
591
        {
592
            LogCommand(startingTimestamp, ex);
1✔
593
            startingTimestamp = 0;
1✔
594

595
            throw;
1✔
596
        }
597
        finally
598
        {
599
            LogCommand(startingTimestamp);
53✔
600

601
            _dataSession.ReleaseConnection();
53✔
602
            Dispose();
53✔
603
        }
53✔
604
    }
52✔
605

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

614
        AssertDisposed();
103✔
615

616
        var startingTimestamp = Stopwatch.GetTimestamp();
103✔
617

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

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

626
            await _dataSession.EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
101✔
627

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

634
            var results = await query(cancellationToken).ConfigureAwait(false);
101✔
635

636
            TriggerCallbacks();
100✔
637

638
            await SetCacheAsync(cacheKey, results, cancellationToken).ConfigureAwait(false);
100✔
639

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

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

653
#if NETCOREAPP3_0_OR_GREATER
654

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

664

665

666
    private string CacheKey<T>(bool supportCache)
667
    {
668
        if (!supportCache)
156✔
669
            return null;
26✔
670

671
        if (_dataSession.Cache == null)
130✔
672
            return null;
79✔
673

674
        if (_slidingExpiration == null && _absoluteExpiration == null)
51✔
675
            return null;
45✔
676

677
        var commandText = Command.CommandText;
6✔
678
        var commandType = Command.CommandType;
6✔
679
        var typeName = typeof(T).FullName;
6✔
680

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

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

691
            hashCode = hashCode
10✔
692
                .Combine(parameter.ParameterName)
10✔
693
                .Combine(parameter.Value)
10✔
694
                .Combine(parameter.DbType);
10✔
695
        }
696

697
        return $"fluent:data:query:{hashCode:X}";
6✔
698
    }
699

700
    private (bool Success, T Value) GetCache<T>(string key)
701
    {
702
        if (_slidingExpiration == null && _absoluteExpiration == null)
53✔
703
            return (false, default);
43✔
704

705
        if (key == null)
10✔
706
            return (false, default);
8✔
707

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

712
        return cache.Get<T>(key);
2✔
713
    }
714

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

720
        if (key == null)
12✔
721
            return (false, default);
8✔
722

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

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

732
    private void SetCache<T>(string key, T value)
733
    {
734
        if (_slidingExpiration == null && _absoluteExpiration == null)
50✔
735
            return;
42✔
736

737
        if (key == null || value == null)
8!
738
            return;
8✔
739

UNCOV
740
        var cache = _dataSession.Cache;
×
UNCOV
741
        if (cache == null)
×
UNCOV
742
            return;
×
743

UNCOV
744
        cache.Set(key, value, _absoluteExpiration, _slidingExpiration);
×
UNCOV
745
    }
×
746

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

752
        if (key == null || value == null)
10✔
753
            return;
8✔
754

755
        var cache = _dataSession.Cache;
2✔
756
        if (cache == null)
2!
UNCOV
757
            return;
×
758

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

764

765
    private void LogCommand(long startingTimestamp, Exception exception = null)
766
    {
767
        // indicates already logged
768
        if (startingTimestamp == 0)
158✔
769
            return;
2✔
770

771
        var endingTimestamp = Stopwatch.GetTimestamp();
156✔
772

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

779
        _dataSession.QueryLogger?.LogCommand(Command, duration, exception, _logState);
156✔
780
    }
151✔
781

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