Как десериализовать Yahoo GeoPlanet REST XML автоматически с помощью WCF DataContracts

#wcf #rest #xml-serialization #wcf-client #yahoo-api

#wcf #rest #xml-сериализация #wcf-клиент #yahoo-api

Вопрос:

Я новичок в WCF. Мне удалось успешно создать клиент для службы GeoNames, но теперь, когда я пытаюсь сделать то же самое для Yahoo GeoPlanet, я, похоже, не могу получить XML для десериализации в мои типы DataContract. Как правильно это сделать? Вот с чем я работаю:

Пример ответа REST:

 <places xmlns="http://where.yahooapis.com/v1/schema.rng" 
    xmlns:yahoo="http://www.yahooapis.com/v1/base.rng" 
    yahoo:start="0" yahoo:count="247" yahoo:total="247">
    <place yahoo:uri="http://where.yahooapis.com/v1/place/23424966" 
        xml:lang="en-US">
        <woeid>23424966</woeid>
        <placeTypeName code="12">Country</placeTypeName>
        <name>Sao Tome and Principe</name>
    </place>
    <place yahoo:uri="http://where.yahooapis.com/v1/place/23424824" 
        xml:lang="en-US">
        <woeid>23424824</woeid>
        <placeTypeName code="12">Country</placeTypeName>
        <name>Ghana</name>
    </place>
    ...
</places>
  

Интерфейс контракта и клиент:

 [ServiceContract]
public interface IConsumeGeoPlanet
{
    [OperationContract]
    [WebGet(
        UriTemplate = "countries?appid={appId}",
        ResponseFormat = WebMessageFormat.Xml,
        BodyStyle = WebMessageBodyStyle.Bare
    )]
    GeoPlanetResults<GeoPlanetPlace> Countries(string appId);
}

public sealed class GeoPlanetConsumer : ClientBase<IConsumeGeoPlanet>
{
    public GeoPlanetResults<GeoPlanetPlace> Countries(string appId)
    {
        return Channel.Countries(appId);
    }
}
  

Типы десериализации:

 [DataContract(Name = "places", 
    Namespace = "http://where.yahooapis.com/v1/schema.rng")]
public sealed class GeoPlanetResults<T> : IEnumerable<T>
{
    public List<T> Items { get; set; }

    public IEnumerator<T> GetEnumerator()
    {
        return Items.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }
}


[DataContract]
public class GeoPlanetPlace
{
    [DataMember(Name = "woeid")]
    public int WoeId { get; set; }

    [DataMember(Name = "placeTypeName")]
    public string Type { get; set; }

    [DataMember(Name = "name")]
    public string Name { get; set; }

}
  

Я знаю, что это неправильно. В моем клиенте geonames мой класс GeoNamesResults имеет [DataContract] атрибут без свойств и [DataMember(Name = "geonames")] атрибут в Items свойстве. Это не работает для GeoPlanet, хотя я продолжал получать исключения десериализации. Единственный способ заставить Countries(appId) метод выполняться без исключений — это указать имя и пространство имен в атрибуте DataContract. Однако, когда я делаю это, я понятия не имею, как десериализовать результаты в коллекцию Items (это значение равно null).

Что мне делать?

Ответ №1:

DataContractSerializer Не поддерживает полную спецификацию XML, только ее подмножество. Единственное, что он не поддерживает, это атрибуты, которые широко используются в показанном вами примере ответа. В этом случае вам нужно будет использовать XmlSerializer и соответствующим образом определить типы (используя атрибуты в System.Xml.Serialization , а не те, что на System.Runtime.Serialization ). Приведенный ниже код показывает, как получить образец XML, который вы опубликовали.

 public class StackOverflow_8022154
{
    const string XML = @"<places xmlns=""http://where.yahooapis.com/v1/schema.rng""  
    xmlns:yahoo=""http://www.yahooapis.com/v1/base.rng""  
    yahoo:start=""0"" yahoo:count=""247"" yahoo:total=""247""> 
    <place yahoo:uri=""http://where.yahooapis.com/v1/place/23424966""  
        xml:lang=""en-US""> 
        <woeid>23424966</woeid> 
        <placeTypeName code=""12"">Country</placeTypeName> 
        <name>Sao Tome and Principe</name> 
    </place> 
    <place yahoo:uri=""http://where.yahooapis.com/v1/place/23424824""  
        xml:lang=""en-US""> 
        <woeid>23424824</woeid> 
        <placeTypeName code=""12"">Country</placeTypeName> 
        <name>Ghana</name> 
    </place> 
</places>";

    const string ElementsNamespace = "http://where.yahooapis.com/v1/schema.rng";
    const string YahooNamespace = "http://www.yahooapis.com/v1/base.rng";
    const string XmlNamespace = "http://www.w3.org/XML/1998/namespace";

    [XmlType(Namespace = ElementsNamespace, TypeName = "places")]
    [XmlRoot(ElementName = "places", Namespace = ElementsNamespace)]
    public class Places
    {
        [XmlAttribute(AttributeName = "start", Namespace = YahooNamespace)]
        public int Start { get; set; }
        [XmlAttribute(AttributeName = "count", Namespace = YahooNamespace)]
        public int Count;
        [XmlAttribute(AttributeName = "total", Namespace = YahooNamespace)]
        public int Total;
        [XmlElement(ElementName = "place", Namespace = ElementsNamespace)]
        public List<Place> AllPlaces { get; set; }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            sb.AppendFormat("Places[start={0},count={1},total={2}]:", this.Start, this.Count, this.Total);
            sb.AppendLine();
            foreach (var place in this.AllPlaces)
            {
                sb.AppendLine("   "   place.ToString());
            }

            return sb.ToString();
        }
    }
    [XmlType(TypeName = "place", Namespace = ElementsNamespace)]
    public class Place
    {
        [XmlAttribute(AttributeName = "uri", Namespace = YahooNamespace)]
        public string Uri { get; set; }
        [XmlAttribute(AttributeName = "lang", Namespace = XmlNamespace)]
        public string Lang { get; set; }
        [XmlElement(ElementName = "woeid")]
        public string Woeid { get; set; }
        [XmlElement(ElementName = "placeTypeName")]
        public PlaceTypeName PlaceTypeName;
        [XmlElement(ElementName = "name")]
        public string Name { get; set; }

        public override string ToString()
        {
            return string.Format("Place[Uri={0},Lang={1},Woeid={2},PlaceTypeName={3},Name={4}]",
                this.Uri, this.Lang, this.Woeid, this.PlaceTypeName, this.Name);
        }
    }
    [XmlType(TypeName = "placeTypeName", Namespace = ElementsNamespace)]
    public class PlaceTypeName
    {
        [XmlAttribute(AttributeName = "code")]
        public string Code { get; set; }
        [XmlText]
        public string Value { get; set; }

        public override string ToString()
        {
            return string.Format("TypeName[Code={0},Value={1}]", this.Code, this.Value);
        }
    }
    [ServiceContract]
    public interface IConsumeGeoPlanet
    {
        [OperationContract]
        [WebGet(
            UriTemplate = "countries?appid={appId}",
            ResponseFormat = WebMessageFormat.Xml,
            BodyStyle = WebMessageBodyStyle.Bare
        )]
        [XmlSerializerFormat]
        Places Countries(string appId);
    }

    public sealed class GeoPlanetConsumer : ClientBase<IConsumeGeoPlanet>
    {
        public GeoPlanetConsumer(string address)
            : base(new WebHttpBinding(), new EndpointAddress(address))
        {
            this.Endpoint.Behaviors.Add(new WebHttpBehavior());
        }

        public Places Countries(string appId)
        {
            return Channel.Countries(appId);
        }
    }

    [ServiceContract]
    public class SimulatedYahooService
    {
        [WebGet(UriTemplate = "*")]
        public Stream GetData()
        {
            WebOperationContext.Current.OutgoingResponse.ContentType = "text/xml";
            return new MemoryStream(Encoding.UTF8.GetBytes(XML));
        }
    }

    public static void Test()
    {
        Console.WriteLine("First a simpler test with serialization only.");
        XmlSerializer xs = new XmlSerializer(typeof(Places));
        MemoryStream ms = new MemoryStream(Encoding.UTF8.GetBytes(XML));
        object o = xs.Deserialize(ms);
        Console.WriteLine(o);

        Console.WriteLine();
        Console.WriteLine("Now in a real service");
        Console.WriteLine();
        string baseAddress = "http://"   Environment.MachineName   ":8000/Service";
        WebServiceHost host = new WebServiceHost(typeof(SimulatedYahooService), new Uri(baseAddress));
        host.Open();
        Console.WriteLine("Host opened");

        GeoPlanetConsumer consumer = new GeoPlanetConsumer(baseAddress);
        Places places = consumer.Countries("abcdef");
        Console.WriteLine(places);
    }
}
  

Комментарии:

1. Да, спасибо. Я много читал о необходимости использования xml serializer для rest с атрибутами xml, но помещал атрибут XmlSerializerFormatAttribute не в то место. Теперь у меня есть это в интерфейсе ServiceContract вместо метода operation contract.

2. Это должно сработать, если вы также разместите его в интерфейсе контракта на обслуживание.