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

loresoft / FluentCommand / 23278216331

19 Mar 2026 03:19AM UTC coverage: 57.398% (+0.7%) from 56.658%
23278216331

push

github

pwelter34
Enable nullable and improve source generators

1403 of 3069 branches covered (45.72%)

Branch coverage included in aggregate %.

527 of 907 new or added lines in 58 files covered. (58.1%)

22 existing lines in 10 files now uncovered.

4288 of 6846 relevant lines covered (62.64%)

330.58 hits per line

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

81.62
/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; }
1,023✔
28

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

34
    /// <summary>
35
    /// Gets the underlying <see cref="IDataCache"/> for the session.
36
    /// </summary>
37
    public IDataCache? Cache { get; }
145✔
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; }
56✔
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; }
167✔
54

55
    /// <summary>
56
    /// Gets the interceptors registered for this session.
57
    /// </summary>
58
    /// <value>
59
    /// The list of <see cref="IDataInterceptor"/> instances active for this session.
60
    /// </value>
61
    public IReadOnlyList<IDataInterceptor> Interceptors => _interceptors;
4✔
62

63

64
    /// <summary>
65
    /// Initializes a new instance of the <see cref="DataSession" /> class.
66
    /// </summary>
67
    /// <param name="connection">The DbConnection to use for the session.</param>
68
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
69
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
70
    /// <param name="queryGenerator">The query generator provider.</param>
71
    /// <param name="logger">The logger delegate for writing log messages.</param>
72
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
73
    /// <exception cref="ArgumentNullException"><paramref name="connection" /> is null</exception>
74
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="connection" /> instance.</exception>
75
    public DataSession(
10✔
76
        DbConnection connection,
10✔
77
        bool disposeConnection = true,
10✔
78
        IDataCache? cache = null,
10✔
79
        IQueryGenerator? queryGenerator = null,
10✔
80
        IDataQueryLogger? logger = null,
10✔
81
        IEnumerable<IDataInterceptor>? interceptors = null)
10✔
82
    {
83
        if (connection == null)
10!
84
            throw new ArgumentNullException(nameof(connection));
×
85

86
        if (string.IsNullOrEmpty(connection.ConnectionString))
10!
87
            throw new ArgumentException("Invalid connection string", nameof(connection));
×
88

89
        Connection = connection;
10✔
90
        Cache = cache;
10✔
91
        QueryGenerator = queryGenerator ?? new SqlServerGenerator();
10✔
92
        QueryLogger = logger;
10✔
93

94
        _interceptors = interceptors is null ? [] : [.. interceptors];
10✔
95
        _connectionInterceptors = [.. _interceptors.OfType<IDataConnectionInterceptor>()];
10✔
96
        _commandInterceptors = [.. _interceptors.OfType<IDataCommandInterceptor>()];
10✔
97

98
        _disposeConnection = disposeConnection;
10✔
99
    }
10✔
100

101
    /// <summary>
102
    /// Initializes a new instance of the <see cref="DataSession"/> class.
103
    /// </summary>
104
    /// <param name="transaction">The DbTransaction to use for the session.</param>
105
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
106
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
107
    /// <param name="queryGenerator">The query generator provider.</param>
108
    /// <param name="logger">The logger delegate for writing log messages.</param>
109
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
110
    /// <exception cref="ArgumentNullException"><paramref name="transaction" /> is null</exception>
111
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="transaction" /> instance.</exception>
112
    public DataSession(
113
        DbTransaction transaction,
114
        bool disposeConnection = false,
115
        IDataCache? cache = null,
116
        IQueryGenerator? queryGenerator = null,
117
        IDataQueryLogger? logger = null,
118
        IEnumerable<IDataInterceptor>? interceptors = null)
NEW
119
        : this(GetTransactionConnection(transaction), disposeConnection, cache, queryGenerator, logger, interceptors)
×
120
    {
NEW
121
        Transaction = transaction;
×
122
    }
×
123

124
    /// <summary>
125
    /// Initializes a new instance of the <see cref="DataSession" /> class.
126
    /// </summary>
127
    /// <param name="dataConfiguration">The configuration for the session</param>
128
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration"/> is null</exception>
129
    public DataSession(IDataConfiguration dataConfiguration)
151✔
130
    {
131
        if (dataConfiguration == null)
151!
132
            throw new ArgumentNullException(nameof(dataConfiguration));
×
133

134
        Connection = dataConfiguration.CreateConnection();
151✔
135
        Cache = dataConfiguration.DataCache;
151✔
136
        QueryGenerator = dataConfiguration.QueryGenerator;
151✔
137
        QueryLogger = dataConfiguration.QueryLogger;
151✔
138

139
        _interceptors = dataConfiguration.Interceptors is null ? [] : [.. dataConfiguration.Interceptors];
151!
140
        _connectionInterceptors = [.. _interceptors.OfType<IDataConnectionInterceptor>()];
151✔
141
        _commandInterceptors = [.. _interceptors.OfType<IDataCommandInterceptor>()];
151✔
142

143
        _disposeConnection = true;
151✔
144
    }
151✔
145

146

147
    /// <summary>
148
    /// Starts a database transaction with the specified isolation level.
149
    /// </summary>
150
    /// <param name="isolationLevel">Specifies the isolation level for the transaction.</param>
151
    /// <returns>
152
    /// A <see cref="DbTransaction" /> representing the new transaction.
153
    /// </returns>
154
    public DbTransaction BeginTransaction(IsolationLevel isolationLevel = IsolationLevel.Unspecified)
155
    {
156
        EnsureConnection();
1✔
157
        Transaction = Connection.BeginTransaction(isolationLevel);
1✔
158

159
        return Transaction;
1✔
160
    }
161

162
#if NETCOREAPP3_0_OR_GREATER
163
    /// <summary>
164
    /// Starts a database transaction with the specified isolation level.
165
    /// </summary>
166
    /// <param name="isolationLevel">Specifies the isolation level for the transaction.</param>
167
    /// <param name="cancellationToken">The cancellation instruction.</param>
168
    /// <returns>
169
    /// A <see cref="DbTransaction" /> representing the new transaction.
170
    /// </returns>
171
    public async Task<DbTransaction> BeginTransactionAsync(IsolationLevel isolationLevel = IsolationLevel.Unspecified, CancellationToken cancellationToken = default)
172
    {
173
        await EnsureConnectionAsync(cancellationToken).ConfigureAwait(false);
2✔
174
        Transaction = await Connection.BeginTransactionAsync(isolationLevel, cancellationToken).ConfigureAwait(false);
2✔
175

176
        return Transaction;
2✔
177
    }
2✔
178
#endif
179

180
    /// <summary>
181
    /// Starts a data command with the specified SQL.
182
    /// </summary>
183
    /// <param name="sql">The SQL statement.</param>
184
    /// <returns>
185
    /// A fluent <see langword="interface" /> to a data command.
186
    /// </returns>
187
    public IDataCommand Sql(string sql)
188
    {
189
        var dataCommand = new DataCommand(this, Transaction, _commandInterceptors);
159✔
190
        return dataCommand.Sql(sql);
159✔
191
    }
192

193
    /// <summary>
194
    /// Starts a data command with the specified stored procedure name.
195
    /// </summary>
196
    /// <param name="storedProcedureName">Name of the stored procedure.</param>
197
    /// <returns>
198
    /// A fluent <see langword="interface" /> to a data command.
199
    /// </returns>
200
    public IDataCommand StoredProcedure(string storedProcedureName)
201
    {
202
        var dataCommand = new DataCommand(this, Transaction, _commandInterceptors);
9✔
203
        return dataCommand.StoredProcedure(storedProcedureName);
9✔
204
    }
205

206

207
    /// <summary>
208
    /// Ensures the connection is open.
209
    /// </summary>
210
    /// <exception cref="InvalidOperationException">Failed to open connection</exception>
211
    public void EnsureConnection()
212
    {
213
        AssertDisposed();
73✔
214

215
        bool justOpened = false;
73✔
216
        if (ConnectionState.Closed == Connection.State)
73✔
217
        {
218
            Connection.Open();
70✔
219
            _openedConnection = true;
70✔
220
            justOpened = true;
70✔
221
        }
222

223
        if (_openedConnection)
73✔
224
            _connectionRequestCount++;
73✔
225

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

230
        // run connection opened interceptors only when the connection was just opened by this context
231
        if (justOpened)
73✔
232
        {
233
            foreach (var interceptor in _connectionInterceptors)
146✔
234
                interceptor.ConnectionOpened(Connection, this);
3✔
235
        }
236
    }
73✔
237

238
    /// <summary>
239
    /// Ensures the connection is open asynchronous.
240
    /// </summary>
241
    /// <param name="cancellationToken">The cancellation instruction.</param>
242
    /// <returns>A task representing the asynchronous operation.</returns>
243
    /// <exception cref="InvalidOperationException">Failed to open connection</exception>
244
    public async Task EnsureConnectionAsync(CancellationToken cancellationToken = default)
245
    {
246
        AssertDisposed();
109✔
247

248
        bool justOpened = false;
109✔
249
        if (ConnectionState.Closed == Connection.State)
109✔
250
        {
251
            await Connection.OpenAsync(cancellationToken).ConfigureAwait(false);
106✔
252
            _openedConnection = true;
106✔
253
            justOpened = true;
106✔
254
        }
255

256
        if (_openedConnection)
109✔
257
            _connectionRequestCount++;
109✔
258

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

263
        // run connection opened interceptors only when the connection was just opened by this context
264
        if (justOpened)
109✔
265
        {
266
            foreach (var interceptor in _connectionInterceptors)
214✔
267
                await interceptor.ConnectionOpenedAsync(Connection, this, cancellationToken).ConfigureAwait(false);
1✔
268
        }
269
    }
109✔
270

271
    /// <summary>
272
    /// Releases the connection.
273
    /// </summary>
274
    public void ReleaseConnection()
275
    {
276
        AssertDisposed();
76✔
277

278
        if (!_openedConnection)
76✔
279
            return;
2✔
280

281
        if (_connectionRequestCount > 0)
74✔
282
            _connectionRequestCount--;
74✔
283

284
        if (_connectionRequestCount != 0)
74✔
285
            return;
3✔
286

287
        // When no operation is using the connection and the context had opened the connection
288
        // the connection can be closed
289
        Connection.Close();
71✔
290
        _openedConnection = false;
71✔
291
    }
71✔
292

293
#if NETCOREAPP3_0_OR_GREATER
294
    /// <summary>
295
    /// Releases the connection.
296
    /// </summary>
297
    public async Task ReleaseConnectionAsync()
298
    {
299
        AssertDisposed();
107✔
300

301
        if (!_openedConnection)
107✔
302
            return;
2✔
303

304
        if (_connectionRequestCount > 0)
105✔
305
            _connectionRequestCount--;
105✔
306

307
        if (_connectionRequestCount != 0)
105✔
308
            return;
3✔
309

310
        // When no operation is using the connection and the context had opened the connection
311
        // the connection can be closed
312
        await Connection.CloseAsync().ConfigureAwait(false);
102✔
313
        _openedConnection = false;
102✔
314
    }
107✔
315

316
    /// <summary>
317
    /// Disposes the managed resources.
318
    /// </summary>
319
    protected override async ValueTask DisposeResourcesAsync()
320
    {
321
        if (!_disposeConnection)
77!
322
            return;
×
323

324
        if (Transaction is not null)
77✔
325
            await Transaction.DisposeAsync().ConfigureAwait(false);
1✔
326

327
        await Connection.DisposeAsync().ConfigureAwait(false);
77✔
328
    }
77✔
329
#endif
330

331
    /// <summary>
332
    /// Disposes the managed resources.
333
    /// </summary>
334
    protected override void DisposeManagedResources()
335
    {
336
        if (!_disposeConnection)
29!
337
            return;
×
338

339
        Transaction?.Dispose();
29✔
340
        Connection.Dispose();
29✔
341
    }
29✔
342

343
    private static DbConnection GetTransactionConnection(DbTransaction transaction)
344
    {
NEW
345
        if (transaction is null)
×
NEW
346
            throw new ArgumentNullException(nameof(transaction));
×
347

NEW
348
        return transaction.Connection
×
NEW
349
            ?? throw new ArgumentException("Transaction has no associated connection.", nameof(transaction));
×
350
    }
351
}
352

353
/// <summary>
354
/// A fluent class for a data session by discriminator.  Used to register multiple instances of IDataSession.
355
/// </summary>
356
/// <typeparam name="TDiscriminator">The type of the discriminator.</typeparam>
357
/// <seealso cref="FluentCommand.DisposableBase" />
358
/// <seealso cref="FluentCommand.IDataSession" />
359
public class DataSession<TDiscriminator> : DataSession, IDataSession<TDiscriminator>
360
{
361
    /// <summary>
362
    /// Initializes a new instance of the <see cref="DataSession" /> class.
363
    /// </summary>
364
    /// <param name="connection">The DbConnection to use for the session.</param>
365
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
366
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
367
    /// <param name="queryGenerator">The query generator provider.</param>
368
    /// <param name="logger">The logger delegate for writing log messages.</param>
369
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
370
    /// <exception cref="ArgumentNullException"><paramref name="connection" /> is null</exception>
371
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="connection" /> instance.</exception>
372
    public DataSession(
373
        DbConnection connection,
374
        bool disposeConnection = true,
375
        IDataCache? cache = null,
376
        IQueryGenerator? queryGenerator = null,
377
        IDataQueryLogger? logger = null,
378
        IEnumerable<IDataInterceptor>? interceptors = null)
UNCOV
379
        : base(connection, disposeConnection, cache, queryGenerator, logger, interceptors)
×
380
    {
381
    }
×
382

383
    /// <summary>
384
    /// Initializes a new instance of the <see cref="DataSession"/> class.
385
    /// </summary>
386
    /// <param name="transaction">The DbTransaction to use for the session.</param>
387
    /// <param name="disposeConnection">if set to <c>true</c> dispose connection with this session.</param>
388
    /// <param name="cache">The <see cref="IDataCache" /> used to cached results of queries.</param>
389
    /// <param name="queryGenerator">The query generator provider.</param>
390
    /// <param name="logger">The logger delegate for writing log messages.</param>
391
    /// <param name="interceptors">The interceptors to apply during this session's lifetime.</param>
392
    /// <exception cref="ArgumentNullException"><paramref name="transaction" /> is null</exception>
393
    /// <exception cref="ArgumentException">Invalid connection string on <paramref name="transaction" /> instance.</exception>
394
    public DataSession(
395
        DbTransaction transaction,
396
        bool disposeConnection = false,
397
        IDataCache? cache = null,
398
        IQueryGenerator? queryGenerator = null,
399
        IDataQueryLogger? logger = null,
400
        IEnumerable<IDataInterceptor>? interceptors = null)
UNCOV
401
        : base(transaction, disposeConnection, cache, queryGenerator, logger, interceptors)
×
402
    {
403
    }
×
404

405
    /// <summary>
406
    /// Initializes a new instance of the <see cref="DataSession" /> class.
407
    /// </summary>
408
    /// <param name="dataConfiguration">The configuration for the session</param>
409
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration" /> is null</exception>
410
    public DataSession(IDataConfiguration<TDiscriminator> dataConfiguration)
411
        : base(dataConfiguration)
1✔
412
    {
413
    }
1✔
414
}
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