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

HicServices / RDMP / 9253732050

27 May 2024 11:03AM UTC coverage: 56.515% (-0.4%) from 56.916%
9253732050

Pull #1779

github

JFriel
revert change
Pull Request #1779: Task/rdmp 122 update a database extraction

10780 of 20550 branches covered (52.46%)

Branch coverage included in aggregate %.

50 of 58 new or added lines in 3 files covered. (86.21%)

369 existing lines in 20 files now uncovered.

30680 of 52811 relevant lines covered (58.09%)

7662.48 hits per line

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

87.85
/Rdmp.Core/Repositories/Construction/ObjectConstructor.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.Common;
10
using System.Linq;
11
using System.Reflection;
12
using Rdmp.Core.Curation.Data;
13
using Rdmp.Core.Curation.Data.ImportExport;
14
using Rdmp.Core.MapsDirectlyToDatabaseTable;
15

16
namespace Rdmp.Core.Repositories.Construction;
17

18
/// <summary>
19
/// Simplifies identifying and invoking ConstructorInfos on Types (reflection).  This includes identifying a suitable Constructor on a class Type based on the
20
/// provided parameters and invoking it.  Also implicitly supports hypotheticals e.g. 'here's a TableInfo, construct class X with the TableInfo parameter or if
21
/// it has a blank constructor that's fine too or if it takes ITableInfo that's fine too... just use whatever works'.  If there are multiple matching constructors
22
/// it will attempt to find the 'best' (See InvokeBestConstructor for implementation).
23
/// 
24
/// <para>If there are no compatible constructors you will get an ObjectLacksCompatibleConstructorException.</para>
25
/// </summary>
26
public class ObjectConstructor
27
{
28
    private const BindingFlags TargetBindingFlags =
29
        BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
30

31
    /// <summary>
32
    /// Constructs a new instance of Type t using the blank constructor
33
    /// </summary>
34
    /// <param name="t"></param>
35
    /// <returns></returns>
36
    public static object Construct(Type t) => GetUsingBlankConstructor(t);
446✔
37

38
    #region permissable constructor signatures for use with this class
39

40
    /// <summary>
41
    /// Constructs a new instance of Type t using the default constructor or one that takes an IRDMPPlatformRepositoryServiceLocator (or any derived class)
42
    /// </summary>
43
    /// <param name="t"></param>
44
    /// <param name="serviceLocator"></param>
45
    /// <param name="allowBlank"></param>
46
    /// <returns></returns>
47
    public static object
48
        Construct(Type t, IRDMPPlatformRepositoryServiceLocator serviceLocator, bool allowBlank = true) =>
49
        Construct<IRDMPPlatformRepositoryServiceLocator>(t, serviceLocator, allowBlank);
1,938✔
50

51
    /// <summary>
52
    /// Constructs a new instance of Type t using the default constructor or one that takes an ICatalogueRepository (or any derived class)
53
    /// </summary>
54
    /// <param name="t"></param>
55
    /// <param name="catalogueRepository"></param>
56
    /// <param name="allowBlank"></param>
57
    /// <returns></returns>
58
    public static object Construct(Type t, ICatalogueRepository catalogueRepository, bool allowBlank = true) =>
UNCOV
59
        Construct<ICatalogueRepository>(t, catalogueRepository, allowBlank);
×
60

61
    /// <summary>
62
    /// Constructs a new instance of Type objectType by invoking the constructor MyClass(IRepository x, DbDataReader r) (See <see cref="DatabaseEntity"/>).
63
    /// </summary>
64
    /// <typeparam name="T"></typeparam>
65
    /// <param name="objectType"></param>
66
    /// <param name="repositoryOfTypeT"></param>
67
    /// <param name="reader"></param>
68
    /// <returns></returns>
69
    public static IMapsDirectlyToDatabaseTable ConstructIMapsDirectlyToDatabaseObject<T>(Type objectType,
70
        T repositoryOfTypeT, DbDataReader reader) where T : IRepository
71
    {
72
        // Preferred constructor
73
        var constructors = GetConstructors<T, DbDataReader>(objectType);
1,602✔
74

75
        if (!constructors.Any())
1,602!
76
            // Fallback constructor
77
            throw new ObjectLacksCompatibleConstructorException(
×
78
                $"{objectType.Name} requires a constructor ({typeof(T).Name} repo, DbDataReader reader) to be used with ConstructIMapsDirectlyToDatabaseObject");
×
79

80
        return (IMapsDirectlyToDatabaseTable)InvokeBestConstructor(constructors, repositoryOfTypeT, reader);
1,602✔
81
    }
82

83
    #endregion
84

85
    /// <summary>
86
    /// Constructs an instance of object of Type 'typeToConstruct' which should have a compatible constructor taking an object or interface compatible with T
87
    /// or a blank constructor (optionally)
88
    /// </summary>
89
    /// <typeparam name="T">The parameter type expected to be in the constructor</typeparam>
90
    /// <param name="typeToConstruct">The type to construct an instance of</param>
91
    /// <param name="constructorParameter1">a value to feed into the compatible constructor found for Type typeToConstruct in order to produce an instance</param>
92
    /// <param name="allowBlank">true to allow calling the blank constructor if no matching constructor is found that takes a T</param>
93
    /// <returns></returns>
94
    public static object Construct<T>(Type typeToConstruct, T constructorParameter1, bool allowBlank = true)
95
    {
96
        var repositoryLocatorConstructorInfos = GetConstructors<T>(typeToConstruct);
4,956✔
97

98
        if (repositoryLocatorConstructorInfos.Any())
4,956✔
99
            return InvokeBestConstructor(repositoryLocatorConstructorInfos, constructorParameter1);
4,950✔
100
        if (allowBlank)
6!
101
            try
102
            {
103
                return GetUsingBlankConstructor(typeToConstruct);
6✔
104
            }
105
            catch (ObjectLacksCompatibleConstructorException)
2✔
106
            {
107
                throw new ObjectLacksCompatibleConstructorException(
2✔
108
                    $"Type '{typeToConstruct}' does not have a constructor taking an {typeof(T)} - it doesn't even have a blank constructor!");
2✔
109
            }
110

111
        throw new ObjectLacksCompatibleConstructorException(
×
112
            $"Type '{typeToConstruct}' does not have a constructor taking an {typeof(T)}");
×
113
    }
4✔
114

115
    private static List<ConstructorInfo> GetConstructors<T>(Type type)
116
    {
117
        var toReturn = new List<ConstructorInfo>();
4,956✔
118

119
        foreach (var constructor in type.GetConstructors(TargetBindingFlags))
16,716✔
120
        {
121
            var p = constructor.GetParameters();
4,962✔
122

123
            switch (p.Length)
4,962✔
124
            {
125
                //is it an exact match i.e. ctor(T bob)
126
                case 1 when p[0].ParameterType == typeof(T): // Exact match found
4,958✔
127
                    return new List<ConstructorInfo>(new[] { constructor });
3,120✔
128
                case 1:
129
                    {
130
                        if (p[0].ParameterType
1,838✔
131
                            .IsAssignableFrom(
1,838✔
132
                                typeof(T))) //is it a derived class match i.e. ctor(F bob) where F is a derived class of T
1,838✔
133
                            toReturn.Add(constructor);
1,836✔
134
                        break;
135
                    }
136
            }
137
        }
138

139
        return toReturn;
1,836✔
140
    }
141

142

143
    /// <summary>
144
    /// Returns all constructors defined for class 'type' that are compatible with the parameters T and T2
145
    /// </summary>
146
    /// <typeparam name="T"></typeparam>
147
    /// <typeparam name="T2"></typeparam>
148
    /// <param name="type"></param>
149
    /// <returns></returns>
150
    private static List<ConstructorInfo> GetConstructors<T, T2>(Type type)
151
    {
152
        var toReturn = new List<ConstructorInfo>();
1,602✔
153

154
        foreach (var constructor in type.GetConstructors(TargetBindingFlags))
11,380✔
155
        {
156
            var p = constructor.GetParameters();
4,832✔
157

158
            switch (p.Length)
4,832✔
159
            {
160
                case 2 when p[0].ParameterType == typeof(T) && p[1].ParameterType == typeof(T2): // Exact match found
1,708✔
161
                    return new List<ConstructorInfo>(new[] { constructor });
1,488✔
162
                case 2:
163
                    {
164
                        if (p[0].ParameterType.IsAssignableFrom(typeof(T)) &&
220✔
165
                            p[1].ParameterType.IsAssignableFrom(typeof(T2)))
220✔
166
                            toReturn.Add(constructor);
114✔
167
                        break;
168
                    }
169
            }
170
        }
171

172
        return toReturn;
114✔
173
    }
174

175
    /// <summary>
176
    /// Returns all constructors defined for class 'type' which are compatible with any set or subset of the provided parameters.  The return value is a dictionary
177
    /// of all compatible constructors with the objects needed to invoke them.
178
    /// </summary>
179
    /// <param name="type"></param>
180
    /// <param name="allowBlankConstructor"></param>
181
    /// <param name="allowPrivate"></param>
182
    /// <param name="parameterObjects"></param>
183
    /// <returns></returns>
184
    public static Dictionary<ConstructorInfo, List<object>> GetConstructors(Type type, bool allowBlankConstructor,
185
        bool allowPrivate, params object[] parameterObjects)
186
    {
187
        var toReturn = new Dictionary<ConstructorInfo, List<object>>();
446✔
188

189
        foreach (var constructor in type.GetConstructors(TargetBindingFlags))
4,980✔
190
        {
191
            if (constructor.IsPrivate && !allowPrivate)
2,044!
192
                continue;
193

194
            var p = constructor.GetParameters();
2,044✔
195

196
            //if it is a blank constructor
197
            if (!p.Any())
2,044✔
198
            {
199
                if (allowBlankConstructor) //if we do not allow blank constructors ignore it
206!
200
                    toReturn.Add(constructor,
×
201
                        new List<object>()); //otherwise add it to the return list with no objects for invoking (because it's blank duh!)
×
202
            }
203
            else
204
            {
205
                //ok we found a constructor that takes some arguments
206

207
                //do we have clear 1 to 1 winners on what object to drop into which parameter of the constructor?
208
                var canInvoke = true;
1,838✔
209
                var invokeWithObjects = new List<object>();
1,838✔
210

211
                //for each object in the constructor
212
                foreach (var arg in p)
11,144✔
213
                {
214
                    //what object could we populate it with?
215
                    var o = GetBestObjectForPopulating(arg.ParameterType, parameterObjects);
3,734✔
216

217
                    //no matching ones sadly
218
                    if (o == null)
3,734✔
219
                        canInvoke = false;
3,712✔
220
                    else
221
                        invokeWithObjects.Add(o);
22✔
222
                }
223

224
                if (canInvoke)
1,838✔
225
                    toReturn.Add(constructor, invokeWithObjects);
14✔
226
            }
227
        }
228

229
        return toReturn;
446✔
230
    }
231

232
    /// <summary>
233
    /// Returns the best object from parameterObjects for populating an argument of the provided Type.  This is done by looking for an exact Type match first
234
    /// then if none of those exist, it will look for a single object assignable to the parameter type.  If at any point there is two or more matching parameterObjects
235
    /// then an <seealso cref="ObjectLacksCompatibleConstructorException"/> will be thrown.
236
    /// 
237
    /// <para>If there are no objects provided that match any of the provided parameterObjects then null gets returned.</para>
238
    /// </summary>
239
    /// <param name="parameterType"></param>
240
    /// <param name="parameterObjects"></param>
241
    /// <returns></returns>
242
    private static object GetBestObjectForPopulating(Type parameterType, params object[] parameterObjects)
243
    {
244
        var matches = parameterObjects.Where(p => p.GetType() == parameterType).ToArray();
7,468✔
245

246
        //if there are no exact matches look for an assignable one
247
        if (matches.Length == 0)
3,734✔
248
            //look for an assignable one instead
249
            matches = parameterObjects.Where(parameterType.IsInstanceOfType).ToArray();
3,734✔
250

251
        return matches.Length switch
3,734!
252
        {
3,734✔
253
            //if there is one exact match on Type, use that to hydrate it
3,734✔
254
            1 => matches[0],
22✔
255
            0 => null,
3,712✔
256
            _ => throw new ObjectLacksCompatibleConstructorException(
×
257
                $"Could not pick a suitable parameterObject for populating {parameterType} (found {matches.Length} compatible parameter objects)")
×
258
        };
3,734✔
259
    }
260

261
    private static object InvokeBestConstructor(List<ConstructorInfo> constructors, params object[] parameters)
262
    {
263
        if (constructors.Count == 1)
6,970✔
264
            return constructors[0].Invoke(parameters);
6,966✔
265

266
        var importDecorated = constructors.Where(c => Attribute.IsDefined(c, typeof(UseWithObjectConstructorAttribute)))
12✔
267
            .ToArray();
4✔
268
        return importDecorated.Length == 1
4!
269
            ? importDecorated[0].Invoke(parameters)
4✔
270
            : throw new ObjectLacksCompatibleConstructorException(
4✔
271
                $"Could not pick the correct constructor between:{Environment.NewLine}{string.Join($"{Environment.NewLine}", constructors.Select(c => $"{c.Name}({string.Join(",", c.GetParameters().Select(p => p.ParameterType))}"))}");
12✔
272
    }
273

274
    private static object GetUsingBlankConstructor(Type t)
275
    {
276
        var blankConstructor = t.GetConstructor(Type.EmptyTypes) ??
452✔
277
                               throw new ObjectLacksCompatibleConstructorException(
452✔
278
                                   $"Type '{t}' did not contain a blank constructor");
452✔
279
        return blankConstructor.Invoke(Array.Empty<object>());
450✔
280
    }
281

282
    /// <summary>
283
    /// Attempts to construct an instance of Type typeToConstruct using the provided constructorValues.  This must match on parameter number but ignores order
284
    /// so if you pass new Obj1(),new Obj2() it could invoke either MyClass(Obj1 a,Obj2 b) or MyClass(Obj2 a, Obj1 b).
285
    /// <para>Throws <see cref="ObjectLacksCompatibleConstructorException"/> if there are multiple constructors that match the constructorValues</para>
286
    /// 
287
    /// <para>Does not invoke the default constructor unless you leave constructorValues blank</para>
288
    /// <para>returns null if no compatible constructor is found</para>
289
    /// </summary>
290
    /// <param name="typeToConstruct"></param>
291
    /// <param name="constructorValues"></param>
292
    /// <returns></returns>
293
    public static object ConstructIfPossible(Type typeToConstruct, params object[] constructorValues)
294
    {
295
        var compatible = new List<ConstructorInfo>();
492✔
296

297
        foreach (var constructor in typeToConstruct.GetConstructors(TargetBindingFlags))
2,664✔
298
        {
299
            var p = constructor.GetParameters();
840✔
300

301
            //must have the same length of arguments as expected
302
            if (p.Length != constructorValues.Length)
840✔
303
                continue;
304

305
            var isCompatible = true;
682✔
306

307
            for (var index = 0; index < constructorValues.Length; index++)
4,368✔
308
                //if we have been given a null value for this parameter
309
                if (constructorValues[index] == null)
1,502!
310
                {
311
                    //if the parameter is value type null is not ok otherwise it is
312
                    if (p[index].ParameterType.IsEnum || p[index].ParameterType.IsValueType)
×
313
                        isCompatible = false;
×
314
                }
315
                else if (!p[index].ParameterType.IsInstanceOfType(constructorValues[index]))
1,502✔
316
                {
317
                    isCompatible = false;
450✔
318
                }
319

320
            if (isCompatible)
682✔
321
                compatible.Add(constructor);
418✔
322
        }
323

324
        return compatible.Any() ? InvokeBestConstructor(compatible, constructorValues) : null;
492✔
325
    }
326

327
    /// <summary>
328
    /// Returns the most likely constructor for creating new instances of a <see cref="DatabaseEntity"/> class e.g.
329
    /// for <see cref="Catalogue"/> it would return the constructor <see cref="Catalogue(ICatalogueRepository,string)"/>
330
    /// </summary>
331
    /// <param name="type"></param>
332
    /// <returns></returns>
333
    public static ConstructorInfo GetRepositoryConstructor(Type type)
334
    {
335
        var compatible = type.GetConstructors()
226✔
336
            .Select(constructorInfo => new { constructorInfo, parameters = constructorInfo.GetParameters() })
452✔
337
            .Where(@t => @t.parameters.All(p => p.GetType() != typeof(ShareManager)))
914✔
338
            .Where(@t => @t.parameters.All(p => p.GetType() != typeof(DbDataReader)))
914✔
339
            .Where(@t => @t.parameters.Any(p => typeof(IRepository).IsAssignableFrom(p.ParameterType)))
678✔
340
            .Select(@t => @t.constructorInfo).ToList();
452✔
341

342
        return compatible.Count == 1
226!
343
            ? compatible.Single()
226✔
344
            : throw new ObjectLacksCompatibleConstructorException(
226✔
345
                $"No best constructor found for Type {type} (found {compatible.Count})");
226✔
346
    }
347
}
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