Skip to content

Implement IXmlNamespaceResolver in XmlDictionaryReader, use in XmlDictionaryWriter#130507

Draft
StephenMolloy wants to merge 1 commit into
dotnet:mainfrom
StephenMolloy:49010-WriteNode-Namespaces
Draft

Implement IXmlNamespaceResolver in XmlDictionaryReader, use in XmlDictionaryWriter#130507
StephenMolloy wants to merge 1 commit into
dotnet:mainfrom
StephenMolloy:49010-WriteNode-Namespaces

Conversation

@StephenMolloy

Copy link
Copy Markdown
Member

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 local xmlns attributes on the copied elements. Namespace bindings that are inherited from an ancestor and referenced only from content — the classic case being a QName in an xsi:type value 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>):

<s:sourceRoot xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
              xmlns:s="common-schema"
              xmlns:my="my-custom-schema">
  <s:target xsi:type="my:customType">my:otherType</s:target>
</s:sourceRoot>

WriteNode(reader, defattr: true) produces:

<s:target xmlns:s="common-schema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:type="my:customType">my:otherType</s:target>

xmlns:my is gone, so xsi:type="my:customType" (and the my:otherType text) 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:type heavily, 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 to XmlDictionaryWriter only (see "Scope" below).

Approach

Two coordinated pieces:

  1. 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 nested NamespaceManager, which already tracks the full in-scope binding stack. Two helpers were added: GetNamespacesInScope and LookupPrefix.
    • XmlWrappedReader (the CreateDictionaryReader(XmlReader) wrapper) — delegates to the wrapped reader when it is itself an IXmlNamespaceResolver (the core System.Private.Xml readers are), and degrades gracefully otherwise.
  2. Writer side (one new public overload): a new opt-in

    public void WriteNode(XmlDictionaryReader reader, bool defattr, bool preserveNamespacesInScope)

    When preserveNamespacesInScope is true, the writer captures the reader's in-scope namespaces (via reader 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 by LookupPrefix), 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 WriteNode path is completely unchanged.

Design decisions & tradeoffs

This section pre-answers the design questions that tend to come up.

Why implement IXmlNamespaceResolver on the concrete internal readers, not on public XmlDictionaryReader?

  • The abstract base has no namespace state to implement it with. XmlDictionaryReader : XmlReader is a pure abstraction; all in-scope tracking lives in the concrete readers (XmlBaseReader.NamespaceManager, or the reader wrapped by XmlWrappedReader). 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.
  • It keeps the reviewable public surface to a single member. Putting IXmlNamespaceResolver on 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-party XmlDictionaryReader subclass to satisfy it. Implementing on the internal concretes means the only public change to review is the one WriteNode overload.
  • Honesty about capability. Consumers can do reader is IXmlNamespaceResolver and get a truthful answer for the reader they actually have, rather than a base type that always advertises the capability but sometimes returns nothing.
  • It mirrors core System.Xml. XmlReader itself does not implement IXmlNamespaceResolver; only concrete readers such as XmlTextReaderImpl do. This change follows the same shape.

Tradeoff: the cost of not putting it on the base is that preserveNamespacesInScope: true is a silent no-op for any reader outside these two internal hierarchies (e.g. a custom XmlDictionaryReader, or XmlWrappedReader wrapping a reader that isn't an IXmlNamespaceResolver). That is an acceptable, well-defined degradation for an opt-in flag, and it's documented on the API.

Why a per-call WriteNode parameter instead of a writer construction-time option/settings object?

  • The behavior is a property of the copy operation, not of the writer. Namespace re-declaration only means something while copying a subtree from a reader. A writer that writes directly, or copies from several readers, has no single "namespace preservation" identity. A per-call flag expresses the intent exactly where it applies.
  • WriteNode already parameterizes copy fidelity with a bool. The existing defattr argument already controls how much of the source is copied; preserveNamespacesInScope is the natural, consistent second copy-fidelity flag. It also lines up with base XmlWriter.WriteNode(XmlReader, bool). And there is direct precedent for "preserve in-scope namespaces during a copy" already in the framework: the WriteNode(XPathNavigator, bool) overload re-declares in-scope namespaces (because the navigator exposes them). So "preserve namespaces on copy" is already a WriteNode-level concept, not a writer-level one.
  • XmlDictionaryWriter has no natural construction seam. It is abstract with only a protected ctor and is built through 11 factory overloads (CreateTextWriter, CreateBinaryWriter, CreateMtomWriter, CreateDictionaryWriter), with no XmlWriterSettings-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.
  • Per-call control is genuinely useful, including outside DCS/WCF. Re-declaring namespaces is pure overhead (and output noise) when the destination already declares them or when the copied fragment has no QNames-in-content. Whether to pay that cost can legitimately differ between adjacent copies within a single writer's lifetime (e.g. copying a trusted internal fragment vs. an arbitrary user-supplied one). A writer-global switch cannot express that; a per-call bool can. And because XmlDictionaryWriter.WriteNode is public and used standalone for message/fragment plumbing, non-DCS callers hit the same QName-in-content problem.
  • It costs DCS nothing. DCS owns its WriteNode call 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 WriteNode caller, 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 base System.Xml.XmlWriter.WriteNode has the same gap and would need a separate, base-library change (and its own API review); it is out of scope here.

Public API

namespace System.Xml
{
    public abstract partial class XmlDictionaryWriter : XmlWriter
    {
        public void WriteNode(XmlDictionaryReader reader, bool defattr, bool preserveNamespacesInScope);
    }
}

⚠️ API review status: this new overload has not been through API review. Issue #49010 is not api-approved. This PR is opened for design visibility and discussion; the API would need approval (or the overload would need to be internal) before it could merge.

Open sub-decision for reviewers: the existing WriteNode(XmlDictionaryReader, bool) is virtual; the new overload is currently non-virtual. Non-virtual is functionally fine (it decomposes through the virtual Write* primitives, so XmlDictionaryAsyncCheckWriter and subclasses still see every write), but reviewers may prefer virtual for parity.

…onaryReader; enhance WriteNode to preserve namespaces in scope
@StephenMolloy StephenMolloy added this to the Future milestone Jul 10, 2026
@StephenMolloy StephenMolloy self-assigned this Jul 10, 2026
Copilot AI review requested due to automatic review settings July 10, 2026 17:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IXmlNamespaceResolver on internal XmlBaseReader and XmlWrappedReader to 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XmlDictionaryWriter.WriteNode does not preserve the namespaces declared in the original context

2 participants