1
votes

Attempting to create a Web API service to allow my website to access external XML feeds via javascript (avoiding same origin policy issues). These feeds return raw XML as seen below (captured with Fiddler):

Request:

GET http://wxdata.weather.com/wxdata/search/search?where=london HTTP/1.1
Host: wxdata.weather.com
Connection: Keep-Alive

Response:

HTTP/1.1 200 OK
Server: Apache-Coyote/1.1
Content-Type: text/xml;charset=UTF-8
X-Varnish: 3773154810
X-Cache-Hits: 0
Cache-Control: max-age=55
Date: Wed, 27 Nov 2013 19:52:24 GMT
Content-Length: 504
Connection: keep-alive

<search ver="3.0">
          <loc id="UKXX0085" type="1">London, GLA, United Kingdom</loc><loc id="USAL2905" type="1">London, AL</loc><loc id="USAR0340" type="1">London, AR</loc><loc id="USCA9301" type="1">London, CA</loc><loc id="USIN2109" type="1">London, IN</loc><loc id="USKY1090" type="1">London, KY</loc><loc id="USMI2014" type="1">London, MI</loc><loc id="USMN2182" type="1">London, MN</loc><loc id="USMO2769" type="1">London, MO</loc><loc id="USOH0520" type="1">London, OH</loc>
        </search>

I wish to employ the Content Negotiation feature of Web API. However, I'm not sure which type the controller should return given the raw XML feed of the response.

public class SearchController : ApiController
{
    public string Get(string location)
    {
        HttpClient client = new HttpClient();
        client.BaseAddress = new Uri("http://wxdata.weather.com");
        HttpResponseMessage resp = client.GetAsync(String.Format("wxdata/search/search?where={0}", location)).Result;
        resp.EnsureSuccessStatusCode();

    // Need help here

    }
}

I've tried several variations (string via ReadAsStringAsync, XmlDocument via LoadXml, string via JsonConvert.SerializeXmlNode, etc.). None of the approaches I've tried so far works for both Accept: application/xml and Accept: application/json requests. One accept type works while the other yields either an exception or a result which is not formatted as requested.

I'm new to Web API. Everything I've seen seems to suggest getting the data into an appropriate CLR type, then Content Negotiation should take care of the rest. Just not sure how to handle this raw XML feed. I want the controller to return either proper JSON or proper XML (essentially pass through the raw XML from the original feed) as requested.

Any suggestions?

1

1 Answers

2
votes

Well, since this question is going Tumbleweed, I'll post the answer I found on my own...

I found this (among others) which helped with the foundations of .NET XML serialization/deserialization. I tried using xsd.exe, but the results were disappointing. But starting from that base, I created a CLR type for the XML feed:

[XmlRoot("search")]
public class SearchTag
{
    [XmlAttribute("ver")]
    public string ver { get; set; }

    [XmlElement("loc")]
    public SearchLocTag[] loc { get; set; }
}

public class SearchLocTag
{
    [XmlAttribute("id")]
    public string id { get; set; }

    [XmlAttribute("type")]
    public string type { get; set; }

    [XmlText]
    public string text { get; set; }
}

The default serializer XMLMediaTypeFormatter uses (DataContractSerializer) did undesirable things with tag names and inserted unwanted XmlSerializerNamespaces info. But I found this and created a custom XmlObjectSerializer:

public class SearchSerializer : XmlObjectSerializer
{
    XmlSerializer serializer;

    public SearchSerializer()
    {
        this.serializer = new XmlSerializer(typeof(SearchTag));
    }

    public override void WriteObject(XmlDictionaryWriter writer, object graph)
    {
        XmlSerializerNamespaces ns = new XmlSerializerNamespaces();
        ns.Add("", "");
        serializer.Serialize(writer, graph, ns);
    }

    public override bool IsStartObject(XmlDictionaryReader reader)
    {
        throw new NotImplementedException();
    }

    public override object ReadObject(XmlDictionaryReader reader, bool verifyObjectName)
    {
        throw new NotImplementedException();
    }

    public override void WriteEndObject(XmlDictionaryWriter writer)
    {
        throw new NotImplementedException();
    }

    public override void WriteObjectContent(XmlDictionaryWriter writer, object graph)
    {
        throw new NotImplementedException();
    }

    public override void WriteStartObject(XmlDictionaryWriter writer, object graph)
    {
        throw new NotImplementedException();
    }

}

Then added this to Global.asax.cs:

GlobalConfiguration.Configuration.Formatters.XmlFormatter.SetSerializer<SearchTag>(new SearchSerializer());

Then the controller became:

public class SearchController : ApiController
{
    public HttpResponseMessage Get(string location)
    {
        HttpClient client = new HttpClient();
        client.BaseAddress = new Uri("http://wxdata.weather.com");
        HttpResponseMessage response = client.GetAsync(String.Format("wxdata/search/search?where={0}", location)).Result;
        response.EnsureSuccessStatusCode();

        var stream = response.Content.ReadAsStreamAsync().Result;

        var serializer = new XmlSerializer(typeof(SearchTag));
        using (var reader = new XmlTextReader(stream))
        {
            if (serializer.CanDeserialize(reader))
            {
                var xmlData = (SearchTag)serializer.Deserialize(reader);
                return Request.CreateResponse<SearchTag>(HttpStatusCode.OK, xmlData);
            }
            else
            {
                return Request.CreateResponse(HttpStatusCode.NotFound);
            }
        }
    }
}

The resulting application/xml messages:

Request

GET http://localhost:53047/twc/search/london HTTP/1.1
User-Agent: Fiddler
Accept: application/xml
Host: localhost:53047

Response

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/xml; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcQ2xheXRvblxEb2N1bWVudHNcVmlzdWFsIFN0dWRpbyAyMDEzXFByb2plY3RzXFR3Y0FwaVx0d2Ncc2VhcmNoXGxvbmRvbg==?=
X-Powered-By: ASP.NET
Date: Wed, 04 Dec 2013 21:42:46 GMT
Content-Length: 484

<search ver="3.0"><loc id="UKXX0085" type="1">London, GLA, United Kingdom</loc><loc id="USAL2905" type="1">London, AL</loc><loc id="USAR0340" type="1">London, AR</loc><loc id="USCA9301" type="1">London, CA</loc><loc id="USIN2109" type="1">London, IN</loc><loc id="USKY1090" type="1">London, KY</loc><loc id="USMI2014" type="1">London, MI</loc><loc id="USMN2182" type="1">London, MN</loc><loc id="USMO2769" type="1">London, MO</loc><loc id="USOH0520" type="1">London, OH</loc></search>

And the resulting application/json messages:

Request

GET http://localhost:53047/twc/search/london HTTP/1.1
User-Agent: Fiddler
Accept: application/json
Host: localhost:53047

Response

HTTP/1.1 200 OK
Cache-Control: no-cache
Pragma: no-cache
Content-Type: application/json; charset=utf-8
Expires: -1
Server: Microsoft-IIS/8.0
X-AspNet-Version: 4.0.30319
X-SourceFiles: =?UTF-8?B?RDpcQ2xheXRvblxEb2N1bWVudHNcVmlzdWFsIFN0dWRpbyAyMDEzXFByb2plY3RzXFR3Y0FwaVx0d2Ncc2VhcmNoXGxvbmRvbg==?=
X-Powered-By: ASP.NET
Date: Wed, 04 Dec 2013 21:43:18 GMT
Content-Length: 528

{"ver":"3.0","loc":[{"id":"UKXX0085","type":"1","text":"London, GLA, United Kingdom"},{"id":"USAL2905","type":"1","text":"London, AL"},{"id":"USAR0340","type":"1","text":"London, AR"},{"id":"USCA9301","type":"1","text":"London, CA"},{"id":"USIN2109","type":"1","text":"London, IN"},{"id":"USKY1090","type":"1","text":"London, KY"},{"id":"USMI2014","type":"1","text":"London, MI"},{"id":"USMN2182","type":"1","text":"London, MN"},{"id":"USMO2769","type":"1","text":"London, MO"},{"id":"USOH0520","type":"1","text":"London, OH"}]}

Which is exactly what I was after.