Skip to content

.NET 6 XmlSerializer no longer emits line breaks #64885

Closed

Description

@RussKie

Description

This appears to be a behavioural regression in .NET 6 in the way XML is serialized. The following code no longer produces the same result in .NET 6 as it was producing in .NET Framework. .NET Core 3.1 and .NET 5.

            using TextWriter tw = new StreamWriter(path, false);
            XmlSerializer serializer = new(typeof(MyType));
            serializer.Serialize(tw, obj);

I was unable to find any breaking docs related to this.

Reproduction Steps

csproj
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFrameworks>net472;net5.0;net6.0</TargetFrameworks>
    <LangVersion>latest</LangVersion>
  </PropertyGroup>

</Project>
Program.cs
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Xml.Serialization;

namespace XmlSerializerNet6
{
    class Program
    {
        static void Main(string[] args)
        {
            List<TranslationItem> items = new();
            items.Add(new(name: "$this", property: "Text", source: "MyName"));

            List<TranslationCategory> categories = new();

            TranslationCategory category = new(name: "MyForm", sourceLanguage: "en", targetLanguage: "");
            category.Body = new() { TranslationItems = items };
            categories.Add(category);

            TranslationFile file = new()
            {
                 GitExVersion = "33.33.33",
                 SourceLanguage = "en",
                 TargetLanguage = "ru",
                 TranslationCategories = categories
            };

            Serialize(file, "test.xlf");
        }

        public static void Serialize(TranslationFile translation, string path)
        {
            using TextWriter tw = new StreamWriter(path, false);
            XmlSerializer serializer = new(typeof(TranslationFile));
            serializer.Serialize(tw, translation);
        }
    }

    /// <summary>Provides a translation for a specific language.</summary>
    [XmlRoot("xliff")]
    public class TranslationFile
    {
        public TranslationFile()
        {
            Version = "1.0";
            TranslationCategories = new List<TranslationCategory>();
        }

        [XmlAttribute("version")]
        public string Version { get; set; }

        [XmlAttribute("GitExVersion")]
        public string? GitExVersion { get; set; }

        [XmlIgnore]
        public string? SourceLanguage { get; set; }

        [XmlIgnore]
        public string? TargetLanguage { get; set; }

        [XmlElement(ElementName = "file")]
        public List<TranslationCategory> TranslationCategories { get; set; }

    }

    [DebuggerDisplay("{" + nameof(Name) + "}")]
    public class TranslationCategory : IComparable<TranslationCategory>
    {
        public TranslationCategory()
        {
        }

        public TranslationCategory(string name, string? sourceLanguage, string? targetLanguage = null)
        {
            Name = name;
            SourceLanguage = sourceLanguage;
            TargetLanguage = targetLanguage;
        }

        [XmlAttribute("datatype")]
        public string DataType { get; set; } = "plaintext";

        [XmlAttribute("original")]
        public string? Name { get; set; }

        [XmlAttribute("source-language")]
        public string? SourceLanguage { get; set; }

        [XmlAttribute("target-language")]
        public string? TargetLanguage { get; set; }

        [XmlElement(ElementName = "body")]
        public TranslationBody Body { get; set; } = new();

        public int CompareTo(TranslationCategory other) => StringComparer.InvariantCulture.Compare(Name, other.Name);
    }

    public class TranslationBody
    {
        public TranslationBody()
        {
            TranslationItems = new List<TranslationItem>();
        }

        [XmlElement(ElementName = "trans-unit")]
        public List<TranslationItem> TranslationItems { get; set; }
    }

    [DebuggerDisplay("{Name}.{Property}={Value}")]
    public class TranslationItem : IComparable<TranslationItem>, ICloneable
    {
        public TranslationItem()
        {
        }

        public TranslationItem(string name, string property, string source)
        {
            Name = name;
            Property = property;
            Source = source;
        }

        public TranslationItem(string? name, string? property, string? source, string? value)
        {
            Name = name;
            Property = property;
            Source = source;
            Value = value;
        }

        [XmlIgnore]
        public string? Name { get; set; }

        [XmlIgnore]
        public string? Property { get; set; }

        [XmlAttribute("id")]
        public string Id
        {
            get => Name + "." + Property;
            set
            {
                var values = value.Split(new char[] { '.' }, 2);
                Name = values[0];
                Property = values.Length > 1 ? values[1] : "";
            }
        }

        [XmlElement("source")]
        public string? Source { get; set; }

        [XmlElement("target")]
        public string? Value { get; set; }

        public int CompareTo(TranslationItem other)
        {
            int val = string.Compare(Name, other.Name, StringComparison.Ordinal);
            if (val == 0)
            {
                val = string.Compare(Property, other.Property, StringComparison.Ordinal);
            }

            return val;
        }

        object ICloneable.Clone()
        {
            return Clone();
        }

        public TranslationItem Clone()
        {
            return new TranslationItem(Name, Property, Source, Value);
        }
    }
}

Expected behavior

.NET Framework. .NET Core 3.1 and .NET 5 produce the following:

<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="1.0" GitExVersion="33.33.33">
  <file datatype="plaintext" original="MyForm" source-language="en" target-language="">
    <body>
      <trans-unit id="$this.Text">
        <source>MyName</source>
      </trans-unit>
    </body>
  </file>
</xliff>

Actual behavior

.NET 6 produces the following:

<?xml version="1.0" encoding="utf-8"?><xliff xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" version="1.0" GitExVersion="33.33.33"><file datatype="plaintext" original="MyForm" source-language="en" target-language=""><body><trans-unit id="$this.Text"><source>MyName</source></trans-unit></body></file></xliff>

Regression?

Yes, in .NET 6.0.

Known Workarounds

A previously working code has to be rewritten to something like the following:

            XmlWriterSettings xmlWriterSettings = new()
            {
                Indent = true
            };
            XmlSerializer serializer = new(typeof(TranslationFile));
            using XmlWriter xmlWriter = XmlWriter.Create(path, xmlWriterSettings);
            serializer.Serialize(xmlWriter, translation);

However this may not always be possible (e.g., XML serialisation lives in a 3P component) or practical.

Configuration

No response

Other information

No response

Activity

  1. ghost added
    untriagedNew issue has not been triaged by the area owner
    on Feb 7, 2022
  2. stephentoub commented on Feb 7, 2022

    @stephentoub
    Member

    Presumably this is #56524 and thus "by design".
    @StephenMolloy?

  3. RussKie commented on Feb 7, 2022

    @RussKie
    ContributorAuthor

    Thank you. If it is an intended change it needs to be formally documented as a breaking change.

  4. Wraith2 commented on Feb 7, 2022

    @Wraith2
    Contributor

    I thought that indentation was only for human consumers and that being a serialization format it was nice to have and not essential. What does the lack of indentation break other than readability?

  5. kasperk81 commented on Feb 7, 2022

    @kasperk81
    Contributor
    • suddenly api default has changed to output minified instead of indented content, without updating the breaking change doc.
    • the usage is not as simple as changing false to true but using a different api XmlWriter.Create with settings

    xml apis have enough usage to avoid breaking changes like toggling defaults for performance reason.. code concerned with performance should already be using Indent=false.

  6. Wraith2 commented on Feb 7, 2022

    @Wraith2
    Contributor

    It's a change but why is is a breaking change? What is it breaking? What exception occurs as a result of there being no whitespace?

  7. kasperk81 commented on Feb 7, 2022

    @kasperk81
    Contributor

    human readable for "you in your own silo" is maybe not that interesting but imagine you have a website that displays xml output off of this serializer, you planned with your team to upgraded to .net6, worked through list of breaking changes, only to discover it still broke user experience of your website audience.

  8. Wraith2 commented on Feb 7, 2022

    @Wraith2
    Contributor

    So it broke users looking at the code but not any programs that use the data? is that right?

  9. RussKie commented on Feb 7, 2022

    @RussKie
    ContributorAuthor
  10. added
    Priority:3Work that is nice to have
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Mar 17, 2022
  11. opcfoundation-org commented on Mar 30, 2022

    @opcfoundation-org

    This change broke my code too, We generate XML files that are stored in git and we rely on git diff to check for unexpected changes. The serialization is done by a NuGet package that I can't easily get a fix for. This is definitely a breaking change that needs more justification than a "performance improvement".

  12. added a commit that references this issue on Apr 18, 2022
  13. ChristophLindemann commented on Jul 28, 2022

    @ChristophLindemann

    Please note, it is not only the indentation that has changed, but worse: it does now add a UTF8 BOM
    See #63585

  14. added this to the 7.0.0 milestone on Jul 28, 2022
  15. StephenMolloy commented on Aug 13, 2022

    @StephenMolloy
    Contributor

    Fixed by #73731

  16. ghost locked as resolved and limited conversation to collaborators on Sep 12, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions