Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
Add Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767
Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization#126767Conversation
…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>
Separator property to XmlTextAttribute and XmlAttributeAttribute for configurable list serialization
… 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>
There was a problem hiding this comment.
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
Separatorproperties toXmlTextAttributeandXmlAttributeAttribute, 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
| File | Description |
|---|---|
src/libraries/System.Xml.ReaderWriter/ref/System.Xml.ReaderWriter.cs |
Adds the new public API surface to the ref assembly. |
src/libraries/System.Runtime.Serialization.Xml/tests/SerializationTypes.RuntimeOnly.cs |
Adds serializer test model types for separator scenarios. |
src/libraries/System.Private.Xml/tests/XmlSerializer/XmlSerializerTests.RuntimeOnly.cs |
Adds runtime-only XmlSerializer tests for custom separators and validation. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs |
Adds XmlTextAttribute.Separator. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriterILGen.cs |
Updates IL-generated writer paths for custom separators. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationWriter.cs |
Updates source-generated writer paths and adds char literal emission helper. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.cs |
Updates IL-generated reader paths to split on custom separators. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cs |
Updates source-generated reader paths for custom separator parsing. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationGeneratedCode.cs |
Exposes the new char-literal helper to generated-code infrastructure. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs |
Imports separator metadata and validates separator chars. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributeAttribute.cs |
Adds XmlAttributeAttribute.Separator. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs |
Updates reflection-based writer paths for custom separators. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cs |
Updates reflection-based reader paths for custom separator parsing. |
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.cs |
Adds separator storage to text/attribute accessors. |
src/libraries/System.Private.Xml/src/Resources/Strings.resx |
Adds 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 { } } |
There was a problem hiding this comment.
Once code-reviewed within the team, (but before merging of course,) we will go through the API approval process.
🤖 Copilot Code Review — PR #126767Note This review was generated by GitHub Copilot. Holistic AssessmentMotivation: This PR adds a Approach: The implementation is thorough, touching all four serialization code paths (reflection reader/writer, IL-gen reader/writer, and the C#-source-gen reader/writer). The API design using a Summary: ❌ Needs Changes. This PR introduces new public API surface ( Detailed Findings❌ API Approval — No linked
|
Throw InvalidOperationException when XmlTextAttribute.Separator is set on a member that also has [XmlElement] or [XmlAnyElement] mappings. The writer inserts separators between all array items, so on mixed-content members this would emit separator characters around element nodes and corrupt the round-trip. Add tests for both [XmlText]+[XmlElement] and [XmlText]+[XmlAnyElement] mixed-content cases in both ILGen and ReflectionOnly serializer modes. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Track whether the previous array item was written as text in all three writers (Reflection, ILGen, source-gen) so that separators are only emitted between consecutive text items in mixed content arrays. Remove the mixed-content guard from XmlReflectionImporter since the separator now works correctly with mixed content. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Cover three coverage gaps surfaced during the test review: - Mixed content WITHOUT a Separator (default path through the new lastWasText/curIsText tracking, verifies no regression for the unconfigured case). - Null item between two text items in a separator-tracking array (verifies the recently fixed text-branch null guard and that null preserves lastWasText so the next text item still gets its leading separator). - Null item between an element and a text item (verifies the status-quo return preserves lastWasText=false so the following text item does NOT get a stray leading separator). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Keep generated serializers from clearing the text separator state when a choice-identifier item writes nothing, matching the reflection writer behavior. Add coverage for null mixed-content items with XmlChoiceIdentifier. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 17 out of 18 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (1)
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlReflectionImporter.cs:1626
Separatorbecomes part of theAttributeAccessorstate, butReconcileAccessorstill only compares mapping/default when deduplicating qualified attributes. If two different members use the same qualified attribute name/type with different separators, the second import will silently reuse the first accessor and inherit its separator. That makes one of the serializers emit/parse the wrong wire format depending on import order.
if (a.XmlAttribute.Separator != '\0')
{
ValidateSeparatorChar(a.XmlAttribute.Separator, accessorName);
attribute.Separator = a.XmlAttribute.Separator;
}
attribute.Default = GetDefaultValue(model.FieldTypeDesc, model.FieldType, a);
attribute.Any = (a.XmlAnyAttribute != null);
if (attribute.Form == XmlSchemaForm.Qualified && attribute.Namespace != ns)
{
_xsdAttributes ??= new NameTable();
attribute = (AttributeAccessor)ReconcileAccessor(attribute, _xsdAttributes);
XmlText with a SpecialMapping target (XmlNode arrays / mixed content with typeof(XmlNode)) silently breaks round-trip when Separator is set: the writer emits separator-joined text, but the reader's SpecialMapping path creates a single XmlNode from the entire text run with no split. Reject that combination at mapping time and tighten the Separator XML doc to state it applies to arrays of strings. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…es for XML text and attribute handling Co-authored-by: Copilot <copilot@github.com>
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static void ValidateTextSeparatorChar(char separator, string memberName) | ||
| { | ||
| if (!XmlCharType.IsTextChar(separator)) | ||
| throw new InvalidOperationException(SR.Format(SR.XmlInvalidTextSeparatorChar, memberName)); | ||
| } | ||
|
|
||
| [MethodImpl(MethodImplOptions.AggressiveInlining)] | ||
| private static void ValidateAttributeSeparatorChar(char separator, string memberName) | ||
| { | ||
| if (!XmlCharType.IsAttributeValueChar(separator)) | ||
| throw new InvalidOperationException(SR.Format(SR.XmlInvalidAttributeSeparatorChar, memberName)); | ||
| } |
[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-inSeparatorproperty to both attributes so users can control the separator character.API Changes
XmlTextAttribute— newchar Separator { get; set; }(default'\0'= no separator, preserves existing concatenation behavior):XmlAttributeAttribute— samechar Separator { get; set; }(default'\0'= use existing space behavior):'\0'(null char) is the "not set" sentinel — chosen becausechar?is not valid as a C# attribute argument type (CS0655).Implementation
XmlTextAttribute/XmlAttributeAttribute: addchar SeparatorpropertyMappings.cs: addchar? SeparatortoTextAccessorandAttributeAccessor(internal;char?is valid here)XmlReflectionImporter: wire attribute → accessor, validate withXmlConvert.VerifyXmlChars()(rejects XML-illegal chars like\x01;'\0'sentinel skips validation)ReflectionXmlSerializationWriter,XmlSerializationWriter,XmlSerializationWriterILGen): whenTextAccessor.Separator.HasValue, emit items with separator between them; for attributes, useSeparator ?? ' 'ReflectionXmlSerializationReader,XmlSerializationReader,XmlSerializationReaderILGen): whenTextAccessor.Separator.HasValue, split text on separator and populate array; usesstring.Split(char)overload (notchar[]) for IL-gen compatibilitySystem.Xml.ReaderWriterref assembly: updated with new public surfaceBackward Compatibility
[XmlText] string[]with noSeparator→ unchanged concatenation (val1val2)[XmlAttribute] string[]with noSeparatoroverride → unchanged space separation (val1 val2)XML_TypeWithXmlTextAttributeOnArraycontinues to assertval1val2Original 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
IsListboolean, the solution is to add achar? Separatorproperty to bothXmlTextAttributeandXmlAttributeAttribute, allowing users to opt into list-style serialization with a configurable separator character.Key design points:
XmlTextAttribute.Separator— defaults tonull(meaning no separator, preserving current concatenation behavior of[XmlText] string[]). Setting e.g.Separator = ' 'opts into space-separated list serialization for element text content.XmlAttributeAttribute.Separator— defaults to' '(preserving existing space-separated behavior for[XmlAttribute] string[]). Users can override to a different separator if desired.Type should be
char?(nullable char), wherenullmeans "no separator / use default behavior". This eliminates multi-character separator edge cases and is simple to validate and emit.Validation: Use
XmlConvert.VerifyXmlChars()on the separator character at reflection/import time (inXmlReflectionImporter). TheXmlWriterlayer already handles escaping of characters like<,&,>etc. in both attribute values and text content, so those are safe — they'll be entity-escaped in the output. The only truly dangerous characters are those illegal in XML 1.0 entirely (like\0), whichVerifyXmlCharscatches.Implementation Guide
Files that need changes:
Public API surface:
src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlTextAttribute.cs— Addpublic char? Separator { get; set; }property (default:null)src/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlAttributes.cs— TheXmlAttributeAttributeclass needs the samepublic char? Separator { get; set; }property (default:' ')Internal mapping infrastructure:
src/libraries/System.Private.Xml/src/System/Xml/Serialization/Mappings.cs—TextAccessoris currently an empty class (internal sealed class TextAccessor : Accessor { }). Add anIsListproperty (orSeparatorproperty) mirroring whatAttributeAccessoralready has with itsIsListbool. 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— InImportAccessorMapping, around line 1630-1640, where[XmlText]on array-like types creates aTextAccessor, wire up the separator from the attribute to the accessor. Validate the separator character here usingXmlConvert.VerifyXmlChars(). Also around line 1595, whereisListis computed for attributes, incorporate the newSeparatorproperty fromXmlAttributeAttribute.Serialization writers (emitting the separator during write):
src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationWriter.cs— InWriteMember, the attributeIsListpath already writes" "between values. Add analogous logic forTextAccessorwhen 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):
IsListdeserialization currently splits on whitespace and apply similar logic for text content.src/libraries/System.Private.Xml/src/System/Xml/Serialization/ReflectionXmlSerializationReader.cssrc/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReader.cssrc/libraries/System.Private.Xml/src/System/Xml/Serialization/XmlSerializationReaderILGen.csWhat NOT to change:
[XmlAttribute] string[]default behavior must remain space-separated (backward compatible)[XmlText] string[]default behavior must remain concatenated with no separator (backward compatible)XML_TypeWithXmlTextAttributeOnArrayassertsval1val2concatenation — this must continue to passTests 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