|
1 | 1 | using System; |
2 | 2 | using System.Linq; |
3 | 3 | using System.Reflection; |
| 4 | +using System.Collections; |
4 | 5 | using System.Threading.Tasks; |
5 | 6 | using NamespacePrefixPlaceholder.PowerShell.Runtime; |
6 | 7 |
|
7 | 8 | namespace NamespacePrefixPlaceholder.PowerShell.ModelExtensions |
8 | 9 | { |
9 | 10 | public static class ModelExtensions |
10 | 11 | { |
11 | | - /// <summary> |
12 | | - /// Ensures that all properties marked as set on the model have meaningful values. |
13 | | - /// </summary> |
14 | | - /// <param name="model">The model object (must implement IsPropertySet(string)).</param> |
15 | | - /// <param name="failOnExplicitNulls">If true, properties explicitly set to null will be considered invalid (strict mode).</param> |
16 | | - /// <param name="retries">Number of retries for late-bound properties.</param> |
17 | | - /// <param name="delayMs">Delay (ms) between retries.</param> |
18 | 12 | public static async Task EnsurePropertiesAreReady( |
19 | 13 | this object model, |
20 | 14 | bool failOnExplicitNulls = false, |
21 | 15 | int retries = 3, |
22 | | - int delayMs = 1000) |
| 16 | + int delayMs = 1000, |
| 17 | + bool debug = false) |
23 | 18 | { |
24 | 19 | if (model == null) |
25 | 20 | throw new ArgumentNullException(nameof(model)); |
26 | 21 |
|
27 | | - // Ensure the model supports IsPropertySet(string) |
28 | | - var isPropertySetMethod = model.GetType().GetMethod("IsPropertySet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
29 | | - if (isPropertySetMethod == null) |
30 | | - throw new InvalidOperationException($"{model.GetType().Name} does not implement IsPropertySet(string)"); |
| 22 | + var modelType = model.GetType(); |
| 23 | + var isSetMethod = modelType.GetMethod("IsPropertySet", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic); |
31 | 24 |
|
32 | | - var props = model.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public); |
| 25 | + if (isSetMethod == null) |
| 26 | + return; // Skip if model doesn't support property tracking |
33 | 27 |
|
34 | | - for (int attempt = 0; attempt <= retries; attempt++) |
| 28 | + for (int attempt = 0; attempt < retries; attempt++) |
35 | 29 | { |
36 | | - var unready = props |
37 | | - .Where(p => IsUnready(model, p, isPropertySetMethod, failOnExplicitNulls)) |
38 | | - .ToList(); |
| 30 | + bool allReady = true; |
| 31 | + foreach (var prop in modelType.GetProperties(BindingFlags.Public | BindingFlags.Instance)) |
| 32 | + { |
| 33 | + if (!prop.CanRead) continue; |
39 | 34 |
|
40 | | - if (!unready.Any()) |
41 | | - return; // Ready! |
| 35 | + bool isSet = (bool)isSetMethod.Invoke(model, new object[] { prop.Name }); |
| 36 | + object value = null; |
42 | 37 |
|
43 | | - if (attempt < retries) |
44 | | - await Task.Delay(delayMs); |
45 | | - } |
| 38 | + try |
| 39 | + { |
| 40 | + value = prop.GetValue(model); |
| 41 | + } |
| 42 | + catch { /* Handle properties that might throw on get */ } |
46 | 43 |
|
47 | | - // Final check before throwing |
48 | | - var stillUnready = props |
49 | | - .Where(p => IsUnready(model, p, isPropertySetMethod, failOnExplicitNulls)) |
50 | | - .Select(p => p.Name) |
51 | | - .ToList(); |
| 44 | + if (debug) |
| 45 | + Console.WriteLine($"DEBUG: Property={prop.Name}, IsSet={isSet}, Value={(value == null ? "null" : value.ToString())}"); |
52 | 46 |
|
53 | | - if (stillUnready.Any()) |
54 | | - { |
55 | | - throw new InvalidOperationException( |
56 | | - $"Model '{model.GetType().Name}' has properties marked as set but not initialized properly: {string.Join(", ", stillUnready)}" |
57 | | - ); |
58 | | - } |
59 | | - } |
| 47 | + if (!isSet) |
| 48 | + continue; // skip unset properties |
60 | 49 |
|
61 | | - private static bool IsUnready(object model, PropertyInfo prop, MethodInfo isPropertySetMethod, bool failOnExplicitNulls) |
62 | | - { |
63 | | - bool isSet = (bool)isPropertySetMethod.Invoke(model, new object[] { prop.Name }); |
64 | | - if (!isSet) return false; // not marked as set, skip |
| 50 | + if (value == null && failOnExplicitNulls) |
| 51 | + { |
| 52 | + allReady = false; |
| 53 | + break; |
| 54 | + } |
| 55 | + |
| 56 | + if (value != null && IsDefault(value) && failOnExplicitNulls) |
| 57 | + { |
| 58 | + allReady = false; |
| 59 | + break; |
| 60 | + } |
65 | 61 |
|
66 | | - object value = prop.GetValue(model); |
| 62 | + // Recursively check nested models |
| 63 | + if (value != null && !IsSimple(value)) |
| 64 | + { |
| 65 | + if (value is IEnumerable enumerable && !(value is string)) |
| 66 | + { |
| 67 | + foreach (var item in enumerable) |
| 68 | + await EnsurePropertiesAreReady(item, failOnExplicitNulls, 1, 0, debug); |
| 69 | + } |
| 70 | + else |
| 71 | + { |
| 72 | + await EnsurePropertiesAreReady(value, failOnExplicitNulls, 1, 0, debug); |
| 73 | + } |
| 74 | + } |
| 75 | + } |
67 | 76 |
|
68 | | - if (value == null) |
69 | | - return failOnExplicitNulls; // null is OK in relaxed mode, fail in strict |
| 77 | + if (allReady) |
| 78 | + return; |
70 | 79 |
|
71 | | - return IsDefault(value); |
| 80 | + if (attempt < retries - 1) |
| 81 | + await Task.Delay(delayMs); |
| 82 | + } |
| 83 | + |
| 84 | + throw new InvalidOperationException("One or more required properties were not ready after retries."); |
| 85 | + } |
| 86 | + |
| 87 | + private static bool IsSimple(object obj) |
| 88 | + { |
| 89 | + var type = obj.GetType(); |
| 90 | + return type.IsPrimitive |
| 91 | + || type.IsEnum |
| 92 | + || type == typeof(string) |
| 93 | + || type == typeof(DateTime) |
| 94 | + || type == typeof(decimal) |
| 95 | + || type == typeof(Guid) |
| 96 | + || type == typeof(TimeSpan); |
72 | 97 | } |
73 | 98 |
|
74 | | - private static bool IsDefault(object value) |
| 99 | + private static bool IsDefault(object obj) |
75 | 100 | { |
76 | | - Type type = value.GetType(); |
77 | | - if (!type.IsValueType) return false; |
78 | | - object defaultValue = Activator.CreateInstance(type); |
79 | | - return value.Equals(defaultValue); |
| 101 | + var type = obj.GetType(); |
| 102 | + object defaultValue = type.IsValueType ? Activator.CreateInstance(type) : null; |
| 103 | + return Equals(obj, defaultValue); |
80 | 104 | } |
81 | 105 | } |
82 | 106 | } |
0 commit comments