using System.Collections.Generic;
using System.Xml.Serialization;
using System.Threading.Tasks;
using System.Diagnostics.CodeAnalysis;
public static partial class XmlExtensions
const string FirstCommentText = "first";
const string FirstComment = $"<!--{FirstCommentText}-->";
const string SubsequentCommentText = "subsequent";
const string SubsequentComment = $"<!--{SubsequentCommentText}-->";
static Encoding Utf8EncodingNoBom { get; } = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
public static void SerializeFragmentsToXml<T>(this IEnumerable<T> enumerable, Stream stream, XmlSerializer? serializer = null, XmlSerializerNamespaces? ns = null)
var settings = new XmlWriterSettings()
ConformanceLevel = ConformanceLevel.Fragment,
NamespaceHandling = NamespaceHandling.Default,
Encoding = Utf8EncodingNoBom,
NewLineHandling = NewLineHandling.Replace,
OmitXmlDeclaration = true,
WriteEndDocumentOnClose = false,
NewLineOnAttributes = false,
serializer ??= new XmlSerializer(typeof(T));
ns ??= new XmlSerializerNamespaces(new[] { XmlQualifiedName.Empty });
using var innerTextWriter = new StreamWriter(stream, encoding : Utf8EncodingNoBom, leaveOpen : true) { NewLine = newLine };
using var textWriter = new FakeCommentRemovingTextWriter(innerTextWriter, new(FirstComment, ""), new(SubsequentComment, newLine)) { NewLine = newLine };
using var xmlWriter = XmlWriter.Create(textWriter, settings);
foreach (var item in enumerable)
xmlWriter.WriteComment(first ? FirstCommentText : SubsequentCommentText);
serializer.Serialize(xmlWriter, item, ns);
private class FakeCommentRemovingTextWriter : TextWriterDecorator
readonly KeyValuePair<string, string> [] replacements;
public FakeCommentRemovingTextWriter(TextWriter baseWriter, params KeyValuePair<string, string> [] replacements) : base(baseWriter, true) => this.replacements = replacements;
public override void Write(ReadOnlySpan<char> buffer)
foreach (var replacement in replacements)
if ((index = StartsWithIgnoringWhitespace(buffer, replacement.Key)) >= 0)
base.Write(buffer.Slice(0, index));
buffer = buffer.Slice(index).Slice(replacement.Key.Length);
if (buffer.StartsWith(NewLine))
buffer = buffer.Slice(NewLine.Length);
if (!string.IsNullOrEmpty(replacement.Value))
base.Write(replacement.Value);
static int StartsWithIgnoringWhitespace(ReadOnlySpan<char> buffer, ReadOnlySpan<char> value)
for (int index = 0; index < buffer.Length; index++)
if (buffer.Slice(index).StartsWith(value))
if (!XmlConvert.IsWhitespaceChar(buffer[index]) || index >= buffer.Length - value.Length)
public class TextWriterDecorator : TextWriter
readonly bool disposeBase;
readonly Encoding baseEncoding;
public TextWriterDecorator(TextWriter baseWriter, bool disposeBase = true) =>
(this.baseWriter, this.disposeBase, this.baseEncoding) = (baseWriter ?? throw new ArgumentNullException(nameof(baseWriter)), disposeBase, baseWriter.Encoding);
protected TextWriter BaseWriter => baseWriter == null ? throw new ObjectDisposedException(GetType().Name) : baseWriter;
public override Encoding Encoding => baseEncoding;
public override IFormatProvider FormatProvider => baseWriter?.FormatProvider ?? base.FormatProvider;
[AllowNull] public override string NewLine
get => baseWriter?.NewLine ?? base.NewLine;
baseWriter.NewLine = value;
public override void Flush() => BaseWriter.Flush();
public sealed override void Close() => Dispose(true);
public override void Write(char value) => BaseWriter.Write(value);
public sealed override void Write(char[] buffer, int index, int count) => this.Write(buffer.AsSpan(index, count));
public override void Write(ReadOnlySpan<char> buffer) => BaseWriter.Write(buffer);
public sealed override void Write(string? value) => Write(value.AsSpan());
public override Task WriteAsync(char value) => BaseWriter.WriteAsync(value);
public sealed override Task WriteAsync(string? value) => WriteAsync(value.AsMemory());
public sealed override Task WriteAsync(char[] buffer, int index, int count) => WriteAsync(buffer.AsMemory(index, count));
public override Task WriteAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default) => BaseWriter.WriteAsync(buffer, cancellationToken);
public override Task WriteLineAsync(char value) => BaseWriter.WriteLineAsync(value);
public sealed override Task WriteLineAsync(string? value) => WriteLineAsync(value.AsMemory());
public override Task WriteLineAsync(StringBuilder? value, CancellationToken cancellationToken = default) => BaseWriter.WriteLineAsync(value, cancellationToken);
public sealed override Task WriteLineAsync(char[] buffer, int index, int count) => WriteLineAsync(buffer.AsMemory(index, count));
public override Task WriteLineAsync(ReadOnlyMemory<char> buffer, CancellationToken cancellationToken = default) => BaseWriter.WriteLineAsync(buffer, cancellationToken);
public override Task FlushAsync() => BaseWriter.FlushAsync();
public override Task FlushAsync(CancellationToken cancellationToken) => BaseWriter.FlushAsync(cancellationToken);
protected override void Dispose(bool disposing)
if (Interlocked.Exchange(ref this.baseWriter, null) is {} writer)
public override async ValueTask DisposeAsync()
if (Interlocked.Exchange(ref this.baseWriter, null) is {} writer)
await writer.DisposeAsync().ConfigureAwait(false);
await writer.FlushAsync().ConfigureAwait(false);
await base.DisposeAsync().ConfigureAwait(false);
public override string ToString() => string.Format("{0}: {1}", GetType().Name, baseWriter?.ToString() ?? "disposed");
[System.Xml.Serialization.XmlTypeAttribute(TypeName = "flyingMonkey", Namespace=null)]
public class FlyingMonkey
[System.Xml.Serialization.XmlAttributeAttribute()]
public static FlyingMonkey Create(string? name = null) =>
new Limb() { name = "leg" }, new Limb() { name = "arm" },
new Limb() { name = "tail" }, new Limb() { name = "wing" },
[System.Xml.Serialization.XmlTypeAttribute(TypeName = "limb", Namespace=null)]
[System.Xml.Serialization.XmlAttributeAttribute()]
public static void Test()
var items = new [] { "Koko", "POCO", "Loco" }.Select(n => FlyingMonkey.Create(n));
using var stream = new MemoryStream();
var serializer = new XmlSerializer(typeof(FlyingMonkey), defaultNamespace: null);
items.SerializeFragmentsToXml(stream, serializer : serializer);
var xml = Encoding.UTF8.GetString(stream.GetBuffer().AsSpan(0, (int)stream.Length));
public static void Main()
Console.WriteLine("Environment version: {0} ({1}, {2}, NewLine: {3}).",
System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion,
System.Text.Json.JsonSerializer.Serialize(Environment.NewLine));
Console.WriteLine("Failed with unhandled exception: ");