Repository navigation
.NET 6 XmlSerializer no longer emits line breaks #64885
Copy link
Copy link
Closed
Labels
Milestone
Description
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
Reacted by danielotterdahl, Alexander Dove, Kévin Chalet, gparmigiani and Vlad Belozerov
Activity
stephentoub commentedon Feb 7, 2022
Presumably this is #56524 and thus "by design".
@StephenMolloy?
RussKie commentedon Feb 7, 2022
Thank you. If it is an intended change it needs to be formally documented as a breaking change.
Wraith2 commentedon Feb 7, 2022
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?
kasperk81 commentedon Feb 7, 2022
falsetotruebut using a different apiXmlWriter.Createwith settingsxml apis have enough usage to avoid breaking changes like toggling defaults for performance reason.. code concerned with performance should already be using Indent=false.
Wraith2 commentedon Feb 7, 2022
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?
kasperk81 commentedon Feb 7, 2022
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.
Wraith2 commentedon Feb 7, 2022
So it broke users looking at the code but not any programs that use the data? is that right?
RussKie commentedon Feb 7, 2022
opcfoundation-org commentedon Mar 30, 2022
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".
More xml-related fixes
ChristophLindemann commentedon Jul 28, 2022
Please note, it is not only the indentation that has changed, but worse: it does now add a UTF8 BOM
See #63585
StephenMolloy commentedon Aug 13, 2022
Fixed by #73731