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

loresoft / FluentCommand / 26923300515

04 Jun 2026 01:03AM UTC coverage: 65.014% (+9.9%) from 55.157%
26923300515

push

github

pwelter34
Merge branch 'master' of https://github.com/loresoft/FluentCommand

1728 of 3450 branches covered (50.09%)

Branch coverage included in aggregate %.

5510 of 7683 relevant lines covered (71.72%)

297.61 hits per line

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

89.24
/src/FluentCommand/DataCommand.cs
1
using System.Data;
2
using System.Data.Common;
3
using System.Diagnostics;
4
using System.Text.Json;
5

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

9
using HashCode = FluentCommand.Internal.HashCode;
10

11
namespace FluentCommand;
12

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

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

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

42
        Command = dataSession.Connection.CreateCommand();
280✔
43
        Command.Transaction = transaction;
280✔
44

45
        if (commandTimeout.HasValue)
280✔
46
            Command.CommandTimeout = commandTimeout.Value;
4✔
47

48
        _commandInterceptors = commandInterceptors ?? [];
280!
49
    }
280✔
50

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

56
    /// <summary>
57
    /// Gets the JSON serializer options used by JSON command helpers.
58
    /// </summary>
59
    public JsonSerializerOptions? JsonSerializerOptions => _dataSession.JsonSerializerOptions;
5✔
60

61

62
    /// <summary>
63
    /// Set the data command with the specified SQL statement.
64
    /// </summary>
65
    /// <param name="sql">The SQL statement.</param>
66
    /// <returns>
67
    /// A fluent <see langword="interface" /> to a data command.
68
    /// </returns>
69
    public IDataCommand Sql(string sql)
70
    {
71
        Command.CommandText = sql;
267✔
72
        Command.CommandType = CommandType.Text;
267✔
73
        return this;
267✔
74
    }
75

76
    /// <summary>
77
    /// Set the data command with the specified stored procedure name.
78
    /// </summary>
79
    /// <param name="storedProcedure">Name of the stored procedure.</param>
80
    /// <returns>
81
    /// A fluent <see langword="interface" /> to a data command.
82
    /// </returns>
83
    public IDataCommand StoredProcedure(string storedProcedure)
84
    {
85
        Command.CommandText = storedProcedure;
13✔
86
        Command.CommandType = CommandType.StoredProcedure;
13✔
87
        return this;
13✔
88
    }
89

90

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

104

105
    /// <summary>
106
    /// Adds the parameter to the underlying command.
107
    /// </summary>
108
    /// <param name="parameter">The <see cref="DbParameter" /> to add.</param>
109
    /// <returns>
110
    /// A fluent <see langword="interface" /> to the data command.
111
    /// </returns>
112
    /// <exception cref="ArgumentNullException"><paramref name="parameter"/> is null</exception>
113
    public IDataCommand Parameter(DbParameter parameter)
114
    {
115
        ArgumentNullException.ThrowIfNull(parameter);
572✔
116

117
        Command.Parameters.Add(parameter);
572✔
118
        return this;
572✔
119
    }
120

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

135
        return this;
11✔
136
    }
137

138

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

155
        return this;
22✔
156
    }
157

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

172
        return this;
×
173
    }
174

175

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

194
        return this;
×
195
    }
196

197
    /// <summary>
198
    /// Use to pass a state to the <see cref="IDataQueryLogger" />.
199
    /// </summary>
200
    /// <param name="state">The state to pass to the logger.</param>
201
    /// <returns>
202
    /// A fluent <see langword="interface" /> to the data command.
203
    /// </returns>
204
    /// <remarks>
205
    /// Use the state to help control what is logged.
206
    /// </remarks>
207
    public IDataCommand LogState(object? state)
208
    {
209
        _logState = state;
×
210
        return this;
×
211
    }
212

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

228
        return QueryFactory(() =>
22✔
229
        {
22✔
230
            var results = new List<TEntity>();
22✔
231

22✔
232
            using var reader = Command.ExecuteReader(commandBehavior);
22✔
233
            var contextReader = CreateContextReader(reader);
22✔
234

22✔
235
            while (contextReader.Read())
22✔
236
            {
22✔
237
                var entity = factory(contextReader);
22✔
238
                results.Add(entity);
22✔
239
            }
22✔
240

22✔
241
            return results;
22✔
242
        }, true);
22✔
243
    }
244

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

263
        return await QueryFactoryAsync(async (token) =>
35✔
264
        {
35✔
265
            var results = new List<TEntity>();
35✔
266

35✔
267
            using var reader = await Command.ExecuteReaderAsync(commandBehavior, token).ConfigureAwait(false);
35✔
268
            var contextReader = CreateContextReader(reader);
35✔
269

35✔
270
            while (await reader.ReadAsync(token).ConfigureAwait(false))
35✔
271
            {
35✔
272
                var entity = factory(contextReader);
35✔
273
                results.Add(entity);
35✔
274
            }
35✔
275

35✔
276
            return results;
35✔
277

35✔
278
        }, true, cancellationToken).ConfigureAwait(false);
35✔
279
    }
34✔
280

281

282
    /// <summary>
283
    /// Executes the query and returns the first row in the result as a <typeparamref name="TEntity" /> object.
284
    /// </summary>
285
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
286
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
287
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
288
    /// <returns>
289
    /// A instance of <typeparamref name="TEntity" /> if row exists; otherwise null.
290
    /// </returns>
291
    /// <exception cref="System.ArgumentNullException"><paramref name="factory"/> is null</exception>
292
    public TEntity? QuerySingle<TEntity>(
293
        Func<IDataReader, TEntity> factory,
294
        CommandBehavior commandBehavior = CommandBehavior.SingleResult | CommandBehavior.SingleRow)
295
    {
296
        ArgumentNullException.ThrowIfNull(factory);
31✔
297

298
        return QueryFactory(() =>
31✔
299
        {
31✔
300
            using var reader = Command.ExecuteReader(commandBehavior);
31✔
301

31✔
302
            var contextReader = CreateContextReader(reader);
31✔
303

31✔
304
            return contextReader.Read()
31✔
305
                ? factory(contextReader)
31✔
306
                : default;
31✔
307

31✔
308
        }, true);
31✔
309
    }
310

311
    /// <summary>
312
    /// Executes the query and returns the first row in the result as a <typeparamref name="TEntity" /> object asynchronously.
313
    /// </summary>
314
    /// <typeparam name="TEntity">The type of the entity.</typeparam>
315
    /// <param name="factory">The <see langword="delegate" /> factory to convert the <see cref="T:System.Data.IDataReader" /> to <typeparamref name="TEntity" />.</param>
316
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
317
    /// <param name="cancellationToken">The cancellation instruction.</param>
318
    /// <returns>
319
    /// A instance of <typeparamref name="TEntity" /> if row exists; otherwise null.
320
    /// </returns>
321
    /// <exception cref="System.ArgumentNullException"><paramref name="factory"/> is null</exception>
322
    public async Task<TEntity?> QuerySingleAsync<TEntity>(
323
        Func<IDataReader, TEntity> factory,
324
        CommandBehavior commandBehavior = CommandBehavior.SingleResult | CommandBehavior.SingleRow,
325
        CancellationToken cancellationToken = default)
326
    {
327
        ArgumentNullException.ThrowIfNull(factory);
22✔
328

329
        return await QueryFactoryAsync(async (token) =>
22✔
330
        {
22✔
331
            using var reader = await Command
22✔
332
                .ExecuteReaderAsync(commandBehavior, token)
22✔
333
                .ConfigureAwait(false);
22✔
334

22✔
335
            var contextReader = CreateContextReader(reader);
22✔
336

22✔
337
            return await reader.ReadAsync(token).ConfigureAwait(false)
22✔
338
               ? factory(contextReader)
22✔
339
               : default;
22✔
340

22✔
341
        }, true, cancellationToken).ConfigureAwait(false);
22✔
342
    }
22✔
343

344

345
    /// <summary>
346
    /// 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.
347
    /// </summary>
348
    /// <typeparam name="TValue">The type of the value.</typeparam>
349
    /// <param name="convert">The <see langword="delegate" /> to convert the value..</param>
350
    /// <returns>
351
    /// The value of the first column of the first row in the result set.
352
    /// </returns>
353
    public TValue? QueryValue<TValue>(Func<object?, TValue?>? convert)
354
    {
355
        return QueryFactory(() =>
36✔
356
        {
36✔
357
            var result = Command.ExecuteScalar();
36✔
358
            return ConvertExtensions.ConvertValue(result, convert);
36✔
359
        }, true);
36✔
360
    }
361

362
    /// <summary>
363
    /// 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.
364
    /// </summary>
365
    /// <typeparam name="TValue">The type of the value.</typeparam>
366
    /// <param name="convert">The <see langword="delegate" /> to convert the value..</param>
367
    /// <param name="cancellationToken">The cancellation instruction.</param>
368
    /// <returns>
369
    /// The value of the first column of the first row in the result set.
370
    /// </returns>
371
    public async Task<TValue?> QueryValueAsync<TValue>(
372
        Func<object?, TValue?>? convert,
373
        CancellationToken cancellationToken = default)
374
    {
375
        return await QueryFactoryAsync(async (token) =>
31✔
376
        {
31✔
377
            var result = await Command.ExecuteScalarAsync(token).ConfigureAwait(false);
31✔
378
            return ConvertExtensions.ConvertValue(result, convert);
31✔
379
        }, true, cancellationToken).ConfigureAwait(false);
31✔
380
    }
31✔
381

382

383
    /// <summary>
384
    /// Executes the command against the connection and converts the results to a <see cref="DataTable" />.
385
    /// </summary>
386
    /// <returns>
387
    /// A <see cref="DataTable" /> of the results.
388
    /// </returns>
389
    public DataTable QueryTable()
390
    {
391
        return QueryFactory(() =>
3✔
392
        {
3✔
393
            var dataTable = new DataTable();
3✔
394

3✔
395
            using var reader = Command.ExecuteReader();
3✔
396
            dataTable.Load(reader);
3✔
397

3✔
398
            return dataTable;
3✔
399
        }, true);
3✔
400
    }
401

402
    /// <summary>
403
    /// Executes the command against the connection and converts the results to a <see cref="DataTable" /> asynchronously.
404
    /// </summary>
405
    /// <param name="cancellationToken">The cancellation instruction.</param>
406
    /// <returns>
407
    /// A <see cref="DataTable" /> of the results.
408
    /// </returns>
409
    public async Task<DataTable> QueryTableAsync(CancellationToken cancellationToken = default)
410
    {
411
        return await QueryFactoryAsync(async (token) =>
3✔
412
        {
3✔
413
            var dataTable = new DataTable();
3✔
414

3✔
415
            using var reader = await Command.ExecuteReaderAsync(token).ConfigureAwait(false);
3✔
416
            dataTable.Load(reader);
3✔
417

3✔
418
            return dataTable;
3✔
419

3✔
420
        }, true, cancellationToken).ConfigureAwait(false);
3✔
421
    }
3✔
422

423

424
    /// <summary>
425
    /// Executes the command against the connection and sends the resulting <see cref="IDataQuery" /> for reading multiple results sets.
426
    /// </summary>
427
    /// <param name="queryAction">The query action delegate to pass the open <see cref="IDataQuery" /> for reading multiple results.</param>
428
    public void QueryMultiple(Action<IDataQuery> queryAction)
429
    {
430
        ArgumentNullException.ThrowIfNull(queryAction);
3✔
431

432
        QueryFactory(() =>
3✔
433
        {
3✔
434
            using var reader = Command.ExecuteReader();
3✔
435

3✔
436
            var query = new QueryMultipleResult(reader, _dataSession.JsonSerializerOptions);
3✔
437

3✔
438
            queryAction(query);
3✔
439

3✔
440
            return true;
3✔
441
        }, false);
3✔
442

443
    }
3✔
444

445
    /// <summary>
446
    /// Executes the command against the connection and sends the resulting <see cref="IDataQueryAsync" /> for reading multiple results sets.
447
    /// </summary>
448
    /// <param name="queryAction">The query action delegate to pass the open <see cref="IDataQueryAsync" /> for reading multiple results.</param>
449
    /// <param name="cancellationToken">The cancellation instruction.</param>
450
    public async Task QueryMultipleAsync(
451
        Func<IDataQueryAsync, Task> queryAction,
452
        CancellationToken cancellationToken = default)
453
    {
454
        ArgumentNullException.ThrowIfNull(queryAction);
3✔
455

456
        await QueryFactoryAsync(async (token) =>
3✔
457
        {
3✔
458
            using var reader = await Command.ExecuteReaderAsync(token).ConfigureAwait(false);
3✔
459

3✔
460
            var query = new QueryMultipleResult(reader, _dataSession.JsonSerializerOptions);
3✔
461

3✔
462
            await queryAction(query).ConfigureAwait(false);
3✔
463

3✔
464
            return true;
3✔
465
        }, false, cancellationToken).ConfigureAwait(false);
3✔
466
    }
3✔
467

468

469
    /// <summary>
470
    /// Executes the command against a connection.
471
    /// </summary>
472
    /// <returns>
473
    /// The number of rows affected.
474
    /// </returns>
475
    public int Execute()
476
    {
477
        return QueryFactory(Command.ExecuteNonQuery, false);
58✔
478
    }
479

480
    /// <summary>
481
    /// Executes the command against a connection asynchronously.
482
    /// </summary>
483
    /// <returns>
484
    /// The number of rows affected.
485
    /// </returns>
486
    public async Task<int> ExecuteAsync(CancellationToken cancellationToken = default)
487
    {
488
        return await QueryFactoryAsync(Command.ExecuteNonQueryAsync, false, cancellationToken).ConfigureAwait(false);
1✔
489
    }
1✔
490

491

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

10✔
505
            var contextReader = CreateContextReader(reader);
10✔
506

10✔
507
            readAction(contextReader);
10✔
508

10✔
509
            return true;
10✔
510
        }, false);
10✔
511
    }
10✔
512

513
    /// <summary>
514
    /// Executes the command against the connection and sends the resulting <see cref="IDataReader" /> to the readAction delegate.
515
    /// </summary>
516
    /// <param name="readAction">The read action delegate to pass the open <see cref="IDataReader" />.</param>
517
    /// <param name="commandBehavior">Provides a description of the results of the query and its effect on the database.</param>
518
    /// <param name="cancellationToken">The cancellation instruction.</param>
519
    public async Task ReadAsync(
520
        Func<IDataReader, CancellationToken, Task> readAction,
521
        CommandBehavior commandBehavior = CommandBehavior.Default,
522
        CancellationToken cancellationToken = default)
523
    {
524
        await QueryFactoryAsync(async (token) =>
14✔
525
        {
14✔
526
            using var reader = await Command.ExecuteReaderAsync(commandBehavior, token).ConfigureAwait(false);
14✔
527

14✔
528
            var contextReader = CreateContextReader(reader);
14✔
529

14✔
530
            await readAction(contextReader, token).ConfigureAwait(false);
14✔
531

14✔
532
            return true;
14✔
533
        }, false, cancellationToken).ConfigureAwait(false);
14✔
534
    }
14✔
535

536
    private IDataReader CreateContextReader(IDataReader reader)
537
    {
538
        return _dataSession.JsonSerializerOptions is not null
128✔
539
            ? new ContextDataReader(reader, _dataSession.JsonSerializerOptions)
128✔
540
            : reader;
128✔
541
    }
542

543

544
    /// <summary>
545
    /// Disposes the managed resources.
546
    /// </summary>
547
    protected override void DisposeManagedResources()
548
    {
549
        Command?.Dispose();
163!
550
    }
163✔
551

552
#if NETCOREAPP3_0_OR_GREATER
553
    /// <summary>
554
    /// Disposes the managed resources.
555
    /// </summary>
556
    protected override async ValueTask DisposeResourcesAsync()
557
    {
558
        if (Command != null)
109✔
559
            await Command.DisposeAsync().ConfigureAwait(false);
109✔
560
    }
109✔
561
#endif
562

563
    internal void TriggerCallbacks()
564
    {
565
        if (_callbacks.Count == 0)
266✔
566
            return;
255✔
567

568
        while (_callbacks.Count > 0)
22✔
569
        {
570
            var dataCallback = _callbacks.Dequeue();
11✔
571
            dataCallback.Invoke();
11✔
572
        }
573
    }
11✔
574

575

576
    private TResult QueryFactory<TResult>(Func<TResult> query, bool supportCache)
577
    {
578
        ArgumentNullException.ThrowIfNull(query);
163✔
579

580
        AssertDisposed();
163✔
581

582
        var startingTimestamp = Stopwatch.GetTimestamp();
163✔
583

584
        try
585
        {
586
            var cacheKey = CacheKey<TResult>(supportCache);
163✔
587

588
            var (cacheSuccess, cacheValue) = GetCache<TResult>(cacheKey);
163✔
589
            if (cacheSuccess)
163✔
590
                return cacheValue!;
2✔
591

592
            _dataSession.EnsureConnection();
161✔
593

594
            if (_commandInterceptors.Length > 0)
161✔
595
            {
596
                foreach (var ci in _commandInterceptors)
8✔
597
                    ci.CommandExecuting(Command, _dataSession);
2✔
598
            }
599

600
            var results = query();
161✔
601

602
            TriggerCallbacks();
160✔
603

604
            SetCache(cacheKey, results);
160✔
605

606
            return results;
160✔
607
        }
608
        catch (Exception ex)
1✔
609
        {
610
            LogCommand(startingTimestamp, ex);
1✔
611
            startingTimestamp = 0;
1✔
612

613
            throw;
1✔
614
        }
615
        finally
616
        {
617
            LogCommand(startingTimestamp);
163✔
618

619
            _dataSession.ReleaseConnection();
163✔
620
            Dispose();
163✔
621
        }
163✔
622
    }
162✔
623

624
    private async Task<TResult> QueryFactoryAsync<TResult>(
625
        Func<CancellationToken, Task<TResult>> query,
626
        bool supportCache,
627
        CancellationToken cancellationToken = default)
628
    {
629
        ArgumentNullException.ThrowIfNull(query);
109✔
630

631
        AssertDisposed();
109✔
632

633
        var startingTimestamp = Stopwatch.GetTimestamp();
109✔
634

635
        try
636
        {
637
            var cacheKey = CacheKey<TResult>(supportCache);
109✔
638

639
            var (cacheSuccess, cacheValue) = await GetCacheAsync<TResult>(cacheKey, cancellationToken).ConfigureAwait(false);
109✔
640
            if (cacheSuccess)
109✔
641
                return cacheValue!;
2✔
642

643
            await _dataSession.EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
107✔
644

645
            if (_commandInterceptors.Length > 0)
107✔
646
            {
647
                foreach (var ci in _commandInterceptors)
12✔
648
                    await ci.CommandExecutingAsync(Command, _dataSession, cancellationToken).ConfigureAwait(false);
3✔
649
            }
650

651
            var results = await query(cancellationToken).ConfigureAwait(false);
107✔
652

653
            TriggerCallbacks();
106✔
654

655
            await SetCacheAsync(cacheKey, results, cancellationToken).ConfigureAwait(false);
106✔
656

657
            return results;
106✔
658
        }
659
        catch (Exception ex)
1✔
660
        {
661
            LogCommand(startingTimestamp, ex);
1✔
662
            startingTimestamp = 0;
1✔
663

664
            throw;
1✔
665
        }
666
        finally
667
        {
668
            LogCommand(startingTimestamp);
109✔
669

670
#if NETCOREAPP3_0_OR_GREATER
671

672
            await _dataSession.ReleaseConnectionAsync().ConfigureAwait(false);
109✔
673
            await DisposeAsync().ConfigureAwait(false);
109✔
674
#else
675
            _dataSession.ReleaseConnection();
676
            Dispose();
677
#endif
678
        }
679
    }
108✔
680

681

682

683
    private string? CacheKey<T>(bool supportCache)
684
    {
685
        if (!supportCache)
272✔
686
            return null;
89✔
687

688
        if (_dataSession.Cache == null)
183✔
689
            return null;
106✔
690

691
        if (_slidingExpiration == null && _absoluteExpiration == null)
77✔
692
            return null;
71✔
693

694
        var commandText = Command.CommandText;
6✔
695
        var commandType = Command.CommandType;
6✔
696
        var typeName = typeof(T).FullName!;
6✔
697

698
        var hashCode = HashCode.Seed
6✔
699
            .Combine(commandType)
6✔
700
            .Combine(commandText)
6✔
701
            .Combine(typeName);
6✔
702

703
        foreach (IDbDataParameter parameter in Command.Parameters)
32✔
704
        {
705
            if (parameter.Direction is ParameterDirection.InputOutput or ParameterDirection.Output or ParameterDirection.ReturnValue)
10!
706
                throw new InvalidOperationException("A command with Output or Return parameters can not be cached.");
×
707

708
            hashCode = hashCode
10!
709
                .Combine(parameter.ParameterName)
10✔
710
                .Combine(parameter.Value ?? DBNull.Value)
10✔
711
                .Combine(parameter.DbType);
10✔
712
        }
713

714
        return $"fluent:data:query:{hashCode:X}";
6✔
715
    }
716

717
    private (bool Success, T? Value) GetCache<T>(string? key)
718
    {
719
        if (_slidingExpiration == null && _absoluteExpiration == null)
163✔
720
            return (false, default);
153✔
721

722
        if (key == null)
10✔
723
            return (false, default);
8✔
724

725
        var cache = _dataSession.Cache;
2✔
726
        if (cache == null)
2!
727
            return (false, default);
×
728

729
        return cache.Get<T>(key);
2✔
730
    }
731

732
    private async Task<(bool Success, T? Value)> GetCacheAsync<T>(string? key, CancellationToken cancellationToken)
733
    {
734
        if (_slidingExpiration == null && _absoluteExpiration == null)
109✔
735
            return (false, default);
97✔
736

737
        if (key == null)
12✔
738
            return (false, default);
8✔
739

740
        var cache = _dataSession.Cache;
4✔
741
        if (cache == null)
4!
742
            return (false, default);
×
743

744
        return await cache
4✔
745
            .GetAsync<T>(key, cancellationToken)
4✔
746
            .ConfigureAwait(false);
4✔
747
    }
109✔
748

749
    private void SetCache<T>(string? key, T value)
750
    {
751
        if (_slidingExpiration == null && _absoluteExpiration == null)
160✔
752
            return;
152✔
753

754
        if (key == null || value == null)
8!
755
            return;
8✔
756

757
        var cache = _dataSession.Cache;
×
758
        if (cache == null)
×
759
            return;
×
760

761
        cache.Set(key, value, _absoluteExpiration, _slidingExpiration);
×
762
    }
×
763

764
    private async Task SetCacheAsync<T>(string? key, T value, CancellationToken cancellationToken)
765
    {
766
        if (_slidingExpiration == null && _absoluteExpiration == null)
106✔
767
            return;
96✔
768

769
        if (key == null || value == null)
10✔
770
            return;
8✔
771

772
        var cache = _dataSession.Cache;
2✔
773
        if (cache == null)
2!
774
            return;
×
775

776
        await cache
2✔
777
            .SetAsync(key, value, _absoluteExpiration, _slidingExpiration, cancellationToken)
2✔
778
            .ConfigureAwait(false);
2✔
779
    }
106✔
780

781

782
    private void LogCommand(long startingTimestamp, Exception? exception = null)
783
    {
784
        // indicates already logged
785
        if (startingTimestamp == 0)
274✔
786
            return;
2✔
787

788
        var endingTimestamp = Stopwatch.GetTimestamp();
272✔
789

790
#if NET7_0_OR_GREATER
791
        var duration = Stopwatch.GetElapsedTime(startingTimestamp, endingTimestamp);
272✔
792
#else
793
        var duration = new TimeSpan((long)((endingTimestamp - startingTimestamp) * _tickFrequency));
794
#endif
795

796
        _dataSession.QueryLogger?.LogCommand(Command, duration, exception, _logState);
272✔
797
    }
265✔
798

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