Search code examples
c#xmlxsdxml-serializationxml-generation

Dynamic XML serializer in C#


Passing the root to the xsd.exe, successfully generates the Classes with the proper structure according to the XSD,

we can now assign values to the objects of those classes and populate them, the question is how can we serialise them to XML output keeping the original


Solution

  • Your question is unclear, but perhaps your problem is as follows:

    • You have a List<object> of objects that you need to serialize to a single XML file.
    • When serialized, all the objects share an identical XML root element with identical root attributes.
    • So you would like to combine them by combining all the child nodes of all the root elements under a single shared root.

    I.e. if you have two objects that would individually serialize as:

    <CDS-XMLInterchange SchemaVersion="6-2-3" SchemaDate="2012-05-11" xmlns="http://www.nhsia.nhs.uk/DataStandards/XMLschema/CDS/ns">
      <CDSBulkGroup-160-Message CDSProtocolIdentifierCode="020" CDSBulkReplacementGroupCode="160" CDSTypeCode="011">
        <EmergencyCare>
          <PatientPathway>
            <PatientPathwayIdentity>
              <UniqueBookingReferenceNumber_Converted>bar</UniqueBookingReferenceNumber_Converted>
              <OrganisationIdentifier_PatientPathwayIdentifierIssuer>IdentifierIssuer bar</OrganisationIdentifier_PatientPathwayIdentifierIssuer>
            </PatientPathwayIdentity>
          </PatientPathway>
        </EmergencyCare>
      </CDSBulkGroup-160-Message>
    </CDS-XMLInterchange>
    
    <CDS-XMLInterchange SchemaVersion="6-2-3" SchemaDate="2012-05-11" xmlns="http://www.nhsia.nhs.uk/DataStandards/XMLschema/CDS/ns">
      <CDSBulkGroup-160-Message CDSProtocolIdentifierCode="020" CDSBulkReplacementGroupCode="160" CDSTypeCode="011">
        <EmergencyCare>
          <PatientPathway>
            <PatientPathwayIdentity>
              <UniqueBookingReferenceNumber_Converted>foo</UniqueBookingReferenceNumber_Converted>
              <OrganisationIdentifier_PatientPathwayIdentifierIssuer>IdentifierIssuer foo</OrganisationIdentifier_PatientPathwayIdentifierIssuer>
            </PatientPathwayIdentity>
          </PatientPathway>
        </EmergencyCare>
      </CDSBulkGroup-160-Message>
    </CDS-XMLInterchange>
    

    You would like to generate an XML file like the following:

    <?xml version="1.0" encoding="utf-8"?>
    <CDS-XMLInterchange SchemaVersion="6-2-3" SchemaDate="2012-05-11" xmlns="http://www.nhsia.nhs.uk/DataStandards/XMLschema/CDS/ns">
      <CDSBulkGroup-160-Message CDSProtocolIdentifierCode="020" CDSBulkReplacementGroupCode="160" CDSTypeCode="011">
        <EmergencyCare>
          <PatientPathway>
            <PatientPathwayIdentity>
              <UniqueBookingReferenceNumber_Converted>bar</UniqueBookingReferenceNumber_Converted>
              <OrganisationIdentifier_PatientPathwayIdentifierIssuer>IdentifierIssuer bar</OrganisationIdentifier_PatientPathwayIdentifierIssuer>
            </PatientPathwayIdentity>
          </PatientPathway>
        </EmergencyCare>
      </CDSBulkGroup-160-Message>
      <CDSBulkGroup-160-Message CDSProtocolIdentifierCode="020" CDSBulkReplacementGroupCode="160" CDSTypeCode="011">
        <EmergencyCare>
          <PatientPathway>
            <PatientPathwayIdentity>
              <UniqueBookingReferenceNumber_Converted>foo</UniqueBookingReferenceNumber_Converted>
              <OrganisationIdentifier_PatientPathwayIdentifierIssuer>IdentifierIssuer foo</OrganisationIdentifier_PatientPathwayIdentifierIssuer>
            </PatientPathwayIdentity>
          </PatientPathway>
        </EmergencyCare>
      </CDSBulkGroup-160-Message>
    </CDS-XMLInterchange>
    

    If so, the easiest approach may be to serialize each object to some intermediate XDocument, then combine them into the final XML document. The following extension methods do this:

    public static partial class XmlExtensions
    {
        /// Serialize a collection of items to a single XML File, combining their children under the root element of the first item
        public static void SerializeCollectionToCombinedXml<T>(this IEnumerable<T> collection, XmlWriter writer, Func<Type, XmlSerializer>? serializer = default, bool omitStandardNamespaces = false)
        {
            XName? firstRootName = null;
            
            foreach (var item in collection)
            {
                var doc = item.SerializeToXDocument(serializer, omitStandardNamespaces);
                if (doc == null || doc.Root == null)
                    continue;
                if (firstRootName == null)
                {
                    writer.WriteStartDocument();
                    writer.WriteStartElement(doc.Root.GetPrefixOfNamespace(doc.Root.Name.Namespace), doc.Root.Name.LocalName, doc.Root.Name.NamespaceName);
                    foreach (var attr in doc.Root.Attributes())
                    {
                        writer.WriteAttributeString(doc.Root.GetPrefixOfNamespace(attr.Name.Namespace), attr.Name.LocalName, attr.Name.NamespaceName, attr.Value);
                    }
                    firstRootName = doc.Root.Name;
                }
                else
                {
                    // TODO: decide whether to throw an exception if the current doc's root element differs from the first doc's root element;
                    //if (doc.Root.Name != firstRootName)
                    //  throw new XmlException("doc.Root.Name != firstRootName");
                }
                foreach (var node in doc.Root.Nodes())
                {
                    node.WriteTo(writer);
                }
            }
            
            if (firstRootName == null)
                throw new XmlException("Nothing written");
            else
            {
                writer.WriteEndElement();
                writer.WriteEndDocument();
            }
        }
    
        /// Serialize a collection of items to a single XDocument, combining their children under the root element of the first item
        public static XDocument SerializeCollectionToCombinedXDocument<T>(this IEnumerable<T> collection, Func<Type, XmlSerializer>? serializer = default, bool omitStandardNamespaces = false)
        {
            var doc = new XDocument();
            using (var writer = doc.CreateWriter())
            {
                collection.SerializeCollectionToCombinedXml(writer, serializer, omitStandardNamespaces);
            }
            return doc;
        }
        
        public static XDocument SerializeToXDocument<T>(this T obj, Func<Type, XmlSerializer>? serializer = default, bool omitStandardNamespaces = false)
        {
            XmlSerializerNamespaces? ns = null;
            if (omitStandardNamespaces)
                (ns = new XmlSerializerNamespaces()).Add("", ""); // Disable the xmlns:xsi and xmlns:xsd lines.
            return SerializeToXDocument(obj, serializer, ns);
        }
    
        public static XDocument SerializeToXDocument<T>(this T obj, Func<Type, XmlSerializer>? serializer, XmlSerializerNamespaces? ns)
        {
            var doc = new XDocument();
            using (var writer = doc.CreateWriter())
            {
                (serializer?.Invoke(obj?.GetType() ?? typeof(T)) ?? new XmlSerializer(obj?.GetType() ?? typeof(T))).Serialize(writer, obj, ns);
            }
            return doc;
        }       
    }
    
    public static partial class XmlExtensions
    {
        static readonly Dictionary<(Type, string), XmlSerializer> serializers = new();
    
        public static XmlSerializer GetSerializer(Type rootType, string includeClrNamespace)
        {
            lock (serializers)
            {
                if (!serializers.TryGetValue((rootType, includeClrNamespace), out var serializer))
                    serializer = serializers[(rootType, includeClrNamespace)] = CreateSerializer(rootType, includeClrNamespace);
                return serializer;
            }
        }
        
        static XmlSerializer CreateSerializer(Type rootType, string includeClrNamespace)
        {
            // Get all types in the GeneratedClasses namespace
            var generatedTypes = rootType.Assembly.GetTypes().Where(t => t.Namespace == includeClrNamespace).ToArray();
            return new XmlSerializer(rootType, generatedTypes);
        }
    }
    

    And then you would serialize your List<object> objects as follows:

    public static void Serialize<T>(List<T> objects, string filename)
    {
        using (var writer = XmlWriter.Create(filename, new XmlWriterSettings { Indent = true }))
        {
            objects.SerializeCollectionToCombinedXml(writer, t => XmlExtensions.GetSerializer(t, "GenericNamespace"), true);
        }
        // TODO: handle empty collections.
    }
    

    Notes:

    • As explained by Marc Gravell in his answer to Memory Leak using StreamReader and XmlSerializer, if you construct an XmlSerializer with any other constructor than new XmlSerializer(Type) or new XmlSerializer(Type, String), you must statically cache and reuse the serializer to prevent a severe memory leak. The above code does this via a static dictionary protected via lock statements. (This should also substantially improve performance.)

    • You may want to throw an exception if the serialized objects have different root element names and/or root element attributes. See the TODO in the above code for the location to put the necessary checks.

    • The extension method above throws an exception if the collection of objects is empty. You may want to modify that, e.g. by writing some default root element, or by deleting the incomplete empty file.

    • Since you are generating your list of objects from the rows of a DataTable dataTable, you might check to see whether simply writing the DataTable directly using DataTable.WriteXml(filename, XmlWriteMode.IgnoreSchema) meets your requirements.

    Demo fiddle here.