Summary
DatabaseId.ToString() escapes . to %2E so TryParse can find the Server/Name separator. But the only consumer of that string embeds it in a System.Uri, and Uri canonicalisation decodes %2E straight back to .. The escaping therefore delivers none of the disambiguation it was written for, while creating two publicly-reachable spellings of one identity.
The two spellings
public record DatabaseId(string Server, string Name)
{
public string Identity => $"{Server}.{Name}"; // literal dots
public override string ToString()
=> $"{EscapeSegment(Server)}.{EscapeSegment(Name)}"; // %2E
private static string EscapeSegment(string value) => value
.Replace("%", "%25", StringComparison.Ordinal)
.Replace("/", "~", StringComparison.Ordinal)
.Replace(".", "%2E", StringComparison.Ordinal);
}
Wolverine's EventSubscriptionAgentFamily.UriFor interpolates ToString() into an agent URI and calls new Uri(...).
Why the escaping doesn't survive
%2E encodes ., which is unreserved in RFC 3986, so System.Uri normalises it away. Verified on .NET 9:
new Uri("marten://main/database-test%2Ezorg%2Eaws.claims1/p/all/1").ToString()
-> marten://main/database-test.zorg.aws.claims1/p/all/1 // %2E decoded
So EventSubscriptionAgentFamily's own DatabaseId.Parse(uri.Segments[2].Trim('/')) on a round-tripped URI sees literal dots and falls back on LastIndexOf('.') — exactly the ambiguity the escaping was meant to remove. It happens to give the right answer whenever the database name contains no dots, which is why nothing has failed loudly.
The cost downstream
Uri.ToString() decodes; System.Text.Json serialises a Uri through OriginalString, which does not. One agent identity therefore reaches a client spelled two ways depending on whether it travelled as a Uri or as a string, and joins between the two silently miss. That is CritterWatch#878 — agent health rendering as "unknown" because a Uri-keyed map and a ToString()-derived map don't join. I have fixed it on the CritterWatch side by canonicalising to the decoded form, but the root asymmetry is here.
Options
- Escape with something
Uri won't normalise. %2E is the one choice that cannot survive; almost anything else would (~ already does, which is why the / escape works).
- Drop the escaping and change the separator so
Server and Name are unambiguous without it.
- Make
Identity and ToString() agree, whichever way, so there is only one spelling to reason about.
Happy to take a swing at whichever you prefer — flagging rather than patching because the choice affects the agent-URI grammar and its parser, and there may be persisted URIs to consider.
Summary
DatabaseId.ToString()escapes.to%2EsoTryParsecan find theServer/Nameseparator. But the only consumer of that string embeds it in aSystem.Uri, andUricanonicalisation decodes%2Estraight back to.. The escaping therefore delivers none of the disambiguation it was written for, while creating two publicly-reachable spellings of one identity.The two spellings
Wolverine'sEventSubscriptionAgentFamily.UriForinterpolatesToString()into an agent URI and callsnew Uri(...).Why the escaping doesn't survive
%2Eencodes., which is unreserved in RFC 3986, soSystem.Urinormalises it away. Verified on .NET 9:So
EventSubscriptionAgentFamily's ownDatabaseId.Parse(uri.Segments[2].Trim('/'))on a round-tripped URI sees literal dots and falls back onLastIndexOf('.')— exactly the ambiguity the escaping was meant to remove. It happens to give the right answer whenever the database name contains no dots, which is why nothing has failed loudly.The cost downstream
Uri.ToString()decodes;System.Text.Jsonserialises aUrithroughOriginalString, which does not. One agent identity therefore reaches a client spelled two ways depending on whether it travelled as aUrior as a string, and joins between the two silently miss. That is CritterWatch#878 — agent health rendering as "unknown" because aUri-keyed map and aToString()-derived map don't join. I have fixed it on the CritterWatch side by canonicalising to the decoded form, but the root asymmetry is here.Options
Uriwon't normalise.%2Eis the one choice that cannot survive; almost anything else would (~already does, which is why the/escape works).ServerandNameare unambiguous without it.IdentityandToString()agree, whichever way, so there is only one spelling to reason about.Happy to take a swing at whichever you prefer — flagging rather than patching because the choice affects the agent-URI grammar and its parser, and there may be persisted URIs to consider.