Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ public partial class MicrosoftGraphRscConfiguration :
IMicrosoftGraphRscConfigurationInternal,
Runtime.IValidates
{
private readonly PropertyTracker _propertyTracker = new PropertyTracker();
public void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);
public bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);
public T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);
/// <summary>
/// Backing field for Inherited model <see cref= "Microsoft.Graph.Beta.PowerShell.Models.IMicrosoftGraphEntity" />
/// </summary>
Expand Down
5 changes: 5 additions & 0 deletions src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ public partial class MicrosoftGraphTeamsAppPreApproval :
IMicrosoftGraphTeamsAppPreApprovalInternal,
Runtime.IValidates
{
private readonly PropertyTracker _propertyTracker = new PropertyTracker();
public void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);
public bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);
public T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);

/// <summary>
/// Backing field for Inherited model <see cref= "Microsoft.Graph.Beta.PowerShell.Models.IMicrosoftGraphEntity" />
/// </summary>
Expand Down
49 changes: 44 additions & 5 deletions src/readme.graph.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,7 +305,7 @@ directive:
- from: source-file-csharp
where: $
transform: >
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*MicrosoftGraph\w*\d*.json.cs/gm))
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*\d*.json.cs/gm))
{
return $;
} else {
Expand Down Expand Up @@ -333,11 +333,26 @@ directive:
// Ensure dateTime is always serialized as Utc.
let dateTimeToJsonRegex = /(\.Json\.JsonString\()(.*)\?(\.ToString\(@"yyyy'-'MM'-'dd'T'HH':'mm':'ss\.fffffffK")/gm
$ = $.replace(dateTimeToJsonRegex, '$1System.DateTime.SpecifyKind($2.Value.ToUniversalTime(), System.DateTimeKind.Utc)$3');

//The following regex below adds a property tracker to ensure that users can also pass $Null as an alternative to the current "null" string which gets inferred to null.

// Enables null valued properties
$ = $.replace(/AddIf\(\s*null\s*!=\s*(this\._\w+)\s*\?\s*\(\s*Microsoft\.Graph\.PowerShell\.Runtime\.Json\.JsonNode\)\s*(.*)\s*:\s*null\s*,\s*"(.*?)"\s*,\s*container\.Add\s*\)/gm, 'container.Add("$3", $1 != null ? (Microsoft.Graph.PowerShell.Runtime.Json.JsonNode) $2 :"defaultnull")')
const regexP = /AddIf\(\s*null\s*!=\s*\(\(\(object\)this\._(\w+).*?(\(Microsoft.*.PowerShell\.Runtime\.Json\.JsonNode\)).*?"(\w+)".*?container\.Add\s*\);/gm
$ = $.replace(regexP, (match, p1, p2, p3) => {
let capitalizedP1 = p1.charAt(0).toUpperCase() + p1.slice(1); // Capitalize first letter
return `if(this.IsPropertySet("${p1}"))\n\t\t{\n\t\t\tvar propertyInfo = this.GetType().GetProperty("${capitalizedP1}");\n\t\t\tif (propertyInfo != null)\n\t\t\t{\n\t\t\tSystem.Type propertyType = propertyInfo.PropertyType;\n\t\t\t\t\tAddIf(${p2}PropertyTracker.ConvertToJsonNode(propertyType, this._${p1}),"${p1}",container.Add);\n\t\t\t}\n\t\t}`;
});

$ = $.replace(/if\s*\(\s*null\s*!=\s*this\._(\w+)\s*\)/gm, 'if(this.IsPropertySet("$1"))')

let nameSpacePrefixRegex = /(Microsoft(?:\.\w+)*?\.PowerShell)/gm
let nameSpacePrefix = 'Microsoft.Graph.PowerShell';
if($.match(nameSpacePrefixRegex)){
let prefixMatch = nameSpacePrefixRegex.exec($);
nameSpacePrefix = prefixMatch[1];
}
$ = $.replace(/container\.Add\("(\w+)",\s*(__\w+)\);/gm, 'var nullFlag = ('+nameSpacePrefix+'.Runtime.Json.JsonNode)new '+nameSpacePrefix+'.Runtime.Json.JsonString("nullarray");\n\t\tif($2.Count == 0)\n\t\t{\n\t\t\t$2.Add(nullFlag);\n\t\t}\n\t\tcontainer.Add("$1", $2);');

$ = $.replace(/AddIf\(\s*null\s*!=\s*\(\(\(\(object\)\s*(this\._\w+)\)\)?.ToString\(\)\)\s*\?\s*\(\s*Microsoft\.Graph\.PowerShell\.Runtime\.Json\.JsonNode\)\s*new\s*Microsoft\.Graph\.PowerShell\.Runtime\.Json\.JsonString\((this\._\w+).ToString\(\)\)\s*:\s*null\s*,\s*"(.*?)"\s*,\s*container\.Add\s*\)/gm, 'container.Add("$3", $1 != null ? (Microsoft.Graph.PowerShell.Runtime.Json.JsonNode) new Microsoft.Graph.PowerShell.Runtime.Json.JsonString($2.ToString()) :"defaultnull")');
$ =$.replace(/AddIf\(\s+null\s+!=\s+(this\._\w+)\s+\?\s+\((Microsoft\.Graph\..*?)\)\s+this\._(\w+)\.ToJson\(null,serializationMode\)\s+:\s+null,\s+"\w+"\s+,container.Add\s+\);/gm, 'if (this.IsPropertySet("$3")) \n{\n if ($1 != null)\n{\n container.Add("$3", ($2)$1.ToJson(null, serializationMode)); \n}\nelse\n{\n container.Add("$3", "null"); \n}\n}');

return $;
}
Expand Down Expand Up @@ -395,7 +410,7 @@ directive:
- from: source-file-csharp
where: $
transform: >
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*MicrosoftGraph\w*\d*.cs/gm))
if (!$documentPath.match(/generated%2Fapi%2FModels%2F\w*\d*.cs/gm))
{
return $;
} else {
Expand All @@ -404,8 +419,31 @@ directive:
if($.match(additionalPropertiesRegex)) {
$ = $.replace(additionalPropertiesRegex, '$1$2 new $3');
}
//The following regex below adds a property tracker to ensure that users can also pass $Null as an alternative to the current "null" string which gets inferred to null.
$ = $.replace(/\bpublic\s+(\w+\??)\s+(\w+)\s*{\s*get\s*=>\s*this\.(\w+);\s*set\s*=>\s*this\.\3\s*=\s*value;\s*}/gmi,'public $1 $2\n\t{\n\t\tget=>this.$3;\n\t\tset\n\t\t{\n\t\t\tthis.$3=SanitizeValue<$1>(value);\n\t\t\tTrackProperty(nameof($2));\n\t\t}\n\t}')

$ = $.replace(/\bpublic\s+(\w+\[\])\s+(\w+)\s*{\s*get\s*=>\s*this\.(\w+);\s*set\s*=>\s*this\.\3\s*=\s*value;\s*}/gm,'public $1 $2\n\t{\n\t\tget=>this.$3;\n\t\tset\n\t\t{\n\t\t\tthis.$3=value;\n\t\t\tTrackProperty(nameof($2));\n\t\t}\n\t}')

$ = $.replace(/\bpublic\s+(Microsoft\.Graph\.[\w.]+\[\])\s+(\w+)\s*{\s*get\s*=>\s*this\.(\w+);\s*set\s*=>\s*this\.\3\s*=\s*value;\s*}/gm,'public $1 $2\n\t{\n\t\tget=>this.$3;\n\t\tset\n\t\t{\n\t\t\tthis.$3=value;\n\t\t\tTrackProperty(nameof($2));\n\t\t}\n\t}')

const match = $documentPath.match(/generated%2Fapi%2FModels%2F([\w]*[\w\d]*)\.cs/gm);
if (match) {
let fileName = match[0];
fileName = fileName.replace('generated%2Fapi%2FModels%2F','')
fileName = fileName.replace('.cs','')
const interfaceName = 'I'+fileName
$ = $.replace('interface '+interfaceName+' :', 'interface '+interfaceName+' : IPropertyTracker,')
const className = fileName
const regexP = new RegExp(`public\\s+partial\\s+class\\s+${className}\\s*:\\s*[\\s\\S]*?{`, "gm");
var matches = regexP.exec($);
let originalMatch = matches[0];
$ = $.replace(regexP, originalMatch+'\n\t\tprivate readonly PropertyTracker _propertyTracker = new PropertyTracker();\n\t\tpublic void TrackProperty(string propertyName) => _propertyTracker.TrackProperty(propertyName);\n\t\tpublic bool IsPropertySet(string propertyName) =>_propertyTracker.IsPropertySet(propertyName);\n\t\tpublic T SanitizeValue<T>(object value) => PropertyTracker.SanitizeValue<T>(value);');
}

$ = $.replace(/public\s+(Microsoft\.Graph\..*?)\s+(\w+)\s+{\s+get\s+=>\s+\(\s*this\.(\w+)\s+=\s*this\.\3\s+\?\?\s+new\s+(Microsoft\.Graph\..*?)\s+set\s+=>\s+this._\w+\s+=\s+value;\s+}/gm, 'public $1 $2 { \n get => (this.$3 = this.$3 ?? new $4\n set\n {\n this.$3 = value;\n TrackProperty(nameof($2));\n }\n}')

return $;

}
# Modify generated .cs cmdlets.
- from: source-file-csharp
Expand Down Expand Up @@ -658,6 +696,7 @@ directive:

$ = $.replace(/request\.Content\s*=\s*new\s+global::System\.Net\.Http\.StringContent\(\s*null\s*!=\s*body\s*\?\s*new\s+Microsoft\.Graph\.Beta\.PowerShell\.Runtime\.Json\.XNodeArray\(.*?\)\s*:\s*null,\s*global::System\.Text\.Encoding\.UTF8\);/g,'request.Content = new global::System.Net.Http.StringContent(cleanedBody, global::System.Text.Encoding.UTF8);');

$ = $.replace(/cleanedBody = Microsoft.*.ReplaceAndRemoveSlashes\(cleanedBody\);/gm,'')
return $
}

Expand Down
9 changes: 9 additions & 0 deletions tools/Custom/IPropertyTracker.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace NamespacePrefixPlaceholder.PowerShell.Models
{
public interface IPropertyTracker
{
void TrackProperty(string propertyName);
bool IsPropertySet(string propertyName);
T SanitizeValue<T>(object value);
}
}
138 changes: 16 additions & 122 deletions tools/Custom/JsonExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,16 @@ namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities
public static class JsonExtensions
{
/// <summary>
/// Recursively removes properties with the value "defaultnull" from a JSON structure
/// and replaces string values that are "null" with actual null values.
/// This method supports both JObject (JSON objects) and JArray (JSON arrays),
/// ensuring proper cleanup of nested structures.
/// Converts "null" strings to actual null values, replaces empty objects, and cleans up arrays.
/// </summary>
/// <param name="token">The JToken (JObject or JArray) to process.</param>
/// <returns>The cleaned JSON string with "defaultnull" values removed and "null" strings converted to null.</returns>
/// <example>
/// JObject json = JObject.Parse(@"{""name"": ""John"", ""email"": ""defaultnull"", ""address"": ""null""}");
/// string cleanedJson = json.RemoveDefaultNullProperties();
/// Console.WriteLine(cleanedJson);
/// // Output: { "name": "John", "address": null }
/// </example>

/// <param name="token">The JSON token to process.</param>
/// <returns>A cleaned JSON string with unnecessary null values removed.</returns>
public static string RemoveDefaultNullProperties(this JToken token)
{
try
{
ProcessToken(token);

// If the root token is completely empty, return "{}" or "[]"
if (token is JObject obj && !obj.HasValues) return "{}";
if (token is JArray arr && !arr.HasValues) return "[]";

return token.ToString();
}
catch (Exception)
Expand All @@ -44,15 +30,6 @@ private static JToken ProcessToken(JToken token)
{
if (token is JObject jsonObject)
{
// Remove properties with "defaultnull" but keep valid ones
var propertiesToRemove = jsonObject.Properties()
.Where(p => p.Value.Type == JTokenType.String && p.Value.ToString().Equals("defaultnull", StringComparison.Ordinal))
.ToList();

foreach (var property in propertiesToRemove)
{
property.Remove();
}

// Recursively process remaining properties
foreach (var property in jsonObject.Properties().ToList())
Expand All @@ -65,11 +42,12 @@ private static JToken ProcessToken(JToken token)
property.Value = JValue.CreateNull();
}

// Remove the property if it's now empty after processing
if (ShouldRemove(cleanedValue))
if (property.Value.ToString().Equals("{\r\n}", StringComparison.Ordinal))
{
property.Remove();

property.Value = JObject.Parse("{}"); // Convert empty object to {}
}

}

// Remove the object itself if ALL properties are removed (empty object)
Expand All @@ -84,118 +62,34 @@ private static JToken ProcessToken(JToken token)
// Process nested objects/arrays inside the array
if (item is JObject || item is JArray)
{
JToken cleanedItem = ProcessToken(item);

if (ShouldRemove(cleanedItem))
if (item.ToString().Equals("{\r\n}", StringComparison.Ordinal))
{
jsonArray.RemoveAt(i); // Remove empty or unnecessary items
JToken cleanedItem = ProcessToken(item);
jsonArray[i] = JObject.Parse("{}"); // Convert empty object to {}
}
else
{
JToken cleanedItem = ProcessToken(item);
jsonArray[i] = cleanedItem; // Update with cleaned version
}

}
else if (item.Type == JTokenType.String && item.ToString().Equals("null", StringComparison.Ordinal))
{
jsonArray[i] = JValue.CreateNull(); // Convert "null" string to JSON null
}
else if (item.Type == JTokenType.String && item.ToString().Equals("defaultnull", StringComparison.Ordinal))
else if (item.Type == JTokenType.String && item.ToString().Equals("nullarray", StringComparison.Ordinal))
{
jsonArray.RemoveAt(i); // Remove "defaultnull" entries
jsonArray.RemoveAt(i);
i--;
}

}

return jsonArray.HasValues ? jsonArray : null;
}

return token;
}

private static bool ShouldRemove(JToken token)
{
return token == null ||
(token.Type == JTokenType.Object && !token.HasValues) || // Remove empty objects
(token.Type == JTokenType.Array && !token.HasValues); // Remove empty arrays
}


public static string ReplaceAndRemoveSlashes(this string body)
{
try
{
// Parse the JSON using Newtonsoft.Json
JToken jsonToken = JToken.Parse(body);
if (jsonToken == null) return body; // If parsing fails, return original body

// Recursively process JSON to remove escape sequences
ProcessBody(jsonToken);

// Return cleaned JSON string
return JsonConvert.SerializeObject(jsonToken, Formatting.None);
}
catch (Newtonsoft.Json.JsonException)
{
// If it's not valid JSON, apply normal string replacements
return body.Replace("\\", "").Replace("rn", "").Replace("\"{", "{").Replace("}\"", "}");
}
}

private static void ProcessBody(JToken token)
{
if (token is JObject jsonObject)
{
foreach (var property in jsonObject.Properties().ToList())
{
var value = property.Value;

// If the value is a string, attempt to parse it as JSON to remove escaping
if (value.Type == JTokenType.String)
{
string stringValue = value.ToString();
try
{
JToken parsedValue = JToken.Parse(stringValue);
property.Value = parsedValue; // Replace with unescaped JSON object
ProcessBody(stringValue); // Recursively process
}
catch (Newtonsoft.Json.JsonException)
{
// If parsing fails, leave the value as is
}
}
else if (value is JObject || value is JArray)
{
ProcessBody(value); // Recursively process nested objects/arrays
}
}
}
else if (token is JArray jsonArray)
{
for (int i = 0; i < jsonArray.Count; i++)
{
var value = jsonArray[i];

// If the value is a string, attempt to parse it as JSON to remove escaping
if (value.Type == JTokenType.String)
{
string stringValue = value.ToString();
try
{
JToken parsedValue = JToken.Parse(stringValue);
jsonArray[i] = parsedValue; // Replace with unescaped JSON object
ProcessBody(stringValue); // Recursively process
}
catch (Newtonsoft.Json.JsonException)
{
// If parsing fails, leave the value as is
}
}
else if (value is JObject || value is JArray)
{
ProcessBody(value); // Recursively process nested objects/arrays
}
}
}
}
}
}
Loading
Loading