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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions src/Shared/JExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ namespace Microsoft.TemplateEngine
internal static class JExtensions
{
private static readonly JsonDocumentOptions DocOptions = new() { CommentHandling = JsonCommentHandling.Skip, AllowTrailingCommas = true };
private static readonly JsonNodeOptions NodeOptions = new() { PropertyNameCaseInsensitive = true };
private static readonly JsonSerializerOptions SerializerOptions = new()
{
PropertyNameCaseInsensitive = true,
Expand Down Expand Up @@ -387,7 +386,7 @@ internal static JsonObject ReadJObjectFromIFile(this IFile file)
using Stream s = file.OpenRead();
using TextReader tr = new StreamReader(s, System.Text.Encoding.UTF8, true);
string json = tr.ReadToEnd();
return (JsonObject?)JsonNode.Parse(json, NodeOptions, DocOptions)
return (JsonObject?)JsonNode.Parse(json, null, DocOptions)
?? throw new InvalidOperationException("Failed to parse JSON from file.");
}

Expand All @@ -396,7 +395,7 @@ internal static JsonObject ReadObject(this IPhysicalFileSystem fileSystem, strin
using Stream fileStream = fileSystem.OpenRead(path);
using var textReader = new StreamReader(fileStream, System.Text.Encoding.UTF8, true);
string json = textReader.ReadToEnd();
return (JsonObject?)JsonNode.Parse(json, NodeOptions, DocOptions)
return (JsonObject?)JsonNode.Parse(json, null, DocOptions)
?? throw new InvalidOperationException($"Failed to parse JSON from '{path}'.");
}

Expand Down Expand Up @@ -480,7 +479,7 @@ internal static bool TryGetValueCaseInsensitive(this JsonObject obj, string key,
/// </summary>
internal static JsonObject ParseJsonObject(string json)
{
return (JsonObject?)JsonNode.Parse(json, NodeOptions, DocOptions)
return (JsonObject?)JsonNode.Parse(json, null, DocOptions)
?? throw new InvalidOperationException("Failed to parse JSON string as JsonObject.");
}

Expand All @@ -489,7 +488,7 @@ internal static JsonObject ParseJsonObject(string json)
/// </summary>
internal static JsonNode? ParseJsonNode(string json)
{
return JsonNode.Parse(json, NodeOptions, DocOptions);
return JsonNode.Parse(json, null, DocOptions);
}

/// <summary>
Expand All @@ -503,7 +502,7 @@ internal static JsonObject ParseJsonObject(string json)
internal static JsonObject FromObject(object obj)
{
string json = JsonSerializer.Serialize(obj, SerializerOptions);
return (JsonObject?)JsonNode.Parse(json, NodeOptions, DocOptions)
return (JsonObject?)JsonNode.Parse(json, null, DocOptions)
?? throw new InvalidOperationException("Failed to round-trip object to JsonObject.");
}

Expand All @@ -512,7 +511,7 @@ internal static JsonObject FromObject(object obj)
/// </summary>
internal static JsonObject DeepCloneObject(this JsonObject source)
{
return (JsonObject?)JsonNode.Parse(source.ToJsonString(), NodeOptions, DocOptions)
return (JsonObject?)JsonNode.Parse(source.ToJsonString(), null, DocOptions)
?? throw new InvalidOperationException("Failed to deep clone JsonObject.");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,44 @@ public void CanReadTemplateFromStream()
Assert.Equal(2, templateConfigModel.PrimaryOutputs.Count);
Assert.Equal(new[] { "bar.cs", "bar/bar.cs" }, templateConfigModel.PrimaryOutputs.Select(po => po.Path).OrderBy(po => po));
}

[Fact]
public void CanReadTemplateWithDuplicateCaseInsensitiveSymbolKeys()
{
// Regression test: template.json with symbols that differ only by case
// (e.g. "Empty" and "empty") should load without throwing.
// See https://github.com/dotnet/templating/issues/10047
string templateWithDuplicateKeys = /*lang=json*/ """
{
"author": "Test Asset",
"classifications": [ "Test Asset" ],
"name": "TemplateWithDuplicateKeys",
"identity": "TestAssets.TemplateWithDuplicateKeys",
"shortName": "dupkeys",
"symbols": {
"Empty": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"description": "PascalCase variant"
},
"empty": {
"type": "parameter",
"datatype": "bool",
"defaultValue": "false",
"description": "lowercase variant"
}
}
}
""";

var exception = Record.Exception(() => TemplateConfigModel.FromString(templateWithDuplicateKeys));
Assert.Null(exception);

TemplateConfigModel configModel = TemplateConfigModel.FromString(templateWithDuplicateKeys);
Assert.Equal("TemplateWithDuplicateKeys", configModel.Name);
// Both symbols should be accessible (last-in-wins for case-sensitive dict, both kept)
Assert.NotEmpty(configModel.Symbols);
}
}
}