• 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

83.58
/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(
20✔
92
        DbConnection connection,
20✔
93
        bool disposeConnection = true,
20✔
94
        IDataCache? cache = null,
20✔
95
        IQueryGenerator? queryGenerator = null,
20✔
96
        IDataQueryLogger? logger = null,
20✔
97
        IEnumerable<IDataInterceptor>? interceptors = null,
20✔
98
        int? commandTimeout = null,
20✔
99
        JsonSerializerOptions? jsonSerializerOptions = null)
20✔
100
    {
101
        ArgumentNullException.ThrowIfNull(connection);
20✔
102

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

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

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

117
        _disposeConnection = disposeConnection;
20✔
118
    }
20✔
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);
267✔
214
        return dataCommand.Sql(sql);
267✔
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();
172✔
238

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

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

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

254
        // run connection opened interceptors only when the connection was just opened by this context
255
        if (justOpened)
172✔
256
        {
257
            foreach (var interceptor in _connectionInterceptors)
346✔
258
                interceptor.ConnectionOpened(Connection, this);
4✔
259
        }
260
    }
172✔
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)
115!
285
            throw new InvalidOperationException($"Execution of the command requires an open and available connection. The connection's current state is {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();
174✔
301

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

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

308
        if (_connectionRequestCount != 0)
172✔
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)
346✔
314
            interceptor.ConnectionClosing(Connection, this);
4✔
315

316
        Connection.Close();
169✔
317
        _openedConnection = false;
169✔
318
    }
169✔
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)
72!
375
            return;
×
376

377
        Transaction?.Dispose();
72✔
378

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

384
            _openedConnection = false;
1✔
385
        }
386

387
        Connection.Dispose();
72✔
388
    }
72✔
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

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

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

455
    /// <summary>
456
    /// Initializes a new instance of the <see cref="DataSession" /> class.
457
    /// </summary>
458
    /// <param name="dataConfiguration">The configuration for the session</param>
459
    /// <exception cref="ArgumentNullException"><paramref name="dataConfiguration" /> is null</exception>
460
    public DataSession(IDataConfiguration<TDiscriminator> dataConfiguration)
461
        : base(dataConfiguration)
4✔
462
    {
463
    }
4✔
464
}
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