Conversation
Elements are serialized with a built-in prefix for their namespace, so a
worksheet is written as <x:worksheet xmlns:x="..."> where Office writes
<worksheet xmlns="...">. Both are valid, but consumers that parse without a
namespace manager are sensitive to the difference, and there was no way to
choose.
This adds an opt-in feature registered on the package:
document.Features.SetNamespacePrefixOverride(
OpenXmlNamespacePrefix.DefaultFor(spreadsheetNamespace));
The override is applied by an XmlWriter decorator wrapped around the part
root, rather than while resolving each element's prefix. That placement is
load-bearing: a namespace-qualified attribute such as w:rsidR forces the
underlying writer to declare the built-in prefix on its element, and from
that point XmlWriter.LookupPrefix reports that prefix, so any decision taken
from the writer's namespace scope silently reverts the rest of the subtree.
Forcing the prefix as each element is written keeps the whole tree
consistent, covers unknown and extension elements for free, and costs a
single feature lookup per save when no override is registered.
A prefix already owned by another Open XML namespace is rejected with an
InvalidOperationException naming both namespaces. Taking one has no good
outcome: declared on the same element the writer refuses it, declared
further out the writer silently rebinds the other namespace to a generated
prefix, so r:id would ship as p3:id with no error.
Existing namespace declarations are left in place. Binding both a prefix and
the default prefix to one namespace is legal, it does not affect element
names once the prefix is forced, and it lets qualified attributes resolve
against the root instead of re-declaring the prefix on every element that
carries one. Dropping them would also strand mc:Ignorable values.
Scope and limits are documented on the interface: OpenXmlElement.Prefix is
untouched so validation XPaths stay well formed, an unparsed root is still
copied verbatim, and the LINQ-to-XML save path is unaffected.
Output and OpenXmlElement.Prefix are unchanged when no feature is
registered.
The override feature checked a supplied prefix only against the built-in prefix table, and only the first time it saw each namespace. A prefix the loaded document itself declared for another namespace slipped through to the writer, which failed with a redefinition error on the start tag, and a feature that changed its answer for a namespace bypassed the check entirely. Both checks also ran lazily on the first element write, after the part stream had been opened with FileMode.Create. NamespacePrefixOverride now walks the tree once before the part is opened, collecting every namespace declaration and every namespace in use, and validates each supplied prefix against the built-in table and the document's own declarations. Answers are re-validated whenever they change. The root's namespace-hoisting pass reuses the collected declarations rather than walking the tree again, so the override path performs one walk per save, the same as the un-overridden path. Descendants written under the override no longer resolve the namespace resolver through their features, which walked to the part root on every element; they read the resolver the root already resolved from the decorator. Extended attributes with an empty prefix fall back to the built-in prefix like parsed attributes do, instead of receiving a generated one when the namespace is bound as the default. The registration extension no longer duplicates the read-only guard every feature collection already enforces, and the two XmlWriter decorators share a forwarding base class.
Author
|
@dotnet-policy-service agree |
8 tasks
This branch has not been deployed
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
Elements are serialized with a built-in prefix for their namespace, so a worksheet is written as
<x:worksheet xmlns:x="...">where Office writes<worksheet xmlns="...">. Both are valid, but consumers that parse without a namespace manager are sensitive to the difference, and there was no way to choose.This adds an opt-in feature registered on the package:
OpenXmlNamespacePrefix.Create(Func<string, string?>)supports arbitrary prefixes per namespace. Output andOpenXmlElement.Prefixare unchanged when no feature is registered.Resolves #90
Problem
Elements are serialized with a built-in prefix for their namespace, so the SDK writes
where Office writes
Both are valid and Excel reads either. The difference matters to consumers that parse the XML without a namespace manager — XPath, regex, strict diffing — and there is currently no way to choose. Opening an Office-authored file and saving it also converts it to the prefixed form, because default-namespace declarations are discarded at parse time (
OpenXmlElement.cs:1583).Design
XmlWriterdecorator wrapped around the part root rather than while resolving each element's prefix. A namespace-qualified attribute such asw:rsidRforces the underlying writer to declare the built-in prefix on its element, after whichXmlWriter.LookupPrefixreports that prefix, so any decision taken from the writer's scope would silently revert the rest of the subtree. Forcing the prefix as each element is written keeps the whole tree consistent and covers unknown and extension elements.InvalidOperationExceptionnaming both namespaces. Taking one would otherwise either be refused by the writer as a redefinition or silently rebind the other namespace to a generated prefix (r:idshipping asp3:id).mc:Ignorablevalues are not stranded.XmlWriterdecorators share a forwarding base class.Scope and limits
Documented on
IOpenXmlNamespacePrefixFeature:OpenXmlElement.Prefixis untouched so validation XPaths stay well formed; an unparsed part root is still copied verbatim;OpenXmlPartWriterand the LINQ-to-XML save path are unaffected.Tests
33 tests in
NamespacePrefixOverrideTestscover SpreadsheetML and WordprocessingML output, qualified attributes, unknown elements, declared and reserved prefixes, validation ordering, round-tripping, and the unchanged default behavior.Performance
Saving a 50,000-row, 250,000-cell worksheet to memory, best of five runs, net10.0 Release:
The remaining overhead is the decorator's per-element dispatch and the feature's prefix lookup.