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

net-daemon / netdaemon / 9343360009

26 May 2024 02:03PM UTC coverage: 82.867% (-0.1%) from 82.999%
9343360009

push

github

web-flow
Housecleaning  (#1108)

* Housecleaning - step 1

* More small fixes

863 of 1189 branches covered (72.58%)

Branch coverage included in aggregate %.

29 of 34 new or added lines in 23 files covered. (85.29%)

6 existing lines in 2 files now uncovered.

3224 of 3743 relevant lines covered (86.13%)

81.64 hits per line

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

84.19
/src/AppModel/NetDaemon.AppModel/Internal/Config/ConfigurationBinding.cs
1
using System.ComponentModel;
2
using System.Globalization;
3
using System.Reflection;
4
using Microsoft.Extensions.Configuration;
5

6
namespace NetDaemon.AppModel.Internal.Config;
7

8
internal interface IConfigurationBinding
9
{
10
    T? ToObject<T>(IConfiguration configuration);
11
}
12

13
/// <summary>
14
///     Class that allows binding strongly typed objects to configuration values.
15
/// </summary>
16
/// <remarks>
17
///     This is a scaled down and modified version of .NET 7 implementation.
18
/// </remarks>
19
internal class ConfigurationBinding : IConfigurationBinding
20
{
21
    private readonly IServiceProvider _provider;
22

23
    public ConfigurationBinding(IServiceProvider provider)
30✔
24
    {
25
        _provider = provider;
30✔
26
    }
30✔
27

28
    /// <summary>
29
    ///     Attempts to bind the configuration instance to a new instance of type T.
30
    ///     If  configuration section has a value, that will be used.
31
    ///     Otherwise binding by matching property names against configuration keys recursively.
32
    /// </summary>
33
    /// <typeparam name="T">The type of the new instance to bind.</typeparam>
34
    /// <param name="configuration">The configuration instance to bind.</param>
35
    /// <returns>The new instance of T if successful, default(T) otherwise.</returns>
36
    public T? ToObject<T>(IConfiguration configuration)
37
    {
38
        var result = GetObject(configuration, typeof(T));
29✔
39
        if (result == null) return default;
20!
40
        return (T)result;
20✔
41
    }
42

43
    /// <summary>
44
    ///     Attempts to bind the configuration instance to a new instance of type T.
45
    ///     If  configuration section has a value, that will be used.
46
    ///     Otherwise binding by matching property names against configuration keys recursively.
47
    /// </summary>
48
    /// <param name="configuration">The configuration instance to bind.</param>
49
    /// <param name="type">The type of the new instance to bind.</param>
50
    /// <returns>The new instance if successful, null otherwise.</returns>
51
    private object? GetObject(IConfiguration configuration, Type type)
52
    {
53
        return BindInstance(type, null, configuration);
29✔
54
    }
55

56
    private void BindNonScalar(IConfiguration configuration, object? instance)
57
    {
58
        if (instance == null) return;
21!
59

60
        // If we came this far and it is readonly collection
61
        // it means that the collection was already initialized
62
        // that is not a valid operation
63
        ThrowIfReadonlyCollection(instance);
21✔
64

65
        foreach (var property in GetAllProperties(instance.GetType().GetTypeInfo()))
122✔
66
            BindProperty(property, instance, configuration);
46✔
67
    }
13✔
68

69
    private static void ThrowIfReadonlyCollection(object? instance)
70
    {
71
        var typ = instance?.GetType()!;
21!
72

73
        if (FindOpenGenericInterface(typeof(IEnumerable<>), typ) != null)
21✔
74
        {
75
            throw new InvalidOperationException("Read-only collections cannot be initialized and needs to be nullable!");
4✔
76
        }
77

78
        if (FindOpenGenericInterface(typeof(IReadOnlyCollection<>), typ) != null)
17!
79
        {
80
            throw new InvalidOperationException("Read-only collections cannot be initialized and needs to be nullable!");
×
81
        }
82
    }
17✔
83

84
    private void BindProperty(PropertyInfo property, object? instance, IConfiguration config)
85
    {
86
        // We don't support set only, non public, or indexer properties
87
        if (property.GetMethod?.IsPublic != true ||
46!
88
            property.GetMethod.GetParameters().Length > 0)
46✔
89
            return;
5✔
90

91
        var propertyValue = property.GetValue(instance);
41✔
92
        var hasSetter = property.SetMethod?.IsPublic == true;
41!
93

94
        if (propertyValue == null && !hasSetter)
41!
95
            // Property doesn't have a value and we cannot set it so there is no
96
            // point in going further down the graph
97
            return;
×
98

99
        propertyValue = BindInstance(property.PropertyType, propertyValue, config.GetSection(property.Name));
41✔
100

101
        if (propertyValue != null && hasSetter) property.SetValue(instance, propertyValue);
74✔
102
    }
37✔
103

104
    private object BindToCollection(TypeInfo typeInfo, IConfiguration config)
105
    {
106
        var type = typeof(List<>).MakeGenericType(typeInfo.GenericTypeArguments[0]);
9✔
107
        var instance = Activator.CreateInstance(type) ??
9!
108
                       throw new InvalidOperationException();
9✔
109
        BindCollection(instance, type, config);
9✔
110
        return instance;
9✔
111
    }
112

113
    // Try to create an array/dictionary instance to back various collection interfaces
114
    private object? AttemptBindToCollectionInterfaces(Type type, IConfiguration config)
115
    {
116
        var typeInfo = type.GetTypeInfo();
40✔
117

118
        if (!typeInfo.IsInterface) return null;
63✔
119

120
        var collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyList<>), type);
17✔
121
        if (collectionInterface != null)
17✔
122
            // IEnumerable<T> is guaranteed to have exactly one parameter
123
            return BindToCollection(typeInfo, config);
1✔
124

125
        collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyDictionary<,>), type);
16✔
126
        if (collectionInterface != null)
16✔
127
        {
128
            var dictionaryType =
1✔
129
                typeof(Dictionary<,>).MakeGenericType(typeInfo.GenericTypeArguments[0],
1✔
130
                    typeInfo.GenericTypeArguments[1]);
1✔
131
            var instance = Activator.CreateInstance(dictionaryType);
1✔
132
            BindDictionary(instance, dictionaryType, config);
1✔
133
            return instance;
1✔
134
        }
135

136
        collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
15✔
137
        if (collectionInterface != null)
15✔
138
        {
139
            var instance = Activator.CreateInstance(
6✔
140
                typeof(Dictionary<,>).MakeGenericType(typeInfo.GenericTypeArguments[0],
6✔
141
                    typeInfo.GenericTypeArguments[1]));
6✔
142
            BindDictionary(instance, collectionInterface, config);
6✔
143
            return instance;
6✔
144
        }
145

146
        collectionInterface = FindOpenGenericInterface(typeof(IReadOnlyCollection<>), type);
9✔
147
        if (collectionInterface != null)
9✔
148
            // IReadOnlyCollection<T> is guaranteed to have exactly one parameter
149
            return BindToCollection(typeInfo, config);
1✔
150

151
        collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
8✔
152
        if (collectionInterface != null)
8✔
153
            // ICollection<T> is guaranteed to have exactly one parameter
154
            return BindToCollection(typeInfo, config);
4✔
155

156
        collectionInterface = FindOpenGenericInterface(typeof(IEnumerable<>), type);
4✔
157
        return collectionInterface != null ? BindToCollection(typeInfo, config) : null;
4✔
158
    }
159

160
    private object? BindInstance(Type type, object? instance, IConfiguration config)
161
    {
162
        // if binding IConfigurationSection, break early
163
        if (type == typeof(IConfigurationSection)) return config;
106!
164

165
        var section = config as IConfigurationSection;
106✔
166
        var configValue = section?.Value;
106!
167
        if (configValue != null && TryConvertValue(type, configValue, out var convertedValue, out var error))
106✔
168
        {
169
            if (error != null) throw error;
62!
170

171
            // Leaf nodes are always reinitialized
172
            return convertedValue;
62✔
173
        }
174

175
        if (config.GetChildren().Any() != true) return instance;
44!
176

177
        // If we don't have an instance, try to create one
178
        if (instance == null)
44✔
179
        {
180
            // We are already done if binding to a new collection instance worked
181
            instance = AttemptBindToCollectionInterfaces(type, config);
40✔
182
            if (instance != null) return instance;
56✔
183

184
            instance = CreateInstance(type);
24✔
185
        }
186

187
        // See if its a Dictionary
188
        var collectionInterface = FindOpenGenericInterface(typeof(IDictionary<,>), type);
23✔
189
        if (collectionInterface != null)
23!
190
        {
191
            BindDictionary(instance, collectionInterface, config);
×
192
        }
193
        else if (type.IsArray)
23✔
194
        {
195
            instance = BindArray((Array)instance, config);
1✔
196
        }
197
        else
198
        {
199
            // See if its an ICollection
200
            collectionInterface = FindOpenGenericInterface(typeof(ICollection<>), type);
22✔
201
            if (collectionInterface != null)
22✔
202
                BindCollection(instance, collectionInterface, config);
1✔
203
            // Something else
204
            else
205
                BindNonScalar(config, instance);
21✔
206
        }
207

208
        return instance;
15✔
209
    }
210

211
    private static object CreateInstance(Type type)
212
    {
213
        var typeInfo = type.GetTypeInfo();
24✔
214

215
        if (typeInfo.IsInterface || typeInfo.IsAbstract) throw new InvalidOperationException();
26✔
216

217
        if (type.IsArray)
22✔
218
        {
219
            if (typeInfo.GetArrayRank() > 1) throw new InvalidOperationException();
3✔
220
            var typ = typeInfo.GetElementType()!;
1✔
221
            return Array.CreateInstance(typ, 0);
1✔
222
        }
223

224
        var hasDefaultConstructor =
20✔
225
            typeInfo.DeclaredConstructors.Any(ctor => ctor.IsPublic && ctor.GetParameters().Length == 0);
45✔
226
        if (!hasDefaultConstructor) throw new InvalidOperationException();
21✔
227

228
        try
229
        {
230
            return Activator.CreateInstance(type)!;
19✔
231
        }
232
        catch (Exception)
1✔
233
        {
234
            throw new InvalidOperationException();
1✔
235
        }
236
    }
18✔
237

238
    private void BindDictionary(object? dictionary, Type dictionaryType, IConfiguration config)
239
    {
240
        var typeInfo = dictionaryType.GetTypeInfo();
7✔
241

242
        // IDictionary<K,V> is guaranteed to have exactly two parameters
243
        var keyType = typeInfo.GenericTypeArguments[0];
7✔
244
        var valueType = typeInfo.GenericTypeArguments[1];
7✔
245
        var keyTypeIsEnum = keyType.GetTypeInfo().IsEnum;
7✔
246

247
        if (keyType != typeof(string) && !keyTypeIsEnum)
7✔
248
            // We only support string and enum keys
249
            return;
1✔
250

251
        var setter = typeInfo.GetDeclaredProperty("Item") ??
6!
252
                     throw new InvalidOperationException();
6✔
253

254
        foreach (var child in config.GetChildren())
38✔
255
        {
256
            var item = BindInstance(
13✔
257
                valueType,
13✔
258
                null,
13✔
259
                child);
13✔
260
            if (item == null) continue;
13✔
261
            if (keyType == typeof(string))
13!
262
            {
263
                var key = child.Key;
13✔
264
                setter.SetValue(dictionary, item, [key]);
13✔
265
            }
266
            else if (keyTypeIsEnum)
×
267
            {
268
                var key = Convert.ToInt32(Enum.Parse(keyType, child.Key), CultureInfo.InvariantCulture);
×
NEW
269
                setter.SetValue(dictionary, item, [key]);
×
270
            }
271
        }
272
    }
6✔
273

274
    private void BindCollection(object collection, Type collectionType, IConfiguration config)
275
    {
276
        var typeInfo = collectionType.GetTypeInfo();
10✔
277

278
        // ICollection<T> is guaranteed to have exactly one parameter
279
        var itemType = typeInfo.GenericTypeArguments[0];
10✔
280
        var addMethod = typeInfo.GetDeclaredMethod("Add") ??
10!
281
                        throw new InvalidOperationException();
10✔
282

283
        foreach (var section in config.GetChildren())
62✔
284
            try
285
            {
286
                var item = BindInstance(
21✔
287
                    itemType,
21✔
288
                    null,
21✔
289
                    section);
21✔
290
                if (item != null) addMethod.Invoke(collection, [item]);
42✔
291
            }
21✔
292
            catch
×
293
            {
294
                // ignored
295
            }
×
296
    }
10✔
297

298
    private Array BindArray(Array source, IConfiguration config)
299
    {
300
        var children = config.GetChildren().ToArray();
1✔
301
        var arrayLength = source.Length;
1✔
302
        var elementType = source.GetType().GetElementType() ??
1!
303
                          throw new InvalidOperationException();
1✔
304

305
        var newArray = Array.CreateInstance(elementType, arrayLength + children.Length);
1✔
306

307
        // binding to array has to preserve already initialized arrays with values
308
        if (arrayLength > 0) Array.Copy(source, newArray, arrayLength);
1!
309

310
        for (var i = 0; i < children.Length; i++)
6✔
311
            try
312
            {
313
                var item = BindInstance(
2✔
314
                    elementType,
2✔
315
                    null,
2✔
316
                    children[i]);
2✔
317
                if (item != null) newArray.SetValue(item, arrayLength + i);
4✔
318
            }
2✔
319
            catch
×
320
            {
321
                // ignored
322
            }
×
323

324
        return newArray;
1✔
325
    }
326

327
    private bool TryConvertValue(Type type, string value, out object? result, out Exception? error)
328
    {
329
        error = null;
62✔
330
        result = null;
62✔
331
        if (type == typeof(object))
62!
332
        {
333
            result = value;
×
334
            return true;
×
335
        }
336

337
        if (type.GetTypeInfo().IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
62!
338
        {
339
            if (string.IsNullOrEmpty(value)) return true;
×
340
            var typ = Nullable.GetUnderlyingType(type) ??
×
341
                      throw new InvalidOperationException();
×
342

343
            return TryConvertValue(typ, value, out result, out error);
×
344
        }
345

346
        var converter = TypeDescriptor.GetConverter(type);
62✔
347
        if (converter.CanConvertFrom(typeof(string)))
62✔
348
        {
349
            try
350
            {
351
                result = converter.ConvertFromInvariantString(value);
44✔
352
            }
44✔
353
            catch (Exception)
×
354
            {
355
                error = new InvalidOperationException();
×
356
            }
×
357

358
            return true;
44✔
359
        }
360

361
        // No standard converter is available so lets“s try find
362
        // a registered service that converts to the type from string
363

364
        try
365
        {
366
            result = ActivatorUtilities.CreateInstance(_provider, type, value);
18✔
367
        }
18✔
368
        catch (Exception e)
×
369
        {
370
            throw new InvalidOperationException($"Failed to convert from string to type {type.FullName}, please check you have one string in constructor!", e);
×
371
        }
372

373
        return true;
18✔
374
    }
375

376
    private static Type? FindOpenGenericInterface(Type expected, Type actual)
377
    {
378
        var actualTypeInfo = actual.GetTypeInfo();
152✔
379
        if (actualTypeInfo.IsGenericType &&
152✔
380
            actual.GetGenericTypeDefinition() == expected)
152✔
381
            return actual;
16✔
382

383
        return actualTypeInfo.ImplementedInterfaces.FirstOrDefault(interfaceType =>
136✔
384
            interfaceType.GetTypeInfo().IsGenericType && interfaceType.GetGenericTypeDefinition() == expected);
299✔
385
    }
386

387
    private static IEnumerable<PropertyInfo> GetAllProperties(TypeInfo type)
388
    {
389
        var allProperties = new List<PropertyInfo>();
17✔
390

391
        var currentType = type;
17✔
392
        do
393
        {
394
            allProperties.AddRange(currentType.DeclaredProperties);
17✔
395
            currentType = currentType.BaseType?.GetTypeInfo() ??
17!
396
                          throw new InvalidOperationException();
17✔
397
        } while (currentType != typeof(object).GetTypeInfo());
17✔
398

399
        return allProperties;
17✔
400
    }
401
}
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