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

loresoft / FluentCommand / 29415393795

15 Jul 2026 12:29PM UTC coverage: 65.817% (-0.04%) from 65.856%
29415393795

push

github

pwelter34
Improve connection state checks in DataSession

1874 of 3640 branches covered (51.48%)

Branch coverage included in aggregate %.

2 of 8 new or added lines in 1 file covered. (25.0%)

5741 of 7930 relevant lines covered (72.4%)

298.57 hits per line

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

80.57
/src/FluentCommand/DataSession.cs
1
using System.Data;
2
using System.Data.Common;
3
using System.Text.Json;
4

5
using FluentCommand.Query.Generators;
6

7
namespace FluentCommand;
8

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

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

22
    private bool _openedConnection;
23
    private int _connectionRequestCount;
24

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

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

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

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

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

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

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

72
    /// <summary>
73
    /// Gets the JSON serializer options used by generated JSON column readers.
74
    /// </summary>
75
    public JsonSerializerOptions? JsonSerializerOptions { get; }
76

77

78
    /// <summary>
79
    /// Initializes a new instance of the <see cref="DataSession" /> class.
80
    /// </summary>
81
    /// <param name="connection">The DbConnection to use for the session.</param>
82
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
83
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
84
    /// <param name="queryGenerator">The query generator provider.</param>
85
    /// <param name="logger">The logger delegate for writing log messages.</param>
86
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
87
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
88
    /// <param name="jsonSerializerOptions">The JSON serializer options used by generated JSON column readers.</param>
89
    /// <exception cref="ArgumentNullException"><paramref name="connection" /> is null</exception>
90
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="connection" /> instance.</exception>
91
    public DataSession(
21✔
92
        DbConnection connection,
21✔
93
        bool disposeConnection = true,
21✔
94
        IDataCache? cache = null,
21✔
95
        IQueryGenerator? queryGenerator = null,
21✔
96
        IDataQueryLogger? logger = null,
21✔
97
        IEnumerable<IDataInterceptor>? interceptors = null,
21✔
98
        int? commandTimeout = null,
21✔
99
        JsonSerializerOptions? jsonSerializerOptions = null)
21✔
100
    {
101
        ArgumentNullException.ThrowIfNull(connection);
21✔
102

103
        if (string.IsNullOrEmpty(connection.ConnectionString))
21!
104
            throw new ArgumentException("Invalid connection string", nameof(connection));
×
105

106
        Connection = connection;
21✔
107
        Cache = cache;
21✔
108
        QueryGenerator = queryGenerator ?? new SqlServerGenerator();
21✔
109
        QueryLogger = logger;
21✔
110
        CommandTimeout = commandTimeout;
21✔
111
        JsonSerializerOptions = jsonSerializerOptions;
21✔
112

113
        _interceptors = interceptors is null ? [] : [.. interceptors];
21✔
114
        _connectionInterceptors = [.. _interceptors.OfType<IDataConnectionInterceptor>()];
21✔
115
        _commandInterceptors = [.. _interceptors.OfType<IDataCommandInterceptor>()];
21✔
116

117
        _disposeConnection = disposeConnection;
21✔
118
    }
21✔
119

120
    /// <summary>
121
    /// Initializes a new instance of the <see cref="DataSession"/> class.
122
    /// </summary>
123
    /// <param name="transaction">The DbTransaction to use for the session.</param>
124
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
125
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
126
    /// <param name="queryGenerator">The query generator provider.</param>
127
    /// <param name="logger">The logger delegate for writing log messages.</param>
128
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
129
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
130
    /// <param name="jsonSerializerOptions">The JSON serializer options used by generated JSON column readers.</param>
131
    /// <exception cref="ArgumentNullException"><paramref name="transaction" /> is null</exception>
132
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="transaction" /> instance.</exception>
133
    public DataSession(
134
        DbTransaction transaction,
135
        bool disposeConnection = false,
136
        IDataCache? cache = null,
137
        IQueryGenerator? queryGenerator = null,
138
        IDataQueryLogger? logger = null,
139
        IEnumerable<IDataInterceptor>? interceptors = null,
140
        int? commandTimeout = null,
141
        JsonSerializerOptions? jsonSerializerOptions = null)
142
        : this(GetTransactionConnection(transaction), disposeConnection, cache, queryGenerator, logger, interceptors, commandTimeout, jsonSerializerOptions)
×
143
    {
144
        Transaction = transaction;
×
145
    }
×
146

147
    /// <summary>
148
    /// Initializes a new instance of the <see cref="DataSession" /> class.
149
    /// </summary>
150
    /// <param name="dataConfiguration">The configuration for the session</param>
151
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration"/> is null</exception>
152
    public DataSession(IDataConfiguration dataConfiguration)
194✔
153
    {
154
        ArgumentNullException.ThrowIfNull(dataConfiguration);
194✔
155

156
        Connection = dataConfiguration.CreateConnection();
194✔
157
        Cache = dataConfiguration.DataCache;
194✔
158
        QueryGenerator = dataConfiguration.QueryGenerator;
194✔
159
        QueryLogger = dataConfiguration.QueryLogger;
194✔
160
        CommandTimeout = dataConfiguration.CommandTimeout;
194✔
161
        JsonSerializerOptions = dataConfiguration.JsonSerializerOptions;
194✔
162

163
        _interceptors = dataConfiguration.Interceptors is null ? [] : [.. dataConfiguration.Interceptors];
194!
164
        _connectionInterceptors = [.. _interceptors.OfType<IDataConnectionInterceptor>()];
194✔
165
        _commandInterceptors = [.. _interceptors.OfType<IDataCommandInterceptor>()];
194✔
166

167
        _disposeConnection = true;
194✔
168
    }
194✔
169

170

171
    /// <summary>
172
    /// Starts a database transaction with the specified isolation level.
173
    /// </summary>
174
    /// <param name="isolationLevel">Specifies the isolation level for the transaction.</param>
175
    /// <returns>
176
    /// A <see cref="DbTransaction" /> representing the new transaction.
177
    /// </returns>
178
    public DbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
179
    {
180
        EnsureConnection();
1✔
181
        Transaction = Connection.BeginTransaction(isolationLevel);
1✔
182

183
        return Transaction;
1✔
184
    }
185

186
#if NETCOREAPP3_0_OR_GREATER
187
    /// <summary>
188
    /// Starts a database transaction with the specified isolation level.
189
    /// </summary>
190
    /// <param name="isolationLevel">Specifies the isolation level for the transaction.</param>
191
    /// <param name="cancellationToken">The cancellation instruction.</param>
192
    /// <returns>
193
    /// A <see cref="DbTransaction" /> representing the new transaction.
194
    /// </returns>
195
    public async Task<DbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default)
196
    {
197
        await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2✔
198
        Transaction = await Connection.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false);
2✔
199

200
        return Transaction;
2✔
201
    }
2✔
202
#endif
203

204
    /// <summary>
205
    /// Starts a data command with the specified SQL.
206
    /// </summary>
207
    /// <param name="sql">The SQL statement.</param>
208
    /// <returns>
209
    /// A fluent <see langword="interface" /> to a data command.
210
    /// </returns>
211
    public IDataCommand Sql(string sql)
212
    {
213
        var dataCommand = new DataCommand(this, Transaction, _commandInterceptors, commandTimeout: CommandTimeout);
268✔
214
        return dataCommand.Sql(sql);
268✔
215
    }
216

217
    /// <summary>
218
    /// Starts a data command with the specified stored procedure name.
219
    /// </summary>
220
    /// <param name="storedProcedureName">Name of the stored procedure.</param>
221
    /// <returns>
222
    /// A fluent <see langword="interface" /> to a data command.
223
    /// </returns>
224
    public IDataCommand StoredProcedure(string storedProcedureName)
225
    {
226
        var dataCommand = new DataCommand(this, Transaction, _commandInterceptors, commandTimeout: CommandTimeout);
13✔
227
        return dataCommand.StoredProcedure(storedProcedureName);
13✔
228
    }
229

230

231
    /// <summary>
232
    /// Ensures the connection is open.
233
    /// </summary>
234
    /// <exception cref="InvalidOperationException">Failed to open connection</exception>
235
    public void EnsureConnection()
236
    {
237
        AssertDisposed();
173✔
238

239
        bool justOpened = false;
173✔
240
        if (ConnectionState.Closed == Connection.State)
173✔
241
        {
242
            Connection.Open();
170✔
243
            _openedConnection = true;
170✔
244
            justOpened = true;
170✔
245
        }
246

247
        if (_openedConnection)
173!
248
            _connectionRequestCount++;
173✔
249

250
        // Check the connection was opened correctly
251
        if (Connection.State is ConnectionState.Closed or ConnectionState.Broken or ConnectionState.Connecting)
173!
NEW
252
            throw new InvalidOperationException(CreateConnectionUnavailableMessage(Connection.State));
×
253

254
        // run connection opened interceptors only when the connection was just opened by this context
255
        if (justOpened)
173✔
256
        {
257
            foreach (var interceptor in _connectionInterceptors)
350✔
258
                interceptor.ConnectionOpened(Connection, this);
5✔
259
        }
260
    }
173✔
261

262
    /// <summary>
263
    /// Ensures the connection is open asynchronous.
264
    /// </summary>
265
    /// <param name="cancellationToken">The cancellation instruction.</param>
266
    /// <returns>A task representing the asynchronous operation.</returns>
267
    /// <exception cref="InvalidOperationException">Failed to open connection</exception>
268
    public async Task EnsureConnectionAsync(CancellationToken cancellationToken = default)
269
    {
270
        AssertDisposed();
115✔
271

272
        bool justOpened = false;
115✔
273
        if (ConnectionState.Closed == Connection.State)
115✔
274
        {
275
            await Connection.OpenAsync(cancellationToken).ConfigureAwait(false);
112✔
276
            _openedConnection = true;
112✔
277
            justOpened = true;
112✔
278
        }
279

280
        if (_openedConnection)
115✔
281
            _connectionRequestCount++;
115✔
282

283
        // Check the connection was opened correctly
284
        if (Connection.State is ConnectionState.Closed or ConnectionState.Broken or ConnectionState.Connecting)
115!
NEW
285
            throw new InvalidOperationException(CreateConnectionUnavailableMessage(Connection.State));
×
286

287
        // run connection opened interceptors only when the connection was just opened by this context
288
        if (justOpened)
115✔
289
        {
290
            foreach (var interceptor in _connectionInterceptors)
226✔
291
                await interceptor.ConnectionOpenedAsync(Connection, this, cancellationToken).ConfigureAwait(false);
1✔
292
        }
293
    }
115✔
294

295
    /// <summary>
296
    /// Releases the connection.
297
    /// </summary>
298
    public void ReleaseConnection()
299
    {
300
        AssertDisposed();
175✔
301

302
        if (!_openedConnection)
175✔
303
            return;
2✔
304

305
        if (_connectionRequestCount > 0)
173!
306
            _connectionRequestCount--;
173✔
307

308
        if (_connectionRequestCount != 0)
173✔
309
            return;
3✔
310

311
        // When no operation is using the connection and the context had opened the connection
312
        // the connection can be closed
313
        foreach (var interceptor in _connectionInterceptors)
350✔
314
            interceptor.ConnectionClosing(Connection, this);
5✔
315

316
        Connection.Close();
170✔
317
        _openedConnection = false;
170✔
318
    }
170✔
319

320
#if NETCOREAPP3_0_OR_GREATER
321
    /// <summary>
322
    /// Releases the connection.
323
    /// </summary>
324
    public async Task ReleaseConnectionAsync()
325
    {
326
        AssertDisposed();
114✔
327

328
        if (!_openedConnection)
114✔
329
            return;
2✔
330

331
        if (_connectionRequestCount > 0)
112✔
332
            _connectionRequestCount--;
112✔
333

334
        if (_connectionRequestCount != 0)
112✔
335
            return;
3✔
336

337
        // When no operation is using the connection and the context had opened the connection
338
        // the connection can be closed
339
        foreach (var interceptor in _connectionInterceptors)
220✔
340
            await interceptor.ConnectionClosingAsync(Connection, this).ConfigureAwait(false);
1✔
341

342
        await Connection.CloseAsync().ConfigureAwait(false);
109✔
343
        _openedConnection = false;
109✔
344
    }
114✔
345

346
    /// <summary>
347
    /// Disposes the managed resources.
348
    /// </summary>
349
    protected override async ValueTask DisposeResourcesAsync()
350
    {
351
        if (!_disposeConnection)
83!
352
            return;
×
353

354
        if (Transaction is not null)
83✔
355
            await Transaction.DisposeAsync().ConfigureAwait(false);
1✔
356

357
        if (_openedConnection)
83✔
358
        {
359
            foreach (var interceptor in _connectionInterceptors)
2!
360
                await interceptor.ConnectionClosingAsync(Connection, this).ConfigureAwait(false);
×
361

362
            _openedConnection = false;
1✔
363
        }
364

365
        await Connection.DisposeAsync().ConfigureAwait(false);
83✔
366
    }
83✔
367
#endif
368

369
    /// <summary>
370
    /// Disposes the managed resources.
371
    /// </summary>
372
    protected override void DisposeManagedResources()
373
    {
374
        if (!_disposeConnection)
73!
375
            return;
×
376

377
        Transaction?.Dispose();
73✔
378

379
        if (_openedConnection)
73✔
380
        {
381
            foreach (var interceptor in _connectionInterceptors)
2!
382
                interceptor.ConnectionClosing(Connection, this);
×
383

384
            _openedConnection = false;
1✔
385
        }
386

387
        Connection.Dispose();
73✔
388
    }
73✔
389

390
    private static DbConnection GetTransactionConnection(DbTransaction transaction)
391
    {
392
        ArgumentNullException.ThrowIfNull(transaction);
×
393

394
        return transaction.Connection
×
395
            ?? throw new ArgumentException("Transaction has no associated connection.", nameof(transaction));
×
396
    }
397

398
    private static string CreateConnectionUnavailableMessage(ConnectionState state)
399
    {
NEW
400
        var message = $"Execution of the command requires an open and available connection. The connection's current state is {state}.";
×
401

NEW
402
        if (state == ConnectionState.Connecting)
×
NEW
403
            return message + " The Connecting state usually indicates the same DataSession is being used by multiple operations at the same time. DataSession is not thread-safe; create a separate session per operation.";
×
404

NEW
405
        return message;
×
406
    }
407
}
408

409
/// <summary>
410
/// A fluent class for a data session by discriminator.  Used to register multiple instances of IDataSession.
411
/// </summary>
412
/// <typeparam name="TDiscriminator">The type of the discriminator.</typeparam>
413
/// <seealso cref="FluentCommand.DisposableBase" />
414
/// <seealso cref="FluentCommand.IDataSession" />
415
public class DataSession<TDiscriminator> : DataSession, IDataSession<TDiscriminator>
416
{
417
    /// <summary>
418
    /// Initializes a new instance of the <see cref="DataSession" /> class.
419
    /// </summary>
420
    /// <param name="connection">The DbConnection to use for the session.</param>
421
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
422
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
423
    /// <param name="queryGenerator">The query generator provider.</param>
424
    /// <param name="logger">The logger delegate for writing log messages.</param>
425
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
426
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
427
    /// <exception cref="ArgumentNullException"><paramref name="connection" /> is null</exception>
428
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="connection" /> instance.</exception>
429
    public DataSession(
430
        DbConnection connection,
431
        bool disposeConnection = true,
432
        IDataCache? cache = null,
433
        IQueryGenerator? queryGenerator = null,
434
        IDataQueryLogger? logger = null,
435
        IEnumerable<IDataInterceptor>? interceptors = null,
436
        int? commandTimeout = null)
437
        : base(connection, disposeConnection, cache, queryGenerator, logger, interceptors, commandTimeout)
×
438
    {
439
    }
×
440

441
    /// <summary>
442
    /// Initializes a new instance of the <see cref="DataSession"/> class.
443
    /// </summary>
444
    /// <param name="transaction">The DbTransaction to use for the session.</param>
445
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
446
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
447
    /// <param name="queryGenerator">The query generator provider.</param>
448
    /// <param name="logger">The logger delegate for writing log messages.</param>
449
    /// <param name="commandTimeout">The default command timeout in seconds.</param>
450
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
451
    /// <exception cref="ArgumentNullException"><paramref name="transaction" /> is null</exception>
452
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="transaction" /> instance.</exception>
453
    public DataSession(
454
        DbTransaction transaction,
455
        bool disposeConnection = false,
456
        IDataCache? cache = null,
457
        IQueryGenerator? queryGenerator = null,
458
        IDataQueryLogger? logger = null,
459
        IEnumerable<IDataInterceptor>? interceptors = null,
460
        int? commandTimeout = null)
461
        : base(transaction, disposeConnection, cache, queryGenerator, logger, interceptors, commandTimeout)
×
462
    {
463
    }
×
464

465
    /// <summary>
466
    /// Initializes a new instance of the <see cref="DataSession" /> class.
467
    /// </summary>
468
    /// <param name="dataConfiguration">The configuration for the session</param>
469
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration" /> is null</exception>
470
    public DataSession(IDataConfiguration<TDiscriminator> dataConfiguration)
471
        : base(dataConfiguration)
4✔
472
    {
473
    }
4✔
474
}
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