Implement IXmlNamespaceResolver in XmlDictionaryReader, use in XmlDictionaryWriter#130507
Draft
StephenMolloy wants to merge 1 commit into
Draft
Implement IXmlNamespaceResolver in XmlDictionaryReader, use in XmlDictionaryWriter#130507StephenMolloy wants to merge 1 commit into
StephenMolloy wants to merge 1 commit into
Conversation
…onaryReader; enhance WriteNode to preserve namespaces in scope
Contributor
There was a problem hiding this comment.
Pull request overview
This PR extends XmlDictionaryWriter.WriteNode with an opt-in mode to preserve the reader’s in-scope namespace bindings when copying a subtree, addressing cases where prefixes are only referenced from content (e.g., xsi:type values). It does so by teaching the internal XmlDictionaryReader implementations to expose in-scope namespaces via IXmlNamespaceResolver, and by having the writer re-declare those namespaces on the copied subtree’s root element.
Changes:
- Add a new public overload
XmlDictionaryWriter.WriteNode(XmlDictionaryReader reader, bool defattr, bool preserveNamespacesInScope). - Implement
IXmlNamespaceResolveron internalXmlBaseReaderandXmlWrappedReaderto provide in-scope namespace enumeration / lookup. - Add regression tests covering the default behavior vs. the opt-in namespace preservation behavior.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/libraries/System.Runtime.Serialization.Xml/tests/XmlDictionaryWriterTest.cs | Adds tests validating that the opt-in path preserves inherited namespaces used only in content. |
| src/libraries/System.Runtime.Serialization.Xml/ref/System.Runtime.Serialization.Xml.cs | Adds the new public WriteNode overload to the reference assembly surface. |
| src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlDictionaryWriter.cs | Implements the new overload and the logic to re-declare in-scope namespaces on the copied root element. |
| src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlDictionaryReader.cs | Updates the XmlWrappedReader wrapper to implement IXmlNamespaceResolver by delegating when possible. |
| src/libraries/System.Private.DataContractSerialization/src/System/Xml/XmlBaseReader.cs | Updates the native reader implementation to implement IXmlNamespaceResolver backed by its existing namespace tracking. |
| public void WriteElementString(string? prefix, System.Xml.XmlDictionaryString localName, System.Xml.XmlDictionaryString? namespaceUri, string? value) { } | ||
| public void WriteElementString(System.Xml.XmlDictionaryString localName, System.Xml.XmlDictionaryString? namespaceUri, string? value) { } | ||
| public virtual void WriteNode(System.Xml.XmlDictionaryReader reader, bool defattr) { } | ||
| public void WriteNode(System.Xml.XmlDictionaryReader reader, bool defattr, bool preserveNamespacesInScope) { } |
| WriteNodeCore(reader, defattr, preserveNamespacesInScope: false); | ||
| } | ||
|
|
||
| public void WriteNode(XmlDictionaryReader reader, bool defattr, bool preserveNamespacesInScope) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
XmlDictionaryWriter.WriteNode(XmlDictionaryReader, bool)copies an element subtree from a reader to the writer, but it only re-emits namespace declarations that appear as localxmlnsattributes on the copied elements. Namespace bindings that are inherited from an ancestor and referenced only from content — the classic case being a QName in anxsi:typevalue or in element/attribute text — are silently dropped. The copied fragment is still namespace-well-formed, but any QName-in-content that relied on the inherited prefix can no longer be resolved at the destination.Fixes #49010.
Repro
Source document (reader positioned at
<s:target>):WriteNode(reader, defattr: true)produces:xmlns:myis gone, soxsi:type="my:customType"(and themy:otherTypetext) is now unresolvable.Is this a bug?
Strictly, no — it is spec-conformant. The XML Namespaces machinery governs only element and attribute names; it says nothing about prefixes that appear inside attribute values or text content (QNames-in-content are an application-level convention, and the W3C TAG finding on "QNames as Identifiers" explicitly cautions against relying on them). The copied output remains namespace-well-formed and namespace-valid. So this is unspecified behavior, not a spec violation.
But it is a real and repeatedly-reported usability sharp edge: DCS/WCF payloads use
xsi:typeheavily, and "copy this element somewhere else" losing the type annotation is surprising and hard to diagnose. This change makes the correct-for-content behavior available opt-in, without changing any existing default.Note this is not DCS-specific — the base
System.Xml.XmlWriter.WriteNode(XmlReader, bool)has the identical gap. This PR scopes the fix toXmlDictionaryWriteronly (see "Scope" below).Approach
Two coordinated pieces:
Reader side (no public API change): the internal DCS readers learn to report their in-scope namespaces by implementing
System.Xml.IXmlNamespaceResolver:XmlBaseReader(native text/binary readers) — backed by its existing nestedNamespaceManager, which already tracks the full in-scope binding stack. Two helpers were added:GetNamespacesInScopeandLookupPrefix.XmlWrappedReader(theCreateDictionaryReader(XmlReader)wrapper) — delegates to the wrapped reader when it is itself anIXmlNamespaceResolver(the coreSystem.Private.Xmlreaders are), and degrades gracefully otherwise.Writer side (one new public overload): a new opt-in
When
preserveNamespacesInScopeistrue, the writer captures the reader's in-scope namespaces (viareader as IXmlNamespaceResolver) and re-declares them on the root element of the copied subtree. Declarations the writer has already emitted for the element's own name/attributes are skipped (guarded byLookupPrefix), so there is no duplication. Descendants inherit the root re-declarations, and any namespace declared inside the subtree is still copied as a local attribute as before — so the root is the only place that needs the extra declarations.The default two-argument
WriteNodepath is completely unchanged.Design decisions & tradeoffs
This section pre-answers the design questions that tend to come up.
Why implement
IXmlNamespaceResolveron the concrete internal readers, not on publicXmlDictionaryReader?XmlDictionaryReader : XmlReaderis a pure abstraction; all in-scope tracking lives in the concrete readers (XmlBaseReader.NamespaceManager, or the reader wrapped byXmlWrappedReader). A base-level implementation would have nothing to implement from and would have to either throw or return empty — i.e., lie about the capability.IXmlNamespaceResolveron the public base is the alternative "option 1" from the issue: it's a second public API addition and it would contractually obligate every existing and third-partyXmlDictionaryReadersubclass to satisfy it. Implementing on theinternalconcretes means the only public change to review is the oneWriteNodeoverload.reader is IXmlNamespaceResolverand get a truthful answer for the reader they actually have, rather than a base type that always advertises the capability but sometimes returns nothing.System.Xml.XmlReaderitself does not implementIXmlNamespaceResolver; only concrete readers such asXmlTextReaderImpldo. This change follows the same shape.Tradeoff: the cost of not putting it on the base is that
preserveNamespacesInScope: trueis a silent no-op for any reader outside these two internal hierarchies (e.g. a customXmlDictionaryReader, orXmlWrappedReaderwrapping a reader that isn't anIXmlNamespaceResolver). That is an acceptable, well-defined degradation for an opt-in flag, and it's documented on the API.Why a per-call
WriteNodeparameter instead of a writer construction-time option/settings object?WriteNodealready parameterizes copy fidelity with a bool. The existingdefattrargument already controls how much of the source is copied;preserveNamespacesInScopeis the natural, consistent second copy-fidelity flag. It also lines up with baseXmlWriter.WriteNode(XmlReader, bool). And there is direct precedent for "preserve in-scope namespaces during a copy" already in the framework: theWriteNode(XPathNavigator, bool)overload re-declares in-scope namespaces (because the navigator exposes them). So "preserve namespaces on copy" is already aWriteNode-level concept, not a writer-level one.XmlDictionaryWriterhas no natural construction seam. It is abstract with only aprotectedctor and is built through 11 factory overloads (CreateTextWriter,CreateBinaryWriter,CreateMtomWriter,CreateDictionaryWriter), with noXmlWriterSettings-style object. A construction-time option would require threading a parameter through all those factories or inventing a settings type — a much larger API addition for a coarser result.XmlDictionaryWriter.WriteNodeis public and used standalone for message/fragment plumbing, non-DCS callers hit the same QName-in-content problem.WriteNodecall sites, so opting in per-call is trivial where it wants the behavior — while external callers gain strictly more control than a global setting would give them.Why not just change the default?
Rejected. Changing the default two-arg behavior would be a silent output/behavioral change for every existing
WriteNodecaller, would add declarations (and cost) that most callers don't need, and would risk breaking consumers that depend on the current exact output. Opt-in preserves compatibility.Scope
This PR intentionally fixes only
XmlDictionaryWriter. The baseSystem.Xml.XmlWriter.WriteNodehas the same gap and would need a separate, base-library change (and its own API review); it is out of scope here.Public API
Open sub-decision for reviewers: the existing
WriteNode(XmlDictionaryReader, bool)isvirtual; the new overload is currently non-virtual. Non-virtual is functionally fine (it decomposes through the virtualWrite*primitives, soXmlDictionaryAsyncCheckWriterand subclasses still see every write), but reviewers may prefervirtualfor parity.