WeatherKit C#/.NET Example

WeatherKit is Apple's new weather API built on what was previously Dark Sky. It includes both a Swift API and a REST API. Since it has a REST API, it's fairly straightforward to use in C#/.NET, and this post describes how to use the REST API with C#/.NET. The full sample that goes along with this post is available on GitHub.

A couple notes on this example:

  • WeatherKit is currently in Beta, so the API is subject to change, and could break this example.
  • This example is not complete (it doesn't cover all the methods and data structures in the API — e.g., it doesn't cover anything around alerts), not thoroughly tested, is missing a lot of error handling, and is not production code.
  • This example calls multiple WeatherKit APIs in an ASP.NET controller before it returns a view. This is something you probably don't want to do in production, since it can lead to long page load times the way it's structured.

The WeatherKit API uses JWT like some of Apple's other APIs and JavaScript frameworks. There are some steps you need to take on your Apple Developer account to get set up, so you'll also want to read Apple's documentation on creating a service id and key to generate a token.

The example has spots in the appsettings.json & appsettings.Development.json for these values, but it does not include valid values, so it will not run as-is. You'll want to put your own values for ServiceID, PrivateKey (as a Base64 string -- you can take the p8 file you download from Apple, and copy/paste the private key out of the file with the line breaks removed), KeyIdentifier from the key you set up in your Apple Developer account, and TeamID that you can also grab from your Apple Developer account.

One of the other fields in the payload is the expiration -- our example has tokens that expire after 60 minutes. You can set the expiration to shorter or longer -- shorter is more secure; longer is more convenient. The WeatherKit API will error if you use a token that's expired. Note that you will definitely want to create the JWT on your server -- you don't want to have your private key exposed anywhere on the web, or even in an app bundle that's installed on an iOS or Android device. (It's a fair amount of work, but it's best to create a web application that generates tokens using your private information, then call that web app/API from your app on the device.)

Below is the main piece of code in the example that creates the JWT token (and of course, the JWT here is different than the JWT used in different Apple services):

public string GetToken()
{
    var header = new
    {
        // The encryption algorithm (alg) used to encrypt the token. ES256 should be used to
        // encrypt your token, and the value for this field should be "ES256".
        alg = "ES256",

        // A 10-character key identifier (kid) key, obtained from your Apple Developer account.
        kid = _weatherKitSettings.KeyIdentifier,

        // An identifier that consists of your 10-character Team ID and Service ID, separated by a period.
        id = _weatherKitSettings.TeamID + "." + _weatherKitSettings.ServiceID
    };

    var payload = new
    {
        // The Issuer (iss) registered claim key. This key's value is your 10-character Team ID,
        // obtained from your developer account.
        iss = _weatherKitSettings.TeamID,

        // The Issued At (iat) registered claim key. The value of this key indicates the time at
        // which the token was generated, in terms of the number of seconds since UNIX Epoch, in UTC.
        iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),

        // The Expiration Time (exp) registered claim key, in terms of the number of seconds since
        // UNIX Epoch, in UTC.
        exp = DateTimeOffset.UtcNow.AddMinutes(_weatherKitSettings.TokenExpirationMinutes ?? 30).ToUnixTimeSeconds(),

        // The subject public claim key. This value is your registered Service ID.
        sub = _weatherKitSettings.ServiceID
    };

    var headerBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(header));
    var payloadBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(payload));

    var message = EncodingHelper.JwtBase64Encode(headerBytes)
        + "." + EncodingHelper.JwtBase64Encode(payloadBytes);

    var messageBytes = Encoding.UTF8.GetBytes(message);

    var crypto = ECDsa.Create();
    crypto.ImportPkcs8PrivateKey(Convert.FromBase64String(_weatherKitSettings.PrivateKey ?? string.Empty), out _);

    var signature = crypto.SignData(messageBytes, HashAlgorithmName.SHA256);

    return message + "." + EncodingHelper.JwtBase64Encode(signature);
}

The below helper class has a single method that produces a Base64 string version of a byte array to the JWT specifications, which includes a few additional character replacements and one removal beyond the stock Base64 string conversion.

public static class EncodingHelper
{
    // Base64 Encode Per the JWT specifications
    public static string JwtBase64Encode(byte[] bytes)
    {
        if (bytes == null)
            throw new ArgumentNullException(nameof(bytes));
        if (bytes.Length == 0)
            throw new ArgumentOutOfRangeException(nameof(bytes));
        return Convert.ToBase64String(bytes)
            .Replace('+', '-')
            .Replace('/', '_')
            .TrimEnd('=');
    }
}

After generating the JWT, there are a few different ways to go. You could expose that in an API (there is an example in the GitHub example, though there's nothing that calls it), then call that and use the JWT wherever you'd like. Or, you could call the REST API itself from C# code. We're going to do that here, in this example. To do that nicely, we're going to deserialize the JSON returned from the WeatherKit API into C# objects.

Here are the objects (the code is based on code autogenerated from quicktype.io, except this is modified so that we can use System.Text.Json to deserialize it, since it has nicer async methods than Newtonsoft (in my opinion):

public partial class Weather
{
    [JsonPropertyName("currentWeather")]
    public CurrentWeather? CurrentWeather { get; set; }

    [JsonPropertyName("forecastDaily")]
    public ForecastDaily? ForecastDaily { get; set; }

    [JsonPropertyName("forecastHourly")]
    public ForecastHourly? ForecastHourly { get; set; }
}

public partial class CurrentWeather
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("metadata")]
    public Metadata? Metadata { get; set; }

    [JsonPropertyName("asOf")]
    public DateTimeOffset? AsOf { get; set; }

    [JsonPropertyName("cloudCover")]
    public double? CloudCover { get; set; }

    [JsonPropertyName("conditionCode")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public ConditionCode? ConditionCode { get; set; }

    [JsonPropertyName("daylight")]
    public bool? Daylight { get; set; }

    [JsonPropertyName("humidity")]
    public double? Humidity { get; set; }

    [JsonPropertyName("precipitationIntensity")]
    public double? PrecipitationIntensity { get; set; }

    [JsonPropertyName("pressure")]
    public double? Pressure { get; set; }

    [JsonPropertyName("pressureTrend")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public PressureTrend? PressureTrend { get; set; }

    [JsonPropertyName("temperature")]
    public double? Temperature { get; set; }

    [JsonPropertyName("temperatureApparent")]
    public double? TemperatureApparent { get; set; }

    [JsonPropertyName("temperatureDewPoint")]
    public double? TemperatureDewPoint { get; set; }

    [JsonPropertyName("uvIndex")]
    public long? UvIndex { get; set; }

    [JsonPropertyName("visibility")]
    public double? Visibility { get; set; }

    [JsonPropertyName("windDirection")]
    public long? WindDirection { get; set; }

    [JsonPropertyName("windGust")]
    public double? WindGust { get; set; }

    [JsonPropertyName("windSpeed")]
    public double? WindSpeed { get; set; }

    [JsonPropertyName("forecastStart")]
    public DateTimeOffset? ForecastStart { get; set; }

    [JsonPropertyName("precipitationAmount")]
    public double? PrecipitationAmount { get; set; }

    [JsonPropertyName("precipitationChance")]
    public double? PrecipitationChance { get; set; }

    [JsonPropertyName("precipitationType")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public PrecipitationType? PrecipitationType { get; set; }

    [JsonPropertyName("snowfallIntensity")]
    public double? SnowfallIntensity { get; set; }
}

public partial class Metadata
{
    [JsonPropertyName("attributionURL")]
    public Uri? AttributionUrl { get; set; }

    [JsonPropertyName("expireTime")]
    public DateTimeOffset? ExpireTime { get; set; }

    [JsonPropertyName("latitude")]
    public double? Latitude { get; set; }

    [JsonPropertyName("longitude")]
    public double? Longitude { get; set; }

    [JsonPropertyName("readTime")]
    public DateTimeOffset? ReadTime { get; set; }

    [JsonPropertyName("reportedTime")]
    public DateTimeOffset? ReportedTime { get; set; }

    [JsonPropertyName("units")]
    public string? Units { get; set; }

    [JsonPropertyName("version")]
    public long? Version { get; set; }
}

public partial class ForecastDaily
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("metadata")]
    public Metadata? Metadata { get; set; }

    [JsonPropertyName("days")]
    public List<Day>? Days { get; set; }
}

public partial class Day
{
    [JsonPropertyName("forecastStart")]
    public DateTimeOffset? ForecastStart { get; set; }

    [JsonPropertyName("forecastEnd")]
    public DateTimeOffset? ForecastEnd { get; set; }

    [JsonPropertyName("conditionCode")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public ConditionCode? ConditionCode { get; set; }

    [JsonPropertyName("maxUvIndex")]
    public long? MaxUvIndex { get; set; }

    [JsonPropertyName("moonPhase")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public MoonPhase? MoonPhase { get; set; }

    [JsonPropertyName("moonrise")]
    public DateTimeOffset? Moonrise { get; set; }

    [JsonPropertyName("moonset")]
    public DateTimeOffset? Moonset { get; set; }

    [JsonPropertyName("precipitationAmount")]
    public double? PrecipitationAmount { get; set; }

    [JsonPropertyName("precipitationChance")]
    public double? PrecipitationChance { get; set; }

    [JsonPropertyName("precipitationType")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public PrecipitationType? PrecipitationType { get; set; }

    [JsonPropertyName("snowfallAmount")]
    public double? SnowfallAmount { get; set; }

    [JsonPropertyName("solarMidnight")]
    public DateTimeOffset? SolarMidnight { get; set; }

    [JsonPropertyName("solarNoon")]
    public DateTimeOffset? SolarNoon { get; set; }

    [JsonPropertyName("sunrise")]
    public DateTimeOffset? Sunrise { get; set; }

    [JsonPropertyName("sunriseCivil")]
    public DateTimeOffset? SunriseCivil { get; set; }

    [JsonPropertyName("sunriseNautical")]
    public DateTimeOffset? SunriseNautical { get; set; }

    [JsonPropertyName("sunriseAstronomical")]
    public DateTimeOffset? SunriseAstronomical { get; set; }

    [JsonPropertyName("sunset")]
    public DateTimeOffset? Sunset { get; set; }

    [JsonPropertyName("sunsetCivil")]
    public DateTimeOffset? SunsetCivil { get; set; }

    [JsonPropertyName("sunsetNautical")]
    public DateTimeOffset? SunsetNautical { get; set; }

    [JsonPropertyName("sunsetAstronomical")]
    public DateTimeOffset? SunsetAstronomical { get; set; }

    [JsonPropertyName("temperatureMax")]
    public double? TemperatureMax { get; set; }

    [JsonPropertyName("temperatureMin")]
    public double? TemperatureMin { get; set; }

    [JsonPropertyName("daytimeForecast")]
    public Forecast? DaytimeForecast { get; set; }

    [JsonPropertyName("overnightForecast")]
    public Forecast? OvernightForecast { get; set; }

    [JsonPropertyName("restOfDayForecast")]
    public Forecast? RestOfDayForecast { get; set; }
}

public partial class Forecast
{
    [JsonPropertyName("forecastStart")]
    public DateTimeOffset? ForecastStart { get; set; }

    [JsonPropertyName("forecastEnd")]
    public DateTimeOffset? ForecastEnd { get; set; }

    [JsonPropertyName("cloudCover")]
    public double? CloudCover { get; set; }

    [JsonPropertyName("conditionCode")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public ConditionCode? ConditionCode { get; set; }

    [JsonPropertyName("humidity")]
    public double? Humidity { get; set; }

    [JsonPropertyName("precipitationAmount")]
    public double? PrecipitationAmount { get; set; }

    [JsonPropertyName("precipitationChance")]
    public double? PrecipitationChance { get; set; }

    [JsonPropertyName("precipitationType")]
    [JsonConverter(typeof(JsonStringEnumConverter))]
    public PrecipitationType? PrecipitationType { get; set; }

    [JsonPropertyName("snowfallAmount")]
    public double? SnowfallAmount { get; set; }

    [JsonPropertyName("windDirection")]
    public long? WindDirection { get; set; }

    [JsonPropertyName("windSpeed")]
    public double? WindSpeed { get; set; }
}

public partial class ForecastHourly
{
    [JsonPropertyName("name")]
    public string? Name { get; set; }

    [JsonPropertyName("metadata")]
    public Metadata? Metadata { get; set; }

    [JsonPropertyName("hours")]
    public List<CurrentWeather>? Hours { get; set; }
}

public enum ConditionCode
{
    [Display(Name = "Clear")]
    Clear,
    [Display(Name = "Cloudy")]
    Cloudy,
    [Display(Name = "Haze")]
    Haze,
    [Display(Name = "Mostly Clear")]
    MostlyClear,
    [Display(Name = "Mostly Cloudy")]
    MostlyCloudy,
    [Display(Name = "Partly Cloudy")]
    PartlyCloudy,
    [Display(Name = "Scattered Thunderstorms")]
    ScatteredThunderstorms,
    [Display(Name = "Breezy")]
    Breezy,
    [Display(Name = "Windy")]
    Windy,
    [Display(Name = "Drizzle")]
    Drizzle,
    [Display(Name = "Heavy Rain")]
    HeavyRain,
    [Display(Name = "Rain")]
    Rain,
    [Display(Name = "Flurries")]
    Flurries,
    [Display(Name = "Heavy Snow")]
    HeavySnow,
    [Display(Name = "Sleet")]
    Sleet,
    [Display(Name = "Snow")]
    Snow,
    [Display(Name = "Blizzard")]
    Blizzard,
    [Display(Name = "Blowing Snow")]
    BlowingSnow,
    [Display(Name = "Freezing Drizzle")]
    FreezingDrizzle,
    [Display(Name = "Freezing Rain")]
    FreezingRain,
    [Display(Name = "Frigid")]
    Frigid,
    [Display(Name = "Hail")]
    Hail,
    [Display(Name = "Hot")]
    Hot,
    [Display(Name = "Hurricane")]
    Hurricane,
    [Display(Name = "Isolated Thunderstorms")]
    IsolatedThunderstorms,
    [Display(Name = "Tropical Storm")]
    TropicalStorm,
    [Display(Name = "Blowing Dust")]
    BlowingDust,
    [Display(Name = "Foggy")]
    Foggy,
    [Display(Name = "Smoky")]
    Smoky,
    [Display(Name = "Strong Storms")]
    StrongStorms,
    [Display(Name = "Sun Flurries")]
    SunFlurries,
    [Display(Name = "Sun Showers")]
    SunShowers,
    [Display(Name = "Thunderstorms")]
    Thunderstorms,
    [Display(Name = "Wintry Mix")]
    WintryMix
}

public enum PrecipitationType
{
    Clear,
    Precipitation,
    Rain,
    Snow,
    Sleet,
    Hail,
    Mixed
}

public enum PressureTrend
{
    Falling,
    Rising,
    Steady
}

public enum MoonPhase
{
    New,
    WaxingCrescent,
    FirstQuarter,
    Full,
    WaxingGibbous,
    WaningGibbous,
    ThirdQuarter,
    WaningCrescent
}

That's quite a bit of code, but it's really pretty straightforward, and you can read about what the fields are in Apple's documentation.

Next, we'll actually call one of the API methods. The most basic one is availability, which takes a latitude and longitude, and returns a list of datasets that are supported at that location. You can read more about at Apple's availability documentation.

public enum WeatherKitDataSetType
{
    None = -1,
    CurrentWeather = 0,
    ForecastDaily,
    ForecastHourly,
    ForecastNextHour,
    WeatherAlerts,
    TrendComparison
}

public async Task<List<WeatherKitDataSetType>?> GetAvailability(double latitude, double longitude)
{
    var handler = new HttpClientHandler()
    {
        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
    };

    using (var client = new HttpClient(handler))
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("User-Agent", "WeatherKitExample/1.0 (contact@yourdomain.com)");
        client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
        client.DefaultRequestHeaders.Add("Authorization", "Bearer " + GetToken());

        var requestUri = new Uri("https://weatherkit.apple.com/api/v1").Append(string.Format("availability/{0}/{1}", latitude, longitude));

        using (var response = await client.GetAsync(requestUri))
        {
            response.EnsureSuccessStatusCode();

            using (var httpStream = await response.Content.ReadAsStreamAsync())
            {
                return (await JsonSerializer.DeserializeAsync>(httpStream))?
                    .Select(d => SafeDataSetParse(d))
                    .ToList();
            }
        }
    }
}

private WeatherKitDataSetType SafeDataSetParse(string? value)
{
    WeatherKitDataSetType dataSetType;

    if (Enum.TryParse(value, true, out dataSetType))
    {
        return dataSetType;
    }

    return WeatherKitDataSetType.None;
}

We then can use one or more of the datasets from the above call to get the actual weather forecasts from weather (Apple's Documentation). This call also takes the latitude and longitude, a list of datasets from the above return, the timezone (in Iana format), and the language as required parameters. There are a number of other parameters that are optional that you can read about in the documentation (and are not implemented below). The timezone shifts dates/times in the return, so if you don't pass the timezone that the coordinates are in, the API does not return forecasts in local time.

public async Task<Weather?> GetWeather(double latitude, double longitude, List<WeatherKitDataSetType> datasets, string timezoneId, string language = "en")
{
    if (datasets.Count < 1)
        throw new ArgumentOutOfRangeException(nameof(datasets));

    var timezoneInfo = TZConvert.GetTimeZoneInfo(timezoneId);

    var ianaTimezone = (timezoneInfo.HasIanaId) ? timezoneInfo.Id : TZConvert.WindowsToIana(timezoneInfo.Id);

    if (timezoneInfo == null)
        throw new ArgumentOutOfRangeException(nameof(datasets));

    var handler = new HttpClientHandler()
    {
        AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
    };

    using (var client = new HttpClient(handler))
    {
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
        client.DefaultRequestHeaders.Add("User-Agent", "WeatherKitExample/1.0 (contact@yourdomain.com)");
        client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue("gzip"));
        client.DefaultRequestHeaders.Add("Authorization", "Bearer " + GetToken());

        var datasetsString = string.Join(",", datasets.Select(d => d.ToString().FirstCharToLowerCase()));

        var requestUri = new Uri("https://weatherkit.apple.com/api/v1")
            .Append(string.Format("weather/{0}/{1}/{2}?dataSets={3}&timezone={4}",
                language,
                latitude,
                longitude,
                Uri.EscapeDataString(datasetsString),
                Uri.EscapeDataString(ianaTimezone)));

        using (var response = await client.GetAsync(requestUri))
        {
            response.EnsureSuccessStatusCode();

            using (var httpStream = await response.Content.ReadAsStreamAsync())
            {
                return await JsonSerializer.DeserializeAsync<Weather>(httpStream);
            }
        }
    }
}

If you're actually implementing this, it's easier to grab the sample itself and start from there rather than copying/pasting. After replacing the configuration values with your own, you should be able to run the project and get a simple sample output like below. The full sample is also posted on GitHub.

Here's a screenshot of what the simple example looks like:

WeatherKit Example Screenshot