diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs index d179a8c4feb..afdc300d717 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/ConvertToJsonCommand.cs @@ -2,22 +2,12 @@ // Licensed under the MIT License. using System; -using System.Collections; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; -using System.Globalization; -using System.Reflection; -using System.Text; -using Dbg = System.Management.Automation; using System.Management.Automation; using System.Management.Automation.Internal; - +using System.Threading; using Newtonsoft.Json; -using Newtonsoft.Json.Converters; - -// FxCop suppressions for resource strings: -[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope = "resource", Target = "WebCmdletStrings.resources", MessageId = "json")] -[module: SuppressMessage("Microsoft.Naming", "CA1703:ResourceStringsShouldBeSpelledCorrectly", Scope = "resource", Target = "WebCmdletStrings.resources", MessageId = "Json")] namespace Microsoft.PowerShell.Commands { @@ -29,7 +19,6 @@ namespace Microsoft.PowerShell.Commands [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] public class ConvertToJsonCommand : PSCmdlet { - #region parameters /// /// Gets or sets the InputObject property. /// @@ -39,13 +28,18 @@ public class ConvertToJsonCommand : PSCmdlet private int _depth = 2; private const int maxDepthAllowed = 100; + private readonly CancellationTokenSource cancellationSource = new CancellationTokenSource(); /// /// Gets or sets the Depth property. /// [Parameter] [ValidateRange(1, int.MaxValue)] - public int Depth { get { return _depth; } set { _depth = value; } } + public int Depth + { + get { return _depth; } + set { _depth = value; } + } /// /// Gets or sets the Compress property. @@ -82,10 +76,6 @@ public class ConvertToJsonCommand : PSCmdlet [Parameter] public StringEscapeHandling EscapeHandling { get; set; } = StringEscapeHandling.Default; - #endregion parameters - - #region overrides - /// /// Prerequisite checks. /// @@ -105,7 +95,7 @@ protected override void BeginProcessing() private List _inputObjects = new List(); /// - /// Caching the input objects for the convertto-json command. + /// Caching the input objects for the command. /// protected override void ProcessRecord() { @@ -122,344 +112,32 @@ protected override void EndProcessing() { if (_inputObjects.Count > 0) { - object objectToProcess = (_inputObjects.Count > 1 || AsArray) ? (_inputObjects.ToArray() as object) : (_inputObjects[0]); - // Pre-process the object so that it serializes the same, except that properties whose - // values cannot be evaluated are treated as having the value null. - object preprocessedObject = null; - try - { - preprocessedObject = ProcessValue(objectToProcess, 0); - } - catch (StoppingException) - { - return; - } - - JsonSerializerSettings jsonSettings = new JsonSerializerSettings - { - TypeNameHandling = TypeNameHandling.None, - MaxDepth = 1024, - StringEscapeHandling = EscapeHandling - }; - if (EnumsAsStrings) - { - jsonSettings.Converters.Add(new StringEnumConverter()); - } - - if (!Compress) - { - jsonSettings.Formatting = Formatting.Indented; - } - - string output = JsonConvert.SerializeObject(preprocessedObject, jsonSettings); - WriteObject(output); - } - } - - #endregion overrides - - /// - /// Return an alternate representation of the specified object that serializes the same JSON, except - /// that properties that cannot be evaluated are treated as having the value null. - /// Primitive types are returned verbatim. Aggregate types are processed recursively. - /// - /// The object to be processed. - /// The current depth into the object graph. - /// An object suitable for serializing to JSON. - private object ProcessValue(object obj, int depth) - { - if (Stopping) - { - throw new StoppingException(); - } - - PSObject pso = obj as PSObject; - - if (pso != null) - obj = pso.BaseObject; - - object rv = obj; - bool isPurePSObj = false; - bool isCustomObj = false; - - if (obj == null - || DBNull.Value.Equals(obj) - || obj is string - || obj is char - || obj is bool - || obj is DateTime - || obj is DateTimeOffset - || obj is Guid - || obj is Uri - || obj is double - || obj is float - || obj is decimal) - { - rv = obj; - } - else if (obj is Newtonsoft.Json.Linq.JObject jObject) - { - rv = jObject.ToObject>(); - } - else - { - TypeInfo t = obj.GetType().GetTypeInfo(); - - if (t.IsPrimitive) - { - rv = obj; - } - else if (t.IsEnum) - { - // Win8:378368 Enums based on System.Int64 or System.UInt64 are not JSON-serializable - // because JavaScript does not support the necessary precision. - Type enumUnderlyingType = Enum.GetUnderlyingType(obj.GetType()); - if (enumUnderlyingType.Equals(typeof(Int64)) || enumUnderlyingType.Equals(typeof(UInt64))) - { - rv = obj.ToString(); - } - else - { - rv = obj; - } - } - else - { - if (depth > Depth) - { - if (pso != null && pso.immediateBaseObjectIsEmpty) - { - // The obj is a pure PSObject, we convert the original PSObject to a string, - // instead of its base object in this case - rv = LanguagePrimitives.ConvertTo(pso, typeof(string), - CultureInfo.InvariantCulture); - isPurePSObj = true; - } - else - { - rv = LanguagePrimitives.ConvertTo(obj, typeof(String), - CultureInfo.InvariantCulture); - } - } - else - { - IDictionary dict = obj as IDictionary; - if (dict != null) - { - rv = ProcessDictionary(dict, depth); - } - else - { - IEnumerable enumerable = obj as IEnumerable; - if (enumerable != null) - { - rv = ProcessEnumerable(enumerable, depth); - } - else - { - rv = ProcessCustomObject(obj, depth); - isCustomObj = true; - } - } - } - } - } - - rv = AddPsProperties(pso, rv, depth, isPurePSObj, isCustomObj); - - return rv; - } - - /// - /// Add to a base object any properties that might have been added to an object (via PSObject) through the Add-Member cmdlet. - /// - /// The containing PSObject, or null if the base object was not contained in a PSObject. - /// The base object that might have been decorated with additional properties. - /// The current depth into the object graph. - /// The processed object is a pure PSObject. - /// The processed object is a custom object. - /// - /// The original base object if no additional properties had been added, - /// otherwise a dictionary containing the value of the original base object in the "value" key - /// as well as the names and values of an additional properties. - /// - private object AddPsProperties(object psobj, object obj, int depth, bool isPurePSObj, bool isCustomObj) - { - PSObject pso = psobj as PSObject; - - if (pso == null) - return obj; - - // when isPurePSObj is true, the obj is guaranteed to be a string converted by LanguagePrimitives - if (isPurePSObj) - return obj; - - bool wasDictionary = true; - IDictionary dict = obj as IDictionary; - - if (dict == null) - { - wasDictionary = false; - dict = new Dictionary(); - dict.Add("value", obj); - } - - AppendPsProperties(pso, dict, depth, isCustomObj); - - if (wasDictionary == false && dict.Count == 1) - return obj; - - return dict; - } - - /// - /// Append to a dictionary any properties that might have been added to an object (via PSObject) through the Add-Member cmdlet. - /// If the passed in object is a custom object (not a simple object, not a dictionary, not a list, get processed in ProcessCustomObject method), - /// we also take Adapted properties into account. Otherwise, we only consider the Extended properties. - /// When the object is a pure PSObject, it also gets processed in "ProcessCustomObject" before reaching this method, so we will - /// iterate both extended and adapted properties for it. Since it's a pure PSObject, there will be no adapted properties. - /// - /// The containing PSObject, or null if the base object was not contained in a PSObject. - /// The dictionary to which any additional properties will be appended. - /// The current depth into the object graph. - /// The processed object is a custom object. - private void AppendPsProperties(PSObject psobj, IDictionary receiver, int depth, bool isCustomObject) - { - // serialize only Extended and Adapted properties.. - PSMemberInfoCollection srcPropertiesToSearch = - new PSMemberInfoIntegratingCollection(psobj, - isCustomObject ? PSObject.GetPropertyCollection(PSMemberViewTypes.Extended | PSMemberViewTypes.Adapted) : - PSObject.GetPropertyCollection(PSMemberViewTypes.Extended)); - - foreach (PSPropertyInfo prop in srcPropertiesToSearch) - { - object value = null; - try - { - value = prop.Value; - } - catch (Exception) - { - } - - if (!receiver.Contains(prop.Name)) - { - receiver[prop.Name] = ProcessValue(value, depth + 1); - } - } - } - - /// - /// Return an alternate representation of the specified dictionary that serializes the same JSON, except - /// that any contained properties that cannot be evaluated are treated as having the value null. - /// - /// - /// - /// - private object ProcessDictionary(IDictionary dict, int depth) - { - Dictionary result = new Dictionary(dict.Count); - - foreach (DictionaryEntry entry in dict) - { - string name = entry.Key as string; - if (name == null) + object objectToProcess = (_inputObjects.Count > 1 || AsArray) ? (_inputObjects.ToArray() as object) : _inputObjects[0]; + + var context = new JsonObject.ConvertToJsonContext( + Depth, + EnumsAsStrings.IsPresent, + Compress.IsPresent, + cancellationSource.Token, + EscapeHandling, + targetCmdlet: this); + + // null is returned only if the pipeline is stopping (e.g. ctrl+c is signaled). + // in that case, we shouldn't write the null to the output pipe. + string output = JsonObject.ConvertToJson(objectToProcess, in context); + if (output != null) { - // use the error string that matches the message from JavaScriptSerializer - var exception = - new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, - WebCmdletStrings.NonStringKeyInDictionary, - dict.GetType().FullName)); - ThrowTerminatingError(new ErrorRecord(exception, "NonStringKeyInDictionary", ErrorCategory.InvalidOperation, dict)); + WriteObject(output); } - - result.Add(name, ProcessValue(entry.Value, depth + 1)); } - - return result; } /// - /// Return an alternate representation of the specified collection that serializes the same JSON, except - /// that any contained properties that cannot be evaluated are treated as having the value null. + /// Process the Ctrl+C signal. /// - /// - /// - /// - private object ProcessEnumerable(IEnumerable enumerable, int depth) + protected override void StopProcessing() { - List result = new List(); - - foreach (object o in enumerable) - { - result.Add(ProcessValue(o, depth + 1)); - } - - return result; + cancellationSource.Cancel(); } - - /// - /// Return an alternate representation of the specified aggregate object that serializes the same JSON, except - /// that any contained properties that cannot be evaluated are treated as having the value null. - /// - /// The result is a dictionary in which all public fields and public gettable properties of the original object - /// are represented. If any exception occurs while retrieving the value of a field or property, that entity - /// is included in the output dictionary with a value of null. - /// - /// - /// - /// - private object ProcessCustomObject(object o, int depth) - { - Dictionary result = new Dictionary(); - Type t = o.GetType(); - - foreach (FieldInfo info in t.GetFields(BindingFlags.Public | BindingFlags.Instance)) - { - if (!info.IsDefined(typeof(T), true)) - { - object value; - try - { - value = info.GetValue(o); - } - catch (Exception) - { - value = null; - } - - result.Add(info.Name, ProcessValue(value, depth + 1)); - } - } - - foreach (PropertyInfo info2 in t.GetProperties(BindingFlags.Public | BindingFlags.Instance)) - { - if (!info2.IsDefined(typeof(T), true)) - { - MethodInfo getMethod = info2.GetGetMethod(); - if ((getMethod != null) && (getMethod.GetParameters().Length <= 0)) - { - object value; - try - { - value = getMethod.Invoke(o, new object[0]); - } - catch (Exception) - { - value = null; - } - - result.Add(info2.Name, ProcessValue(value, depth + 1)); - } - } - } - - return result; - } - - /// - /// Exception used for Stopping. - /// - private class StoppingException : System.Exception { } } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs index 5f341a77b2b..784b8ad0260 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/WebCmdlet/JsonObject.cs @@ -7,8 +7,11 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Management.Automation; +using System.Reflection; using System.Text.RegularExpressions; +using System.Threading; using Newtonsoft.Json; +using Newtonsoft.Json.Converters; using Newtonsoft.Json.Linq; namespace Microsoft.PowerShell.Commands @@ -19,13 +22,90 @@ namespace Microsoft.PowerShell.Commands [SuppressMessage("Microsoft.Naming", "CA1704:IdentifiersShouldBeSpelledCorrectly")] public static class JsonObject { + #region HelperTypes + + /// + /// Context for convert-to-json operation. + /// + public readonly struct ConvertToJsonContext + { + /// + /// Gets the maximum depth for walking the object graph. + /// + public readonly int MaxDepth; + + /// + /// Gets the cancellation token. + /// + public readonly CancellationToken CancellationToken; + + /// + /// Gets the StringEscapeHandling setting. + /// + public readonly StringEscapeHandling StringEscapeHandling; + + /// + /// Gets the EnumsAsStrings setting. + /// + public readonly bool EnumsAsStrings; + + /// + /// Gets the CompressOutput setting. + /// + public readonly bool CompressOutput; + + /// + /// Gets the target cmdlet that is doing the convert-to-json operation. + /// + public readonly PSCmdlet Cmdlet; + + /// + /// Constructor of ConvertToJsonContext. + /// + public ConvertToJsonContext(int maxDepth, bool enumsAsStrings, bool compressOutput) + : this(maxDepth, enumsAsStrings, compressOutput, CancellationToken.None, StringEscapeHandling.Default, targetCmdlet: null) + { + } + + /// + /// Constructor of ConvertToJsonContext. + /// + public ConvertToJsonContext( + int maxDepth, + bool enumsAsStrings, + bool compressOutput, + CancellationToken cancellationToken, + StringEscapeHandling stringEscapeHandling, + PSCmdlet targetCmdlet) + { + this.MaxDepth = maxDepth; + this.CancellationToken = cancellationToken; + this.StringEscapeHandling = stringEscapeHandling; + this.EnumsAsStrings = enumsAsStrings; + this.CompressOutput = compressOutput; + this.Cmdlet = targetCmdlet; + } + } + private class DuplicateMemberHashSet : HashSet { - public DuplicateMemberHashSet(int capacity) : base(capacity, StringComparer.OrdinalIgnoreCase) + public DuplicateMemberHashSet(int capacity) + : base(capacity, StringComparer.OrdinalIgnoreCase) { } } + /// + /// Exception used for cancellation. + /// + private class StoppingException : System.Exception + { + } + + #endregion HelperTypes + + #region ConvertFromJson + /// /// Convert a Json string back to an object of type PSObject. /// @@ -79,6 +159,7 @@ public static object ConvertFromJson(string input, bool returnHashtable, out Err input, new JsonSerializerSettings { + // This TypeNameHandling setting is required to be secure. TypeNameHandling = TypeNameHandling.None, MetadataPropertyHandling = MetadataPropertyHandling.Ignore, MaxDepth = 1024 @@ -340,5 +421,358 @@ private static ICollection PopulateHashTableFromJArray(JArray list, out return result; } + + #endregion ConvertFromJson + + #region ConvertToJson + + /// + /// Convert an object to JSON string. + /// + public static string ConvertToJson(object objectToProcess, in ConvertToJsonContext context) + { + try + { + // Pre-process the object so that it serializes the same, except that properties whose + // values cannot be evaluated are treated as having the value null. + object preprocessedObject = ProcessValue(objectToProcess, currentDepth: 0, in context); + var jsonSettings = new JsonSerializerSettings + { + // This TypeNameHandling setting is required to be secure. + TypeNameHandling = TypeNameHandling.None, + MaxDepth = 1024, + StringEscapeHandling = context.StringEscapeHandling + }; + + if (context.EnumsAsStrings) + { + jsonSettings.Converters.Add(new StringEnumConverter()); + } + + if (!context.CompressOutput) + { + jsonSettings.Formatting = Formatting.Indented; + } + + return JsonConvert.SerializeObject(preprocessedObject, jsonSettings); + } + catch (StoppingException) + { + return null; + } + } + + /// + /// Return an alternate representation of the specified object that serializes the same JSON, except + /// that properties that cannot be evaluated are treated as having the value null. + /// Primitive types are returned verbatim. Aggregate types are processed recursively. + /// + /// The object to be processed. + /// The current depth into the object graph. + /// The context to use for the convert-to-json operation. + /// An object suitable for serializing to JSON. + private static object ProcessValue(object obj, int currentDepth, in ConvertToJsonContext context) + { + if (context.CancellationToken.IsCancellationRequested) + { + throw new StoppingException(); + } + + PSObject pso = obj as PSObject; + + if (pso != null) + { + obj = pso.BaseObject; + } + + object rv = obj; + bool isPurePSObj = false; + bool isCustomObj = false; + + if (obj == null + || DBNull.Value.Equals(obj) + || obj is string + || obj is char + || obj is bool + || obj is DateTime + || obj is DateTimeOffset + || obj is Guid + || obj is Uri + || obj is double + || obj is float + || obj is decimal) + { + rv = obj; + } + else if (obj is Newtonsoft.Json.Linq.JObject jObject) + { + rv = jObject.ToObject>(); + } + else + { + Type t = obj.GetType(); + + if (t.IsPrimitive) + { + rv = obj; + } + else if (t.IsEnum) + { + // Win8:378368 Enums based on System.Int64 or System.UInt64 are not JSON-serializable + // because JavaScript does not support the necessary precision. + Type enumUnderlyingType = Enum.GetUnderlyingType(obj.GetType()); + if (enumUnderlyingType.Equals(typeof(Int64)) || enumUnderlyingType.Equals(typeof(UInt64))) + { + rv = obj.ToString(); + } + else + { + rv = obj; + } + } + else + { + if (currentDepth > context.MaxDepth) + { + if (pso != null && pso.immediateBaseObjectIsEmpty) + { + // The obj is a pure PSObject, we convert the original PSObject to a string, + // instead of its base object in this case + rv = LanguagePrimitives.ConvertTo(pso, typeof(string), + CultureInfo.InvariantCulture); + isPurePSObj = true; + } + else + { + rv = LanguagePrimitives.ConvertTo(obj, typeof(String), + CultureInfo.InvariantCulture); + } + } + else + { + IDictionary dict = obj as IDictionary; + if (dict != null) + { + rv = ProcessDictionary(dict, currentDepth, in context); + } + else + { + IEnumerable enumerable = obj as IEnumerable; + if (enumerable != null) + { + rv = ProcessEnumerable(enumerable, currentDepth, in context); + } + else + { + rv = ProcessCustomObject(obj, currentDepth, in context); + isCustomObj = true; + } + } + } + } + } + + rv = AddPsProperties(pso, rv, currentDepth, isPurePSObj, isCustomObj, in context); + + return rv; + } + + /// + /// Add to a base object any properties that might have been added to an object (via PSObject) through the Add-Member cmdlet. + /// + /// The containing PSObject, or null if the base object was not contained in a PSObject. + /// The base object that might have been decorated with additional properties. + /// The current depth into the object graph. + /// The processed object is a pure PSObject. + /// The processed object is a custom object. + /// The context for the operation. + /// + /// The original base object if no additional properties had been added, + /// otherwise a dictionary containing the value of the original base object in the "value" key + /// as well as the names and values of an additional properties. + /// + private static object AddPsProperties(object psObj, object obj, int depth, bool isPurePSObj, bool isCustomObj, in ConvertToJsonContext context) + { + PSObject pso = psObj as PSObject; + + if (pso == null) + { + return obj; + } + + // when isPurePSObj is true, the obj is guaranteed to be a string converted by LanguagePrimitives + if (isPurePSObj) + { + return obj; + } + + bool wasDictionary = true; + IDictionary dict = obj as IDictionary; + + if (dict == null) + { + wasDictionary = false; + dict = new Dictionary(); + dict.Add("value", obj); + } + + AppendPsProperties(pso, dict, depth, isCustomObj, in context); + + if (wasDictionary == false && dict.Count == 1) + { + return obj; + } + + return dict; + } + + /// + /// Append to a dictionary any properties that might have been added to an object (via PSObject) through the Add-Member cmdlet. + /// If the passed in object is a custom object (not a simple object, not a dictionary, not a list, get processed in ProcessCustomObject method), + /// we also take Adapted properties into account. Otherwise, we only consider the Extended properties. + /// When the object is a pure PSObject, it also gets processed in "ProcessCustomObject" before reaching this method, so we will + /// iterate both extended and adapted properties for it. Since it's a pure PSObject, there will be no adapted properties. + /// + /// The containing PSObject, or null if the base object was not contained in a PSObject. + /// The dictionary to which any additional properties will be appended. + /// The current depth into the object graph. + /// The processed object is a custom object. + /// The context for the operation. + private static void AppendPsProperties(PSObject psObj, IDictionary receiver, int depth, bool isCustomObject, in ConvertToJsonContext context) + { + // serialize only Extended and Adapted properties.. + PSMemberInfoCollection srcPropertiesToSearch = + new PSMemberInfoIntegratingCollection(psObj, + isCustomObject ? PSObject.GetPropertyCollection(PSMemberViewTypes.Extended | PSMemberViewTypes.Adapted) : + PSObject.GetPropertyCollection(PSMemberViewTypes.Extended)); + + foreach (PSPropertyInfo prop in srcPropertiesToSearch) + { + object value = null; + try + { + value = prop.Value; + } + catch (Exception) + { + } + + if (!receiver.Contains(prop.Name)) + { + receiver[prop.Name] = ProcessValue(value, depth + 1, in context); + } + } + } + + /// + /// Return an alternate representation of the specified dictionary that serializes the same JSON, except + /// that any contained properties that cannot be evaluated are treated as having the value null. + /// + private static object ProcessDictionary(IDictionary dict, int depth, in ConvertToJsonContext context) + { + Dictionary result = new Dictionary(dict.Count); + + foreach (DictionaryEntry entry in dict) + { + string name = entry.Key as string; + if (name == null) + { + // use the error string that matches the message from JavaScriptSerializer + string errorMsg = string.Format( + CultureInfo.CurrentCulture, + WebCmdletStrings.NonStringKeyInDictionary, + dict.GetType().FullName); + + var exception = new InvalidOperationException(errorMsg); + if (context.Cmdlet != null) + { + var errorRecord = new ErrorRecord(exception, "NonStringKeyInDictionary", ErrorCategory.InvalidOperation, dict); + context.Cmdlet.ThrowTerminatingError(errorRecord); + } + else + { + throw exception; + } + } + + result.Add(name, ProcessValue(entry.Value, depth + 1, in context)); + } + + return result; + } + + /// + /// Return an alternate representation of the specified collection that serializes the same JSON, except + /// that any contained properties that cannot be evaluated are treated as having the value null. + /// + private static object ProcessEnumerable(IEnumerable enumerable, int depth, in ConvertToJsonContext context) + { + List result = new List(); + + foreach (object o in enumerable) + { + result.Add(ProcessValue(o, depth + 1, in context)); + } + + return result; + } + + /// + /// Return an alternate representation of the specified aggregate object that serializes the same JSON, except + /// that any contained properties that cannot be evaluated are treated as having the value null. + /// + /// The result is a dictionary in which all public fields and public gettable properties of the original object + /// are represented. If any exception occurs while retrieving the value of a field or property, that entity + /// is included in the output dictionary with a value of null. + /// + private static object ProcessCustomObject(object o, int depth, in ConvertToJsonContext context) + { + Dictionary result = new Dictionary(); + Type t = o.GetType(); + + foreach (FieldInfo info in t.GetFields(BindingFlags.Public | BindingFlags.Instance)) + { + if (!info.IsDefined(typeof(T), true)) + { + object value; + try + { + value = info.GetValue(o); + } + catch (Exception) + { + value = null; + } + + result.Add(info.Name, ProcessValue(value, depth + 1, in context)); + } + } + + foreach (PropertyInfo info2 in t.GetProperties(BindingFlags.Public | BindingFlags.Instance)) + { + if (!info2.IsDefined(typeof(T), true)) + { + MethodInfo getMethod = info2.GetGetMethod(); + if ((getMethod != null) && (getMethod.GetParameters().Length <= 0)) + { + object value; + try + { + value = getMethod.Invoke(o, new object[0]); + } + catch (Exception) + { + value = null; + } + + result.Add(info2.Name, ProcessValue(value, depth + 1, in context)); + } + } + } + + return result; + } + + #endregion ConvertToJson } } diff --git a/test/Test.Common.props b/test/Test.Common.props index 11b20a9fed7..1329718edc4 100644 --- a/test/Test.Common.props +++ b/test/Test.Common.props @@ -6,6 +6,7 @@ netcoreapp2.1 2.1.5 + Latest true true diff --git a/test/xUnit/csharp/test_Utils.cs b/test/xUnit/csharp/test_Utils.cs index 9d1aeb249b9..2abe69d37d2 100644 --- a/test/xUnit/csharp/test_Utils.cs +++ b/test/xUnit/csharp/test_Utils.cs @@ -2,9 +2,13 @@ // Licensed under the MIT License. using System; +using System.Collections; +using System.Collections.Specialized; using System.Management.Automation; using System.Management.Automation.Internal; +using System.Threading; using System.Reflection; +using Microsoft.PowerShell.Commands; using Xunit; namespace PSTests.Parallel @@ -72,5 +76,75 @@ public static void TestBoundedStack() Assert.Throws(() => boundedStack.Pop()); } + + [Fact] + public static void TestConvertToJsonBasic() + { + var context = new JsonObject.ConvertToJsonContext(maxDepth: 1, enumsAsStrings: false, compressOutput: true); + string expected = "{\"name\":\"req\",\"type\":\"http\"}"; + OrderedDictionary hash = new OrderedDictionary { + {"name", "req"}, + {"type", "http"} + }; + string json = JsonObject.ConvertToJson(hash, in context); + Assert.Equal(expected, json); + + hash.Add("self", hash); + json = JsonObject.ConvertToJson(hash, context); + expected = "{\"name\":\"req\",\"type\":\"http\",\"self\":{\"name\":\"req\",\"type\":\"http\",\"self\":\"System.Collections.Specialized.OrderedDictionary\"}}"; + Assert.Equal(expected, json); + } + + [Fact] + public static void TestConvertToJsonWithEnum() + { + var context = new JsonObject.ConvertToJsonContext(maxDepth: 1, enumsAsStrings: false, compressOutput: true); + string expected = "{\"type\":1}"; + Hashtable hash = new Hashtable { + {"type", CommandTypes.Alias} + }; + string json = JsonObject.ConvertToJson(hash, in context); + Assert.Equal(expected, json); + + context = new JsonObject.ConvertToJsonContext(maxDepth: 1, enumsAsStrings: true, compressOutput: true); + json = JsonObject.ConvertToJson(hash, in context); + expected = "{\"type\":\"Alias\"}"; + Assert.Equal(expected, json); + } + + [Fact] + public static void TestConvertToJsonWithoutCompress() + { + var context = new JsonObject.ConvertToJsonContext(maxDepth: 1, enumsAsStrings: true, compressOutput: false); + string expected = @"{ + ""type"": ""Alias"" +}"; + Hashtable hash = new Hashtable { + {"type", CommandTypes.Alias} + }; + string json = JsonObject.ConvertToJson(hash, in context); + Assert.Equal(expected, json); + } + + [Fact] + public static void TestConvertToJsonCancellation() + { + var source = new CancellationTokenSource(); + var context = new JsonObject.ConvertToJsonContext( + maxDepth: 1, + enumsAsStrings: true, + compressOutput: false, + source.Token, + Newtonsoft.Json.StringEscapeHandling.Default, + targetCmdlet: null); + + source.Cancel(); + Hashtable hash = new Hashtable { + {"type", CommandTypes.Alias} + }; + + string json = JsonObject.ConvertToJson(hash, in context); + Assert.Null(json); + } } }