-2

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

Ariox66
  • 620
  • 2
  • 9
  • 29
  • The following may be helpful: https://stackoverflow.com/a/73640395/10024425 and https://stackoverflow.com/a/72589790/10024425 – Tu deschizi eu inchid Feb 15 '23 at 03:39
  • 1
    From the error message I would say your problem is that you try to serialize multiple objects without an encapsulating root element. – FLUXparticle Feb 15 '23 at 05:33
  • @FLUXparticle that's a good point, I thought so as well. I found the root attribute and looped through the objects, if it's a root, then write start element once and one end element outside loop, if it's not then start and end elements inside the loop. it works and generates the XML. but I don't think this is the best idea – Ariox66 Feb 15 '23 at 05:43
  • A well form Xml document has only one root tag. Xml specification allows for arrays, but the Net library doesn't default to arrays and gives errors. In your code objects.GetType() is an array. You have two choices 1) Wrap the array in a root. 2) Add to you writer the following setting : Settings settings = new XmlWriterSettings(); settings.ConformanceLevel = ConformanceLevel.Fragment; – jdweng Feb 15 '23 at 07:59
  • I cannot reproduce this, see https://dotnetfiddle.net/Fxz1Vl. `XmlSerializer` has no problem serializing an array of objects as the root XML node. Please [edit] to share a [mcve]. My only guess is that perhaps your `objects` object implements `IXmlSerializable` -- and does so wrongly. – dbc Feb 16 '23 at 15:41
  • @dbc thanks for your response, I put the entire code on https://dotnetfiddle.net/#&togetherjs=qyjyuc6xeF not sure how can I put the XSD files there for you to see – Ariox66 Feb 16 '23 at 23:43
  • @dbc added the XSD files here: https://www.filemail.com/d/lcvcnyfjgztyjmq – Ariox66 Feb 16 '23 at 23:54
  • @dbc the goal was to create classes at runtime, create objects dynamically, assign values received from SQL server to the objects when name matches, serialize to XML – Ariox66 Feb 17 '23 at 00:05
  • Any chance you could share a [mcve] **in the question itself**, [not as an external link](https://meta.stackoverflow.com/q/254428/3744182)? Your [Full code fiddle](https://www.filemail.com/d/lcvcnyfjgztyjmq) only links to the XSD files not to any code, and the link https://dotnetfiddle.net/#&togetherjs=qyjyuc6xeF only brings up an empty fiddle. Absent a [mcve] I'm not sure how your question can be answered even with a bounty. – dbc Feb 21 '23 at 18:32
  • I manually converted your XSD files to C# classes and I still can't reproduce your problem, see https://dotnetfiddle.net/wyJLah. – dbc Feb 21 '23 at 21:32
  • @dbc sorry about the mess up, I fixed the link. the full code is not that long, only converts the xsds to classes, creates objects and assign values. my issue is when creating objects I lose the hierarchy and structure. that's why serializer doesn't build the output according to the XSDs – Ariox66 Feb 21 '23 at 23:57
  • Still nothing. If I go to https://dotnetfiddle.net/#&togetherjs=qyjyuc6xeF I get an empty fiddle + a somewhat untrustworthy-looking prompt to "Join TogetherJS Session", see https://i.stack.imgur.com/24wqs.jpg. Even if I agree to do so the fiddle is still empty, see https://i.stack.imgur.com/CQpan.jpg. Absent code that reproduces the problem I can't see how we can help you. Did you remember to save you fiddle? If you don't, everything you do is simply cached in your session cookies and so is invisible to the rest of us. – dbc Feb 22 '23 at 02:26
  • @dbc this was the first time ever I used fiddle, sorry about that. let me add all the code here – Ariox66 Feb 22 '23 at 03:08
  • 1
    OK, your problem is that you are creating a single `XmlWriter` and serializing multiple objects to it. But a well-formed XML document must have one and only one [root element](https://en.wikipedia.org/wiki/Root_element) so `XmlWriter` will not allow you to do that. If you are trying to create a well-formed XML document you will need to create a container c# object, and serialize that. – dbc Feb 22 '23 at 03:24
  • Or are you willing to create a malformed XML file with multiple roots? – dbc Feb 22 '23 at 03:27
  • 1
    Incidentally (this is unrelated to your current problem) you should statically cache your `XmlSerializer(typeof(object), expectedTypes.ToArray())` serializer and only construct it once, for reasons described in [Memory Leak using StreamReader and XmlSerializer](https://stackoverflow.com/q/23897145). – dbc Feb 22 '23 at 03:30
  • @dbc you're right, I needed to create the objects dynamically inside a container and assign values to them and pass a single container to the serializer. I did that and now looks better. what should I do to avoid the headers repeating for each row? need them only once. things like document version, time generated etc. – Ariox66 Feb 22 '23 at 03:33
  • What does your XML look like now, and how do you want it to look? – dbc Feb 22 '23 at 03:45
  • @dbc if I select one row it looks ok and matches the XSD structure. but if I pass multiple rows of data it repeats the header fields as well. – Ariox66 Feb 22 '23 at 04:05
  • @Ariox66 - I have no way of reproducing that problem because I don't have access to your SQL Server and cannot select anything. But is this what you want? https://dotnetfiddle.net/JRubJa – dbc Feb 22 '23 at 04:24
  • @dbc the goal is to create objects dynamically while keeping their structure, assign values when their name matches with the column names of the data we get from SQL Server. for test purpose you can have "select 'ABCD' as AmbulanceCallIdentifier " as query. our problem is when we have multiple rows in our select statement the header fields get generated multiple times in the output xml. how is this normally avoided? – Ariox66 Feb 23 '23 at 02:00
  • Your terminology may be confusing people. By "header row" do you perhaps mean the [XML root element](https://en.wikipedia.org/wiki/Root_element)? Are you trying to combine multiple XML documents into one by concatenating all the child elements of the root elements of each document under one root element? – dbc Feb 27 '23 at 17:08
  • By the way, did you try simply writing the DataTable directly using [DataTable.WriteXml(filename, XmlWriteMode.IgnoreSchema)`](https://learn.microsoft.com/en-us/dotnet/api/system.data.datatable.writexml?view=net-7.0#system-data-datatable-writexml(system-string-system-data-xmlwritemode))? – dbc Feb 27 '23 at 20:34

1 Answers1

1

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.

dbc
  • 104,963
  • 20
  • 228
  • 340