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

loresoft / FluentCommand / 25142543665

30 Apr 2026 01:24AM UTC coverage: 55.101% (+0.2%) from 54.862%
25142543665

push

github

pwelter34
Add configurable default command timeout

1421 of 3237 branches covered (43.9%)

Branch coverage included in aggregate %.

23 of 27 new or added lines in 4 files covered. (85.19%)

12 existing lines in 2 files now uncovered.

4158 of 6888 relevant lines covered (60.37%)

314.16 hits per line

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

81.64
/src/FluentCommand/DataSession.cs
1
using System.Data;
2
using System.Data.Common;
3

4
using FluentCommand.Query.Generators;
5

6
namespace FluentCommand;
7

8
/// <summary>
9
/// A fluent class for a data session.
10
/// </summary>
11
/// <seealso cref="FluentCommand.DisposableBase" />
12
/// <seealso cref="FluentCommand.IDataSession" />
13
public class DataSession : DisposableBase, IDataSession
14
{
15
    private readonly bool _disposeConnection;
16

17
    private readonly IDataInterceptor[] _interceptors;
18
    private readonly IDataConnectionInterceptor[] _connectionInterceptors;
19
    private readonly IDataCommandInterceptor[] _commandInterceptors;
20

21
    private bool _openedConnection;
22
    private int _connectionRequestCount;
23

24
    /// <summary>
25
    /// Gets the underlying <see cref="DbConnection"/> for the session.
26
    /// </summary>
27
    public DbConnection Connection { get; }
28

29
    /// <summary>
30
    /// Gets the underlying <see cref="DbTransaction"/> for the session.
31
    /// </summary>
32
    public DbTransaction? Transaction { get; private set; }
33

34
    /// <summary>
35
    /// Gets the underlying <see cref="IDataCache"/> for the session.
36
    /// </summary>
37
    public IDataCache? Cache { get; }
38

39
    /// <summary>
40
    /// Gets the query generator provider.
41
    /// </summary>
42
    /// <value>
43
    /// The query generator provider.
44
    /// </value>
45
    public IQueryGenerator QueryGenerator { get; }
46

47
    /// <summary>
48
    /// Gets the data command query logger.
49
    /// </summary>
50
    /// <value>
51
    /// The data command query logger.
52
    /// </value>
53
    public IDataQueryLogger? QueryLogger { get; }
54

55
    /// <summary>
56
    /// Gets the default command timeout in seconds.
57
    /// </summary>
58
    /// <value>
59
    /// The default command timeout in seconds.
60
    /// </value>
61
    public int? CommandTimeout { get; }
62

63
    /// <summary>
64
    /// Gets the interceptors registered for this session.
65
    /// </summary>
66
    /// <value>
67
    /// The list of <see cref="IDataInterceptor"/> instances active for this session.
68
    /// </value>
69
    public IReadOnlyList<IDataInterceptor> Interceptors => _interceptors;
4✔
70

71

72
    /// <summary>
73
    /// Initializes a new instance of the <see cref="DataSession" /> class.
74
    /// </summary>
75
    /// <param name="connection">The DbConnection to use for the session.</param>
76
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
77
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
78
    /// <param name="queryGenerator">The query generator provider.</param>
79
    /// <param name="logger">The logger delegate for writing log messages.</param>
80
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
81
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
82
    /// <exception cref="ArgumentNullException"><paramref name="connection" /> is null</exception>
83
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="connection" /> instance.</exception>
84
    public DataSession(
15✔
85
        DbConnection connection,
15✔
86
        bool disposeConnection = true,
15✔
87
        IDataCache? cache = null,
15✔
88
        IQueryGenerator? queryGenerator = null,
15✔
89
        IDataQueryLogger? logger = null,
15✔
90
        IEnumerable<IDataInterceptor>? interceptors = null,
15✔
91
        int? commandTimeout = null)
15✔
92
    {
93
        if (connection == null)
15!
94
            throw new ArgumentNullException(nameof(connection));
×
95

96
        if (string.IsNullOrEmpty(connection.ConnectionString))
15!
97
            throw new ArgumentException("Invalid connection string", nameof(connection));
×
98

99
        Connection = connection;
15✔
100
        Cache = cache;
15✔
101
        QueryGenerator = queryGenerator ?? new SqlServerGenerator();
15✔
102
        QueryLogger = logger;
15✔
103
        CommandTimeout = commandTimeout;
15✔
104

105
        _interceptors = interceptors is null ? [] : [.. interceptors];
15✔
106
        _connectionInterceptors = [.. _interceptors.OfType<IDataConnectionInterceptor>()];
15✔
107
        _commandInterceptors = [.. _interceptors.OfType<IDataCommandInterceptor>()];
15✔
108

109
        _disposeConnection = disposeConnection;
15✔
110
    }
15✔
111

112
    /// <summary>
113
    /// Initializes a new instance of the <see cref="DataSession"/> class.
114
    /// </summary>
115
    /// <param name="transaction">The DbTransaction to use for the session.</param>
116
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
117
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
118
    /// <param name="queryGenerator">The query generator provider.</param>
119
    /// <param name="logger">The logger delegate for writing log messages.</param>
120
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
121
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
122
    /// <exception cref="ArgumentNullException"><paramref name="transaction" /> is null</exception>
123
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="transaction" /> instance.</exception>
124
    public DataSession(
125
        DbTransaction transaction,
126
        bool disposeConnection = false,
127
        IDataCache? cache = null,
128
        IQueryGenerator? queryGenerator = null,
129
        IDataQueryLogger? logger = null,
130
        IEnumerable<IDataInterceptor>? interceptors = null,
131
        int? commandTimeout = null)
NEW
132
        : this(GetTransactionConnection(transaction), disposeConnection, cache, queryGenerator, logger, interceptors, commandTimeout)
×
133
    {
134
        Transaction = transaction;
×
135
    }
×
136

137
    /// <summary>
138
    /// Initializes a new instance of the <see cref="DataSession" /> class.
139
    /// </summary>
140
    /// <param name="dataConfiguration">The configuration for the session</param>
141
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration"/> is null</exception>
142
    public DataSession(IDataConfiguration dataConfiguration)
154✔
143
    {
144
        if (dataConfiguration == null)
154!
145
            throw new ArgumentNullException(nameof(dataConfiguration));
×
146

147
        Connection = dataConfiguration.CreateConnection();
154✔
148
        Cache = dataConfiguration.DataCache;
154✔
149
        QueryGenerator = dataConfiguration.QueryGenerator;
154✔
150
        QueryLogger = dataConfiguration.QueryLogger;
154✔
151
        CommandTimeout = dataConfiguration.CommandTimeout;
154✔
152

153
        _interceptors = dataConfiguration.Interceptors is null ? [] : [.. dataConfiguration.Interceptors];
154!
154
        _connectionInterceptors = [.. _interceptors.OfType<IDataConnectionInterceptor>()];
154✔
155
        _commandInterceptors = [.. _interceptors.OfType<IDataCommandInterceptor>()];
154✔
156

157
        _disposeConnection = true;
154✔
158
    }
154✔
159

160

161
    /// <summary>
162
    /// Starts a database transaction with the specified isolation level.
163
    /// </summary>
164
    /// <param name="isolationLevel">Specifies the isolation level for the transaction.</param>
165
    /// <returns>
166
    /// A <see cref="DbTransaction" /> representing the new transaction.
167
    /// </returns>
168
    public DbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
169
    {
170
        EnsureConnection();
1✔
171
        Transaction = Connection.BeginTransaction(isolationLevel);
1✔
172

173
        return Transaction;
1✔
174
    }
175

176
#if NETCOREAPP3_0_OR_GREATER
177
    /// <summary>
178
    /// Starts a database transaction with the specified isolation level.
179
    /// </summary>
180
    /// <param name="isolationLevel">Specifies the isolation level for the transaction.</param>
181
    /// <param name="cancellationToken">The cancellation instruction.</param>
182
    /// <returns>
183
    /// A <see cref="DbTransaction" /> representing the new transaction.
184
    /// </returns>
185
    public async Task<DbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default)
186
    {
187
        await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2✔
188
        Transaction = await Connection.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false);
2✔
189

190
        return Transaction;
2✔
191
    }
2✔
192
#endif
193

194
    /// <summary>
195
    /// Starts a data command with the specified SQL.
196
    /// </summary>
197
    /// <param name="sql">The SQL statement.</param>
198
    /// <returns>
199
    /// A fluent <see langword="interface" /> to a data command.
200
    /// </returns>
201
    public IDataCommand Sql(string sql)
202
    {
203
        var dataCommand = new DataCommand(this, Transaction, _commandInterceptors, commandTimeout: CommandTimeout);
163✔
204
        return dataCommand.Sql(sql);
163✔
205
    }
206

207
    /// <summary>
208
    /// Starts a data command with the specified stored procedure name.
209
    /// </summary>
210
    /// <param name="storedProcedureName">Name of the stored procedure.</param>
211
    /// <returns>
212
    /// A fluent <see langword="interface" /> to a data command.
213
    /// </returns>
214
    public IDataCommand StoredProcedure(string storedProcedureName)
215
    {
216
        var dataCommand = new DataCommand(this, Transaction, _commandInterceptors, commandTimeout: CommandTimeout);
13✔
217
        return dataCommand.StoredProcedure(storedProcedureName);
13✔
218
    }
219

220

221
    /// <summary>
222
    /// Ensures the connection is open.
223
    /// </summary>
224
    /// <exception cref="InvalidOperationException">Failed to open connection</exception>
225
    public void EnsureConnection()
226
    {
227
        AssertDisposed();
77✔
228

229
        bool justOpened = false;
77✔
230
        if (ConnectionState.Closed == Connection.State)
77✔
231
        {
232
            Connection.Open();
74✔
233
            _openedConnection = true;
74✔
234
            justOpened = true;
74✔
235
        }
236

237
        if (_openedConnection)
77✔
238
            _connectionRequestCount++;
77✔
239

240
        // Check the connection was opened correctly
241
        if (Connection.State is ConnectionState.Closed or ConnectionState.Broken)
77!
242
            throw new InvalidOperationException($"Execution of the command requires an open and available connection. The connection's current state is {Connection.State}.");
×
243

244
        // run connection opened interceptors only when the connection was just opened by this context
245
        if (justOpened)
77✔
246
        {
247
            foreach (var interceptor in _connectionInterceptors)
156✔
248
                interceptor.ConnectionOpened(Connection, this);
4✔
249
        }
250
    }
77✔
251

252
    /// <summary>
253
    /// Ensures the connection is open asynchronous.
254
    /// </summary>
255
    /// <param name="cancellationToken">The cancellation instruction.</param>
256
    /// <returns>A task representing the asynchronous operation.</returns>
257
    /// <exception cref="InvalidOperationException">Failed to open connection</exception>
258
    public async Task EnsureConnectionAsync(CancellationToken cancellationToken = default)
259
    {
260
        AssertDisposed();
109✔
261

262
        bool justOpened = false;
109✔
263
        if (ConnectionState.Closed == Connection.State)
109✔
264
        {
265
            await Connection.OpenAsync(cancellationToken).ConfigureAwait(false);
106✔
266
            _openedConnection = true;
106✔
267
            justOpened = true;
106✔
268
        }
269

270
        if (_openedConnection)
109✔
271
            _connectionRequestCount++;
109✔
272

273
        // Check the connection was opened correctly
274
        if (Connection.State is ConnectionState.Closed or ConnectionState.Broken)
109!
275
            throw new InvalidOperationException($"Execution of the command requires an open and available connection. The connection's current state is {Connection.State}.");
×
276

277
        // run connection opened interceptors only when the connection was just opened by this context
278
        if (justOpened)
109✔
279
        {
280
            foreach (var interceptor in _connectionInterceptors)
214✔
281
                await interceptor.ConnectionOpenedAsync(Connection, this, cancellationToken).ConfigureAwait(false);
1✔
282
        }
283
    }
109✔
284

285
    /// <summary>
286
    /// Releases the connection.
287
    /// </summary>
288
    public void ReleaseConnection()
289
    {
290
        AssertDisposed();
79✔
291

292
        if (!_openedConnection)
79✔
293
            return;
2✔
294

295
        if (_connectionRequestCount > 0)
77✔
296
            _connectionRequestCount--;
77✔
297

298
        if (_connectionRequestCount != 0)
77✔
299
            return;
3✔
300

301
        // When no operation is using the connection and the context had opened the connection
302
        // the connection can be closed
303
        foreach (var interceptor in _connectionInterceptors)
156✔
304
            interceptor.ConnectionClosing(Connection, this);
4✔
305

306
        Connection.Close();
74✔
307
        _openedConnection = false;
74✔
308
    }
74✔
309

310
#if NETCOREAPP3_0_OR_GREATER
311
    /// <summary>
312
    /// Releases the connection.
313
    /// </summary>
314
    public async Task ReleaseConnectionAsync()
315
    {
316
        AssertDisposed();
108✔
317

318
        if (!_openedConnection)
108✔
319
            return;
2✔
320

321
        if (_connectionRequestCount > 0)
106✔
322
            _connectionRequestCount--;
106✔
323

324
        if (_connectionRequestCount != 0)
106✔
325
            return;
3✔
326

327
        // When no operation is using the connection and the context had opened the connection
328
        // the connection can be closed
329
        foreach (var interceptor in _connectionInterceptors)
208✔
330
            await interceptor.ConnectionClosingAsync(Connection, this).ConfigureAwait(false);
1✔
331

332
        await Connection.CloseAsync().ConfigureAwait(false);
103✔
333
        _openedConnection = false;
103✔
334
    }
108✔
335

336
    /// <summary>
337
    /// Disposes the managed resources.
338
    /// </summary>
339
    protected override async ValueTask DisposeResourcesAsync()
340
    {
341
        if (!_disposeConnection)
77!
UNCOV
342
            return;
×
343

344
        if (Transaction is not null)
77✔
345
            await Transaction.DisposeAsync().ConfigureAwait(false);
1✔
346

347
        if (_openedConnection)
77✔
348
        {
349
            foreach (var interceptor in _connectionInterceptors)
2!
UNCOV
350
                await interceptor.ConnectionClosingAsync(Connection, this).ConfigureAwait(false);
×
351

352
            _openedConnection = false;
1✔
353
        }
354

355
        await Connection.DisposeAsync().ConfigureAwait(false);
77✔
356
    }
77✔
357
#endif
358

359
    /// <summary>
360
    /// Disposes the managed resources.
361
    /// </summary>
362
    protected override void DisposeManagedResources()
363
    {
364
        if (!_disposeConnection)
37!
UNCOV
365
            return;
×
366

367
        Transaction?.Dispose();
37✔
368

369
        if (_openedConnection)
37✔
370
        {
371
            foreach (var interceptor in _connectionInterceptors)
2!
UNCOV
372
                interceptor.ConnectionClosing(Connection, this);
×
373

374
            _openedConnection = false;
1✔
375
        }
376

377
        Connection.Dispose();
37✔
378
    }
37✔
379

380
    private static DbConnection GetTransactionConnection(DbTransaction transaction)
381
    {
UNCOV
382
        if (transaction is null)
×
UNCOV
383
            throw new ArgumentNullException(nameof(transaction));
×
384

UNCOV
385
        return transaction.Connection
×
UNCOV
386
            ?? throw new ArgumentException("Transaction has no associated connection.", nameof(transaction));
×
387
    }
388
}
389

390
/// <summary>
391
/// A fluent class for a data session by discriminator.  Used to register multiple instances of IDataSession.
392
/// </summary>
393
/// <typeparam name="TDiscriminator">The type of the discriminator.</typeparam>
394
/// <seealso cref="FluentCommand.DisposableBase" />
395
/// <seealso cref="FluentCommand.IDataSession" />
396
public class DataSession<TDiscriminator> : DataSession, IDataSession<TDiscriminator>
397
{
398
    /// <summary>
399
    /// Initializes a new instance of the <see cref="DataSession" /> class.
400
    /// </summary>
401
    /// <param name="connection">The DbConnection to use for the session.</param>
402
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
403
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
404
    /// <param name="queryGenerator">The query generator provider.</param>
405
    /// <param name="logger">The logger delegate for writing log messages.</param>
406
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
407
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
408
    /// <exception cref="ArgumentNullException"><paramref name="connection" /> is null</exception>
409
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="connection" /> instance.</exception>
410
    public DataSession(
411
        DbConnection connection,
412
        bool disposeConnection = true,
413
        IDataCache? cache = null,
414
        IQueryGenerator? queryGenerator = null,
415
        IDataQueryLogger? logger = null,
416
        IEnumerable<IDataInterceptor>? interceptors = null,
417
        int? commandTimeout = null)
NEW
418
        : base(connection, disposeConnection, cache, queryGenerator, logger, interceptors, commandTimeout)
×
419
    {
UNCOV
420
    }
×
421

422
    /// <summary>
423
    /// Initializes a new instance of the <see cref="DataSession"/> class.
424
    /// </summary>
425
    /// <param name="transaction">The DbTransaction to use for the session.</param>
426
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
427
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
428
    /// <param name="queryGenerator">The query generator provider.</param>
429
    /// <param name="logger">The logger delegate for writing log messages.</param>
430
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
431
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
432
    /// <exception cref="ArgumentNullException"><paramref name="transaction" /> is null</exception>
433
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="transaction" /> instance.</exception>
434
    public DataSession(
435
        DbTransaction transaction,
436
        bool disposeConnection = false,
437
        IDataCache? cache = null,
438
        IQueryGenerator? queryGenerator = null,
439
        IDataQueryLogger? logger = null,
440
        IEnumerable<IDataInterceptor>? interceptors = null,
441
        int? commandTimeout = null)
NEW
442
        : base(transaction, disposeConnection, cache, queryGenerator, logger, interceptors, commandTimeout)
×
443
    {
UNCOV
444
    }
×
445

446
    /// <summary>
447
    /// Initializes a new instance of the <see cref="DataSession" /> class.
448
    /// </summary>
449
    /// <param name="dataConfiguration">The configuration for the session</param>
450
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration" /> is null</exception>
451
    public DataSession(IDataConfiguration<TDiscriminator> dataConfiguration)
452
        : base(dataConfiguration)
1✔
453
    {
454
    }
1✔
455
}
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