riskfirst / riskfirst.hateoas

Powerful HATEOAS functionality for .NET web api
MIT License
78 stars 25 forks source link

XmlOutputFormatter not able to serialize LinkContainer as it has Dictionary<string, Link> resulting in a 406 #17

Closed vamseemudradi7 closed 5 years ago

vamseemudradi7 commented 5 years ago

Need to solve this through use of SerializableDictionary (instead of using Dictionary) with the help of https://stackoverflow.com/questions/67959/net-xml-serialization-gotchas or https://msdn.microsoft.com/en-us/library/gg496181.aspx in your source code in LinkContainer.cs and ILinkContainer.cs

dnikolovv commented 5 years ago

Alright, this one is a bit tricky.

I'm assuming that we want to have resources serialized like

<?xml version="1.0"?>
<account>
    <account_number>12345</account_number>
    <balance currency="usd">100.00</balance>
    <link rel="deposit" href="/accounts/12345/deposit" />
    <link rel="withdraw" href="/accounts/12345/withdraw" /> 
    <link rel="transfer" href="/accounts/12345/transfer" />
    <link rel="close" href="/accounts/12345/close" />
</account>

and not

<?xml version="1.0"?>
<account>
    <account_number>12345</account_number>
    <balance currency="usd">100.00</balance>
    <links>
        <link rel="deposit" href="/accounts/12345/deposit" />
        <link rel="withdraw" href="/accounts/12345/withdraw" /> 
        <link rel="transfer" href="/accounts/12345/transfer" />
        <link rel="close" href="/accounts/12345/close" />
    </links>
</account>

since that appears to be the most widely spread approach.

You see, it turns out that while serializing a class, you can't skip wrapping a property into its own XML element unless that property implements IEnumerable. (or at least I couldn't find a way to do so)

If we use SerializableDictionary, we will always get the second result.

It seems that the simplest approach to getting a proper result would be to wrap the current links dictionary into a LinkCollection. E.g.

public class LinkCollection : IEnumerable<Link>
{
    private readonly Dictionary<string, Link> links = new Dictionary<string, Link>();

    public int Count { get; set; }

    public void Add(string name, Link link) =>
        links.Add(name, link);

    [Obsolete("This method is here only for the XML serialization to work. It should not be used.", true)]
    public void Add(Link link) =>
       throw new NotImplementedException();

    public bool ContainsKey(string key) =>
        links.ContainsKey(key);

    public IEnumerator<Link> GetEnumerator() =>
        links.Values.GetEnumerator();

    IEnumerator IEnumerable.GetEnumerator() =>
        GetEnumerator();

    public Link this[string name]
    {
        get => links[name];
        set => links[name] = value;
    }
}

This will get us what we want but it comes with another downside.

XmlSerializer will not work on types implementing IEnumerable if they don't publicly expose an Add(object obj) method.

This means we must expose an Add(Link link) method.

Fortunately, the ObsoleteAttribute allows us to emit a compile-time error if someone were to use it, so besides being kind of ugly, it doesn't bring any other problems.

Switch Dictionary<string, Link> with LinkCollection and you now get proper serialization in the form of

<?xml version="1.0" encoding="UTF-8"?>
<TestLinkContainer xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
   <link rel="test" hrev="test" method="GET" />
   <link rel="test" hrev="test" method="GET" />
   <link rel="test" hrev="test" method="GET" />
   <link rel="test" hrev="test" method="GET" />
   <Id>123</Id>
</TestLinkContainer>

This is only at first sight. If you have any other ideas, please share. Otherwise, if @jam13c approves of this approach, I could dig deeper and submit a PR.