Building a .Net Weather Widget

I saw other blogs displaying current conditions for their city and wanted that on mine. It's a small window into Minneapolis weather for people reading from other parts of the world. The widget source is part of my .NET Core website repository but can easily be built into its own standalone component.

Layout.cshtml

The widget is part of the header so it's visible on every webpage. To enable that I created a partial view called _WeatherWidget that is rendered in _Layout.cshtml. _Layout.cshtml is the shared layout file for the entire application and loads on each page request. It's the parent for every webpage.

WeatherService.GetCurrentAsync() is called while the page is being built. This method returns the current conditions for Minneapolis/St. Paul, Minnesota. After the service returns, the partial view is rendered and the current conditions data is passed as a parameter into the view.

<body class="d-flex flex-column min-vh-100">
    @{
        WeatherSnapshot? weather = await WeatherService.GetCurrentAsync();
    }
		...rest of the html body
<header class="site-header d-flex flex-wrap justify-content-center py-3 mb-4 border-bottom">
        <nav class="navbar navbar-expand-md w-100">
            <div class="container">
                <a href="/" class="navbar-brand me-auto">Clint McMahon</a>
                @if (weather != null)
                {
                    @await Html.PartialAsync("_WeatherWidget", weather)
                }
								...rest of the header 

_WeatherWidget.cshtml

The view accepts a view model with three properties: Description, Icon and TempF. The view model is just a vehicle from the back end C# code to deliver the data to the front end HTML. All the work is done on the backend.

  • Description Displays the current description when the user hovers over the icons. This example below would render the text Clear in Minneapolis
  • Icon Based on the current conditions this icon is an emoji that I have defined in the source code. I'll talk about how this emoji is stored in the backend section of the blog post.
  • TempF The current temperature in Fahrenheit degrees
@model Website.Models.WeatherSnapshot

<div class="weather-widget" title="@Model.Description in Minneapolis">
    <span class="weather-icon" aria-hidden="true">@Model.Icon</span>
    <span class="weather-temp">@Model.TempF&deg;F</span>
    <span class="weather-loc mono">MSP</span>
</div>
namespace Website.Models;

public record WeatherSnapshot(double TempF, string Description, string Icon);

WeatherService.cs

The WeatherService service has one public method that gets the current weather for Minneapolis/St. Paul based on the hard coded lat/long. This should be kept at the config level and not hard coded. It was a quick thing to write and I left it hard coded because it was working and currently there's not a good reason to move it. In the future if I add other cities to the widget I'd move those locations into the config file.

Data is fetched from Open-Meteo using the request:

https://api.open-meteo.com/v1/forecast?latitude=44.9778&longitude=-93.2650&current=temperature_2m,weather_code&temperature_unit=fahrenheit&timezone=America%2FChicago

The response to the GET request returns everything that is needed to display the current weather conditions. When the response comes through, the current property is parsed and used to put together the current conditions output. The response is cached at the server-side for 20 minutes. There's no need to hit the API on every page refresh, the weather isn't changing that fast and this is a free API so they shouldn't be overloaded by this request. I thought 20 minutes is a good amount of time to capture the current conditions before needing to get a refresh of the data.

{
  "latitude": 44.96949,
  "longitude": -93.26296,
  "generationtime_ms": 0.3192424774169922,
  "utc_offset_seconds": -18000,
  "timezone": "America/Chicago",
  "timezone_abbreviation": "GMT-5",
  "elevation": 253.0,
  "current_units": {
    "time": "iso8601",
    "interval": "seconds",
    "temperature_2m": "°F",
    "weather_code": "wmo code"
  },
  "current": {
    "time": "2026-09-22T17:30",
    "interval": 900,
    "temperature_2m": 65.8,
    "weather_code": 0
  }
}
using System.Text.Json;
using Microsoft.Extensions.Caching.Memory;
using Website.Models;

namespace Website.Services;

// Current weather for Minneapolis, via Open-Meteo (free, no API key). Cached
// server-side so we're not hitting the API on every page load.
public class WeatherService
{
    private const string CacheKey = "weather:minneapolis";
    private static readonly TimeSpan CacheDuration = TimeSpan.FromMinutes(20);

    // Minneapolis, MN
    private const double Latitude = 44.9778;
    private const double Longitude = -93.2650;

    private readonly IHttpClientFactory _httpClientFactory;
    private readonly IMemoryCache _cache;
    private readonly ILogger<WeatherService> _logger;

    public WeatherService(IHttpClientFactory httpClientFactory, IMemoryCache cache, ILogger<WeatherService> logger)
    {
        _httpClientFactory = httpClientFactory;
        _cache = cache;
        _logger = logger;
    }

    public async Task<WeatherSnapshot?> GetCurrentAsync()
    {
        if (_cache.TryGetValue(CacheKey, out WeatherSnapshot? cached))
            return cached;

        var result = await Fetch();

        // Cache misses too, briefly, so a down API doesn't get hit on every request.
        _cache.Set(CacheKey, result, result != null ? CacheDuration : TimeSpan.FromMinutes(2));
        return result;
    }

    private async Task<WeatherSnapshot?> Fetch()
    {
        try
        {
            var client = _httpClientFactory.CreateClient("Weather");
            var url = "https://api.open-meteo.com/v1/forecast" +
                      $"?latitude={Latitude}&longitude={Longitude}" +
                      "&current=temperature_2m,weather_code" +
                      "&temperature_unit=fahrenheit" +
                      "&timezone=America%2FChicago";

            using var response = await client.GetAsync(url);
            if (!response.IsSuccessStatusCode) return null;

            using var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
            if (!doc.RootElement.TryGetProperty("current", out var current)) return null;

            var tempF = current.GetProperty("temperature_2m").GetDouble();
            var code = current.GetProperty("weather_code").GetInt32();
            var (description, icon) = WeatherCodeMap.Describe(code);

            return new WeatherSnapshot(Math.Round(tempF), description, icon);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Weather fetch failed");
            return null;
        }
    }
}

WeatherCodeMap.cs

The WeatherCodeMap class has a single method called Describe(). The Describe method takes in the current.weather_code property from the response and returns the description and emoji that corresponds to the code. Weather codes are based on the data provided by Open-Meteo, the utility method goes through the different types of weather that I wanted to display in the header. I return a basic thermometer 🌡️ when a code is returned that I don't handle in this mapping method.

This is done with emojis because I like the look of the weather emoji as well as it's easy to copy/paste the emojis into source code. The alternative would be to render a png or svg for each code but that's more overhead than what's needed.

public static class WeatherCodeMap
{
    public static (string Description, string Icon) Describe(int code) => code switch
    {
        0 => ("Clear", "☀️"),
        1 => ("Mostly clear", "🌤️"),
        2 => ("Partly cloudy", "⛅"),
        3 => ("Overcast", "☁️"),
        45 or 48 => ("Foggy", "🌫️"),
        51 or 53 or 55 => ("Drizzle", "🌦️"),
        56 or 57 => ("Freezing drizzle", "🌧️"),
        61 or 63 or 65 => ("Rain", "🌧️"),
        66 or 67 => ("Freezing rain", "🌧️"),
        71 or 73 or 75 => ("Snow", "❄️"),
        77 => ("Snow grains", "❄️"),
        80 or 81 or 82 => ("Rain showers", "🌧️"),
        85 or 86 => ("Snow showers", "🌨️"),
        95 => ("Thunderstorm", "⛈️"),
        96 or 99 => ("Thunderstorm w/ hail", "⛈️"),
        _ => ("Weather", "🌡️"),
    };
}

Next I'll build a full JavaScript enabled weather component with no server involved at all, just JS on the client.

This blog doesn't have a comments section. Reply by email - I'd love to hear what you think.