Commit 791a389
authored
Fix duplicate key handling (#10049)
Fixes #10025
## Problem
After migrating from Newtonsoft.Json to System.Text.Json, loading
template.json files with duplicate case-insensitive property keys (e.g.
`"Empty"` and `"empty"`) throws an `ArgumentException`:
```
System.ArgumentException: An item with the same key has already been added. Key: empty (Parameter 'key')
at System.Collections.Generic.OrderedDictionary`2.Add(TKey key, TValue value)
at System.Text.Json.Nodes.JsonObject.InitializeDictionary()
at System.Text.Json.Nodes.JsonObject.get_Count()
...
at Microsoft.TemplateEngine.Orchestrator.RunnableProjects.ConfigModel.TemplateConfigModel..ctor(...)
```
This affected MAUI templates (`maui-blazor`, `maui-blazor-solution`)
which intentionally had both `"Empty"` (PascalCase backward-compat
alias) and `"empty"` (lowercase) symbol definitions.
## Root Cause
In `src/Shared/JExtensions.cs`, the `NodeOptions` field was configured
with `PropertyNameCaseInsensitive = true`:
```csharp
private static readonly JsonNodeOptions NodeOptions = new() { PropertyNameCaseInsensitive = true };
```
This caused `JsonObject` to use a case-insensitive internal dictionary.
When any operation triggered `JsonObject.InitializeDictionary()` (e.g.
accessing `.Count`, iterating via `.ToList()`), it attempted to insert
both `"Empty"` and `"empty"` into the same case-insensitive dictionary,
throwing on the duplicate.
This setting was **redundant** — all case-insensitive property lookups
in the codebase already go through the `GetPropertyCaseInsensitive()`
helper method, which manually performs a case-insensitive fallback
search.
## Fix
**File changed:** `src/Shared/JExtensions.cs`
- Removed the `NodeOptions` field entirely (it only existed to set
`PropertyNameCaseInsensitive = true`)
- Replaced all 6 usages of `NodeOptions` in `JsonNode.Parse()` calls
with `null` (uses default `JsonNodeOptions`, which is case-sensitive)
This means `JsonObject` now stores properties with their original casing
and uses a case-sensitive internal dictionary — which tolerates keys
like `"Empty"` and `"empty"` coexisting. Case-insensitive lookups
continue to work through the existing `GetPropertyCaseInsensitive()`
helper.
## Test Added
**File:**
`test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests/GenericTests.cs`
Added `CanReadTemplateWithDuplicateCaseInsensitiveSymbolKeys` — a
regression test that verifies a template.json with symbols `"Empty"` and
`"empty"` loads without throwing.File tree
2 files changed
+45
-7
lines changed- test/Microsoft.TemplateEngine.Orchestrator.RunnableProjects.UnitTests/TemplateConfigTests
2 files changed
+45
-7
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
17 | 17 | | |
18 | 18 | | |
19 | 19 | | |
20 | | - | |
21 | 20 | | |
22 | 21 | | |
23 | 22 | | |
| |||
387 | 386 | | |
388 | 387 | | |
389 | 388 | | |
390 | | - | |
| 389 | + | |
391 | 390 | | |
392 | 391 | | |
393 | 392 | | |
| |||
396 | 395 | | |
397 | 396 | | |
398 | 397 | | |
399 | | - | |
| 398 | + | |
400 | 399 | | |
401 | 400 | | |
402 | 401 | | |
| |||
480 | 479 | | |
481 | 480 | | |
482 | 481 | | |
483 | | - | |
| 482 | + | |
484 | 483 | | |
485 | 484 | | |
486 | 485 | | |
| |||
489 | 488 | | |
490 | 489 | | |
491 | 490 | | |
492 | | - | |
| 491 | + | |
493 | 492 | | |
494 | 493 | | |
495 | 494 | | |
| |||
503 | 502 | | |
504 | 503 | | |
505 | 504 | | |
506 | | - | |
| 505 | + | |
507 | 506 | | |
508 | 507 | | |
509 | 508 | | |
| |||
512 | 511 | | |
513 | 512 | | |
514 | 513 | | |
515 | | - | |
| 514 | + | |
516 | 515 | | |
517 | 516 | | |
518 | 517 | | |
| |||
Lines changed: 39 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
54 | 54 | | |
55 | 55 | | |
56 | 56 | | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
57 | 96 | | |
58 | 97 | | |
0 commit comments