The Address String Form
The rule:
Address.ToString(),Address.ToFullString(), the implicitstringconversion and the JSONAddressConverterall write ONE form:outermost_host~…~host~path. For an unhosted address that is just itsPath. The implicitstring → Addressparser reads that same form back, so(Address)address.ToString()equalsaddress, including nested hosts.
Why this needed fixing (#6604)
Until #6604, ToString() rendered path~host, but the parser splits on the FIRST ~ and treats
the left side as the host. Every key minted with ToString() and then parsed back therefore came back
REVERSED. sync/g~ui/c became ui/c hosted on sync/g. The keys that do this round trip are:
- the
IPodHubGrainkey.PodHubGrainturns its primary key into anAddress(Address address = AddressPath) and looks up the local route with it. - a
MessageHubGrainkey, resolved byGetAddress(key)on activation.
Neither raised an error. The pod-hub grain simply looked up the wrong local route, answered
PodHubNotHere, and the router fell back.
The parser, the wire serializer, AccessContext.ObjectId and UserNodeType.IsPortalIdentity all
used host~path already. ToString() was the outlier, and it now delegates to ToFullString().
Rules for callers
- Want the node path? Use
Address.Path. The host chain says where an address is activated, not what it is. Never splitToString()to find a path or a type.Address.TypeisSegments[0]. - A segment holds neither
/nor~. Both are structural in the string form. A node id that could contain~is escaped before it becomes a segment (AppDirectoryencodes~as~7E). A raw~in a segment cannot round-trip, and the parser will read it as a host separator. - Exact equality round-trips only when no segment holds
/. Production does build such addresses:KernelContainerwritesnew Address("kernelExec", hub.Address.Path), a whole path in ONE segment. Its string is the same text, and it parses back with the samePath,Typeand host chain, but re-segmented (kernelExec,activity,x,yinstead ofkernelExec,activity/x/y), soAddress.Equalsis false.StreamRoutingHotPathTestpins that shape as production behaviour. So the guarantee is:ToString()and parsing agree onPathand on the host chain for every address, and onEqualsfor an address whose segments hold no/. Compare a key that may carry an embedded/byPath(or by its string), never byEqualsagainst a parsed copy.
The one text that crosses a roll
Grain keys and memory-stream ids are in-memory only, so nothing persisted used the old form. The one
cross-process TEXT that did is the shutdown banner, Hub {subject} is shutting down, inside a NACK
message. During a roll an old pod's NACK is read by a new pod. So:
ShutdownNack.Bannerwrites the owner'sPathas the subject, with no host chain.ShutdownNack.IsAnsweredByOwnerreads a subject up to its first~. A banner from this release has no~. A banner from the previous release hadpath~host, and its path is the part before the first~.
Where it is pinned
AddressStringRoundTripTest(MeshWeaver.Messaging.Hub.Test): seeded property test over one to four segments and zero to three nested hosts. It checks the parse round trip and that the wire form agrees. A negative control shows the old rendering does not round-trip.AnEmbeddedSlash_KeepsPathAndHost_ButNotSegmentationpins the narrower guarantee for a segment that holds/.OwnerAnswerRecognitionGuard.ABannerFromThePreviousRelease_IsStillRecognised(MeshWeaver.Hosting.Test): the roll window.