1

I've searched for hours and found similar results but not for this specific scenario. Consider the following XML file.

<root>
<largeImages>
    <largeImage>
        <url>./imageLarge.jpg</url>
        <height>480</height>
        <width>640</width>
    </largeImage>
</largeImages>
<smallImages>
    <smallImage>
        <url>./imageSmall.jpg</url>
        <height>240</height>
        <width>320</width>
    </smallImage>
</smallImages>
</root>

What I'm trying to do is deserialize this into a single array of images instead of 2 arrays.

public class root {
    [XmlArray("largeImages")]
    [XmlArrayItem("largeImage")]
    public image[] largeImages { get; set; }

    [XmlArray("smallImages")]
    [XmlArrayItem("smallImage")]
    public image[] smallImages { get; set; }
}

This class gets me 2 arrays. root.largeImages and root.smallImages. Since my application doesn't care about large or small images I'd like to deserialize into the single array root.images. I've tried variations of XmlArray, XmlArrayItem, XmlElement and even XmlChoiceIdentifier without any success. I'm thinking something along the lines of the following which won't compile because apparently the XmlArrayAttribute can only be used once per property.

[XmlArray("largeImages")]
[XmlArray("smallImages")]
[XmlArrayItem("largeImage")]
[XmlArrayItem("smallImage")]
public image[] images { get; set; }

Obviously I could merge the 2 arrays in code after the XML is deserialized but it seems like this should be a simple thing to do.

Brad R.
  • 13
  • 5
  • Does this help? [How to define multiple names for XmlElement field?](https://stackoverflow.com/q/24707399). That's – dbc Apr 09 '18 at 23:34
  • 1
    @dbc - I did see that post before creating this one. It lead me to a good exploration of XmlChoiceIdentifier but the problem is that it only seems to work with elements, not arrays. In my experiment with it I created a 3rd class for the largeImages/smallImages elements but still ended up with 2 arrays. root.images[0].images and root.images[1].images. – Brad R. Apr 10 '18 at 18:54

4 Answers4

1

XPATH is probably your answer assuming you don't really care about having it mapped to a class. XPath wildcards on node name gives an example of how you'd select multiple items - http://csharp.net-tutorials.com/xml/using-xpath-with-the-xmldocument-class/ gives an example of how it's used in C#.

Another way muight be using XSLT: Using the code: How to apply an XSLT Stylesheet in C# and XSLT like Combining elements from 2 lists in XSLT should get you what you want.

Personally I'd go with whatever makes life easiest since it doesn't look like you really care in this example what the intermediate data structure is.

cyborg
  • 5,638
  • 1
  • 19
  • 25
  • Thank you cyborg. I was hoping for a simple way to control it during the deserialization process with Xml attributes. If that turns out to just not be possible, the idea of transforming it with XSLT might work. – Brad R. Apr 10 '18 at 18:39
0

Simple!!! I do it all the time. The trick is with root that you need to use Elements() and then use FirstOrDefault() to get only one.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml;
using System.Xml.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        const string FILENAME = @"c:\temp\test.xml";
        static void Main(string[] args)
        {
            XDocument doc = XDocument.Load(FILENAME);

            Root root = doc.Elements("root").Select(x => new Root() {
                Images = x.Descendants("largeImage").Select(z => new Image() {
                    url = (string)z.Element("url"),
                    height = (int)z.Element("height"),
                    width = (int)z.Element("width")
                }).ToList()
            }).FirstOrDefault();

            root.Images.AddRange(doc.Descendants("smallImage").Select(z => new Image() {
                    url = (string)z.Element("url"),
                    height = (int)z.Element("height"),
                    width = (int)z.Element("width")
                }).ToList());
        }
    }
    public class Root
    {
        public List<Image> Images { get; set; }

    }
    public class Image
    {
        public string url { get; set; }
        public int height { get; set; }
        public int width { get; set; }
    }
}
jdweng
  • 33,250
  • 2
  • 15
  • 20
  • Thank you jdweng. However, the result is still the same as just using the XmlSerializer with my original class. The goal is to use XmlSerializer and Xml attributes and end up with a single array containing both sets of images. – Brad R. Apr 10 '18 at 18:42
  • Change code to use a List<> instead of an array so I can add both type images to same object. – jdweng Apr 10 '18 at 19:25
  • That does indeed load all the images into a single list. I think it could be a good choice for a simple data model like my sample. For a more complex data model with dozens of properties I think using XmlSerializer would be a better way to go. – Brad R. Apr 10 '18 at 22:10
0

Given that the answer from How to define multiple names for XmlElement field? works primarily for elements not arrays, the easiest solution would seem to be to introduce a surrogate property for one of the image elements, say <smallImages>:

public class root 
{
    [XmlArray("largeImages")]
    [XmlArrayItem("largeImage")]
    public List<image> Images { get; set; } = new List<image>();

    [XmlArray("smallImages")]
    [XmlArrayItem("smallImage")]
    [Browsable(false), EditorBrowsable(EditorBrowsableState.Never), DebuggerBrowsable(DebuggerBrowsableState.Never)]
    public List<image> SmallImagesSurrogate { get { return Images; } }

    public bool ShouldSerializeSmallImagesSurrogate() { return false; }
}

Notes:

  • When deserializing to a pre-allocated List<T>, XmlSerializer will simply append deserialized elements to the existing list. That allows elements from both <largeImages> and <smallImages> to be concatenated into the same collection without data loss.

  • The surrogate property must be public but can be made "less visible" by setting attributes such as [Browsable(false)].

  • XmlSerializer can successfully deserialize get-only collection properties as long as the collection is pre-allocated. I took advantage of this to avoid adding a setter for the surrogate.

  • In case you need to re-serialize your root, the method ShouldSerializeSmallImagesSurrogate() will prevent the images array from being double-serialized. (For an explanation of why, see ShouldSerialize*() vs *Specified Conditional Serialization Pattern.) Instead all the images will serialize under <largeImages>. This method does not affect deserialization.

Sample working .Net fiddle.

dbc
  • 104,963
  • 20
  • 228
  • 340
  • This is a brilliant solution and seems to do exactly what is desired. One thing I don't understand is why smallImages gets deserialized into Images instead of SmallImagesSurrogate. Is it because using a getter only that returns the Images property causes the compiler to use the same underlying variable for both properties? – Brad R. Apr 10 '18 at 22:28
  • @BradR. - Yes. `SmallImagesSurrogate` just returns the value of the property `Images` and so has no backing field of its own. Here's another example: [Are “proxy properties” good style?](https://stackoverflow.com/q/87459). See also [Automatic vs Explicit Properties](https://blogs.msdn.microsoft.com/ericlippert/2009/01/14/automatic-vs-explicit-properties/) and [What is the difference between a field and a property?](https://stackoverflow.com/q/295104). – dbc Apr 10 '18 at 22:41
0

I had a challenge with model serialization, and thanks to MSDN I found the answer. Here is my solution:

public class Document
{
    [XmlElement(ElementName = "seller_id")]
    public string SellerId { get; set; }

    [XmlArray(ElementName = "order_details")]
    [XmlArrayItem(Type = typeof(SgtinCode), ElementName = "sgtin")]
    [XmlArrayItem(Type = typeof(SsccCode), ElementName = "sscc")]
    public Code[] Codes { get; set; }
}

public abstract class Code
{
    [XmlText]
    public string Value { get; set; }
}

public class SgtinCode : Code
{ }

public class SsccCode : Code
{ }

Model setup:

var document = new Document
{
    SellerId = Guid.NewGuid().ToString(),
    Codes = new Code[]
    {
        new SsccCode { Value = "111700126101510000000000011" },
        new SsccCode { Value ="111700126101510000000000012" },
        new SsccCode { Value ="111700126101510000000000013" },
        new SgtinCode { Value = "abc" }
    }
}

And XML output:

<?xml version="1.0" encoding="utf-8"?>
<documents>
  <foreign_shipment>
    <seller_id>fb2d35e7-c5d1-43ad-a272-89f897f41058</seller_id>
    <order_details>
      <sscc>111700126101510000000000011</sscc>
      <sscc>111700126101510000000000012</sscc>
      <sscc>111700126101510000000000013</sscc>
      <sgtin>abc</sgtin>
    </order_details>
  </foreign_shipment>
</documents>
Max K.
  • 1
  • 2