How to XML-serialize a dictionary

Viewed 85372

I have been able to serialize an IEnumerable this way:

[XmlArray("TRANSACTIONS")]
[XmlArrayItem("TRANSACTION", typeof(Record))]
public IEnumerable<BudgetRecord> Records
{
    get 
    {
        foreach(Record br in _budget)
        {
            yield return br;
        }
    }
}

However, I realised that now I need a dictionary containing a collection Dictionary<string, RecordCollection> (RecordCollection implements IEnumerable).

How can I achieve that?

6 Answers

Please try this alternative easy way:

    void Main()
    {
        var model = new Foo() 
        { 
            RealDictionary = new Dictionary<string, int> { {"A", 23}, {"B", 40} } 
        };
        model.RealDictionary.Add("C", 69);
        
        using (var writer = XmlWriter.Create("c:\\test1.xml"))
            (new XmlSerializer(typeof(Foo))).Serialize(writer, model);
    }
    
    [Serializable]
    public class Foo
    {
        [XmlArray(ElementName ="Elements")]
        [XmlArrayItem(ElementName = "Element")]
        public List<KeyValueElement> SerializableDictionary 
        { 
            get { return RealDictionary.Select(x => new KeyValueElement {Key = x.Key, Value = x.Value}).ToList(); } 
            set { RealDictionary = value.ToDictionary(x=> x.Key, x => x.Value); }
        }
    
        [XmlIgnore]
        public Dictionary<string, int> RealDictionary {get;set;}  
    }
    
    [Serializable]
    public class KeyValueElement
    {
        public string Key { get; set; }
        public int Value { get; set; }  
    }

Here the xml result:

    <Foo xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema">
        <Elements>
            <Element>
                <Key>A</Key><Value>23</Value>
            </Element>
            <Element>
                <Key>B</Key><Value>40</Value>
            </Element>
            <Element>
                <Key>C</Key><Value>69</Value>
            </Element>
        </Elements>
    </Foo>

This SerializableDictionary class can use like a normal Dictionary, and can be Serialize()

Gist

SerializableDictionary.cs

using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.Schema;
using System.Xml.Serialization;

/// <summary>
/// Base on https://weblogs.asp.net/pwelter34/444961
/// </summary>
/// <typeparam name="TKey"></typeparam>
/// <typeparam name="TValue"></typeparam>
[XmlRoot("dictionary")]
public class SerializableDictionary<TKey, TValue> : Dictionary<TKey, TValue>, IXmlSerializable
{
    // XmlSerializer.Deserialize() will create a new Object, and then call ReadXml()
    // So cannot use instance field, use class field.

    public static string itemTag = "item";
    public static string keyTag = "key";
    public static string valueTag = "value";

    public XmlSchema GetSchema()
    {
        return null;
    }

    public void ReadXml(XmlReader reader)
    {
        if (reader.IsEmptyElement)
            return;

        var keySerializer = new XmlSerializer(typeof(TKey));
        var valueSerializer = new XmlSerializer(typeof(TValue));

        reader.ReadStartElement();

        // IsStartElement() will call MoveToContent()
        // reader.MoveToContent();
        while (reader.IsStartElement(itemTag))
        {
            reader.ReadStartElement(itemTag);

            reader.ReadStartElement(keyTag);
            TKey key = (TKey)keySerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadStartElement(valueTag);
            TValue value = (TValue)valueSerializer.Deserialize(reader);
            reader.ReadEndElement();

            reader.ReadEndElement();
            this.Add(key, value);

            // IsStartElement() will call MoveToContent()
            // reader.MoveToContent();
        }
        reader.ReadEndElement();
    }

    public void WriteXml(XmlWriter writer)
    {
        var keySerializer = new XmlSerializer(typeof(TKey));
        var valueSerializer = new XmlSerializer(typeof(TValue));

        foreach (var kvp in this)
        {
            writer.WriteStartElement(itemTag);

            writer.WriteStartElement(keyTag);
            keySerializer.Serialize(writer, kvp.Key);
            writer.WriteEndElement();

            writer.WriteStartElement(valueTag);
            valueSerializer.Serialize(writer, kvp.Value);
            writer.WriteEndElement();

            writer.WriteEndElement();
        }
    }
}

Demo.cs

using System;
using System.IO;
using System.Xml.Serialization;

//[XmlRoot("AnimalDictionary")]
public class SDict : SerializableDictionary<string, string>
{
}

namespace Test
{
    public class Program
    {

        static void Main(string[] args)
        {
            var dict = new SDict();
            //SDict.itemTag = "AnimalCount";
            //SDict.keyTag = "Type";
            //SDict.valueTag = "Count";
            dict.Add("Cat", "1");
            dict.Add("Dog", "2");

            foreach (var kvp in dict)
                Console.WriteLine(kvp);

            XmlSerializer serializer = new XmlSerializer(typeof(SDict));

            using (var writer = new StreamWriter("dict.xml"))
            {
                serializer.Serialize(writer, dict);
            }

            Console.WriteLine("\nXML File:");
            Console.WriteLine(File.ReadAllText("dict.xml"));

            using (var reader = new StreamReader("dict.xml"))
            {
                dict = serializer.Deserialize(reader) as SDict;
            }

            Console.WriteLine("\nAfter Deserialize");

            foreach (var kvp in dict)
                Console.WriteLine(kvp);

            Console.ReadLine();
        }
    }
}

If you are learning C# you might as well create a class with the logic you want. In this example I made a ProgressiveTax object that you can call .Evaluate() with to calculate taxes.

You can also write or read from XML strings (which can be written to files)

For example, first populate the tax brackets from PAYE information provided and save to a file PAYE.xml. Then forget the tax brackets (out of scope from { }). And then read the file to populate the tax table from a file

static class Program
{
    static void Main(string[] args)
    {
        {   
            // create a tax table and save it to a file
            var tax = ProgressiveTax.PAYE();
            File.WriteAllText("PAYE.xml", tax.ToXml());
        }
        {   
            // read a tax table from a file
            var tax = ProgressiveTax.FromXml(File.ReadAllText("PAYE.xml"));

            // use the tax table
            var x = tax.Evaluate(42250m);
            Debug.WriteLine($"Tax={x}");
        }            
    }
}

The xml file looks like this, which can be edited manually, or be generated from a database/webservice.

<?xml version="1.0" encoding="utf-16"?>
<ProgressiveTax xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Credit="2400">
  <Brackets>
    <Bracket>
      <Item1>0</Item1>
      <Item2>0.1</Item2>
    </Bracket>
    <Bracket>
      <Item1>24000</Item1>
      <Item2>0.15</Item2>
    </Bracket>
    <Bracket>
      <Item1>40667</Item1>
      <Item2>0.2</Item2>
    </Bracket>
    <Bracket>
      <Item1>57334</Item1>
      <Item2>0.25</Item2>
    </Bracket>
  </Brackets>
</ProgressiveTax>

and the class that actually holds the tax information and calculates the tax amount

public class ProgressiveTax
{
    public ProgressiveTax()
    {
        this.Table = new SortedDictionary<decimal, float>();
    }
    public ProgressiveTax(SortedDictionary<decimal, float> table)
    {
        this.Table = table;
    }

    public static ProgressiveTax PAYE()
    {
        var tax = new ProgressiveTax();
        tax.Credit = 2400m;
        tax.Table[0m] = 0.1f;
        tax.Table[24000m] = 0.15f;
        tax.Table[40667m] = 0.20f;
        tax.Table[57334m] = 0.25f;
        return tax;
    }

    public string ToXml()
    {
        var fs = new StringWriter();
        var xs = new XmlSerializer(typeof(ProgressiveTax));
        xs.Serialize(fs, this);
        fs.Close();
        return fs.ToString();
    }

    public static ProgressiveTax FromXml(string xml)
    {
        var fs = new StringReader(xml);
        var xs = new XmlSerializer(typeof(ProgressiveTax));
        var tax = xs.Deserialize(fs) as ProgressiveTax;
        fs.Close();
        return tax;
    }

    [XmlAttribute]
    public decimal Credit { get; set; }
  
    [XmlIgnore()]  
    SortedDictionary<decimal, float> Table { get; }

    [XmlArrayItem(ElementName = "Bracket")]
    public (decimal lower, float rate)[] Brackets
    {
        get
        {
            var parts = new (decimal lower, float rate)[Table.Count];
            int index = 0;
            foreach (var item in Table)
            {
                parts[index++] = (item.Key, item.Value);
            }
            return parts;
        }
        set
        {
            Table.Clear();
            foreach (var (lower, rate) in value)
            {
                Table[lower] = rate;
            }
        }
    }

    public decimal Evaluate(decimal income)
    {
        decimal result = -Credit;
        foreach (var item in Table.Reverse())
        {
            if (item.Key <= income)
            {
                Debug.WriteLine($"Assess {item.Value:P2} tax on {income - item.Key}");
                result += (decimal)( item.Value * (float) (income - item.Key));
                income = item.Key;
            }
        }
        return Math.Max(0m, result);
    }
}

The sample program produces the following output in the debugger.

Assess 20.00% tax on 1583
Assess 15.00% tax on 16667
Assess 10.00% tax on 24000
Tax=2816.65

If you add up 1583 + 16667 + 24000 = 42250 which the total amount of income. Since this is a progressive tax, the above rates and amounts are used, and then 2400 is credited. Also not the result must be 0 or positive.

Related