From 634c060c2090be6bdd5ec88cb22c7c8cc1da23dd Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Tue, 21 Apr 2026 11:10:24 -0700 Subject: [PATCH 1/2] STJ conversion --- .../Alias/AliasModel.cs | 4 +- .../Alias/AliasRegistry.cs | 54 ++--- .../HostSpecificDataLoader.cs | 11 +- .../HostSpecificTemplateData.cs | 98 +++++++-- .../JExtensions.cs | 198 +++++++++++------- .../ChmodPostActionProcessor.cs | 6 +- .../PostActionProcessorBase.cs | 15 +- .../TemplateSearch/CliHostSearchCacheData.cs | 12 +- .../AliasAssignmentTests.cs | 14 +- .../HostDataLoaderTests.cs | 34 +-- .../TemplateSearchCoordinatorTests.cs | 10 +- 11 files changed, 273 insertions(+), 183 deletions(-) diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasModel.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasModel.cs index 2228284f581c..ec8b35dd3af8 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasModel.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasModel.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Newtonsoft.Json; +using System.Text.Json.Serialization; namespace Microsoft.TemplateEngine.Cli.Alias { @@ -17,7 +17,7 @@ internal AliasModel(IReadOnlyDictionary> commandAl CommandAliases = new Dictionary>(commandAliases.ToDictionary(x => x.Key, x => x.Value), StringComparer.OrdinalIgnoreCase); } - [JsonProperty] + [JsonInclude] internal Dictionary> CommandAliases { get; set; } internal void AddCommandAlias(string aliasName, IReadOnlyList aliasTokens) diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs index faa3c0f34179..5ab6f9e7894e 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs @@ -3,7 +3,8 @@ using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Utils; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Microsoft.TemplateEngine.Cli.Alias { @@ -135,7 +136,7 @@ private void EnsureLoaded() _aliases = new AliasModel(); return; } - JObject parsed = _environmentSettings.Host.FileSystem.ReadObject(_aliasesFilePath); + JsonObject parsed = _environmentSettings.Host.FileSystem.ReadObject(_aliasesFilePath); IReadOnlyDictionary> commandAliases = ToStringListDictionary(parsed, StringComparer.OrdinalIgnoreCase, "CommandAliases"); _aliases = new AliasModel(commandAliases); @@ -145,7 +146,19 @@ private void Save() { if (_aliases is AliasModel { CommandAliases: { Count: > 0 } }) { - _environmentSettings.Host.FileSystem.WriteObject(_aliasesFilePath, _aliases); + JsonObject root = new(); + JsonObject commandAliases = new(); + foreach (var kvp in _aliases.CommandAliases) + { + JsonArray arr = new(); + foreach (string item in kvp.Value) + { + arr.Add((JsonNode)JsonValue.Create(item)!); + } + commandAliases[kvp.Key] = arr; + } + root["CommandAliases"] = commandAliases; + _environmentSettings.Host.FileSystem.WriteObject(_aliasesFilePath, root); } else { @@ -154,50 +167,41 @@ private void Save() } // reads a dictionary whose values can either be string literals, or arrays of strings. - private IReadOnlyDictionary> ToStringListDictionary(JToken token, StringComparer? comparer = null, string? propertyName = null) + private IReadOnlyDictionary> ToStringListDictionary(JsonObject token, StringComparer? comparer = null, string? propertyName = null) { Dictionary> result = new(comparer ?? StringComparer.Ordinal); - JObject? jObj = token as JObject; - if (jObj == null || propertyName == null || !jObj.TryGetValue(propertyName, StringComparison.OrdinalIgnoreCase, out JToken? element)) + + if (propertyName == null || !token.TryGetPropertyValue(propertyName, out JsonNode? element)) { return result; } - jObj = element as JObject; - if (jObj == null) + if (element is not JsonObject jObj) { return result; } - foreach (JProperty property in jObj.Properties()) + foreach (KeyValuePair property in jObj) { if (property.Value == null) { continue; } - else if (property.Value.Type == JTokenType.String) + else if (property.Value.GetValueKind() == JsonValueKind.String) { - result[property.Name] = new List() { property.Value.ToString() }; + result[property.Key] = new List() { property.Value.GetValue() }; } - else if (property.Value.Type == JTokenType.Array) + else if (property.Value is JsonArray arr) { - JArray? arr = property.Value as JArray; - if (arr == null) - { - result[property.Name] = Array.Empty(); - } - else + List values = new(); + foreach (JsonNode? item in arr) { - List values = new(); - foreach (JToken item in arr) + if (item != null && item.GetValueKind() == JsonValueKind.String) { - if (item != null && item.Type == JTokenType.String) - { - values.Add(item.ToString()); - } + values.Add(item.GetValue()); } - result[property.Name] = values; } + result[property.Key] = values; } } diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs index 3f421e11b12a..d4696b0e6b12 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs @@ -2,13 +2,12 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Abstractions.Mount; using Microsoft.TemplateEngine.Edge.Settings; using Microsoft.TemplateEngine.Utils; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.TemplateEngine.Cli { @@ -39,7 +38,7 @@ private HostSpecificTemplateData ReadHostSpecificTemplateDataUncached(ITemplateI { if (!string.IsNullOrWhiteSpace(hostData)) { - JObject jObject = JObject.Parse(hostData); + JsonObject? jObject = JsonNode.Parse(hostData)?.AsObject(); return new HostSpecificTemplateData(jObject); } } @@ -60,12 +59,10 @@ private HostSpecificTemplateData ReadHostSpecificTemplateDataUncached(ITemplateI file = mountPoint.FileInfo(templateInfo.HostConfigPlace); if (file != null && file.Exists) { - JObject jsonData; + JsonObject? jsonData; using (Stream stream = file.OpenRead()) - using (TextReader textReader = new StreamReader(stream, true)) - using (JsonReader jsonReader = new JsonTextReader(textReader)) { - jsonData = JObject.Load(jsonReader); + jsonData = JsonNode.Parse(stream)?.AsObject(); } return new HostSpecificTemplateData(jsonData); diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs index c5f3f3204546..db71fb3aaf6e 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs @@ -1,8 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Microsoft.TemplateEngine.Cli { @@ -14,7 +15,7 @@ public class HostSpecificTemplateData private const string ShortNameKey = "shortName"; private const string AlwaysShowKey = "alwaysShow"; - internal HostSpecificTemplateData(JObject? jObject) + internal HostSpecificTemplateData(JsonObject? jObject) { var symbolsInfo = new Dictionary>(); @@ -24,34 +25,61 @@ internal HostSpecificTemplateData(JObject? jObject) return; } - if (jObject.GetValue(nameof(UsageExamples), StringComparison.OrdinalIgnoreCase) is JArray usagesArray) + JsonNode? usagesNode = GetPropertyCaseInsensitive(jObject, nameof(UsageExamples)); + if (usagesNode is JsonArray usagesArray) { - UsageExamples = new List(usagesArray.Values().Where(v => v != null).OfType()); + UsageExamples = new List(usagesArray.Select(v => v?.GetValue()).Where(v => v != null).OfType()); } - if (jObject.GetValue(nameof(SymbolInfo), StringComparison.OrdinalIgnoreCase) is JObject symbols) + JsonNode? symbolsNode = GetPropertyCaseInsensitive(jObject, nameof(SymbolInfo)); + if (symbolsNode is JsonObject symbols) { - foreach (var symbolInfo in symbols.Properties()) + foreach (var symbolInfo in symbols) { - if (!(symbolInfo.Value is JObject symbol)) + if (symbolInfo.Value is not JsonObject symbol) { continue; } var symbolProperties = new Dictionary(); - foreach (var symbolProperty in symbol.Properties()) + foreach (var symbolProperty in symbol) { - symbolProperties[symbolProperty.Name] = symbolProperty.Value.Value() ?? ""; + if (symbolProperty.Value is null) + { + symbolProperties[symbolProperty.Key] = ""; + } + else + { + var kind = symbolProperty.Value.GetValueKind(); + symbolProperties[symbolProperty.Key] = kind switch + { + JsonValueKind.String => symbolProperty.Value.GetValue(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + _ => symbolProperty.Value.ToJsonString() + }; + } } - symbolsInfo[symbolInfo.Name] = symbolProperties; + symbolsInfo[symbolInfo.Key] = symbolProperties; } } SymbolInfo = symbolsInfo; - IsHidden = jObject.Value(nameof(IsHidden)); - + JsonNode? isHiddenNode = GetPropertyCaseInsensitive(jObject, nameof(IsHidden)); + if (isHiddenNode != null) + { + var kind = isHiddenNode.GetValueKind(); + if (kind == JsonValueKind.True) + { + IsHidden = true; + } + else if (kind == JsonValueKind.String && bool.TryParse(isHiddenNode.GetValue(), out bool hidden)) + { + IsHidden = hidden; + } + } } internal HostSpecificTemplateData( @@ -144,7 +172,7 @@ public Dictionary ShortNameOverrides } } - internal static HostSpecificTemplateData Default { get; } = new HostSpecificTemplateData((JObject?)null); + internal static HostSpecificTemplateData Default { get; } = new HostSpecificTemplateData((JsonObject?)null); internal string DisplayNameForParameter(string parameterName) { @@ -157,26 +185,50 @@ internal string DisplayNameForParameter(string parameterName) return parameterName; } - private class HostSpecificTemplateDataJsonConverter : JsonConverter + private static JsonNode? GetPropertyCaseInsensitive(JsonObject obj, string key) { - public override HostSpecificTemplateData ReadJson(JsonReader reader, Type objectType, HostSpecificTemplateData? existingValue, bool hasExistingValue, JsonSerializer serializer) => throw new NotImplementedException(); + if (obj.TryGetPropertyValue(key, out JsonNode? result)) + { + return result; + } - public override void WriteJson(JsonWriter writer, HostSpecificTemplateData? value, JsonSerializer serializer) + foreach (var kvp in obj) { - if (value == null) + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) { - return; + return kvp.Value; } + } + + return null; + } + + private class HostSpecificTemplateDataJsonConverter : JsonConverter + { + public override HostSpecificTemplateData Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => throw new NotImplementedException(); + + public override void Write(Utf8JsonWriter writer, HostSpecificTemplateData value, JsonSerializerOptions options) + { writer.WriteStartObject(); if (value.IsHidden) { - writer.WritePropertyName(nameof(IsHidden)); - writer.WriteValue(value.IsHidden); + writer.WriteBoolean(nameof(IsHidden), value.IsHidden); } if (value.SymbolInfo.Any()) { writer.WritePropertyName(nameof(SymbolInfo)); - serializer.Serialize(writer, value.SymbolInfo); + writer.WriteStartObject(); + foreach (var symbol in value.SymbolInfo) + { + writer.WritePropertyName(symbol.Key); + writer.WriteStartObject(); + foreach (var prop in symbol.Value) + { + writer.WriteString(prop.Key, prop.Value); + } + writer.WriteEndObject(); + } + writer.WriteEndObject(); } if (value.UsageExamples != null && value.UsageExamples.Any(e => !string.IsNullOrWhiteSpace(e))) @@ -187,7 +239,7 @@ public override void WriteJson(JsonWriter writer, HostSpecificTemplateData? valu { if (!string.IsNullOrWhiteSpace(example)) { - writer.WriteValue(example); + writer.WriteStringValue(example); } } writer.WriteEndArray(); diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/JExtensions.cs b/src/Cli/Microsoft.TemplateEngine.Cli/JExtensions.cs index 5cd2d77b6f96..e9602d353c1e 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/JExtensions.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/JExtensions.cs @@ -1,45 +1,48 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#if !NET6_0_OR_GREATER -using System; -using System.Collections.Generic; -using System.IO; -#endif +using System.Text.Json; +using System.Text.Json.Nodes; using Microsoft.TemplateEngine.Abstractions.PhysicalFileSystem; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.TemplateEngine { internal static class JExtensions { - internal static string? ToString(this JToken? token, string? key) + private static readonly JsonDocumentOptions DocOptions = new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true }; + + internal static string? ToString(this JsonNode? token, string? key) { if (key == null) { - if (token == null || token.Type != JTokenType.String) + if (token == null) { return null; } - return token.ToString(); + if (token is JsonValue val && val.GetValueKind() == JsonValueKind.String) + { + return val.GetValue(); + } + + return null; } - if (token is not JObject obj) + if (token is not JsonObject obj) { return null; } - if (!obj.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out JToken? element) || element.Type != JTokenType.String) + JsonNode? element = GetPropertyCaseInsensitive(obj, key); + if (element == null || element.GetValueKind() != JsonValueKind.String) { return null; } - return element.ToString(); + return element.GetValue(); } - internal static bool TryGetValue(this JToken? token, string? key, out JToken? result) + internal static bool TryGetValue(this JsonNode? token, string? key, out JsonNode? result) { result = null; @@ -52,25 +55,42 @@ internal static bool TryGetValue(this JToken? token, string? key, out JToken? re { result = token; } - else if (!((JObject)token).TryGetValue(key, StringComparison.OrdinalIgnoreCase, out result)) + else { - return false; + result = GetPropertyCaseInsensitive(token.AsObject(), key); + if (result == null) + { + return false; + } } return true; } - internal static bool TryParseBool(this JToken token, out bool result) + internal static bool TryParseBool(this JsonNode token, out bool result) { result = false; - return (token.Type == JTokenType.Boolean || token.Type == JTokenType.String) - && - bool.TryParse(token.ToString(), out result); + var kind = token.GetValueKind(); + if (kind == JsonValueKind.True) + { + result = true; + return true; + } + if (kind == JsonValueKind.False) + { + result = false; + return true; + } + if (kind == JsonValueKind.String) + { + return bool.TryParse(token.GetValue(), out result); + } + return false; } - internal static bool ToBool(this JToken? token, string? key = null, bool defaultValue = false) + internal static bool ToBool(this JsonNode? token, string? key = null, bool defaultValue = false) { - if (!token.TryGetValue(key, out JToken? checkToken)) + if (!token.TryGetValue(key, out JsonNode? checkToken)) { return defaultValue; } @@ -83,12 +103,11 @@ internal static bool ToBool(this JToken? token, string? key = null, bool default return result; } - internal static int ToInt32(this JToken? token, string? key = null, int defaultValue = 0) + internal static int ToInt32(this JsonNode? token, string? key = null, int defaultValue = 0) { - int value; if (key == null) { - if (token == null || token.Type != JTokenType.Integer || !int.TryParse(token.ToString(), out value)) + if (token == null || !token.TryParseInt(out int value)) { return defaultValue; } @@ -96,28 +115,21 @@ internal static int ToInt32(this JToken? token, string? key = null, int defaultV return value; } - if (token is not JObject obj) + if (token is not JsonObject obj) { return defaultValue; } - if (!obj.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out JToken? element)) + JsonNode? element = GetPropertyCaseInsensitive(obj, key); + if (element == null || !element.TryParseInt(out int result)) { return defaultValue; } - else if (element.Type == JTokenType.Integer) - { - return element.ToInt32(); - } - else if (int.TryParse(element.ToString(), out value)) - { - return value; - } - return defaultValue; + return result; } - internal static T ToEnum(this JToken token, string? key = null, T defaultValue = default) + internal static T ToEnum(this JsonNode token, string? key = null, T defaultValue = default) where T : struct { string? val = token.ToString(key); @@ -129,7 +141,7 @@ internal static T ToEnum(this JToken token, string? key = null, T defaultValu return result; } - internal static Guid ToGuid(this JToken token, string? key = null, Guid defaultValue = default) + internal static Guid ToGuid(this JsonNode token, string? key = null, Guid defaultValue = default) { string? val = token.ToString(key); if (val == null || !Guid.TryParse(val, out Guid result)) @@ -140,97 +152,84 @@ internal static Guid ToGuid(this JToken token, string? key = null, Guid defaultV return result; } - internal static IEnumerable PropertiesOf(this JToken? token, string? key = null) + internal static IEnumerable> PropertiesOf(this JsonNode? token, string? key = null) { - JObject? currentJObj = token as JObject; - if (currentJObj == null) + if (token is not JsonObject currentJObj) { - return Array.Empty(); + return Array.Empty>(); } if (key != null) { - if (!currentJObj.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out JToken? element)) + JsonNode? element = GetPropertyCaseInsensitive(currentJObj, key); + if (element is not JsonObject nested) { - return Array.Empty(); + return Array.Empty>(); } - currentJObj = element as JObject; - } - if (currentJObj == null) - { - return Array.Empty(); + return nested.ToList(); } - return currentJObj.Properties(); + return currentJObj.ToList(); } - internal static T? Get(this JToken? token, string? key) - where T : JToken + internal static T? Get(this JsonNode? token, string? key) + where T : JsonNode { - if (token is not JObject obj || key == null) - { - return default; - } - - if (!obj.TryGetValue(key, StringComparison.OrdinalIgnoreCase, out JToken? res)) + if (token is not JsonObject obj || key == null) { return default; } + JsonNode? res = GetPropertyCaseInsensitive(obj, key); return res as T; } - internal static IReadOnlyList ArrayAsStrings(this JToken? token, string? propertyName = null) + internal static IReadOnlyList ArrayAsStrings(this JsonNode? token, string? propertyName = null) { if (propertyName != null) { - token = token.Get(propertyName); + token = token.Get(propertyName); } - if (token is not JArray arr) + if (token is not JsonArray arr) { return Array.Empty(); } List values = new(); - foreach (JToken item in arr) + foreach (JsonNode? item in arr) { - if (item != null && item.Type == JTokenType.String) + if (item != null && item.GetValueKind() == JsonValueKind.String) { - values.Add(item.ToString()); + values.Add(item.GetValue()); } } return values; } - internal static JObject ReadObject(this IPhysicalFileSystem fileSystem, string path) + internal static JsonObject ReadObject(this IPhysicalFileSystem fileSystem, string path) { - using (Stream fileStream = fileSystem.OpenRead(path)) - using (var textReader = new StreamReader(fileStream, Encoding.UTF8, true)) - using (var jsonReader = new JsonTextReader(textReader)) - { - return JObject.Load(jsonReader); - } + using Stream fileStream = fileSystem.OpenRead(path); + using var textReader = new StreamReader(fileStream, Encoding.UTF8, true); + string json = textReader.ReadToEnd(); + return (JsonObject?)JsonNode.Parse(json, null, DocOptions) + ?? throw new InvalidOperationException($"Failed to parse JSON from '{path}'."); } - internal static void WriteObject(this IPhysicalFileSystem fileSystem, string path, object obj) + internal static void WriteObject(this IPhysicalFileSystem fileSystem, string path, JsonNode obj) { - using (Stream fileStream = fileSystem.CreateFile(path)) - using (var textWriter = new StreamWriter(fileStream, Encoding.UTF8)) - using (var jsonWriter = new JsonTextWriter(textWriter)) - { - var serializer = new JsonSerializer(); - serializer.Serialize(jsonWriter, obj); - } + using Stream fileStream = fileSystem.CreateFile(path); + using var writer = new Utf8JsonWriter(fileStream); + obj.WriteTo(writer); } - internal static bool TryParse(this string arg, out JToken? token) + internal static bool TryParse(this string arg, out JsonNode? token) { try { - token = JToken.Parse(arg); + token = JsonNode.Parse(arg, null, DocOptions); return true; } catch @@ -240,5 +239,42 @@ internal static bool TryParse(this string arg, out JToken? token) } } + private static bool TryParseInt(this JsonNode token, out int result) + { + result = default; + var kind = token.GetValueKind(); + if (kind == JsonValueKind.Number) + { + if (token is JsonValue jv && jv.TryGetValue(out int intVal)) + { + result = intVal; + return true; + } + return int.TryParse(token.ToJsonString(), out result); + } + if (kind == JsonValueKind.String) + { + return int.TryParse(token.GetValue(), out result); + } + return false; + } + + private static JsonNode? GetPropertyCaseInsensitive(JsonObject obj, string key) + { + if (obj.TryGetPropertyValue(key, out JsonNode? result)) + { + return result; + } + + foreach (var kvp in obj) + { + if (string.Equals(kvp.Key, key, StringComparison.OrdinalIgnoreCase)) + { + return kvp.Value; + } + } + + return null; + } } } diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/ChmodPostActionProcessor.cs b/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/ChmodPostActionProcessor.cs index b3b919c87680..3fb827553386 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/ChmodPostActionProcessor.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/ChmodPostActionProcessor.cs @@ -4,7 +4,7 @@ using System.Diagnostics; using Microsoft.DotNet.Cli.Utils; using Microsoft.TemplateEngine.Abstractions; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Microsoft.TemplateEngine.Cli.PostActionProcessors { @@ -22,12 +22,12 @@ protected override bool ProcessInternal(IEngineEnvironmentSettings environment, string[] values; try { - JArray valueArray = JArray.Parse(entry.Value); + JsonArray valueArray = JsonNode.Parse(entry.Value)!.AsArray(); values = new string[valueArray.Count]; for (int i = 0; i < valueArray.Count; ++i) { - values[i] = valueArray[i].ToString(); + values[i] = valueArray[i]?.GetValue() ?? ""; } } catch diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/PostActionProcessorBase.cs b/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/PostActionProcessorBase.cs index 266cfb653e34..35e41b8652a3 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/PostActionProcessorBase.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/PostActionProcessors/PostActionProcessorBase.cs @@ -3,7 +3,8 @@ using Microsoft.TemplateEngine.Abstractions; using Microsoft.TemplateEngine.Utils; -using Newtonsoft.Json.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; namespace Microsoft.TemplateEngine.Cli.PostActionProcessors { @@ -122,26 +123,26 @@ protected abstract bool ProcessInternal( private static bool TryParseAsJson(string targetFiles, out IReadOnlyList paths) { paths = new List(); - targetFiles.TryParse(out JToken? config); + targetFiles.TryParse(out JsonNode? config); if (config is null) { return false; } - if (config.Type == JTokenType.String) + if (config.GetValueKind() == JsonValueKind.String) { - paths = config.ToString().Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); + paths = config.GetValue().Split(new[] { ';' }, StringSplitOptions.RemoveEmptyEntries); return true; } - if (config is not JArray arr) + if (config is not JsonArray arr) { return false; } var parts = arr - .Where(token => token.Type == JTokenType.String) - .Select(token => token.ToString()).ToList(); + .Where(token => token != null && token.GetValueKind() == JsonValueKind.String) + .Select(token => token!.GetValue()).ToList(); if (parts.Count == 0) { diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs b/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs index cc1a2d8a1f48..f54e70ab4b53 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs @@ -2,7 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using Microsoft.DotNet.Cli.Utils; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Microsoft.TemplateEngine.Cli.TemplateSearch { @@ -13,29 +13,29 @@ public static class CliHostSearchCacheData public static Func Reader => (obj) => { - JObject? cacheObject = obj as JObject; + JsonObject? cacheObject = obj as JsonObject; if (cacheObject == null) { return HostSpecificTemplateData.Default; } try { - if (_hostDataPropertyNames.Contains(cacheObject.Properties().First().Name, StringComparer.OrdinalIgnoreCase)) + if (_hostDataPropertyNames.Any(name => cacheObject.ContainsKey(name) || cacheObject.Any(p => string.Equals(p.Key, name, StringComparison.OrdinalIgnoreCase)))) { return new HostSpecificTemplateData(cacheObject); } //fallback to old behavior Dictionary cliData = new(); - foreach (JProperty data in cacheObject.Properties()) + foreach (KeyValuePair data in cacheObject) { try { - cliData[data.Name] = new HostSpecificTemplateData(data.Value as JObject); + cliData[data.Key] = new HostSpecificTemplateData(data.Value as JsonObject); } catch (Exception ex) { - Reporter.Verbose.WriteLine($"Error deserializing the cli host specific template data for template {data.Name}, details:{ex}"); + Reporter.Verbose.WriteLine($"Error deserializing the cli host specific template data for template {data.Key}, details:{ex}"); } } return cliData; diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs b/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs index 5ddc70b76a8b..00b23735bb85 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/AliasAssignmentTests.cs @@ -9,7 +9,7 @@ using Microsoft.TemplateEngine.Edge.Settings; using Microsoft.TemplateEngine.Mocks; using Microsoft.TemplateEngine.Utils; -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Microsoft.TemplateEngine.Cli.UnitTests { @@ -275,8 +275,8 @@ public void CanAssignAliasForParameterWithReservedAlias(string parameterName, st [MemberData(nameof(GetTemplateData))] public void CanOverrideAliasesForParameterWithHostData(string hostJsonData, string expectedJsonResult) { - var hostData = new HostSpecificTemplateData(string.IsNullOrEmpty(hostJsonData) ? null : JObject.Parse(hostJsonData)); - var expectedResults = JObject.Parse(expectedJsonResult); + var hostData = new HostSpecificTemplateData(string.IsNullOrEmpty(hostJsonData) ? null : JsonNode.Parse(hostJsonData)?.AsObject()); + var expectedResults = JsonNode.Parse(expectedJsonResult)!.AsObject(); var template = new MockTemplateInfo("foo", identity: "foo.1", groupIdentity: "foo.group"); foreach (var expectedResult in expectedResults) { @@ -297,10 +297,10 @@ public void CanOverrideAliasesForParameterWithHostData(string hostJsonData, stri Assert.Single(templateCommands); foreach (var expectedResult in expectedResults) { - var expectedValues = expectedResult.Value!.Select(s => ((JValue)s).Value).ToArray(); - var expectedLongAlias = expectedValues[0]; - var expectedShortAlias = expectedValues[1]; - var expectedIsHidden = expectedValues[2]; + var expectedArr = expectedResult.Value!.AsArray(); + var expectedLongAlias = expectedArr[0]?.GetValue(); + var expectedShortAlias = expectedArr[1]?.GetValue(); + var expectedIsHidden = expectedArr[2]?.GetValue() ?? false; var templateOptions = templateCommands.Single().TemplateOptions; Assert.NotNull(templateOptions); Assert.Contains(expectedResult.Key, templateOptions.Keys); diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/HostDataLoaderTests.cs b/test/Microsoft.TemplateEngine.Cli.UnitTests/HostDataLoaderTests.cs index 948fd5e0ca50..4403ed4e04a2 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/HostDataLoaderTests.cs +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/HostDataLoaderTests.cs @@ -7,7 +7,7 @@ using Microsoft.TemplateEngine.Edge.Settings; using Microsoft.TemplateEngine.TestHelper; using Microsoft.TemplateEngine.Utils; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace Microsoft.TemplateEngine.Cli.UnitTests { @@ -145,14 +145,14 @@ public void CanSerializeData() } }; var data = new HostSpecificTemplateData(symbolInfo, usageExamples, isHidden: true); - var serialized = JObject.FromObject(data); + var serialized = JsonSerializer.SerializeToNode(data)!.AsObject(); Assert.NotNull(serialized); - Assert.Equal(3, serialized.Children().Count()); + Assert.Equal(3, serialized.Count); - Assert.Single(serialized.Properties(), p => p.Name == "UsageExamples"); - Assert.Single(serialized.Properties(), p => p.Name == "SymbolInfo"); - Assert.Single(serialized.Properties(), p => p.Name == "IsHidden"); + Assert.Contains("UsageExamples", serialized.Select(p => p.Key)); + Assert.Contains("SymbolInfo", serialized.Select(p => p.Key)); + Assert.Contains("IsHidden", serialized.Select(p => p.Key)); } [Fact] @@ -188,23 +188,23 @@ public void CanSerializeData_SkipsEmpty() } }; var data = new HostSpecificTemplateData(symbolInfo, usageExamples, isHidden: false); - var serialized = JObject.FromObject(data); + var serialized = JsonSerializer.SerializeToNode(data)!.AsObject(); Assert.NotNull(serialized); - Assert.Single(serialized.Children()); + Assert.Single(serialized); - Assert.Single(serialized.Properties(), p => p.Name == "SymbolInfo"); + Assert.Contains("SymbolInfo", serialized.Select(p => p.Key)); - var symbolInfoArray = serialized.Properties().Single().Value as JObject; - Assert.NotNull(symbolInfoArray); + var symbolInfoObj = serialized["SymbolInfo"]!.AsObject(); + Assert.NotNull(symbolInfoObj); //empty values should stay when deserializing symbol info - Assert.Equal(3, ((JObject)symbolInfoArray!["param1"]!).Properties().Count()); - Assert.Equal("", symbolInfoArray!["param2"]!["longName"]); - Assert.Equal(3, ((JObject)symbolInfoArray!["param2"]!).Properties().Count()); - Assert.Single(((JObject)symbolInfoArray!["param3"]!).Properties()); + Assert.Equal(3, symbolInfoObj["param1"]!.AsObject().Count); + Assert.Equal("", symbolInfoObj["param2"]!["longName"]!.GetValue()); + Assert.Equal(3, symbolInfoObj["param2"]!.AsObject().Count); + Assert.Single(symbolInfoObj["param3"]!.AsObject()); - Assert.DoesNotContain(serialized.Properties(), p => p.Name == "IsHidden"); - Assert.DoesNotContain(serialized.Properties(), p => p.Name == "UsageExamples"); + Assert.DoesNotContain("IsHidden", serialized.Select(p => p.Key)); + Assert.DoesNotContain("UsageExamples", serialized.Select(p => p.Key)); } } } diff --git a/test/Microsoft.TemplateEngine.Cli.UnitTests/TemplateSearchCoordinatorTests.cs b/test/Microsoft.TemplateEngine.Cli.UnitTests/TemplateSearchCoordinatorTests.cs index f33269a7a7d6..e5fc3b350f05 100644 --- a/test/Microsoft.TemplateEngine.Cli.UnitTests/TemplateSearchCoordinatorTests.cs +++ b/test/Microsoft.TemplateEngine.Cli.UnitTests/TemplateSearchCoordinatorTests.cs @@ -15,7 +15,7 @@ using Microsoft.TemplateSearch.Common; using Microsoft.TemplateSearch.Common.Abstractions; using Microsoft.TemplateSearch.Common.Providers; -using Newtonsoft.Json.Linq; +using System.Text.Json; namespace Microsoft.TemplateEngine.Cli.UnitTests { @@ -600,9 +600,9 @@ private static string SetupTemplateCache(string fileLocation, bool includehostDa var cache = new TemplateSearchCache(new[] { packOne, packTwo, packThree }); - JObject toSerialize = JObject.FromObject(cache); + string json = JsonSerializer.Serialize(cache); string targetPath = Path.Combine(fileLocation, "searchCacheV2.json"); - File.WriteAllText(targetPath, toSerialize.ToString()); + File.WriteAllText(targetPath, json); return targetPath; } @@ -611,9 +611,9 @@ private static string SetupInvalidTemplateCache(string fileLocation) var packOne = new TemplatePackageSearchData(new MockTemplatePackageInfo("PackOne", "1.0.0"), new[] { new TemplateSearchData(new MockTemplateInfo("foo", "foo", "foo").WithParameters("Config type", "Main type", "unknown")) }); var cache = new TemplateSearchCache(new[] { packOne }); - JObject toSerialize = JObject.FromObject(cache); + string jsonToSerialize = JsonSerializer.Serialize(cache); string targetPath = Path.Combine(fileLocation, "searchCacheV2.json"); - File.WriteAllText(targetPath, toSerialize.ToString()); + File.WriteAllText(targetPath, jsonToSerialize); return targetPath; } } From 3b21a825e3fd5cefc4a7a97605144e777b735989 Mon Sep 17 00:00:00 2001 From: Nikola Milosavljevic Date: Tue, 21 Apr 2026 12:57:17 -0700 Subject: [PATCH 2/2] Address comments --- .../Alias/AliasRegistry.cs | 13 ++++++++++++- .../HostSpecificDataLoader.cs | 11 +++++++++-- .../HostSpecificTemplateData.cs | 4 +++- .../TemplateSearch/CliHostSearchCacheData.cs | 3 ++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs b/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs index 5ab6f9e7894e..05b2f9d7c8e0 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/Alias/AliasRegistry.cs @@ -171,11 +171,22 @@ private IReadOnlyDictionary> ToStringListDictionar { Dictionary> result = new(comparer ?? StringComparer.Ordinal); - if (propertyName == null || !token.TryGetPropertyValue(propertyName, out JsonNode? element)) + if (propertyName == null) { return result; } + // Case-insensitive property lookup for compatibility with Newtonsoft.Json behavior + JsonNode? element = null; + foreach (var prop in token) + { + if (string.Equals(prop.Key, propertyName, StringComparison.OrdinalIgnoreCase)) + { + element = prop.Value; + break; + } + } + if (element is not JsonObject jObj) { return result; diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs index d4696b0e6b12..5ba02797f1d3 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificDataLoader.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Text.Json; using System.Text.Json.Nodes; using Microsoft.Extensions.Logging; using Microsoft.TemplateEngine.Abstractions; @@ -15,6 +16,12 @@ public class HostSpecificDataLoader : IHostSpecificDataLoader { private readonly IEngineEnvironmentSettings _engineEnvironment; + private static readonly JsonDocumentOptions s_jsonDocumentOptions = new() + { + CommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + private readonly ConcurrentDictionary _cache = new(); @@ -38,7 +45,7 @@ private HostSpecificTemplateData ReadHostSpecificTemplateDataUncached(ITemplateI { if (!string.IsNullOrWhiteSpace(hostData)) { - JsonObject? jObject = JsonNode.Parse(hostData)?.AsObject(); + JsonObject? jObject = JsonNode.Parse(hostData, nodeOptions: null, s_jsonDocumentOptions)?.AsObject(); return new HostSpecificTemplateData(jObject); } } @@ -62,7 +69,7 @@ private HostSpecificTemplateData ReadHostSpecificTemplateDataUncached(ITemplateI JsonObject? jsonData; using (Stream stream = file.OpenRead()) { - jsonData = JsonNode.Parse(stream)?.AsObject(); + jsonData = JsonNode.Parse(stream, nodeOptions: null, s_jsonDocumentOptions)?.AsObject(); } return new HostSpecificTemplateData(jsonData); diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs index db71fb3aaf6e..3ed21b27aac6 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/HostSpecificTemplateData.cs @@ -28,7 +28,9 @@ internal HostSpecificTemplateData(JsonObject? jObject) JsonNode? usagesNode = GetPropertyCaseInsensitive(jObject, nameof(UsageExamples)); if (usagesNode is JsonArray usagesArray) { - UsageExamples = new List(usagesArray.Select(v => v?.GetValue()).Where(v => v != null).OfType()); + UsageExamples = new List(usagesArray + .Where(v => v != null && v.GetValueKind() == JsonValueKind.String) + .Select(v => v!.GetValue())); } JsonNode? symbolsNode = GetPropertyCaseInsensitive(jObject, nameof(SymbolInfo)); diff --git a/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs b/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs index f54e70ab4b53..3df2d3d5ee9c 100644 --- a/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs +++ b/src/Cli/Microsoft.TemplateEngine.Cli/TemplateSearch/CliHostSearchCacheData.cs @@ -20,7 +20,8 @@ public static class CliHostSearchCacheData } try { - if (_hostDataPropertyNames.Any(name => cacheObject.ContainsKey(name) || cacheObject.Any(p => string.Equals(p.Key, name, StringComparison.OrdinalIgnoreCase)))) + var keys = new HashSet(cacheObject.Select(p => p.Key), StringComparer.OrdinalIgnoreCase); + if (_hostDataPropertyNames.Any(keys.Contains)) { return new HostSpecificTemplateData(cacheObject); }