Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization - #126767

Closed
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property
Closed

Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
StephenMolloy with Copilot wants to merge 16 commits into
mainfrom
copilot/add-xmltextattribute-separator-property

Conversation

CopilotAI commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

[XmlText] string[] has always concatenated array items with no separator (val1val2), while [XmlAttribute] string[] uses space separation (val1 val2). This inconsistency is intentional and tested — changing the default would be breaking. This PR adds an opt-in Separator property to both attributes so users can control the separator character.

API Changes

XmlTextAttribute — new char Separator { get; set; } (default '\0' = no separator, preserves existing concatenation behavior):

// opt-in: space-separated text content, round-trips correctly[XmlText(Separator=' ')]publicstring[]Items{get;set;}// opt-in: comma-separated[XmlText(Separator=',')]publicstring[]Tags{get;set;}

XmlAttributeAttribute — same char Separator { get; set; } (default '\0' = use existing space behavior):

// override attribute separator to comma[XmlAttribute(Separator=',')]publicstring[]Values{get;set;}

'\0' (null char) is the "not set" sentinel — chosen because char? is not valid as a C# attribute argument type (CS0655).

Implementation

  • XmlTextAttribute / XmlAttributeAttribute: add char Separator property
  • Mappings.cs: add char? Separator to TextAccessor and AttributeAccessor (internal; char? is valid here)
  • XmlReflectionImporter: wire attribute → accessor, validate with the internal XmlCharType utility (context-dependent rejection of characters which are not valid for text or attributes; '\0' sentinel skips validation)
  • Writers (ReflectionXmlSerializationWriter, XmlSerializationWriter, XmlSerializationWriterILGen): when TextAccessor.Separator.HasValue, emit items with separator between them; for attributes, use Separator ?? ' '
  • Readers (ReflectionXmlSerializationReader, XmlSerializationReader, XmlSerializationReaderILGen): when TextAccessor.Separator.HasValue, split text on separator and populate array; uses string.Split(char) overload (not char[]) for IL-gen compatibility
  • System.Xml.ReaderWriter ref assembly: updated with new public surface

Backward Compatibility

  • [XmlText] string[] with no Separator → unchanged concatenation (val1val2)
  • [XmlAttribute] string[] with no Separator override → unchanged space separation (val1 val2)
  • Existing test XML_TypeWithXmlTextAttributeOnArray continues to assert val1val2
Original prompt

Context

Issue #115837 reports that [XmlText] string[] on element content serializes array items concatenated without any separator (abcd), while [XmlAttribute] string[] serializes them space-separated (a b c d). This inconsistency has existed since .NET Framework and is the documented/tested behavior — changing the default would be a breaking change.

Design Decision (from discussion with area owner)

Rather than adding a simple IsList boolean, the solution is to add a char? Separator property to both XmlTextAttribute and XmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.

Key design points:

  1. XmlTextAttribute.Separator — defaults to null (meaning no separator, preserving current concatenation behavior of [XmlText] string[]). Setting e.g. Separator = ' ' opts into space-separated list serialization for element text content.

  2. XmlAttributeAttribute.Separator — defaults to ' ' (preserving existing space-separated behavior for [XmlAttribute] string[]). Users can override to a different separator if desired.

  3. Type should be char? (nullable char), where null means "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.

  4. Validation: Use internal utility XmlCharType on the separator character at reflection/import time (in XmlReflectionImporter).

Implementation Guide

Files that need changes:

Public API surface:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs — Add public char? Separator { get; set; } property (default: null)
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs — The XmlAttributeAttribute class needs the same public char? Separator { get; set; } property (default: ' ')

Internal mapping infrastructure:

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csTextAccessor is currently an empty class (internal sealed class TextAccessor : Accessor { }). Add an IsList property (or Separator property) mirroring what AttributeAccessor already has with its IsList bool. Consider whether to store the separator char itself or just a bool here.

Reflection import (wiring up the attribute to the mapping):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs — In ImportAccessorMapping, around line 1630-1640, where [XmlText] on array-like types creates a TextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here using the internal XmlCharType utility. Also around line 1595, where isList is computed for attributes, incorporate the new Separator property from XmlAttributeAttribute.

Serialization writers (emitting the separator during write):

  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs — In WriteMember, the attribute IsList path already writes " " between values. Add analogous logic for TextAccessor when it has a separator.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs — Similar changes for the non-reflection writer path.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs — IL-gen'd serializer path needs the same logic.

Deserialization readers (splitting on the separator during read):

  • The reader paths need to split text content on the separator character when deserializing back to an array. Look at how attribute IsList deserialization currently splits on whitespace and apply similar logic for text content.
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs
  • src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs

What NOT to change:

  • The existing [XmlAttribute] string[] default behavior must remain space-separated (backward compatible)
  • The existing [XmlText] string[] default behavior must remain concatenated with no separator (backward compatible)
  • The existing test XML_TypeWithXmlTextAttributeOnArray asserts val1val2 concatenation — this must continue to pass

Tests to add:

  • [XmlText(Separator = ' ')] string[] round-trips as space-separated text content
  • [XmlText(Separator = ',')] string[] round-trips with comma separation
  • [XmlText] string[] (no separa...

This pull request was created from Copilot chat.

Fixes#115837

…for configurable list serialization
Agent-Logs-Url: https://github.com/dotnet/runtime/sessions/68db653f-3600-4ba3-b399-78995d7c9d4b
Co-authored-by: StephenMolloy <19562826+StephenMolloy@users.noreply.github.com>
StephenMolloyand others added 3 commits May 1, 2026 14:47
… update error messages and add tests for invalid characters
…add tests for no-separator cases
Co-authored-by: Copilot <copilot@github.com>
…ests
- Guard WriteValue(separatorStr) calls in WriteArrayItems with hasSeparator
so the reflection writer makes no inter-element call when no Separator
is configured (matches pre-PR behavior; the prior unconditional
WriteValue("") call was a no-op for the built-in XmlWriter but
observable to custom subclasses).
- Hoist separatorChar.ToString() outside the WriteMember enumeration
loop. Char.ToString() allocates a fresh single-char string per call.
- Convert existing separator-set round-trip tests from skipStringCompare
to explicit XML baselines for byte-level wire-format coverage of
[XmlText(Separator=' ')], [XmlText(Separator=',')] and
[XmlAttribute(Separator=',')].
- Add edge-case tests: single-element arrays (no spurious separator
emitted) and embedded empty strings (e.g. ['a','','c'] with separator
',' round-trips through 'a,,c') for both [XmlText] and [XmlAttribute].
- Add wire-format preservation tests asserting that, when no Separator
is specified, the output is byte-for-byte identical to pre-PR
behavior for single-element [XmlText] and [XmlAttribute] string
arrays.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 2, 2026 04:08
@StephenMolloyStephenMolloy added this to the 11.0.0 milestone May 2, 2026
@StephenMolloy
StephenMolloy marked this pull request as ready for review May 2, 2026 04:12

CopilotAI 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

Adds a new Separator option to XmlTextAttribute / XmlAttributeAttribute so XML serializer list-like members can use a caller-chosen delimiter without changing existing defaults.

Changes:

  • Adds new public Separator properties to XmlTextAttribute and XmlAttributeAttribute, plus ref assembly updates.
  • Threads separator metadata through XmlSerializer mappings/importer and updates reflection, generated-code, and IL-gen reader/writer paths.
  • Adds runtime-only serializer tests covering default behavior, custom separators, and separator validation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
FileDescription
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.csAdds the new public API surface to the ref assembly.
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.csAdds serializer test model types for separator scenarios.
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.csAdds runtime-only XmlSerializer tests for custom separators and validation.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.csAdds XmlTextAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.csUpdates IL-generated writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.csUpdates source-generated writer paths and adds char literal emission helper.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csUpdates IL-generated reader paths to split on custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.csUpdates source-generated reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.csExposes the new char-literal helper to generated-code infrastructure.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.csImports separator metadata and validates separator chars.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.csAdds XmlAttributeAttribute.Separator.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.csUpdates reflection-based writer paths for custom separators.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.csUpdates reflection-based reader paths for custom separator parsing.
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.csAdds separator storage to text/attribute accessors.
src/libraries/System.Private.Xml/src/Resources/Strings.resxAdds the separator-validation resource string.

public string DataType { get { throw null; } set { } }
public System.Xml.Schema.XmlSchemaForm Form { get { throw null; } set { } }
public string? Namespace { get { throw null; } set { } }
public char Separator { get { throw null; } set { } }

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.

Comment threadsrc/libraries/System.Private.Xml/src/Resources/Strings.resx Outdated
@github-actions

This comment has been minimized.

…es for XML text and attribute handling
Co-authored-by: Copilot <copilot@github.com>
CopilotAI review requested due to automatic review settings May 6, 2026 04:37

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 1 comment.

StephenMolloyand others added 2 commits May 8, 2026 11:39
Fold the per-shape-and-per-type Separator round-trip facts into [Theory]
blocks parameterised by the array shape:
* TypeWithXmlTextSeparatorCommaOnStringArray: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorSpaceOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlTextNoSeparatorOnStringArray: 2 facts -> 1 theory.
* TypeWithXmlAttributeWithSeparatorComma: 3 facts -> 1 theory.
* TypeWithXmlTextSeparatorOnMixedContentWithElement: 3 facts -> 1 theory
driven by [MemberData] (object?[] with nulls is awkward in [InlineData]).
Facts that exercise a unique type with a single shape (Quote in text,
CloseBracket in attribute, no-separator attribute baseline, no-separator
mixed content) stay as [Fact] because they have no peers to share a row with.
Net test methods: 17 -> 11. Test case coverage is unchanged.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…mbly.XmlSerializers.cs
The merge from main accidentally replaced the build-time placeholder
'%%ParentAssemblyId%%' with a hardcoded MVID. The MSBuild target
'SetParentAssemblyId' uses File.ReadAllText/Replace to substitute that
placeholder with the actual MVID of the freshly-compiled
SerializableAssembly.dll. With the placeholder gone the replacement is a
no-op, so the embedded ParentAssemblyId in
SerializableAssembly.XmlSerializers.dll becomes stale whenever the parent
assembly's MVID differs from the previously hardcoded value. At runtime
TempAssembly.IsSerializerVersionMatch then returns false, the pre-gen
assembly is treated as not loadable, and every test running under
PreGenOnly mode fails with FailLoadAssemblyUnderPregenMode.
Local builds happened to pass because deterministic compilation produced
the same MVID as the hardcoded value; CI machines produced a different
MVID and exposed the regression.
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
CopilotAI review requested due to automatic review settings May 8, 2026 19:04

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated no new comments.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 Copilot Code Review — PR #126767

Note

This review was generated by Copilot (Claude Opus 4.6) with additional perspectives from Claude Sonnet 4.5. A GPT-5.3-Codex sub-agent was also launched but did not complete within the 10-minute timeout window.

Holistic Assessment

Motivation: The PR addresses a real usability gap — XmlSerializer has no built-in way to control the separator for xs:list-style serialization. The linked issue (#115837) describes a valid scenario where users need non-whitespace separators for array members. The problem is real and the feature is reasonable.

Approach: The approach adds a Separator property (type char, default '\0' as sentinel) to both XmlTextAttribute and XmlAttributeAttribute, threading the value through four distinct serialization code paths (reflection reader/writer, ILGen reader/writer, generated-code reader/writer). The choice of char over char? is forced by C# attribute parameter type restrictions (CS0655). The implementation is thorough — all four paths are updated consistently, validation rejects invalid XML characters, and mixed-content separator tracking is handled correctly.

Summary: ❌ Needs Changes. The implementation quality is solid and the tests are comprehensive, but this PR adds new public API surface without an api-approved issue, which is a blocking requirement in dotnet/runtime. One additional warning is noted below. The code itself appears correct across all serialization paths.


Detailed Findings

❌ API Approval — New public API lacks api-approved issue (merge-blocking)

This PR adds two new public properties to the ref assembly (System.Xml.ReaderWriter.cs):

// XmlAttributeAttributepubliccharSeparator{get{thrownull;}set{}}// XmlTextAttributepubliccharSeparator{get{thrownull;}set{}}

The linked issue (#115837) is a bug report with the area-Serialization label only — it has no api-approved, api-ready-for-review, or api-suggestion labels. Per dotnet/runtime API review process, all new public API surface must go through API review and receive the api-approved label before a PR can merge.

Action required: File a formal API proposal issue (or convert #115837) with the proposed API shape, get it through API review with the api-approved label, then link it to this PR. Alternatively, mark both Separator properties as internal pending API review.

Design questions that API review should address:

  • Should Separator on XmlAttributeAttribute have ' ' (space) as the default rather than '\0', since space is the current implicit separator?
  • How should values containing the separator character be handled? (Raised by @krwq in XML serialization of xs:list elements doesn't respect white-space separation #115837 but never resolved.)
  • Is char the right type, or would string? provide more flexibility (e.g., multi-character separators)?

⚠️ Code Quality — [AggressiveInlining] on validation methods (advisory)

In XmlReflectionImporter.cs (lines 2278–2290):

[MethodImpl(MethodImplOptions.AggressiveInlining)]privatestaticvoidValidateTextSeparatorChar(charseparator,stringmemberName){if(!XmlCharType.IsTextChar(separator))thrownewInvalidOperationException(...);}

AggressiveInlining is inappropriate for methods that throw exceptions on the non-fast path. The JIT already handles small methods well, and the attribute can cause the exception-throwing code to be inlined into the caller, bloating the caller's generated code. This contradicts the dotnet/runtime convention of extracting throw helpers into [DoesNotReturn] methods.

These methods are only called during type mapping (not in serialization hot paths), so the performance impact is nil either way — but removing AggressiveInlining would be more consistent with repo conventions.

✅ Correctness — All four serialization paths are consistent

I verified that the separator logic is applied consistently across all code paths:

PathWriter separator trackingReader split logic
ReflectionlastWasText bool, WriteValue(separatorStr)rawText.Split(separator)
Generated codelastWasTextVar string variable in emitted coderawText.Split(charLiteral) emitted
ILGenlastWasTextLoc LocalBuilderString.Split(char, StringSplitOptions) IL
Attribute (all paths)separator ?? ' ' fallbackSplit(separator) or Split((char[]?)null)

The ReadContentAsString usage in the ILGen path (vs Reader.ReadString() in generated code) is a pre-existing inconsistency — the original code before this PR already used ReadContentAsString in the ILGen path (the variable was misleadingly named XmlReader_ReadString). This PR correctly renamed it to XmlReader_ReadContentAsString for clarity but did not change the behavior.

✅ Correctness — Mixed-content separator tracking handles nulls correctly

The WriteElements method in ReflectionXmlSerializationWriter.cs correctly handles null items in mixed content:

  • When text != null && o is not null: writes text with separator, returns true
  • When text != null && o is null: falls through to return emitSeparator (line 313), preserving the separator state without writing anything
  • This is verified by the test case "a", null, "b""a,b" (no stray separator from null)

✅ Correctness — Default behavior preserved

When Separator is not set ('\0'):

  • XmlAttribute: attribute.Separator remains null, falling back to Split((char[]?)null) (whitespace split) and ' ' separator in writers — identical to pre-PR behavior
  • XmlText: text.Separator remains null, no split on read, no separator on write — identical to pre-PR behavior (concatenation)

✅ Validation — Correct char validity checks

  • ValidateTextSeparatorChar uses XmlCharType.IsTextChar → rejects <, &, ], control chars — correct for text content
  • ValidateAttributeSeparatorChar uses XmlCharType.IsAttributeValueChar → additionally rejects ", ', > — correct for attribute values
  • The test TypeWithXmlTextSeparatorQuote confirms " is valid as a text separator but invalid as an attribute separator ✅

✅ Test Coverage — Comprehensive

Tests cover: round-trip with comma/space/quote separators, empty strings between separators, single-element arrays, no-separator backward compatibility, invalid separator character rejection, mixed content with elements, null items in mixed content, XmlNode text mapping with separator rejection, and choice-based mixed content.

💡 Suggestion — WriteQuotedCSharpChar could use \u for non-ASCII chars

The WriteQuotedCSharpChar method handles control chars below 32 with \x escapes, but chars between 128-255 could theoretically produce ambiguous \x sequences in generated C# if followed by hex digits. Since validation rejects most problematic chars anyway, this is low risk, but using \u escapes (4-digit, unambiguous) would be more defensive:

<(char)32=> $"\\u{(int)value:X4}",

This is a non-blocking suggestion for robustness.

💡 Observation — Expected.SerializableAssembly.XmlSerializers.cs

The Expected.SerializableAssembly.XmlSerializers.cs file (~5300 lines changed) is a large auto-generated expected-output file that got renumbered because the two new test types shifted all generated method indices by +2. The %%ParentAssemblyId%% placeholder was also restored. This is expected mechanical churn from adding new serializable types.


Models contributing: Claude Opus 4.6 (primary), Claude Sonnet 4.5 (sub-agent). GPT-5.3-Codex sub-agent timed out after 10 minutes.

Generated by Code Review for issue #126767 ·

@mconnew

Copy link
Copy Markdown
Member

The initial statement:

[XmlAttribute] string[] uses space separation (val1 val2).

Isn't quite right. When reading, it accepts any whitespace character. This includes the typically known tab, form feed, carriage return, new line, and actual space, and any Unicode whitespace. This include unicode codespace of a language which has some more exotic whitespace characters, those are included too. It's conceivable that you want to accept spaces and tabs only, and don't want to allow new lines/carriage returns. Would it be better to have Separator be Separators and a string of characters you want to use for separators?

@mconnew

Copy link
Copy Markdown
Member

The implementation looks good to me based on the intended design. My only feedback is my previous comment, should we be accepting multiple delimiters as the existing functionality already does and this doesn't allow a subset to be used.

@StephenMolloyStephenMolloy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

@StephenMolloy

Copy link
Copy Markdown
Member

API Proposal for this is #129001

@leculverleculver 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.

Just one suggestion, but the test file doesn't compile.


object[] actualAll = (object[])field.GetValue(actual);
Assert.NotNull(actualAll);
Assert.Equal(expected, actualAll);

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.

Missing a }, so this doesn't compile/run through CI.

internal static void WriteQuotedCSharpChar(IndentedWriter writer, char value)
{
writer.Write("'");
string? escapedValue = value switch

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.

I think you also need to escape U+0085/U+2028/U+2029. C# treates these codepoints as newlines:

  • U+000D (CR) (handled with \r below)
  • U+000A (LF) (handled with \n below)
  • U+0085 (NEL)
  • U+2028 (LINE SEPARATOR)
  • U+2029 (PARAGRAPH SEPARATOR)

… curly bracket in tests that went missing on merge
CopilotAI review requested due to automatic review settings June 10, 2026 20:23

CopilotAI 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

Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.

@jkotas
jkotas deleted the copilot/add-xmltextattribute-separator-property branch July 3, 2026 18:51
@github-actionsgithub-actionsBot locked and limited conversation to collaborators Aug 3, 2026
Sign up for freeto subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XML serialization of xs:list elements doesn't respect white-space separation

5 participants

@mconnew@StephenMolloy@leculver