From 419e1af2904d3f1a0fbc4a5cd5122b9f3a6d8398 Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Sat, 15 Mar 2025 12:47:10 +0300 Subject: [PATCH 1/3] Refactoring of Json extension class and added property tracker functionality --- src/readme.graph.md | 49 +++++- tools/Custom/IPropertyTracker.cs | 9 + tools/Custom/JsonExtensions.cs | 138 ++-------------- tools/Custom/PropertyTracker.cs | 72 ++++++++ tools/GenerateModules.ps1 | 6 +- .../JsonUtilitiesTest/JsonExtensionsTests.cs | 155 ++++++++---------- 6 files changed, 210 insertions(+), 219 deletions(-) create mode 100644 tools/Custom/IPropertyTracker.cs create mode 100644 tools/Custom/PropertyTracker.cs diff --git a/src/readme.graph.md b/src/readme.graph.md index 2983702ed7..798ccff4a1 100644 --- a/src/readme.graph.md +++ b/src/readme.graph.md @@ -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 { @@ -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 $; } @@ -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 { @@ -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(object value) => PropertyTracker.SanitizeValue(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 @@ -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 $ } diff --git a/tools/Custom/IPropertyTracker.cs b/tools/Custom/IPropertyTracker.cs new file mode 100644 index 0000000000..1eb7db3416 --- /dev/null +++ b/tools/Custom/IPropertyTracker.cs @@ -0,0 +1,9 @@ +namespace NamespacePrefixPlaceholder.PowerShell.Models +{ + public interface IPropertyTracker + { + void TrackProperty(string propertyName); + bool IsPropertySet(string propertyName); + T SanitizeValue(object value); + } +} \ No newline at end of file diff --git a/tools/Custom/JsonExtensions.cs b/tools/Custom/JsonExtensions.cs index 62422ca0bf..245a4742b5 100644 --- a/tools/Custom/JsonExtensions.cs +++ b/tools/Custom/JsonExtensions.cs @@ -8,30 +8,16 @@ namespace NamespacePrefixPlaceholder.PowerShell.JsonUtilities public static class JsonExtensions { /// - /// 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. /// - /// The JToken (JObject or JArray) to process. - /// The cleaned JSON string with "defaultnull" values removed and "null" strings converted to null. - /// - /// JObject json = JObject.Parse(@"{""name"": ""John"", ""email"": ""defaultnull"", ""address"": ""null""}"); - /// string cleanedJson = json.RemoveDefaultNullProperties(); - /// Console.WriteLine(cleanedJson); - /// // Output: { "name": "John", "address": null } - /// - + /// The JSON token to process. + /// A cleaned JSON string with unnecessary null values removed. 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) @@ -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()) @@ -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) @@ -84,25 +62,28 @@ 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; @@ -110,92 +91,5 @@ private static JToken ProcessToken(JToken token) 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 - } - } - } - } } } diff --git a/tools/Custom/PropertyTracker.cs b/tools/Custom/PropertyTracker.cs new file mode 100644 index 0000000000..1d8dde56db --- /dev/null +++ b/tools/Custom/PropertyTracker.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; + + +namespace NamespacePrefixPlaceholder.PowerShell.Models +{ + public class PropertyTracker + { + private readonly HashSet _trackedProperties = new HashSet(); + + public void TrackProperty(string propertyName) + { + _trackedProperties.Add(propertyName); // ✅ Track properties that are set + } + + public bool IsPropertySet(string propertyName) + { + // Ensure that the first character of the property name is UpperCase + if (propertyName.Length > 0) + { + propertyName = char.ToUpper(propertyName[0]) + propertyName.Substring(1); + } + return _trackedProperties.Contains(propertyName); + } + public static T SanitizeValue(object value) + { + if (typeof(T) == typeof(string)) + { + return (T)(object)(string.IsNullOrEmpty(value as string) ? null : value); + } + return (T)value; + } + public static NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonNode ConvertToJsonNode(Type propertyType, object value) + { + if (value == null) + { + return new NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonString("null"); // Explicitly return null if the property is set to null + } + + // Get the declared property type using reflection + + + // Handle different types based on the declared type + if (propertyType == typeof(string)) + { + return new NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonString(value.ToString()); + } + else if (propertyType == typeof(int) || propertyType == typeof(int?) || + propertyType == typeof(long) || propertyType == typeof(long?) || + propertyType == typeof(short) || propertyType == typeof(short?)) + { + return new NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonNumber(Convert.ToDouble(value)); + } + else if (propertyType == typeof(bool) || propertyType == typeof(bool?)) + { + return new NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonBoolean((bool)value); + } + else if (propertyType.IsEnum) + { + return new NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonString(value.ToString()); + } + else if (propertyType == typeof(DateTime) || propertyType == typeof(DateTime?)) + { + return new NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonString(((DateTime)value).ToString("o")); // ISO 8601 format + } + + // Fallback to JSON object if the type is complex + return NamespacePrefixPlaceholder.PowerShell.Runtime.Json.JsonObject.FromObject(value); + } + } +} + diff --git a/tools/GenerateModules.ps1 b/tools/GenerateModules.ps1 index 044f09876a..e6fd15d771 100644 --- a/tools/GenerateModules.ps1 +++ b/tools/GenerateModules.ps1 @@ -85,7 +85,7 @@ $AutoRestTempFolder | ForEach-Object { Write-Debug "Removing cached file $File" if (Test-Path $File.FullName) { #Remove the file - Remove-Item -Path $File.FullName -Force + #Remove-Item -Path $File.FullName -Force -confirm:$false } } } @@ -97,7 +97,7 @@ Get-ChildItem -Path $TempPath -Recurse | Where-Object { $_.Extension -match ".tm Write-Debug "Removing cached file $File" if (Test-Path $File.FullName) { #Remove the file - Remove-Item -Path $File.FullName -Force + #Remove-Item -Path $File.FullName -Force -confirm:$false } } $Stopwatch = [system.diagnostics.stopwatch]::StartNew() @@ -152,7 +152,7 @@ $ModuleToGenerate | ForEach-Object -Parallel { if (Test-Path $File.FullName) { #Remove the file try{ - Remove-Item -Path $File.FullName -Force + #Remove-Item -Path $File.FullName -Force -confirm:$false } catch { Write-Warning "Failed to remove file $File" diff --git a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs index 2cd1e56e44..343f36b5d7 100644 --- a/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs +++ b/tools/Tests/JsonUtilitiesTest/JsonExtensionsTests.cs @@ -8,49 +8,40 @@ public class JsonExtensionsTests { [Fact] - public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValues() + public void RemoveDefaultNullProperties_ShouldConvertStringNullToJsonNull() { // Arrange - JObject json = JObject.Parse(@"{ - ""displayname"": ""Tim"", - ""position"": ""defaultnull"", - ""salary"": 2000000, - ""team"": ""defaultnull"" + JToken json = JToken.Parse(@"{ + ""displayname"": ""null"", }"); // Act string cleanedJson = json.RemoveDefaultNullProperties(); - JObject result = JObject.Parse(cleanedJson); - + String expectedJson = @"{ + ""displayname"": null + }"; // Assert - Assert.False(result.ContainsKey("position")); - Assert.False(result.ContainsKey("team")); - Assert.Equal("Tim", result["displayname"]?.ToString()); - Assert.Equal(2000000, result["salary"]?.ToObject()); + Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(cleanedJson)); } [Fact] - public void RemoveDefaultNullProperties_ShouldConvertStringNullToJsonNull() + public void RemoveDefaultNullProperties_ShouldConvertNullToJsonNull() { // Arrange - JObject json = JObject.Parse(@"{ - ""displayname"": ""Tim"", - ""position"": ""null"", - ""salary"": 2000000, - ""team"": """" + JToken json = JToken.Parse(@"{ + ""displayname"": null, }"); // Act string cleanedJson = json.RemoveDefaultNullProperties(); - JObject result = JObject.Parse(cleanedJson); - + String expectedJson = @"{ + ""displayname"": null + }"; // Assert - Assert.Null(result["position"]?.Value()); - Assert.Equal("", result["team"]?.ToString()); - Assert.Equal("Tim", result["displayname"]?.ToString()); - Assert.Equal(2000000, result["salary"]?.ToObject()); + Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(cleanedJson)); } + [Fact] public void RemoveDefaultNullProperties_ShouldHandleNestedObjects() { @@ -62,14 +53,14 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects() ""jobProfile"": { ""dept"": ""ICT"", ""manager"": false, - ""supervisor"" : ""defaultnull"" + ""supervisor"" : ""null"" }, ""metadata"": { - ""phone"": ""defaultnull"", + ""phone"": ""null"", ""location"": ""null"", ""address"": { ""city"": ""Nairobi"", - ""street"": ""defaultnull"" + ""street"": ""null"" }, ""station"": { } @@ -78,16 +69,27 @@ public void RemoveDefaultNullProperties_ShouldHandleNestedObjects() // Act string cleanedJson = json.RemoveDefaultNullProperties(); - JObject result = JObject.Parse(cleanedJson); + string expectedJson = @"{ + ""displayname"": ""Tim"", + ""professions"": {}, + ""jobProfile"": { + ""dept"": ""ICT"", + ""manager"": false, + ""supervisor"": null + }, + ""metadata"": { + ""phone"": null, + ""location"": null, + ""address"": { + ""city"": ""Nairobi"", + ""street"": null + }, + ""station"": {} + } + }"; // Assert - Assert.False(result["metadata"]?.ToObject()?.ContainsKey("phone")); - Assert.Equal("ICT", result["jobProfile"]?["dept"]?.ToString()); - Assert.Equal("Nairobi", result["metadata"]?["address"]?["city"]?.ToString()); - Assert.Null(result["metadata"]?["location"]?.Value()); - // Check if emptynested object is removed - Assert.False(result["metadata"]?.ToObject()?.ContainsKey("station ")); - Assert.False(result?.ToObject()?.ContainsKey("professions")); + Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(cleanedJson)); } [Fact] @@ -110,7 +112,7 @@ public void RemoveDefaultNullProperties_ShouldHandleJsonArrays() // Arrange JObject json = JObject.Parse(@"{ ""users"": [ - { ""displayname"": ""Tim"", ""email"": ""defaultnull"" }, + { ""displayname"": ""Tim"", ""email"": ""null"" }, { ""displayname"": ""Mayabi"", ""email"": ""mayabi@example.com"" } ] }"); @@ -146,31 +148,31 @@ public void RemoveDefaultNullProperties_ShouldNotAlterValidData() //Add tests for json arrays [Fact] - public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonArray() + public void RemoveDefaultNullProperties_ShouldConvertNullStringValuesIntoNullInJsonArray() { // Arrange JArray json = JArray.Parse(@"[ - { ""displayname"": ""Tim"", ""email"": ""defaultnull"" } + { ""displayname"": ""Tim"", ""email"": ""null"" } ]"); // Act string cleanedJson = json.RemoveDefaultNullProperties(); - JArray result = JArray.Parse(cleanedJson); - + String expectedJson = @"[ + { ""displayname"": ""Tim"", ""email"": null } + ]"; // Assert - Assert.Equal("Tim", result[0]?["displayname"]?.ToString()); - Assert.False(result[0].ToObject().ContainsKey("email")); + Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(cleanedJson)); } - [Fact] + [Fact] public void ReplaceAndRemoveSlashes_Should_Preserve_Json_Property_Values() { // Arrange - string inputJson = @"{ + JObject inputJson = JObject.Parse(@"{ ""RedirectUris"": [""http://localhost/.auth/login/aad/callback""], ""DirectoryPath"": ""/this/is/a/directory/and/should/not/be/removed"" - }"; + }"); string expectedJson = @"{ ""RedirectUris"": [""http://localhost/.auth/login/aad/callback""], @@ -178,37 +180,10 @@ public void ReplaceAndRemoveSlashes_Should_Preserve_Json_Property_Values() }"; // Act - string result = inputJson.ReplaceAndRemoveSlashes(); - - // Assert - Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(result)); - } - - [Fact] - public void ReplaceAndRemoveSlashes_Should_Remove_Backslashes() - { - // Arrange - string input = @"Some \random \slashes that \should be removed."; - string expected = "Some random slashes that should be removed."; - - // Act - string result = input.ReplaceAndRemoveSlashes(); + string result = inputJson.RemoveDefaultNullProperties(); // Assert - Assert.Equal(expected, result); - } - - [Fact] - public void ReplaceAndRemoveSlashes_Should_Handle_Invalid_Json_Gracefully() - { - // Arrange - string invalidJson = "{Invalid Json \\with /slashes}"; - - // Act - string result = invalidJson.ReplaceAndRemoveSlashes(); - - // Assert - Assert.DoesNotContain("\\", result); + Assert.Equal(NormalizeJson(result), NormalizeJson(expectedJson)); } /// @@ -225,38 +200,40 @@ private string NormalizeJson(string json) } [Fact] - public void RemoveDefaultNullProperties_ShouldRemoveDefaultNullValuesInJsonObjectWithBothDeeplyNestedObjectsAndArrays(){ + public void RemoveDefaultNullProperties_ShouldRemoveConvertNullStringValuesIntoNullInJsonObjectWithBothDeeplyNestedObjectsAndArrays() + { // Arrange JObject json = JObject.Parse(@"{ ""body"":{ ""users"": [ - { ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } } + { ""displayname"": ""Tim"", ""email"": ""null"", ""metadata"": { ""phone"": ""254714390915"" } } ] }, ""users"": [ - { ""displayname"": ""Tim"", ""email"": ""defaultnull"", ""metadata"": { ""phone"": ""254714390915"" } }]}"); + { ""displayname"": ""Tim"", ""email"": ""null"", ""metadata"": { ""phone"": ""254714390915"" } }]}"); // Act string cleanedJson = json.RemoveDefaultNullProperties(); JObject result = JObject.Parse(cleanedJson); // Assert - Assert.False(result["users"][0]?.ToObject().ContainsKey("email")); Assert.True(result["users"][0]?["metadata"]?.ToObject().ContainsKey("phone")); - Assert.False(result["body"]?["users"][0]?.ToObject().ContainsKey("email")); Assert.True(result["body"]?["users"][0]?["metadata"]?.ToObject().ContainsKey("phone")); } - /* - Test for unescaping json object while maintaining original property definition of values - instead of auto inferencing the type of the value - "fields": "{\r\n \"BasicTag\": \"v2.31.0\",\r\n \"BuildID\": \"3599\",\r\n \"MWSCommitID\": \"a5c7998252f2366c8cbbb03ba46e9b\",\r\n \"MWSTag\": \"v2.21.0\",\r\n \"BasicCommitID\": \"9c3d0f36362dd25caa0da2ecab06a1859ce2\",\r\n \"CustomerCommitID\": \"c40241be9fd2f1cd2f2f2fc961c37f720c\"\r\n}" - */ [Fact] - public void RemoveDefaultNullProperties_ShouldUnescapeJsonString(){ + public void RemoveDefaultNullProperties_ShouldNotDoAutomaticInferrencing() + { // Arrange JObject json = JObject.Parse(@"{ - ""fields"": ""{\r\n \""BasicTag\"": \""v2.31.0\"",\r\n \""BuildID\"": \""3599\"",\r\n \""MWSCommitID\"": \""a5c7998252f2366c8cbbb03ba46e9b\"",\r\n \""MWSTag\"": \""v2.21.0\"",\r\n \""BasicCommitID\"": \""9c3d0f36362dd25caa0da2ecab06a1859ce2\"",\r\n \""CustomerCommitID\"": \""c40241be9fd2f1cd2f2f2fc961c37f720c\""\r\n}"" + ""fields"": { + ""BasicTag"": ""v2.31.0"", + ""BuildID"": ""3599"", + ""MWSCommitID"": ""a5c7998252f2366c8cbbb03ba46e9b"", + ""MWSTag"": ""v2.21.0"", + ""BasicCommitID"": ""9c3d0f36362dd25caa0da2ecab06a1859ce2"", + ""CustomerCommitID"": ""c40241be9fd2f1cd2f2f2fc961c37f720c"" + } }"); String expectedJson = @"{ @@ -272,13 +249,13 @@ public void RemoveDefaultNullProperties_ShouldUnescapeJsonString(){ // Act //Convert Json object to string then pass it to RemoveAndReplaceSlashes method - string cleanedJson = json.ToString()?.ReplaceAndRemoveSlashes(); + string cleanedJson = json.RemoveDefaultNullProperties(); // Assert Assert.Equal(NormalizeJson(expectedJson), NormalizeJson(cleanedJson)); } - - + + } From b4be7d5f2ced85dd20e9fd1c02e3d9c27ce6dbe7 Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Sat, 15 Mar 2025 22:38:25 +0300 Subject: [PATCH 2/3] Updated custom file in teams module to implement interface --- src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs | 4 ++++ src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs b/src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs index 260f2b3698..59c19bc9ed 100644 --- a/src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs +++ b/src/Teams/beta/custom/MicrosoftGraphRscConfiguration.cs @@ -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(object value) => PropertyTracker.SanitizeValue(value); /// /// Backing field for Inherited model /// diff --git a/src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs b/src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs index ac9b21f842..af7f1c44b9 100644 --- a/src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs +++ b/src/Teams/beta/custom/MicrosoftGraphTeamsAppPreApproval.cs @@ -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(object value) => PropertyTracker.SanitizeValue(value); + /// /// Backing field for Inherited model /// From 5a33c6614a13dcad66085978adee3bd581df9220 Mon Sep 17 00:00:00 2001 From: Microsoft Graph DevX Tooling Date: Sun, 16 Mar 2025 19:05:12 +0300 Subject: [PATCH 3/3] Updated generate module file --- tools/GenerateModules.ps1 | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/tools/GenerateModules.ps1 b/tools/GenerateModules.ps1 index e6fd15d771..c7d265094f 100644 --- a/tools/GenerateModules.ps1 +++ b/tools/GenerateModules.ps1 @@ -85,21 +85,12 @@ $AutoRestTempFolder | ForEach-Object { Write-Debug "Removing cached file $File" if (Test-Path $File.FullName) { #Remove the file - #Remove-Item -Path $File.FullName -Force -confirm:$false + Remove-Item -Path $File.FullName -Force -confirm:$false } } } } -#Delete any file in temp folder with the extension .tmp or .log or .db -Get-ChildItem -Path $TempPath -Recurse | Where-Object { $_.Extension -match ".tmp|.log|.db" } | ForEach-Object { - $File = $_ - Write-Debug "Removing cached file $File" - if (Test-Path $File.FullName) { - #Remove the file - #Remove-Item -Path $File.FullName -Force -confirm:$false - } -} $Stopwatch = [system.diagnostics.stopwatch]::StartNew() $CpuCount = (Get-CimInstance Win32_Processor).NumberOfLogicalProcessors $Throttle = [math]::Min(4, $cpuCount / 2) # Use half the CPU count but max 4 @@ -144,22 +135,6 @@ $ModuleToGenerate | ForEach-Object -Parallel { if ($OpenFiles.Count -gt 0) { $OpenFiles = Get-OpenFiles -Path $TempPath } - - #Delete any file in temp folder with the extension .tmp or .log or .db - Get-ChildItem -Path $TempPath -Recurse | Where-Object { $_.Extension -match ".tmp|.log|.db|.db-shm|.db-wal" } | ForEach-Object { - $File = $_ - Write-Debug "Removing cached file $File" - if (Test-Path $File.FullName) { - #Remove the file - try{ - #Remove-Item -Path $File.FullName -Force -confirm:$false - } - catch { - Write-Warning "Failed to remove file $File" - } - } - } - } -ThrottleLimit $Throttle $stopwatch.Stop()