Skip to content

Commit 315ea12

Browse files
committed
Update Model extensions class
1 parent 9f71f32 commit 315ea12

File tree

4 files changed

+120
-79
lines changed

4 files changed

+120
-79
lines changed

config/ModuleMetadata.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,15 @@
2727
"versions": {
2828
"authentication": {
2929
"prerelease": "",
30-
"version": "2.27.4"
30+
"version": "2.27.5"
3131
},
3232
"beta": {
3333
"prerelease": "",
34-
"version": "2.27.4"
34+
"version": "2.27.5"
3535
},
3636
"v1.0": {
3737
"prerelease": "",
38-
"version": "2.27.4"
38+
"version": "2.27.5"
3939
}
4040
}
4141
}

config/ModulesMapping.jsonc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"Applications": "^applicationTemplates\\.|^applications\\.|^servicePrincipals\\.|^onPremisesPublishingProfiles\\.|^users.appRoleAssignment$|^groups.appRoleAssignment$",
2+
// "Applications": "^applicationTemplates\\.|^applications\\.|^servicePrincipals\\.|^onPremisesPublishingProfiles\\.|^users.appRoleAssignment$|^groups.appRoleAssignment$",
33
// "Bookings": "^bookingBusinesses\\.|^bookingCurrencies\\.|^solutions.booking.*.Actions$|^solutions.bookingBusiness$|^solutions.bookingCurrency$|^solutions.virtualEventsRoot$|^solutions.booking.*.Functions$|^solutions.solutionsRoot$",
44
// "BusinessScenario": "^solutions.businessScenario$|^solutions.BusinessScenario.*.Actions$|^solutions.BusinessScenario.*.Functions$",
55
// "BackupRestore": "^solutions.backupRestoreRoot$|^solutions.backupRestore.*.Actions$|^solutions.backupRestore.*.Functions$",
@@ -19,7 +19,7 @@
1919
// "Education": "^education\\.",
2020
// "Files": "^drives\\.|^shares\\.|^users.drive$|^groups.drive$",
2121
// "Financials": "^financials\\.",
22-
// "Groups": "^groups.group$|^groups.directoryObject$|^groups.conversation$|^groups.endpoint$|^groups.extension$|^groups.groupLifecyclePolicy$|^groups.resourceSpecificPermissionGrant$|^groups.profilePhoto$|^groups.conversationThread$|^groupLifecyclePolicies\\.|^users.group$|^groups.directorySetting$|^groups.*.Actions$|^groups.*.Functions$|^groupSettings\\.|^groups.groupSetting$|^groupSettingTemplates\\.",
22+
"Groups": "^groups.group$|^groups.directoryObject$|^groups.conversation$|^groups.endpoint$|^groups.extension$|^groups.groupLifecyclePolicy$|^groups.resourceSpecificPermissionGrant$|^groups.profilePhoto$|^groups.conversationThread$|^groupLifecyclePolicies\\.|^users.group$|^groups.directorySetting$|^groups.*.Actions$|^groups.*.Functions$|^groupSettings\\.|^groups.groupSetting$|^groupSettingTemplates\\.",
2323
// "Identity.DirectoryManagement": "^administrativeUnits\\.|^contacts\\.|^devices\\.|^domains\\.|^directoryRoles\\.|^directoryRoleTemplates\\.|^directorySettingTemplates\\.|^settings\\.|^subscribedSkus\\.|^contracts\\.|^directory\\.|^users.scopedRoleMembership$|^organization.organization$|^organization.organizationalBranding$|^organization.organizationSettings$|^organization.*.Actions$|^organization.extension$|^tenantRelationships.*.Actions$|^tenantRelationships.*.Functions$|admin.peopleAdminSettings$|^organization\\.partnerInformation$",
2424
// "Identity.Governance": "^accessReviews\\.|^businessFlowTemplates\\.|^programs\\.|^programControls\\.|^programControlTypes\\.|^privilegedRoles\\.|^privilegedRoleAssignments\\.|^privilegedRoleAssignmentRequests\\.|^privilegedApproval\\.|^privilegedOperationEvents\\.|^privilegedAccess\\.|^agreements\\.|^users.agreementAcceptance$|^identityGovernance\\.|^roleManagement.rbacApplication$|^roleManagement.*.Functions$|roleManagement.*.Actions$",
2525
// "Identity.SignIns": "^organization.certificateBasedAuthConfiguration$|^invitations\\.|^identityProviders\\.|^oauth2PermissionGrants\\.|^identityProtection\\.|^dataPolicyOperations\\.|^identity\\.|^trustFramework\\.|^informationProtection\\.|^policies\\.|^users.authentication$|^users.informationProtection$|^tenantRelationships.multiTenantOrganization$|^policies.deviceRegistrationPolicy$|^policies.deviceRegistrationPolicy$",

src/readme.graph.md

Lines changed: 44 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -503,38 +503,55 @@ directive:
503503
nameSpacePrefix = prefixMatch[1];
504504
}
505505
506-
let ensureCall = '';
507-
if ($.includes('BodyParameter')) {
508-
ensureCall = `
509-
if (BodyParameter != null)
510-
{
511-
foreach (var prop in BodyParameter.GetType().GetProperties())
506+
$ = $.replace(
507+
/await this\.Client\.(\w+)\(\s*Headers\s*,\s*(BodyParameter|_body)\b([^;]+);/g,
508+
(match, methodName, bodyParam, rest) => {
509+
let ensureCall = '';
510+
511+
if (bodyParam === 'BodyParameter') {
512+
ensureCall = `
513+
if (BodyParameter != null)
512514
{
513-
var val = prop.GetValue(BodyParameter); // force materialization
514-
}
515-
${nameSpacePrefix}.ModelExtensions.ModelExtensions.EnsurePropertiesAreReady(BodyParameter, failOnExplicitNulls: false).GetAwaiter().GetResult();
516-
}`;
517-
} else if ($.includes('_body')) {
518-
ensureCall = `
519-
if (_body != null)
520-
{
521-
foreach (var prop in _body.GetType().GetProperties())
515+
foreach (var prop in BodyParameter.GetType().GetProperties())
516+
{
517+
// Skip indexer properties
518+
if (prop.GetIndexParameters().Length == 0)
519+
{
520+
try
521+
{
522+
var val = prop.GetValue(BodyParameter); // force materialization
523+
}
524+
catch
525+
{}
526+
}
527+
}
528+
await ${nameSpacePrefix}.ModelExtensions.ModelExtensions.EnsurePropertiesAreReady(BodyParameter, failOnExplicitNulls: false);
529+
}`;
530+
} else if (bodyParam === '_body') {
531+
ensureCall = `
532+
if (_body != null)
522533
{
523-
var val = prop.GetValue(_body); // force materialization
524-
}
525-
${nameSpacePrefix}.ModelExtensions.ModelExtensions.EnsurePropertiesAreReady(_body, failOnExplicitNulls: false).GetAwaiter().GetResult();
526-
}`;
527-
}
534+
foreach (var prop in _body.GetType().GetProperties())
535+
{
536+
// Skip indexer properties
537+
if (prop.GetIndexParameters().Length == 0)
538+
{
539+
try
540+
{
541+
var val = prop.GetValue(_body); // force materialization
542+
}
543+
catch
544+
{}
545+
}
546+
}
547+
await ${nameSpacePrefix}.ModelExtensions.ModelExtensions.EnsurePropertiesAreReady(_body, failOnExplicitNulls: false);
548+
}`;
549+
}
528550
529-
$ = $.replace(
530-
/protected override void BeginProcessing\(\)\s*\{\s*([\s\S]*?)(if\s*\(Break\))/g,
531-
`protected override void BeginProcessing() {
532-
Module.Instance.SetProxyConfiguration(Proxy, ProxyCredential, ProxyUseDefaultCredentials);
533-
${ensureCall}
534-
$1$2`
551+
return `${ensureCall}\nawait this.Client.${methodName}(Headers, ${bodyParam}${rest};`;
552+
}
535553
);
536554
537-
538555
return $;
539556
}
540557

tools/Custom/ModelExtensions.cs

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,106 @@
11
using System;
22
using System.Linq;
33
using System.Reflection;
4+
using System.Collections;
45
using System.Threading.Tasks;
56
using NamespacePrefixPlaceholder.PowerShell.Runtime;
67

78
namespace NamespacePrefixPlaceholder.PowerShell.ModelExtensions
89
{
910
public static class ModelExtensions
1011
{
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>
1812
public static async Task EnsurePropertiesAreReady(
1913
this object model,
2014
bool failOnExplicitNulls = false,
2115
int retries = 3,
22-
int delayMs = 1000)
16+
int delayMs = 1000,
17+
bool debug = false)
2318
{
2419
if (model == null)
2520
throw new ArgumentNullException(nameof(model));
2621

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);
3124

32-
var props = model.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public);
25+
if (isSetMethod == null)
26+
return; // Skip if model doesn't support property tracking
3327

34-
for (int attempt = 0; attempt <= retries; attempt++)
28+
for (int attempt = 0; attempt < retries; attempt++)
3529
{
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;
3934

40-
if (!unready.Any())
41-
return; // Ready!
35+
bool isSet = (bool)isSetMethod.Invoke(model, new object[] { prop.Name });
36+
object value = null;
4237

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 */ }
4643

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())}");
5246

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
6049

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+
}
6561

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+
}
6776

68-
if (value == null)
69-
return failOnExplicitNulls; // null is OK in relaxed mode, fail in strict
77+
if (allReady)
78+
return;
7079

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);
7297
}
7398

74-
private static bool IsDefault(object value)
99+
private static bool IsDefault(object obj)
75100
{
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);
80104
}
81105
}
82106
}

0 commit comments

Comments
 (0)