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

loresoft / FluentCommand / 26594173245

28 May 2026 06:28PM UTC coverage: 55.553% (+0.7%) from 54.902%
26594173245

push

github

pwelter34
Move JSON support, add docs and examples

1358 of 3215 branches covered (42.24%)

Branch coverage included in aggregate %.

103 of 234 new or added lines in 9 files covered. (44.02%)

371 existing lines in 26 files now uncovered.

4389 of 7130 relevant lines covered (61.56%)

312.89 hits per line

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

83.33
/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
        ArgumentNullException.ThrowIfNull(connection);
15✔
94

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

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

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

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

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

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

145
        Connection = dataConfiguration.CreateConnection();
187✔
146
        Cache = dataConfiguration.DataCache;
187✔
147
        QueryGenerator = dataConfiguration.QueryGenerator;
187✔
148
        QueryLogger = dataConfiguration.QueryLogger;
187✔
149
        CommandTimeout = dataConfiguration.CommandTimeout;
187✔
150

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

155
        _disposeConnection = true;
187✔
156
    }
187✔
157

158

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

171
        return Transaction;
1✔
172
    }
173

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

188
        return Transaction;
2✔
189
    }
2✔
190
#endif
191

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

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

218

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

227
        bool justOpened = false;
160✔
228
        if (ConnectionState.Closed == Connection.State)
160✔
229
        {
230
            Connection.Open();
157✔
231
            _openedConnection = true;
157✔
232
            justOpened = true;
157✔
233
        }
234

235
        if (_openedConnection)
160!
236
            _connectionRequestCount++;
160✔
237

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

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

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

260
        bool justOpened = false;
113✔
261
        if (ConnectionState.Closed == Connection.State)
113✔
262
        {
263
            await Connection.OpenAsync(cancellationToken).ConfigureAwait(false);
110✔
264
            _openedConnection = true;
110✔
265
            justOpened = true;
110✔
266
        }
267

268
        if (_openedConnection)
113✔
269
            _connectionRequestCount++;
113✔
270

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

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

283
    /// <summary>
284
    /// Releases the connection.
285
    /// </summary>
286
    public void ReleaseConnection()
287
    {
288
        AssertDisposed();
162✔
289

290
        if (!_openedConnection)
162✔
291
            return;
2✔
292

293
        if (_connectionRequestCount > 0)
160!
294
            _connectionRequestCount--;
160✔
295

296
        if (_connectionRequestCount != 0)
160✔
297
            return;
3✔
298

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

304
        Connection.Close();
157✔
305
        _openedConnection = false;
157✔
306
    }
157✔
307

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

316
        if (!_openedConnection)
112✔
317
            return;
2✔
318

319
        if (_connectionRequestCount > 0)
110✔
320
            _connectionRequestCount--;
110✔
321

322
        if (_connectionRequestCount != 0)
110✔
323
            return;
3✔
324

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

330
        await Connection.CloseAsync().ConfigureAwait(false);
107✔
331
        _openedConnection = false;
107✔
332
    }
112✔
333

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

342
        if (Transaction is not null)
80✔
343
            await Transaction.DisposeAsync().ConfigureAwait(false);
1✔
344

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

350
            _openedConnection = false;
1✔
351
        }
352

353
        await Connection.DisposeAsync().ConfigureAwait(false);
80✔
354
    }
80✔
355
#endif
356

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

365
        Transaction?.Dispose();
64✔
366

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

372
            _openedConnection = false;
1✔
373
        }
374

375
        Connection.Dispose();
64✔
376
    }
64✔
377

378
    private static DbConnection GetTransactionConnection(DbTransaction transaction)
379
    {
UNCOV
380
        ArgumentNullException.ThrowIfNull(transaction);
×
381

382
        return transaction.Connection
×
383
            ?? throw new ArgumentException("Transaction has no associated connection.", nameof(transaction));
×
384
    }
385
}
386

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

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

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