1

Given this XML, I need to deserialize it to a class. I'm not sure how to distinguish between the two 'MealPeriod' elements of different types, but of the same name.

<SalesSummary>
  <MealPeriod1>Lunch</MealPeriod1>
  <MealPeriod1Sales>6447.58</MealPeriod1Sales>
  <MealPeriod2>Dinner</MealPeriod2>
  <MealPeriod2Sales>12074.04</MealPeriod2Sales>
  <MealPeriod3>Late-Night</MealPeriod3>
  <MealPeriod3Sales>5156.90</MealPeriod3Sales>
  <MealPeriod4></MealPeriod4>
  <MealPeriod4Sales>0.00</MealPeriod4Sales>
  <MealPeriod1>
    <Interval>
      <Name>10:00am-11:00am</Name>
      <Checks>1</Checks>
      <Guests>1</Guests>
      <AvgCheck>$31.50</AvgCheck>
      <AvgGuest>$31.50</AvgGuest>
      <Sales>$31.50</Sales>
    </Interval>
    <Interval>
      <Name>11:00am-12:00pm</Name>
      <Checks>8</Checks>
      <Guests>29</Guests>
      <AvgCheck>$85.22</AvgCheck>
      <AvgGuest>$23.51</AvgGuest>
      <Sales>$681.75</Sales>
    </Interval>
    <Interval>
      <Name>12:00pm-01:00pm</Name>
      <Checks>27</Checks>
      <Guests>53</Guests>
      <AvgCheck>$48.76</AvgCheck>
      <AvgGuest>$24.84</AvgGuest>
      <Sales>$1,316.58</Sales>
    </Interval>
    <Interval>
      <Name>01:00pm-02:00pm</Name>
      <Checks>17</Checks>
      <Guests>35</Guests>
      <AvgCheck>$58.76</AvgCheck>
      <AvgGuest>$28.54</AvgGuest>
      <Sales>$999.00</Sales>
    </Interval>
    <Interval>
      <Name>02:00pm-03:00pm</Name>
      <Checks>22</Checks>
      <Guests>31</Guests>
      <AvgCheck>$38.99</AvgCheck>
      <AvgGuest>$27.67</AvgGuest>
      <Sales>$857.75</Sales>
    </Interval>
    <Interval>
      <Name>03:00pm-04:00pm</Name>
      <Checks>21</Checks>
      <Guests>44</Guests>
      <AvgCheck>$33.76</AvgCheck>
      <AvgGuest>$16.11</AvgGuest>
      <Sales>$709.00</Sales>
    </Interval>
    <Interval>
      <Name>04:00pm-05:00pm</Name>
      <Checks>32</Checks>
      <Guests>55</Guests>
      <AvgCheck>$57.88</AvgCheck>
      <AvgGuest>$33.67</AvgGuest>
      <Sales>$1,852.00</Sales>
    </Interval>
    <Totals>
      <Checks>128</Checks>
      <Guests>248</Guests>
      <AvgCheck>$50.37</AvgCheck>
      <AvgGuest>$26.00</AvgGuest>
      <Sales>$6,447.58</Sales>
    </Totals>
  </MealPeriod1>
  <MealPeriod2>
    <Interval>
      <Name>05:00pm-06:00pm</Name>
      <Checks>36</Checks>
      <Guests>71</Guests>
      <AvgCheck>$47.85</AvgCheck>
      <AvgGuest>$24.26</AvgGuest>
      <Sales>$1,722.75</Sales>
    </Interval>
    <Interval>
      <Name>06:00pm-07:00pm</Name>
      <Checks>40</Checks>
      <Guests>79</Guests>
      <AvgCheck>$49.01</AvgCheck>
      <AvgGuest>$24.81</AvgGuest>
      <Sales>$1,960.25</Sales>
    </Interval>
    <Interval>
      <Name>07:00pm-08:00pm</Name>
      <Checks>46</Checks>
      <Guests>82</Guests>
      <AvgCheck>$51.03</AvgCheck>
      <AvgGuest>$28.63</AvgGuest>
      <Sales>$2,347.29</Sales>
    </Interval>
    <Interval>
      <Name>08:00pm-09:00pm</Name>
      <Checks>53</Checks>
      <Guests>80</Guests>
      <AvgCheck>$42.04</AvgCheck>
      <AvgGuest>$27.85</AvgGuest>
      <Sales>$2,228.25</Sales>
    </Interval>
    <Interval>
      <Name>09:00pm-10:00pm</Name>
      <Checks>39</Checks>
      <Guests>68</Guests>
      <AvgCheck>$46.94</AvgCheck>
      <AvgGuest>$26.92</AvgGuest>
      <Sales>$1,830.50</Sales>
    </Interval>
    <Interval>
      <Name>10:00pm-11:00pm</Name>
      <Checks>39</Checks>
      <Guests>56</Guests>
      <AvgCheck>$50.90</AvgCheck>
      <AvgGuest>$35.45</AvgGuest>
      <Sales>$1,985.00</Sales>
    </Interval>
    <Totals>
      <Checks>253</Checks>
      <Guests>436</Guests>
      <AvgCheck>$47.72</AvgCheck>
      <AvgGuest>$27.69</AvgGuest>
      <Sales>$12,074.04</Sales>
    </Totals>
  </MealPeriod2>
  <MealPeriod3>
    <Interval>
      <Name>11:00pm-12:00am</Name>
      <Checks>35</Checks>
      <Guests>54</Guests>
      <AvgCheck>$35.80</AvgCheck>
      <AvgGuest>$23.20</AvgGuest>
      <Sales>$1,253.04</Sales>
    </Interval>
    <Interval>
      <Name>12:00am-01:00am</Name>
      <Checks>34</Checks>
      <Guests>45</Guests>
      <AvgCheck>$38.21</AvgCheck>
      <AvgGuest>$28.87</AvgGuest>
      <Sales>$1,299.16</Sales>
    </Interval>
    <Interval>
      <Name>01:00am-02:00am</Name>
      <Checks>23</Checks>
      <Guests>45</Guests>
      <AvgCheck>$40.30</AvgCheck>
      <AvgGuest>$20.60</AvgGuest>
      <Sales>$926.87</Sales>
    </Interval>
    <Interval>
      <Name>02:00am-03:00am</Name>
      <Checks>30</Checks>
      <Guests>59</Guests>
      <AvgCheck>$48.38</AvgCheck>
      <AvgGuest>$24.60</AvgGuest>
      <Sales>$1,451.33</Sales>
    </Interval>
    <Interval>
      <Name>03:00am-04:00am</Name>
      <Checks>1</Checks>
      <Guests>6</Guests>
      <AvgCheck>$226.50</AvgCheck>
      <AvgGuest>$37.75</AvgGuest>
      <Sales>$226.50</Sales>
    </Interval>
    <Totals>
      <Checks>123</Checks>
      <Guests>209</Guests>
      <AvgCheck>$41.93</AvgCheck>
      <AvgGuest>$24.67</AvgGuest>
      <Sales>$5,156.90</Sales>
    </Totals>
  </MealPeriod3>
</SalesSummary>

Here is my class so far. Using multiple XmlElement attributes with the same name isn't going to work, but I'm not sure how to decorate the class properties in a way that will. Any pointers would be much appreciated. Thanks!

public partial class SalesSummary
{
[XmlElement("MealPeriod1")]
public List<SalesSummaryMealPeriod1> MealPeriod1List { get; set; }

[XmlElement("MealPeriod1")]
public string MealPeriod1 { get; set; }

[XmlElement("MealPeriod1Sales")]
public string MealPeriod1Sales { get; set; }

[XmlElement("MealPeriod2")]
public List<SalesSummaryMealPeriod2> MealPeriod2List { get; set; }

[XmlElement("MealPeriod2")]
public string MealPeriod2 { get; set; }

[XmlElement("MealPeriod2Sales")]
public string MealPeriod2Sales { get; set; }

[XmlElement("MealPeriod3")]
public List<SalesSummaryMealPeriod3> MealPeriod3List { get; set; }

[XmlElement("MealPeriod3")]
public string MealPeriod3 { get; set; }

[XmlElement("MealPeriod3Sales")]
public string MealPeriod3Sales { get; set; }

[XmlElement("MealPeriod4")]
public List<SalesSummaryMealPeriod4> MealPeriod4List { get; set; }

[XmlElement("MealPeriod4")]
public string MealPeriod4 { get; set; }

[XmlElement("MealPeriod4Sales")]
public string MealPeriod4Sales { get; set; }
}

Note that the MealPeriodx elements are all fixed.

dbc
  • 104,963
  • 20
  • 228
  • 340
Justin Lutsky
  • 263
  • 3
  • 9
  • You may need to deserialize "by hand", by scanning the xml recursively with your favorite parser, and populating the model as appropriate. With that approach, it's easy to disinguish between an element with content, and a element with sub-elements. – glenebob Apr 25 '18 at 19:02
  • Is the max number of `` elements fixed or unlimited? – dbc Apr 25 '18 at 20:38
  • Possibly useful: [Correct XML serialization and deserialization of “mixed” types in .NET](https://stackoverflow.com/q/2567414/3744182). – dbc Apr 25 '18 at 22:09
  • @dbc MealPeriodx elements are all fixed. – Justin Lutsky Apr 26 '18 at 02:16

1 Answers1

1

Each of your <MealPeriodx> elements consists of mixed content, and may contain:

  • A string value;
  • A sequence of <Interval> elements;
  • A <Totals> element.

Building on the answer to Correct XML serialization and deserialization of "mixed" types in .NET by Stefan, you can bind such an element to a c# type as follows:

public class MealPeriod
{
    // A polymorphic array of "mixed" types.
    // See https://stackoverflow.com/questions/2567414/correct-xml-serialization-and-deserialization-of-mixed-types-in-net
    // and the solution by Stefan, https://stackoverflow.com/users/307747/stefan
    // for why this works.
    [XmlElement("Interval", typeof(Interval))]
    [XmlElement("Totals", typeof(Totals))]
    [XmlText(typeof(string))]
    public List<object> Items { get; set; }
}

public class Interval
{
    public string Name { get; set; }
    public int Checks { get; set; }
    public int Guests { get; set; }
    public string AvgCheck { get; set; }
    public string AvgGuest { get; set; }
    public string Sales { get; set; }
}

public class Totals
{
    public int Checks { get; set; }
    public int Guests { get; set; }
    public string AvgCheck { get; set; }
    public string AvgGuest { get; set; }
    public string Sales { get; set; }
}

Then, you can define a root object SalesSummary as follows, where each MealPeriodx property must be a collection of MealPeriod objects, since each <MealPeriodx> appears multiple times in the source XML:

public class SalesSummary
{
    [XmlElement("MealPeriod1")]
    public List<MealPeriod> MealPeriod1 { get; set; }

    [XmlElement("MealPeriod1Sales")]
    public decimal MealPeriod1Sales { get; set; }

    [XmlElement("MealPeriod2")]
    public List<MealPeriod> MealPeriod2 { get; set; }

    [XmlElement("MealPeriod2Sales")]
    public decimal MealPeriod2Sales { get; set; }

    [XmlElement("MealPeriod3")]
    public List<MealPeriod> MealPeriod3 { get; set; }

    [XmlElement("MealPeriod3Sales")]
    public decimal MealPeriod3Sales { get; set; }

    [XmlElement("MealPeriod4")]
    public List<MealPeriod> MealPeriod4 { get; set; }

    [XmlElement("MealPeriod4Sales")]
    public decimal MealPeriod4Sales { get; set; }
}

public static class MealPeriodExtensions
{
    public static string MealPeriodName(this IEnumerable<MealPeriod> mealPeriods)
    {
        if (mealPeriods == null)
            return null;
        return String.Concat(mealPeriods.Where(m => m.Items != null).SelectMany(m => m.Items).OfType<string>());
    }

    public static IEnumerable<T> MealPeriodItems<T>(this IEnumerable<MealPeriod> mealPeriods)
    {
        if (mealPeriods == null)
            return Enumerable.Empty<T>();
        return mealPeriods.Where(m => m.Items != null).SelectMany(m => m.Items).OfType<T>();
    }
}

Then, given a deserialization helper method such as

public static class XmlSerializationHelper
{
    public static T LoadFromXml<T>(this string xmlString, XmlSerializer serial = null)
    {
        serial = serial ?? new XmlSerializer(typeof(T));
        using (var reader = new StringReader(xmlString))
        {
            return (T)serial.Deserialize(reader);
        }
    }
}

You can do:

var summary = xml.LoadFromXml<SalesSummary>();

// Prints:
// MealPeriod1 name: "Lunch"
Console.WriteLine("MealPeriod1 name: \"{0}\"", summary.MealPeriod1.MealPeriodName());

// Prints 
// MealPeriod1 Total Sales: "$6,447.58"
Console.WriteLine("MealPeriod1 Total Sales: \"{0}\"", summary.MealPeriod1.MealPeriodItems<Totals>().Single().Sales);   

Notes:

  • If you re-serialize SalesSummary to XML, then the two <MealPeriodx> elements will be shifted to become adjacent to each other. There is no easy way to avoid this using XmlSerializer, other than implementing IXmlSerializable or manually constructing an [XmlAnyElement] surrogate array as shown in this answer.

    Luckily your question only asks for deserialization.

  • Interval and Totals have many common properties so you might want to have them share a common base class or interface.

Sample working .Net fiddle here.

dbc
  • 104,963
  • 20
  • 228
  • 340