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

HicServices / RDMP / 13318089130

13 Feb 2025 10:13PM UTC coverage: 57.398% (+0.004%) from 57.394%
13318089130

Pull #2134

github

jas88
Update ChildProviderTests.cs

Fix up TestUpTo method
Pull Request #2134: CodeQL fixups

11346 of 21308 branches covered (53.25%)

Branch coverage included in aggregate %.

104 of 175 new or added lines in 45 files covered. (59.43%)

362 existing lines in 23 files now uncovered.

32218 of 54590 relevant lines covered (59.02%)

17091.93 hits per line

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

83.87
/Rdmp.Core/MapsDirectlyToDatabaseTable/TableRepository.cs
1
// Copyright (c) The University of Dundee 2018-2019
2
// This file is part of the Research Data Management Platform (RDMP).
3
// RDMP is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
4
// RDMP is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
5
// You should have received a copy of the GNU General Public License along with RDMP. If not, see <https://www.gnu.org/licenses/>.
6

7
using System;
8
using System.Collections.Generic;
9
using System.Data;
10
using System.Data.Common;
11
using System.Diagnostics;
12
using System.Globalization;
13
using System.Linq;
14
using System.Linq.Expressions;
15
using System.Reflection;
16
using System.Threading;
17
using FAnsi;
18
using FAnsi.Connections;
19
using FAnsi.Discovery;
20
using NLog;
21
using Rdmp.Core.MapsDirectlyToDatabaseTable.Injection;
22
using Rdmp.Core.MapsDirectlyToDatabaseTable.Revertable;
23
using Rdmp.Core.MapsDirectlyToDatabaseTable.Versioning;
24
using Rdmp.Core.ReusableLibraryCode;
25
using Rdmp.Core.ReusableLibraryCode.DataAccess;
26
using static Terminal.Gui.MainLoop;
27

28
namespace Rdmp.Core.MapsDirectlyToDatabaseTable;
29

30
/// <summary>
31
/// See ITableRepository
32
/// </summary>
33
public abstract class TableRepository : ITableRepository
34
{
35
    //fields
36
    protected DbConnectionStringBuilder _connectionStringBuilder;
37
    public IObscureDependencyFinder ObscureDependencyFinder { get; set; }
7,466✔
38

39
    private static readonly Lock OLockUpdateCommands = new();
4✔
40
    private readonly UpdateCommandStore _updateCommandStore = new();
1,018✔
41
    public bool SupportsCommits => true;
4✔
42

43
    //'accessors'
44
    public string ConnectionString => _connectionStringBuilder.ConnectionString;
2,738✔
45
    public DbConnectionStringBuilder ConnectionStringBuilder => _connectionStringBuilder;
60✔
46

47
    public DiscoveredServer DiscoveredServer { get; protected set; }
1,572,038✔
48

49
    /// <summary>
50
    /// Constructors for quickly resolving <see cref="ConstructEntity"/> calls rather than relying on reflection e.g. ObjectConstructor
51
    /// </summary>
52
    protected Dictionary<Type, Func<IRepository, DbDataReader, IMapsDirectlyToDatabaseTable>> Constructors = new();
1,018✔
53

54
    private readonly Logger _logger = LogManager.GetCurrentClassLogger();
1,018✔
55

56
    private Lazy<DiscoveredTable[]> _tables;
57

58
    //If you are calling this constructor then make sure to set the connection strings in your derived class constructor
59
    public TableRepository()
1,018✔
60
    {
61
        _tables = new Lazy<DiscoveredTable[]>(() => DiscoveredServer.GetCurrentDatabase().DiscoverTables(false));
1,026✔
62
    }
1,018✔
63

64
    public TableRepository(IObscureDependencyFinder obscureDependencyFinder,
65
        DbConnectionStringBuilder connectionStringBuilder) : this()
862✔
66
    {
67
        ObscureDependencyFinder = obscureDependencyFinder;
862✔
68
        _connectionStringBuilder = connectionStringBuilder;
862✔
69
        DiscoveredServer = new DiscoveredServer(connectionStringBuilder);
862✔
70
    }
862✔
71

72
    /// <inheritdoc/>
73
    public void DeleteFromDatabase(IMapsDirectlyToDatabaseTable oTableWrapperObject)
74
    {
75
        //do not log information about access credentials
76
        if (oTableWrapperObject is not IDataAccessCredentials)
2,484✔
77
            _logger.Debug(
2,332✔
78
                $"Deleted,{oTableWrapperObject.GetType().Name},{oTableWrapperObject.ID},{oTableWrapperObject}");
2,332✔
79

80
        lock (OLockUpdateCommands)
81
        {
82
            //if the repository has obscure dependencies
83
            ObscureDependencyFinder
2,484✔
84
                ?.ThrowIfDeleteDisallowed(
2,484✔
85
                    oTableWrapperObject); //confirm that deleting the object is allowed by the dependencies
2,484✔
86

87
            using var con = GetConnection();
2,476✔
88
            using (var cmd = DatabaseCommandHelper.GetCommand(
2,476✔
89
                       $"DELETE FROM {Wrap(oTableWrapperObject.GetType().Name)} WHERE ID =@ID", con.Connection,
2,476✔
90
                       con.Transaction))
2,476✔
91
            {
92
                DatabaseCommandHelper.AddParameterWithValueToCommand("@ID", cmd, oTableWrapperObject.ID);
2,476✔
93
                cmd.ExecuteNonQuery();
2,476✔
94
            }
2,472✔
95

96
            //likewise if there are obscure dependency handlers let them handle cascading this delete into the mists of their obscure functionality (e.g. deleting a Catalogue in CatalogueRepository would delete all Evaluations of that Catalogue in the DQE repository because they would then be orphans)
97
            ObscureDependencyFinder?.HandleCascadeDeletesForDeletedObject(oTableWrapperObject);
2,472✔
98
        }
99

100
        Deleting?.Invoke(this, new IMapsDirectlyToDatabaseTableEventArgs(oTableWrapperObject));
2,472!
UNCOV
101
    }
×
102

103
    /// <inheritdoc/>
104
    public T[] GetAllObjectsWithParent<T, T2>(T2 parent) where T : IMapsDirectlyToDatabaseTable, IInjectKnown<T2>
105
        where T2 : IMapsDirectlyToDatabaseTable
106
    {
107
        var toReturn = GetAllObjectsWithParent<T>(parent);
13,912✔
108
        foreach (var v in toReturn)
428,452✔
109
            v.InjectKnown(parent);
200,314✔
110

111
        return toReturn;
13,912✔
112
    }
113

114

115
    /// <inheritdoc/>
116
    public void SaveToDatabase(IMapsDirectlyToDatabaseTable oTableWrapperObject)
117
    {
118
        var r = (IRevertable)oTableWrapperObject;
40,184✔
119
        var changes = r.HasLocalChanges();
40,184✔
120

121
        if (changes.Evaluation == ChangeDescription.NoChanges)
40,184✔
122
            return;
17,339✔
123

124
        var e = new SaveEventArgs(oTableWrapperObject);
22,845✔
125
        Saving?.Invoke(this, e);
22,845!
126

127
        if (e.Cancel)
22,845!
UNCOV
128
            return;
×
129

130
        foreach (var c in changes.Differences)
122,012✔
131
            _logger.Debug(
38,161✔
132
                $"Save,{oTableWrapperObject.GetType().Name},{oTableWrapperObject.ID},{c.Property},{c.DatabaseValue},{c.LocalValue}");
38,161✔
133

134
        lock (OLockUpdateCommands)
135
        {
136
            using var managedConnection = GetConnection();
22,845✔
137
            var cmd = GetUpdateCommandFromStore(oTableWrapperObject.GetType(), managedConnection);
22,845✔
138

139
            PopulateUpdateCommandValuesWithCurrentState(cmd, oTableWrapperObject);
22,845✔
140

141
            cmd.Connection = managedConnection.Connection;
22,845✔
142

143
            //change the transaction of the update command to the specified transaction but only long enough to run it
144
            var transactionBefore = cmd.Transaction;
22,845✔
145
            cmd.Transaction = managedConnection.Transaction;
22,845✔
146

147
            int affectedRows;
148
            try
149
            {
150
                //run the save
151
                affectedRows = cmd.ExecuteNonQuery();
22,845✔
152
            }
22,841✔
153
            finally
154
            {
155
                //reset the transaction to whatever it was before
156
                cmd.Transaction = transactionBefore;
22,845✔
157
            }
22,845✔
158

159

160
            if (affectedRows != 1)
22,841!
161
                throw new Exception(
×
UNCOV
162
                    $"Attempted to update {oTableWrapperObject.GetType().Name} with ID {oTableWrapperObject.ID} but the UPDATE command resulted in {affectedRows} affected rows");
×
163
        }
164
    }
22,841✔
165

166
    protected static void PopulateUpdateCommandValuesWithCurrentState(DbCommand cmd,
167
        IMapsDirectlyToDatabaseTable oTableWrapperObject)
168
    {
169
        foreach (DbParameter p in cmd.Parameters)
687,932✔
170
        {
171
            var prop = oTableWrapperObject.GetType().GetProperty(p.ParameterName.Trim('@'));
321,121✔
172

173
            var propValue = prop.GetValue(oTableWrapperObject, null);
321,121✔
174

175
            //if it is a complex type but IConvertible e.g. CatalogueFolder
176
            if (!prop.PropertyType.IsValueType && propValue is IConvertible c && c.GetTypeCode() == TypeCode.String)
321,121✔
177
                propValue = c.ToString(CultureInfo.CurrentCulture);
64,739✔
178

179
            SetParameterToValue(p, propValue);
321,121✔
180
        }
181

182
        cmd.Parameters["@ID"].Value = oTableWrapperObject.ID;
22,845✔
183
    }
22,845✔
184

185
    private static void SetParameterToValue(DbParameter p, object propValue)
186
    {
187
        p.Value = propValue switch
740,715!
188
        {
740,715✔
189
            null => DBNull.Value,
147,284✔
190
            string value when string.IsNullOrWhiteSpace(value) => DBNull.Value,
161,861✔
191
            Uri => propValue.ToString(),
4✔
192
            TimeSpan => propValue.ToString(),
26✔
UNCOV
193
            Version => propValue.ToString(),
×
194
            _ => propValue
591,851✔
195
        };
740,715✔
196
    }
740,715✔
197

198
    public bool StillExists<T>(int id) where T : IMapsDirectlyToDatabaseTable => StillExists(typeof(T), id);
8✔
199

200
    public bool StillExists(IMapsDirectlyToDatabaseTable o) => StillExists(o.GetType(), o.ID);
172✔
201

202
    public bool StillExists(Type type, int id)
203
    {
204
        //go to database to see if it exists
205
        using var connection = GetConnection();
338✔
206
        using var selectCommand = DatabaseCommandHelper.GetCommand(
338✔
207
            $"SELECT case when exists(select * FROM {Wrap(type.Name)} WHERE ID= {id}) then 1 else 0 end",
338✔
208
            connection.Connection, connection.Transaction);
338✔
209
        return Convert.ToBoolean(selectCommand.ExecuteScalar());
338✔
210
    }
338✔
211

212
    /// <summary>
213
    /// Gets all the objects of type T that have the parent 'parent' (which will be interrogated by its ID).  Note that for this to work the type T must have a property which is EXACTLY the Parent objects name with _ID afterwards
214
    /// </summary>
215
    /// <typeparam name="T"></typeparam>
216
    /// <param name="parent"></param>
217
    /// <returns></returns>
218
    public T[] GetAllObjectsWithParent<T>(IMapsDirectlyToDatabaseTable parent) where T : IMapsDirectlyToDatabaseTable =>
219
        //no cached result so fallback on regular method
220
        GetAllObjectsWhere<T>($"{parent.GetType().Name}_ID", parent.ID);
212,348✔
221

222
    public T GetObjectByID<T>(int id) where T : IMapsDirectlyToDatabaseTable =>
223
        typeof(T).IsInterface
130,228!
224
            ? throw new Exception(
130,228✔
225
                "GetObjectByID<T> requires a proper class not an interface so that it can access the correct table")
130,228✔
226
            : (T)GetObjectByID(typeof(T), id);
130,228✔
227

228
    public IMapsDirectlyToDatabaseTable GetObjectByID(Type type, int id)
229
    {
230
        if (id == 0)
172,358✔
231
            return null;
3,418✔
232

233
        var typename = Wrap(type.Name);
168,940✔
234

235
        using var connection = GetConnection();
168,940✔
236
        using var selectCommand = DatabaseCommandHelper.GetCommand($"SELECT * FROM {typename} WHERE ID={id}",
168,940✔
237
            connection.Connection, connection.Transaction);
168,940✔
238
        using var r = selectCommand.ExecuteReader();
168,940✔
239
        if (!r.HasRows)
168,940✔
240
            throw new KeyNotFoundException($"Could not find {type.Name} with ID {id}");
30✔
241
        r.Read();
168,910✔
242
        return ConstructEntity(type, r);
168,910✔
243
    }
168,910✔
244

245
    public string Wrap(string name) => DiscoveredServer.GetQuerySyntaxHelper().EnsureWrapped(name);
757,613✔
246

247
    protected abstract IMapsDirectlyToDatabaseTable ConstructEntity(Type t, DbDataReader reader);
248

249
    private T ConstructEntity<T>(DbDataReader reader) where T : IMapsDirectlyToDatabaseTable
250
    {
251
        ArgumentNullException.ThrowIfNull(reader);
257,382✔
252

253
        try
254
        {
255
            return (T)ConstructEntity(typeof(T), reader);
257,382✔
256
        }
UNCOV
257
        catch (Exception e)
×
258
        {
UNCOV
259
            throw new Exception($"Could not construct '{typeof(T).Name}' with ID={reader["ID"]}", e);
×
260
        }
261
    }
257,382✔
262

263
    public virtual T[] GetAllObjects<T>() where T : IMapsDirectlyToDatabaseTable => GetAllObjects<T>(null);
37,335✔
264

265
    public T[] GetAllObjects<T>(string whereSQL) where T : IMapsDirectlyToDatabaseTable
266
    {
267
        var typename = Wrap(typeof(T).Name);
80,789✔
268

269
        //if there is whereSQL make sure it is a legit SQL where
270
        if (!string.IsNullOrWhiteSpace(whereSQL))
80,789✔
271
            if (!whereSQL.Trim().ToUpper().StartsWith("WHERE"))
43,454!
UNCOV
272
                throw new ArgumentException($"whereSQL did not start with the word 'WHERE', it was:{whereSQL}");
×
273

274
        var toReturn = new List<T>();
80,789✔
275

276
        using var opener = GetConnection();
80,789✔
277
        var selectCommand = DatabaseCommandHelper.GetCommand($"SELECT * FROM {typename} {whereSQL ?? ""}",
80,789✔
278
            opener.Connection, opener.Transaction);
80,789✔
279

280
        using var r = selectCommand.ExecuteReader();
80,789✔
281
        while (r.Read())
338,171✔
282
            toReturn.Add(ConstructEntity<T>(r));
257,382✔
283
        return toReturn.ToArray();
80,789✔
284
    }
80,789✔
285

286
    public T[] GetAllObjectsWhere<T>(string whereSQL, Dictionary<string, object> parameters = null)
287
        where T : IMapsDirectlyToDatabaseTable => GetAllObjects(typeof(T), whereSQL, parameters).Cast<T>().ToArray();
220,264✔
288

289
    public T[] GetAllObjectsWhere<T>(string property, object value1) where T : IMapsDirectlyToDatabaseTable =>
290
        GetAllObjectsWhere<T>($"WHERE {property} = @val", new Dictionary<string, object> { { "@val", value1 } });
218,518✔
291

292
    public T[] GetAllObjectsWhere<T>(string property1, object value1, ExpressionType operand, string property2,
293
        object value2) where T : IMapsDirectlyToDatabaseTable
294
    {
295
        var @operator = operand switch
1,746!
296
        {
1,746✔
UNCOV
297
            ExpressionType.AndAlso => "AND",
×
298
            ExpressionType.OrElse => "OR",
1,746✔
UNCOV
299
            _ => throw new NotSupportedException("operand")
×
300
        };
1,746✔
301

302
        return GetAllObjectsWhere<T>(
1,746✔
303
            $"WHERE {property1}=@val1 {@operator} {property2}=@val2", new Dictionary<string, object>
1,746✔
304
            {
1,746✔
305
                { "@val1", value1 },
1,746✔
306
                { "@val2", value2 }
1,746✔
307
            });
1,746✔
308
    }
309

310
    public IEnumerable<IMapsDirectlyToDatabaseTable> GetAllObjects(Type t, string whereSQL,
311
        Dictionary<string, object> parameters = null)
312
    {
313
        var typename = Wrap(t.Name);
221,562✔
314

315
        // if there is whereSQL make sure it is a legit SQL where
316
        if (!whereSQL.Trim().ToUpper().StartsWith("WHERE"))
221,562!
UNCOV
317
            throw new ArgumentException($"whereSQL did not start with the word 'WHERE', it was:{whereSQL}");
×
318

319
        var toReturn = new List<IMapsDirectlyToDatabaseTable>();
221,562✔
320
        using var opener = GetConnection();
221,562✔
321
        var selectCommand = PrepareCommand($"SELECT * FROM {typename} {whereSQL}", parameters, opener.Connection,
221,562✔
322
            opener.Transaction);
221,562✔
323

324
        using var r = selectCommand.ExecuteReader();
221,562✔
325
        while (r.Read())
634,632✔
326
            toReturn.Add(ConstructEntity(t, r));
413,070✔
327

328
        return toReturn.ToArray();
221,562✔
329
    }
221,562✔
330

331
    public IEnumerable<IMapsDirectlyToDatabaseTable> GetAllObjects(Type t)
332
    {
UNCOV
333
        var typename = Wrap(t.Name);
×
334

UNCOV
335
        var toReturn = new List<IMapsDirectlyToDatabaseTable>();
×
336

337
        using var opener = GetConnection();
×
338
        var selectCommand =
×
UNCOV
339
            DatabaseCommandHelper.GetCommand($"SELECT * FROM {typename}", opener.Connection, opener.Transaction);
×
340

341
        using var r = selectCommand.ExecuteReader();
×
342
        while (r.Read())
×
UNCOV
343
            toReturn.Add(ConstructEntity(t, r));
×
344

345
        return toReturn;
×
UNCOV
346
    }
×
347

348
    private DbCommand GetUpdateCommandFromStore(Type type, IManagedConnection managedConnection)
349
    {
350
        if (!_updateCommandStore.ContainsKey(type))
22,845✔
351
            _updateCommandStore.Add(type, _connectionStringBuilder, managedConnection.Connection,
1,692✔
352
                managedConnection.Transaction);
1,692✔
353

354
        return _updateCommandStore[type];
22,845✔
355
    }
356

357
    public Version GetVersion() =>
UNCOV
358
        DatabaseVersionProvider.GetVersionFromDatabase(DiscoveredServer.GetCurrentDatabase());
×
359

360
    public IEnumerable<T> GetAllObjectsInIDList<T>(IEnumerable<int> ids) where T : IMapsDirectlyToDatabaseTable =>
361
        GetAllObjectsInIDList(typeof(T), ids).Cast<T>();
1,694✔
362

363
    public IEnumerable<IMapsDirectlyToDatabaseTable> GetAllObjectsInIDList(Type elementType, IEnumerable<int> ids)
364
    {
365
        var inList = string.Join(",", ids);
1,694✔
366

367
        return string.IsNullOrWhiteSpace(inList)
1,694✔
368
            ? Enumerable.Empty<IMapsDirectlyToDatabaseTable>()
1,694✔
369
            : GetAllObjects(elementType, $" WHERE ID in ({inList})");
1,694✔
370
    }
371

372
    /// <inheritdoc/>
373
    public bool AreEqual(IMapsDirectlyToDatabaseTable obj1, object obj2)
374
    {
375
        if (obj1 == null && obj2 != null)
669,293!
UNCOV
376
            return false;
×
377

378
        if (obj2 == null && obj1 != null)
669,293✔
379
            return false;
562✔
380

381
        if (obj1 == null && obj2 == null)
668,731!
382
            throw new NotSupportedException(
×
UNCOV
383
                "Why are you comparing two null things against one another with this method?");
×
384

385
        return obj1.GetType() == obj2.GetType() && obj1.ID == ((IMapsDirectlyToDatabaseTable)obj2).ID &&
668,731✔
386
               obj1.Repository == ((IMapsDirectlyToDatabaseTable)obj2).Repository;
668,731✔
387
    }
388

389
    /// <inheritdoc/>
390
    public int GetHashCode(IMapsDirectlyToDatabaseTable obj1) => obj1.GetType().GetHashCode() * obj1.ID;
5,870,790✔
391

392
    /// <summary>
393
    /// Gets all public properties of the class that are not decorated with [<see cref="NoMappingToDatabase"/>]
394
    /// </summary>
395
    /// <param name="type"></param>
396
    /// <returns></returns>
397
    public static PropertyInfo[] GetPropertyInfos(Type type)
398
    {
399
        return type.GetProperties().Where(prop => !Attribute.IsDefined(prop, typeof(NoMappingToDatabase))).ToArray();
2,125,666✔
400
    }
401

402
    /// <inheritdoc/>
403
    public void RevertToDatabaseState(IMapsDirectlyToDatabaseTable localCopy)
404
    {
405
        //get new copy out of database
406
        var databaseState = GetObjectByID(localCopy.GetType(), localCopy.ID);
96✔
407

408
        Debug.Assert(localCopy.GetType() == databaseState.GetType());
409

410
        //set all properties on the passed in one to the database state
411
        foreach (var propertyInfo in GetPropertyInfos(localCopy.GetType()))
2,012✔
412
        {
413
            if (!propertyInfo.CanWrite)
910!
414
                throw new InvalidOperationException(
×
UNCOV
415
                    $"The property {propertyInfo.Name} has no setter for type {databaseState.GetType().Name}");
×
416

417
            propertyInfo.SetValue(localCopy, propertyInfo.GetValue(databaseState));
910✔
418
        }
419

420
        //Mark any cached data as out of date
421
        if (localCopy is IInjectKnown inject)
96✔
422
            inject.ClearAllInjections();
36✔
423
    }
96✔
424

425
    /// <inheritdoc/>
426
    public RevertableObjectReport HasLocalChanges(IMapsDirectlyToDatabaseTable localCopy)
427
    {
428
        IMapsDirectlyToDatabaseTable dbCopy;
429

430
        var toReturn = new RevertableObjectReport
41,716✔
431
        {
41,716✔
432
            Evaluation = ChangeDescription.NoChanges
41,716✔
433
        };
41,716✔
434

435
        try
436
        {
437
            dbCopy = GetObjectByID(localCopy.GetType(), localCopy.ID);
41,716✔
438
        }
41,710✔
439
        catch (KeyNotFoundException)
6✔
440
        {
441
            toReturn.Evaluation = ChangeDescription.DatabaseCopyWasDeleted;
6✔
442
            return toReturn;
6✔
443
        }
444

445
        foreach (var propertyInfo in GetPropertyInfos(localCopy.GetType()))
1,279,804✔
446
        {
447
            var local = propertyInfo.GetValue(localCopy);
598,192✔
448
            var db = propertyInfo.GetValue(dbCopy);
598,192✔
449

450
            //don't decided that "" vs null is a legit change
451
            if (local is string local1 && string.IsNullOrWhiteSpace(local1))
598,192✔
452
                local = null;
1,528✔
453

454
            if (db is string s && string.IsNullOrWhiteSpace(s))
598,192✔
455
                db = null;
2,566✔
456

457
            if (!Equals(local, db))
598,192✔
458
            {
459
                toReturn.Differences.Add(new RevertablePropertyDifference(propertyInfo, local, db));
38,893✔
460
                toReturn.Evaluation = ChangeDescription.DatabaseCopyDifferent;
38,893✔
461
            }
462
        }
463

464
        return toReturn;
41,710✔
465
    }
6✔
466

467

468
    #region new
469

470
    public void TestConnection()
471
    {
472
        try
473
        {
474
            using var con = GetConnection();
60✔
475
            if (con.Connection.State != ConnectionState.Open)
56!
UNCOV
476
                throw new Exception($"State of connection was {con.Connection.State}");
×
477
        }
56✔
478
        catch (Exception e)
4✔
479
        {
480
            var msg = _connectionStringBuilder.ConnectionString;
4✔
481

482
            var pass = DiscoveredServer.Helper.GetExplicitPasswordIfAny(_connectionStringBuilder);
4✔
483

484
            if (!string.IsNullOrWhiteSpace(pass))
4✔
485
                msg = msg.Replace(pass, "****");
2✔
486

487
            throw new Exception($"Testing connection failed, connection string was '{msg}'", e);
4✔
488
        }
489
    }
56✔
490

491
    public IEnumerable<T> SelectAll<T>(string selectQuery, string columnWithObjectID = null)
492
        where T : IMapsDirectlyToDatabaseTable
493
    {
494
        columnWithObjectID ??= $"{typeof(T).Name}_ID";
2,526✔
495

496
        using var opener = GetConnection();
2,526✔
497
        var idsToReturn = new List<int>();
2,526✔
498
        using (var cmd = DatabaseCommandHelper.GetCommand(selectQuery, opener.Connection, opener.Transaction))
2,526✔
499
        {
500
            using var r = cmd.ExecuteReader();
2,526✔
501
            while (r.Read()) idsToReturn.Add(Convert.ToInt32(r[columnWithObjectID]));
4,614✔
502
        }
503

504
        return !idsToReturn.Any()
2,526✔
505
            ? Enumerable.Empty<T>()
2,526✔
506
            : GetAllObjects<T>($"WHERE ID in ({string.Join(",", idsToReturn)})");
2,526✔
507
    }
2,526✔
508

509
    /// <summary>
510
    /// Runs the selectQuery (which must be a FULL QUERY) and uses @parameters for each of the kvps in the dictionary.  It expects the query result set to include
511
    /// a field which is named whatever your value in parameter columnWithObjectID is.  If you hate life you can pass a dbNullSubstition (which must also be of type
512
    /// T) in which case whenever a record in the result set is found with a DBNull in it, the substitute appears in the returned list instead.
513
    /// 
514
    /// <para>IMPORTANT: Order is NOT PRESERVED by this method so don't bother trying to sneak an Order by command into your select query </para>
515
    /// </summary>
516
    /// <typeparam name="T"></typeparam>
517
    /// <param name="selectQuery"></param>
518
    /// <param name="parameters"></param>
519
    /// <param name="columnWithObjectID"></param>
520
    /// <param name="dbNullSubstition"></param>
521
    /// <returns></returns>
522
    public IEnumerable<T> SelectAllWhere<T>(string selectQuery, string columnWithObjectID = null,
523
        Dictionary<string, object> parameters = null, T dbNullSubstition = default)
524
        where T : IMapsDirectlyToDatabaseTable
525
    {
526
        columnWithObjectID ??= $"{typeof(T).Name}_ID";
2,682!
527

528
        if (selectQuery.ToLower().Contains("order by "))
2,682!
UNCOV
529
            throw new Exception("Select Query contained an ORDER BY statement in it!");
×
530

531
        var nullsFound = 0;
2,682✔
532

533
        using var opener = GetConnection();
2,682✔
534
        var idsToReturn = new List<int>();
2,682✔
535
        var cmd = PrepareCommand(selectQuery, parameters, opener.Connection, opener.Transaction);
2,682✔
536
        using (var r = cmd.ExecuteReader())
2,682✔
537
        {
538
            while (r.Read())
3,164✔
539
            {
540
                if (r[columnWithObjectID] == DBNull.Value)
482!
541
                {
542
                    nullsFound++;
×
UNCOV
543
                    continue;
×
544
                }
545

546
                idsToReturn.Add(Convert.ToInt32(r[columnWithObjectID]));
482✔
547
            }
548
        }
2,682✔
549

550
        if (!idsToReturn.Any())
2,682✔
551
            return Enumerable.Empty<T>();
2,226✔
552

553

554
        var toReturn = GetAllObjects<T>($"WHERE ID in ({string.Join(",", idsToReturn)})").ToList();
456✔
555

556
        //this bit of hackery is if you're a crazy person who hates transparency and wants something like ColumnInfo.Missing to appear in the return list instead of an empty return list
557
        if (dbNullSubstition != null)
456!
558
            for (var i = 0; i < nullsFound; i++)
×
UNCOV
559
                toReturn.Add(dbNullSubstition);
×
560

561
        return toReturn;
456✔
562
    }
2,682✔
563

564

565
    private int InsertAndReturnID<T>(Dictionary<string, object> parameters = null)
566
        where T : IMapsDirectlyToDatabaseTable
567
    {
568
        using var opener = GetConnection();
55,508✔
569
        var query = CreateInsertStatement<T>(parameters);
55,508✔
570

571
        query += ";SELECT @@IDENTITY;";
55,508✔
572

573
        var cmd = PrepareCommand(query, parameters, opener.Connection, opener.Transaction);
55,508✔
574
        return int.Parse(cmd.ExecuteScalar().ToString());
55,508✔
575
    }
55,504✔
576

577
    private string CreateInsertStatement<T>(Dictionary<string, object> parameters)
578
        where T : IMapsDirectlyToDatabaseTable
579
    {
580
        _logger.Info($"Created New,{typeof(T).Name}");
55,508✔
581

582
        var query = $"INSERT INTO {Wrap(typeof(T).Name)}";
55,508✔
583
        if (parameters != null && parameters.Any())
55,508!
584
        {
585
            if (parameters.Any(kvp => kvp.Key.StartsWith("@")))
235,716!
586
                throw new InvalidOperationException(
×
UNCOV
587
                    $"Invalid parameters for {typeof(T).Name} INSERT. Do not use @ when specifying parameter names, this is SQL-specific and will be added when required: {string.Join(", ", parameters.Where(kvp => kvp.Key.StartsWith("@")))}");
×
588

589
            var columnString = string.Join(", ", parameters.Select(kvp => Wrap(kvp.Key)));
235,716✔
590
            var parameterString = string.Join(", ", parameters.Select(kvp => $"@{kvp.Key}"));
235,716✔
591
            query += $"({columnString}) VALUES ({parameterString})";
55,508✔
592
        }
593
        else
594
        {
UNCOV
595
            query += " DEFAULT VALUES";
×
596
        }
597

598
        return query;
55,508✔
599
    }
600

601

602
    public int Delete(string deleteQuery, Dictionary<string, object> parameters = null,
603
        bool throwOnZeroAffectedRows = true)
604
    {
605
        using var opener = GetConnection();
222✔
606
        var cmd = PrepareCommand(deleteQuery, parameters, opener.Connection, opener.Transaction);
222✔
607
        var affectedRows = cmd.ExecuteNonQuery();
222✔
608

609
        return affectedRows == 0 && throwOnZeroAffectedRows
222!
610
            ? throw new Exception($"Deleted failed, resulted in {affectedRows} affected rows")
222✔
611
            : affectedRows;
222✔
612
    }
222✔
613

614
    public int Update(string updateQuery, Dictionary<string, object> parameters)
615
    {
616
        using var opener = GetConnection();
11,638✔
617
        var cmd = PrepareCommand(updateQuery, parameters, opener.Connection, opener.Transaction);
11,638✔
618
        return cmd.ExecuteNonQuery();
11,638✔
619
    }
11,638✔
620

621
    public static DbCommand PrepareCommand(string sql, Dictionary<string, object> parameters, DbConnection con,
622
        DbTransaction transaction = null)
623
    {
624
        var cmd = DatabaseCommandHelper.GetCommand(sql, con, transaction);
293,140✔
625
        return parameters == null ? cmd : PrepareCommand(cmd, parameters);
293,140✔
626
    }
627

628
    public static DbCommand PrepareCommand(DbCommand cmd, Dictionary<string, object> parameters)
629
    {
630
        foreach (var kvp in parameters)
1,421,588✔
631
        {
632
            var paramName = kvp.Key.StartsWith("@") ? kvp.Key : $"@{kvp.Key}";
419,594✔
633

634
            // Check that this parameter name actually exists in the sql
635
            if (!cmd.CommandText.Contains(paramName))
419,594!
636
                throw new InvalidOperationException(
×
UNCOV
637
                    $"Parameter '{paramName}' does not exist in the SQL command ({cmd.CommandText})");
×
638

639
            //if it isn't yet in the command add it
640
            if (!cmd.Parameters.Contains(paramName))
419,594✔
641
                cmd.Parameters.Add(DatabaseCommandHelper.GetParameter(paramName, cmd));
419,594✔
642

643
            //set its value
644
            SetParameterToValue(cmd.Parameters[paramName], kvp.Value);
419,594✔
645
        }
646

647
        return cmd;
291,200✔
648
    }
649

650
    #endregion
651

652
    public void InsertAndHydrate<T>(T toCreate, Dictionary<string, object> constructorParameters)
653
        where T : IMapsDirectlyToDatabaseTable
654
    {
655
        var id = InsertAndReturnID<T>(constructorParameters);
55,508✔
656

657
        var actual = GetObjectByID<T>(id);
55,504✔
658

659
        //.Repository does not get included in this list because it is [NoMappingToDatabase]
660
        foreach (var prop in GetPropertyInfos(typeof(T)))
1,548,576✔
661
            prop.SetValue(toCreate, prop.GetValue(actual));
718,784✔
662

663
        toCreate.Repository = actual.Repository;
55,504✔
664

665
        NewObjectPool.Add(toCreate);
55,504✔
666

667
        Inserting?.Invoke(this, new IMapsDirectlyToDatabaseTableEventArgs(toCreate));
55,504✔
668
    }
24✔
669

670
    private readonly Lock _ongoingConnectionsLock = new();
1,018✔
671
    private readonly Dictionary<Thread, IManagedConnection> _ongoingConnections = new();
1,018✔
672
    private readonly Dictionary<Thread, IManagedTransaction> _ongoingTransactions = new();
1,018✔
673

674

675
    public IManagedConnection GetConnection()
676
    {
677
        //any existing ongoing connection found on this Thread
678
        GetOngoingActivitiesFromThreadsDictionary(out var ongoingConnection, out var ongoingTransaction);
676,014✔
679

680
        //if we are in the middle of doing stuff we can just reuse the ongoing one
681
        if (ongoingConnection != null &&
676,014✔
682
            ongoingConnection.Connection.State ==
676,014✔
683
            ConnectionState.Open) //as long as it hasn't timed out or been disposed etc
676,014✔
684
            if (ongoingConnection.CloseOnDispose)
47,578!
685
            {
686
                var clone = ongoingConnection.Clone();
47,578✔
687
                clone.CloseOnDispose = false;
47,578✔
688
                return clone;
47,578✔
689
            }
690
            else
691
            {
UNCOV
692
                return ongoingConnection;
×
693
            }
694

695
        ongoingConnection = DiscoveredServer.GetManagedConnection(ongoingTransaction);
628,436✔
696

697
        //record as the active connection on this thread
698
        _ongoingConnections[Thread.CurrentThread] = ongoingConnection;
628,432✔
699

700
        return ongoingConnection;
628,432✔
701
    }
702

703
    private void GetOngoingActivitiesFromThreadsDictionary(out IManagedConnection ongoingConnection,
704
        out IManagedTransaction ongoingTransaction)
705
    {
676,202✔
706
        lock (_ongoingConnectionsLock)
707
        {
708
            //see if Thread dictionary has it
709
            if (!_ongoingConnections.TryGetValue(Thread.CurrentThread, out ongoingConnection))
676,202✔
710
                _ongoingConnections.Add(Thread.CurrentThread, null);
1,136✔
711

712
            //see if Thread dictionary has it
713
            if (!_ongoingTransactions.TryGetValue(Thread.CurrentThread, out ongoingTransaction))
676,202✔
714
                _ongoingTransactions.Add(Thread.CurrentThread, null);
1,136✔
715
        }
676,202✔
716
    }
676,202✔
717

718
    public IManagedConnection BeginNewTransactedConnection()
719
    {
720
        GetOngoingActivitiesFromThreadsDictionary(out _, out var ongoingTransaction);
94✔
721

722
        if (ongoingTransaction != null)
94!
723
            throw new NotSupportedException(
×
UNCOV
724
                "There is already an ongoing transaction on this Thread! Call EndTransactedConnection on the last one first");
×
725

726
        var toReturn = DiscoveredServer.BeginNewTransactedConnection();
94✔
727
        ongoingTransaction = toReturn.ManagedTransaction;
94✔
728
        _ongoingTransactions[Thread.CurrentThread] = ongoingTransaction;
94✔
729

730
        _ongoingConnections[Thread.CurrentThread] = toReturn;
94✔
731
        if (DiscoveredServer.DatabaseType == DatabaseType.MicrosoftSQLServer)
94✔
732
        {
733
            using var cmd = toReturn.Connection.CreateCommand();
92✔
734
            cmd.Transaction = toReturn.Transaction;
92✔
735
            cmd.CommandText = "SET TRANSACTION ISOLATION LEVEL READ COMMITTED";
92✔
736
            cmd.ExecuteNonQuery();
92✔
737
        }
738

739
        return toReturn;
94✔
740
    }
741

742
    /// <summary>
743
    /// True to commit, false to abandon
744
    /// </summary>
745
    /// <param name="commit"></param>
746
    public void EndTransactedConnection(bool commit)
747
    {
748
        GetOngoingActivitiesFromThreadsDictionary(out _, out var ongoingTransaction);
94✔
749

750
        if (ongoingTransaction == null)
94!
751
            throw new NotSupportedException(
×
UNCOV
752
                "There is no ongoing transaction on this Thread, did you try to close the Transaction from another Thread? or did you maybe never start one in the first place?");
×
753

754
        if (commit)
94✔
755
            ongoingTransaction.CommitAndCloseConnection();
78✔
756
        else
757
            ongoingTransaction.AbandonAndCloseConnection();
16✔
758

759
        _ongoingConnections[Thread.CurrentThread] = null;
94✔
760
        _ongoingTransactions[Thread.CurrentThread] = null;
94✔
761
    }
94✔
762

763

764
    public void ClearUpdateCommandCache()
NEW
765
    {
×
766
        lock (OLockUpdateCommands)
767
        {
768
            _updateCommandStore.Clear();
×
769
        }
×
UNCOV
770
    }
×
771

772
    public int? ObjectToNullableInt(object o) => o == null || o == DBNull.Value ? null : int.Parse(o.ToString());
41,364✔
773

774
    public DateTime? ObjectToNullableDateTime(object o) => o == null || o == DBNull.Value ? null : (DateTime)o;
40,000!
775

776
    private readonly Dictionary<Type, bool> _knownSupportedTypes = new();
1,018✔
777
    private readonly Lock _oLockKnownTypes = new();
1,018✔
778

779
    public bool SupportsObjectType(Type type)
780
    {
781
        if (!typeof(IMapsDirectlyToDatabaseTable).IsAssignableFrom(type))
214!
782
            throw new NotSupportedException(
×
UNCOV
783
                "This method can only be passed Types derived from IMapsDirectlyToDatabaseTable");
×
784

785
        lock (_oLockKnownTypes)
786
        {
787
            if (!_knownSupportedTypes.ContainsKey(type))
214✔
788
                _knownSupportedTypes.Add(type, DiscoveredServer.GetCurrentDatabase().ExpectTable(type.Name).Exists());
42✔
789

790
            return _knownSupportedTypes[type];
214✔
791
        }
792
    }
214✔
793

794

795
    public void SaveSpecificPropertyOnlyToDatabase(IMapsDirectlyToDatabaseTable entity, string propertyName,
796
        object propertyValue)
797
    {
798
        var prop = entity.GetType().GetProperty(propertyName);
11,592✔
799
        prop.SetValue(entity, propertyValue);
11,592✔
800

801
        //don't put in the enum number put in the free text enum value (that's what we do everywhere else)
802
        if (prop.PropertyType.IsEnum)
11,592!
UNCOV
803
            propertyValue = propertyValue.ToString();
×
804

805
        Update($"UPDATE {Wrap(entity.GetType().Name)} SET {propertyName}=@val WHERE ID = {entity.ID}",
11,592✔
806
            new Dictionary<string, object>
11,592✔
807
            {
11,592✔
808
                { "@val", propertyValue ?? DBNull.Value }
11,592✔
809
            });
11,592✔
810
    }
11,592✔
811

812
    public int Insert(string sql, Dictionary<string, object> parameters)
813
    {
814
        return Insert(sql, parameters, 30000);
1,504✔
815
    }
816

817

818
    public int Insert(string sql, Dictionary<string, object> parameters,int timeout)
819
    {
820
        using var opener = GetConnection();
1,528✔
821
        using var cmd = PrepareCommand(sql, parameters, opener.Connection, opener.Transaction);
1,528✔
822
        cmd.CommandTimeout = timeout;
1,528✔
823
        return cmd.ExecuteNonQuery();
1,528✔
824
    }
1,528✔
825

826
    private Type[] _compatibleTypes;
827

828
    public event EventHandler<SaveEventArgs> Saving;
829
    public event EventHandler<IMapsDirectlyToDatabaseTableEventArgs> Inserting;
830
    public event EventHandler<IMapsDirectlyToDatabaseTableEventArgs> Deleting;
831

832
    public IMapsDirectlyToDatabaseTable[] GetAllObjectsInDatabase()
833
    {
UNCOV
834
        _compatibleTypes ??= GetCompatibleTypes();
×
835

836
        try
837
        {
UNCOV
838
            return _compatibleTypes.SelectMany(GetAllObjects).ToArray();
×
839
        }
UNCOV
840
        catch (Exception e)
×
841
        {
842
            throw new Exception(
×
UNCOV
843
                $"Failed to GetAllObjects of Type '{string.Join(',', _compatibleTypes.Select(t => t.FullName))}'", e);
×
844
        }
UNCOV
845
    }
×
846

847

848
    /// <inheritdoc/>
849
    public Type[] GetCompatibleTypes()
850
    {
851
        return
16✔
852
            GetType().Assembly.GetTypes()
16✔
853
                .Where(
16✔
854
                    t =>
16✔
855
                        typeof(IMapsDirectlyToDatabaseTable).IsAssignableFrom(t)
38,624✔
856
                        && !t.IsAbstract
38,624✔
857
                        && !t.IsInterface
38,624✔
858

38,624✔
859
                        //nothing called spontaneous
38,624✔
860
                        && !t.Name.Contains("Spontaneous")
38,624✔
861

38,624✔
862
                        //or with a spontaneous base class
38,624✔
863
                        && (t.BaseType == null || !t.BaseType.Name.Contains("Spontaneous"))
38,624✔
864
                        && IsCompatibleType(t)
38,624✔
865
                ).ToArray();
16✔
866
    }
867

868
    /// <summary>
869
    /// Returns True if the type is one for objects that are held in the database.  Types will come from your repository assembly
870
    /// and will include only <see cref="IMapsDirectlyToDatabaseTable"/> Types that are not abstract/interfaces.  Types are only
871
    /// compatible if an accompanying <see cref="DiscoveredTable"/> exists in the database to store the objects.
872
    /// </summary>
873
    /// <param name="type"></param>
874
    /// <returns></returns>
875
    protected virtual bool IsCompatibleType(Type type)
876
    {
877
        return _tables.Value.Any(t => t.GetRuntimeName().Equals(type.Name));
48,752✔
878
    }
879

UNCOV
880
    public virtual T[] GetAllObjectsNoCache<T>() where T : IMapsDirectlyToDatabaseTable => GetAllObjects<T>();
×
881

882
    public IDisposable BeginNewTransaction() => BeginNewTransactedConnection();
76✔
883

884
    public void EndTransaction(bool commit)
885
    {
886
        EndTransactedConnection(commit);
76✔
887
    }
76✔
888
}
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

© 2025 Coveralls, Inc