using System.Collections.Generic;
using System.Xml.Serialization;
using System.Runtime.Serialization;
using System.Runtime.Serialization.Formatters.Binary;
using System.IO.Compression;
public static partial class XNodeExtensions
public static XElement CopyAndGroupChildrenByColumns(this XElement root, Func<XName, int, bool> columnFilter, XName groupName) =>
.Select((row) => (row, key : row.Elements().Where((e, i) => columnFilter(e.Name, i)).Select(e => (e.Name, e.Value)).ToHashSet()))
.GroupByKeyAndSet(pair => pair.row.Name, pair => pair.key)
.Select(g => new XElement(g.Key.Key,
g.Key.Set.Select(p => new XElement(p.Name, p.Value)).Concat(g.Select(i => new XElement(groupName, i.row.Elements().Where((e, i) => !columnFilter(e.Name, i))))))));
public static IEnumerable<IGrouping<(TKey Key, HashSet<TItem> Set), TSource>> GroupByKeyAndSet<TSource, TKey, TItem>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TSource, HashSet<TItem>> setSelector) =>
Enumerable.GroupBy(source, (i) => (keySelector(i), setSelector(i)), new CombinedComparer<TKey, HashSet<TItem>>(null, HashSet<TItem>.CreateSetComparer()));
public class CombinedComparer<T1, T2> : IEqualityComparer<ValueTuple<T1, T2>>
readonly IEqualityComparer<T1> comparer1;
readonly IEqualityComparer<T2> comparer2;
public CombinedComparer(IEqualityComparer<T1> comparer1, IEqualityComparer<T2> comparer2) => (this.comparer1, this.comparer2) = (comparer1 ?? EqualityComparer<T1>.Default, comparer2 ?? EqualityComparer<T2>.Default);
public bool Equals(ValueTuple<T1, T2> x, ValueTuple<T1, T2> y) => comparer1.Equals(x.Item1, y.Item1) && comparer2.Equals(x.Item2, y.Item2);
public int GetHashCode(ValueTuple<T1, T2> obj) => HashCode.Combine(comparer1.GetHashCode(obj.Item1), comparer2.GetHashCode(obj.Item2));
public static partial class XNodeExtensions
public static XDocument ToXDocument(this DataTable dt, XmlWriteMode mode = XmlWriteMode.IgnoreSchema)
var doc = new XDocument();
using (var writer = doc.CreateWriter())
dt.WriteXml(writer, mode);
public static void Test()
Console.WriteLine("Testing grouping of DataTable XML:");
TestDataTable(GetOriginalXml(), GetExpectedXml());
Console.WriteLine("\nTesting grouping of XML with mixed row names:");
var doc = XDocument.Parse(GetXml());
var d1 = TestByIndex(doc);
var d2 = TestByName(doc);
Assert.That(XNode.DeepEquals(d1.Root, d2.Root));
public static void TestDataTable(string originalXml, string expectedXml)
var dt = GetDataTable(originalXml);
var doc = dt.ToXDocument(XmlWriteMode.IgnoreSchema);
XName groupName = doc.Root.Name.Namespace + "BID_INTERVALS";
var grouped = doc.Root.CopyAndGroupChildrenByColumns((n, i) => (i < 4), groupName);
var newDoc = new XDocument(grouped);
Console.WriteLine(newDoc);
var expectedXElement = XElement.Parse(expectedXml);
Assert.That(XNode.DeepEquals(grouped, XElement.Parse(expectedXml)));
public static XDocument TestByIndex(XDocument doc)
var ns = doc.Root.Name.Namespace;
XName groupName = ns + "BID_INTERVALS";
var newRoot = doc.Root.CopyAndGroupChildrenByColumns((n, i) => (i < 4), groupName);
var newDoc = new XDocument(newRoot);
Console.WriteLine(newDoc);
public static XDocument TestByName(XDocument doc)
var ns = doc.Root.Name.Namespace;
XName groupName = ns + "BID_INTERVALS";
HashSet<XName> names = new (new [] { ns + "Col1", ns + "Col2", ns + "Col3", ns + "Date" } );
var newRoot = doc.Root.CopyAndGroupChildrenByColumns((n, i) => names.Contains(n), groupName);
var newDoc = new XDocument(newRoot);
Console.WriteLine(newDoc);
static DataTable GetDataTable(string originalXml)
using var reader = new StringReader(originalXml);
static string GetOriginalXml() => @"<ROOT>
static string GetExpectedXml() => @"<ROOT>
static string GetXml() => @"<ROOT xmlns=""defaultNameSpace"" foo=""bar"">
public static void Main()
Console.WriteLine("Environment version: {0} ({1}, {2})", System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription , Environment.Version, Environment.OSVersion);
Console.WriteLine("Failed with unhandled exception: ");