diff --git a/ZB.MOM.WW.Secrets/Directory.Build.props b/ZB.MOM.WW.Secrets/Directory.Build.props
index 456ba04..974d6cf 100644
--- a/ZB.MOM.WW.Secrets/Directory.Build.props
+++ b/ZB.MOM.WW.Secrets/Directory.Build.props
@@ -5,7 +5,7 @@
enable
enable
latest
- 0.1.1
+ 0.1.2
true
README.md
diff --git a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretReferenceExpander.cs b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretReferenceExpander.cs
index 2edf0ac..acecf37 100644
--- a/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretReferenceExpander.cs
+++ b/ZB.MOM.WW.Secrets/src/ZB.MOM.WW.Secrets/Configuration/SecretReferenceExpander.cs
@@ -83,6 +83,13 @@ public sealed class SecretReferenceExpander
/// or ValidateOnStart executes; otherwise validators may see the unexpanded placeholder.
/// Fail-closed: a missing referenced secret propagates out
/// of startup.
+ ///
+ /// Documentation/comment keys are skipped: any entry whose leaf key segment (the part after the
+ /// last :) begins with an underscore — the widely-used "_comment" JSON convention —
+ /// is left untouched. This lets an appsettings file document the ${secret:...} syntax in a
+ /// _comment value (even with a literal example token) without that example being treated as
+ /// a real reference to resolve. Functional keys are never named with a leading underscore.
+ ///
///
/// The loaded configuration root to mutate.
/// A token to cancel the operation.
@@ -95,7 +102,9 @@ public sealed class SecretReferenceExpander
var pending = new List>();
foreach (KeyValuePair entry in config.AsEnumerable())
{
- if (entry.Value is { } value && value.Contains(TokenMarker, StringComparison.Ordinal))
+ if (entry.Value is { } value
+ && value.Contains(TokenMarker, StringComparison.Ordinal)
+ && !IsCommentKey(entry.Key))
{
pending.Add(new KeyValuePair(entry.Key, value));
}
@@ -106,4 +115,14 @@ public sealed class SecretReferenceExpander
config[entry.Key] = await ExpandAsync(entry.Value, ct).ConfigureAwait(false);
}
}
+
+ // A configuration key is a comment/documentation key when its leaf segment (after the last ':')
+ // starts with '_' — the conventional "_comment" marker. Such values may legitimately contain a
+ // literal ${secret:...} example and must not be resolved.
+ private static bool IsCommentKey(string key)
+ {
+ int lastSeparator = key.LastIndexOf(':');
+ string leaf = lastSeparator >= 0 ? key[(lastSeparator + 1)..] : key;
+ return leaf.StartsWith('_');
+ }
}
diff --git a/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/Configuration/SecretReferenceExpanderTests.cs b/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/Configuration/SecretReferenceExpanderTests.cs
index dd868f8..bcc2eb8 100644
--- a/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/Configuration/SecretReferenceExpanderTests.cs
+++ b/ZB.MOM.WW.Secrets/tests/ZB.MOM.WW.Secrets.Tests/Configuration/SecretReferenceExpanderTests.cs
@@ -103,4 +103,30 @@ public sealed class SecretReferenceExpanderTests
await Assert.ThrowsAsync(
() => expander.ExpandConfigurationAsync(config, CancellationToken.None));
}
+
+ [Fact]
+ public async Task ExpandConfigurationAsync_SkipsCommentKeys()
+ {
+ // A "_comment" key may document the ${secret:...} syntax with a literal (even malformed or
+ // not-yet-seeded) example token; it must be left untouched — never resolved, never throwing —
+ // while a real sibling key still expands. Regression: an unseeded/ellipsis example in a comment
+ // previously crashed boot (SecretName rejects '...').
+ SecretReferenceExpander expander = Expander(("sql/foo", "hunter2"));
+ IConfigurationRoot config = new ConfigurationBuilder()
+ .AddInMemoryCollection(new Dictionary
+ {
+ ["_secretsComment"] = "e.g. Password=${secret:...} or ${secret:sql/not-seeded}",
+ ["Nested:_comment"] = "also skipped: ${secret:sql/also-absent}",
+ ["ConnectionStrings:Db"] = "Server=.;Password=${secret:sql/foo}",
+ })
+ .Build();
+
+ await expander.ExpandConfigurationAsync(config, CancellationToken.None);
+
+ // Comment keys untouched (literal token preserved, no throw despite absent/malformed names).
+ Assert.Equal("e.g. Password=${secret:...} or ${secret:sql/not-seeded}", config["_secretsComment"]);
+ Assert.Contains("${secret:sql/also-absent}", config["Nested:_comment"]);
+ // Real key still expanded.
+ Assert.Equal("Server=.;Password=hunter2", config["ConnectionStrings:Db"]);
+ }
}