1

I'm currently learning how to handle XML in C#, and I'm trying to parse and create an object from this url: https://api.met.no/weatherapi/locationforecast/1.9/?lat=60.10&lon=9.58

Function call

var data = GetXmlData(url);
LocationForecast forecastNextHour = data.First();

Parsing

private IEnumerable<LocationForecast> GetXmlData(string url)
{
    // Create new XDocument
    XDocument xdoc = XDocument.Load(url);

    // Get first element in XML with forecast datatype (next hour)
    XElement selectedElement = xdoc.Descendants().First(x => (string)x.Attribute("datatype") == "forecast");

    var locationData = from x in selectedElement.Descendants()
                   let temperature = x.Element("temperature")
                   where temperature != null
                   let windDirection = x.Element("windDirection")
                   where windDirection != null
                   let windSpeed = x.Element("windSpeed")
                   where windSpeed != null
                   let windGust = x.Element("windGust")
                   where windGust != null
                   let humidity = x.Element("humidity")
                   where humidity != null
                   let pressure = x.Element("pressure")
                   where pressure != null
                   let cloudiness = x.Element("cloudiness")
                   where cloudiness != null
                   let fog = x.Element("fog")
                   where fog != null
                   let lowClouds = x.Element("lowClouds")
                   where lowClouds != null
                   let mediumClouds = x.Element("mediumClouds")
                   where mediumClouds != null
                   let highClouds = x.Element("highClouds")
                   where highClouds != null
                   let dewpointTemperature = x.Element("dewpointTemperature")
                   where dewpointTemperature != null

    // Get data from selected portion of XML
    select new LocationForecast()
    {
        Temperature = (double)temperature.Attribute("value"),
        WindDirection = (double)windDirection.Attribute("deg"),
        WindSpeed = (double)windSpeed.Attribute("mps"),
        WindGust = (double)windGust.Attribute("mps"),
        Humidity = (double)humidity.Attribute("value"),
        Pressure = (double)pressure.Attribute("value"),
        Cloudiness = (double)cloudiness.Attribute("percent"),
        Fog = (double)fog.Attribute("percent"),
        LowClouds = (double)lowClouds.Attribute("percent"),
        MediumClouds = (double)mediumClouds.Attribute("percent"),
        HighClouds = (double)highClouds.Attribute("percent"),
        DewpointTemperature = (double)dewpointTemperature.Attribute("value"),
    };

    return locationData;
}

The code works but it seems a bit inefficient, as I return an IEnumerable where I have to use .First() on the returned object to get the actual object with properties. With this code I'm also unable to get hold of the other time-elements where the from="" and to="" attributes are the same (forecast for that hour) and I would like to get hold of the hourly forecasts for at least the three next hours.

I've tried using xsd.exe to create a class from the schema, but that made no sense so currently I'm sticking with my LocationForecast class.

Is there a more efficient way of doing this parsing, and to get hold of the next hourly forecasts?

miniPax
  • 73
  • 6
  • 1
    Use the XmlSerializer or DataContractSerializer – rene Oct 13 '16 at 15:34
  • I would strongly suggest looking into how to deserialize xml in c#. You'll see how to better handle this where you can create a class to represent XML and your life will hopefully be easier because of it. – KSib Oct 13 '16 at 15:57
  • Try to split up your query. First select all nodes you are interested in, then create your business objects. At the moment you are only taking the first Node because of xdoc.Descendants().First(x => (string)x.Attribute("datatype") == "forecast"); Instead of First(..) it should be Where(...). You can even extract parts of the query into methods to make it more readable. – phatoni Oct 13 '16 at 16:14

1 Answers1

1

If you don't want to juggle with the XDocument and its XElements you can create an class hierarchy that can be used by the XmlSerializer (the DataContractSerializer can't be really leveraged because it doesn't support XML attributes, it will fallback to the XmlSerializer).

The benefit is that it will reduce the complexity of your GetXmlData method. As a drawback, you don't get less code and that you seem to want the values to be doubles needs some operator magic to support implicit casts.

The GetXmlData

This method will now open a stream and use the XmlSerializer to get all weather data. Once obtained it gets the first Location item in the forecasts collection. You can alter this if you need another item.

public Location GetXmlData(string url)
{
    Location loc;
    using(var wc = new WebClient())
    {
        // Type you want to deserialize
        var ser = new XmlSerializer(typeof(WeatherData));
        using(var stream = wc.OpenRead(url))
        {   
            // create the object, cast the result
            var w = (WeatherData) ser.Deserialize(stream);
            // w.Dump(); // linqpad testing
            // what do we need
            loc =  w.Product.ForeCasts.First().Location;
        }
    }
    return loc;
}

(De)Serialization classes

Your XML looked roughly like this:

<weatherdata>
    <product>
        <time from="2016-10-13T20:00:00Z" to="2016-10-13T20:00:00Z">
            <location altitude="485" latitude="60.1000" longitude="9.5800">
                <temperature id="TTT" unit="celsius" value="-0.9"/>
            </location>
        </time>
        <time from="2016-10-13T20:00:00Z" to="2016-10-13T20:00:00Z">
            <location altitude="485" latitude="60.1000" longitude="9.5800">
                <temperature id="TTT" unit="celsius" value="-0.9"/>
                <fog id="FOG" percent="-0.0"/>
            </location>
        </time>
        <!-- many more -->
    </product>       
 </weatherdata>

 

Each XML element becomes a class and each class takes public properties for each child element. It is clear that we need classes for weatherdata, product, time and location. The elements inside location all looked kind of similar so I tried a short cut there. First the clear cut classes. Note the Attributes to steer the correct de-serialization behavior.

// the root
[XmlRoot("weatherdata")]
public class WeatherData
{
    [XmlElement("product")] 
    public Product Product {get; set;}
}

public class Product
{
    [XmlElement("time")] 
    public List<Time> ForeCasts {get;set;}
}

public class Time
{
   [XmlAttribute("from")]
   public DateTime From {get;set;}
   [XmlAttribute("to")]
   public DateTime To {get;set;}
   [XmlElement("location")]
   public Location Location{get;set;}
}


public class Location 
{
   [XmlAttribute("altitude")]
   public string Altitude {get;set;}
   [XmlElement("temperature")]
   public Value Temperature {get;set;}
   [XmlElement("windDirection")]
   public Measurement WindDirection {get;set;} 
   [XmlElement("pressure")]
   public Measurement Pressure {get;set;}
   [XmlElement("fog")]
   public Percent Fog {get;set;}
   // add the rest
}

Special cases

The elements inside location all had similar attributes so I created one main class to capture all that variation:

// covers all kind of things
public class Measurement
{
    [XmlAttribute("id")]
    public string Id {get;set;}
    [XmlAttribute("unit")]
    public string Unit {get;set;}
    [XmlAttribute("deg")]
    public double Deg {get;set;}
    [XmlAttribute("value")]
    public double Value {get;set;}
    [XmlAttribute("mps")]
    public double Mps {get;set;}
    [XmlAttribute("percent")]
    public double Percent {get;set;}
}

Specific measurements

Your use case seems to indicate that you want to use doubles from one of the attributes (now properties) of a measurement. For that purpose I subclassed Measurement for Value and Percent. The class here implement an implicit conversion operator so it doesn't require casting for the consumers of this class.

// for properties that use the Value attribute
public class Value:Measurement
{
    // operators for easy casting
    public static implicit operator Value(Double d) 
    {
        var v = new Value();
        v.Value = d;
        return v;
    }
    public static implicit operator Double(Value v)
    {
        return v.Value;
    }

    public override string ToString() 
    {
       return this.Value.ToString();
    }
}

// for properties that use the Percent attribute
public class Percent:Measurement
{
    // operators for easy casting
    public static implicit operator Percent(Double d) 
    {
        var p = new Percent();
        p.Percent = d;
        return p;
    }
    public static implicit operator Double(Percent p)
    {
        return p.Percent;
    }

    public override string ToString() 
    {
       return this.Percent.ToString();
    }
}

A small test method looks like this:

void Main()
{
    var url = @"https://api.met.no/weatherapi/locationforecast/1.9/?lat=60.10&lon=9.58";
    var loc = GetXmlData(url);
    Double f = loc.Fog;
    f.Dump("Percent as double");
    Double t = loc.Temperature;
    t.Dump("Temperature as double");
}

If you don't fancy that the properties aren't of the type double, you can re-use your LocationForecast class but you'll need to instantiate and map the values your self in that case, something like:

 var lf = new LocationForcast {
     Temperature = loc.Temperature,
     Precent = loc.Percent,
     // etc.
 };

or use a lib like Automapper

Community
  • 1
  • 1
rene
  • 41,474
  • 78
  • 114
  • 152