Playing with the .NET 8 Web API template
Tags: dotnet, csharp, aspnetcore
Published: 2023-12-19 19:33:11

---
title: Playing with the .NET 8 Web API template
published: true
date: 2023-12-19 00:00:00 UTC
tags: dotnet, csharp, aspnetcore
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ce3niaaqe3l0tl6ddb22.jpg
canonical_url: https://techwatching.dev/posts/playing-with-dotnet8
---

In this article, we will explore the latest C# 12 and .NET 8 features by applying them to the basic dotnet Web API template.

## Getting started with the ASP.NET Core Web API template

First, let's install the latest [.NET 8 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/8.0):

```powershell
winget install --id Microsoft.DotNet.SDK.8

```

We can list the available templates:

 ![List of the available dotnet templates](https://techwatching.dev/posts/images/dontnet8_templates.webp)

Let's go for the basic ASP.NET Core Web API template but with the controllers:

```powershell
dotnet new webapi --use-controllers -n WeatherApi

```

> 💡[Minimal APIs] are great too but having controllers is more suited to what I want to show in this article.

 ![Screenshot of the generated project in Rider](https://techwatching.dev/posts/images/dontnet8_webapi_template.webp)

We can run the API and test the `GET /weatherforecast` endpoint using the generated request file:

```http
@WeatherApi_HostAddress = http://localhost:5103

GET {{WeatherApi_HostAddress}}/weatherforecast/
Accept: application/json

```

This is included in the dotnet `webapi` template and is supported by Visual Studio, Rider, and vscode (using the [REST Client extension](https://marketplace.visualstudio.com/items?itemName=humao.rest-client))

> 💡Read my article about [choosing an API Client](https://www.techwatching.dev/posts/http-clients) and why I prefer versioned HTTP files rather than GUI tools like Postman.

If we put a breakpoint in the controller we can see one small ASP.NET 8 improvement concerning the debugging experience: [better debug summaries are displayed for types like `HttpContext`](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#improved-debugging-experience).

 ![Debugging display of the HTTPContext class](https://techwatching.dev/posts/images/dontnet8_httpcontext.webp)
## Enhancing the Weather Forecast API

Currently, the template randomly generates weather forecasts in the controller. It would be nice to retrieve real weather data from a weather API.

To do that we can:

- introduce an `IWeatherService` interface that contains a method to retrieve weather forecasts

- extract the current logic that generates the random weather forecasts in a `RandomWeatherService.cs` that implements this interface

- creates a new implementation `OpenWeatherService` of this interface that retrieves the weather data from the Open Weather Map API

 ![A diagram of the ASP.NETCore Weather API](https://techwatching.dev/posts/images/dontnet8_webapi_diagram.webp)

The `WeatherForecastController` becomes:

```csharp
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
    private readonly IWeatherService _weatherService;

    public WeatherForecastController(IWeatherService weatherService)
    {
        _weatherService = weatherService;
    }

    [HttpGet(Name = "GetWeatherForecast")]
    [ProducesResponseType(typeof(WeatherForecast), StatusCodes.Status200OK)]
    public Task<WeatherForecast[]> Get()
    {
        return _weatherService.GetWeatherForecasts();
    }
}

```

We can get rid of the `typeof` because there are now [generic attributes for some common ASP.NET Core attributes](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#support-for-generic-attributes) like `ProducesResponseType.`

```csharp
    [HttpGet(Name = "GetWeatherForecast")]
    [ProducesResponseType<WeatherForecast>(StatusCodes.Status200OK)]
    public Task<WeatherForecast[]> Get()
    {
        return _weatherService.GetWeatherForecasts();
    }

```

There are now 2 implementations of the `IWeatherService` interface:

- `RandomWeatherService` that contains the code that previously was in the controller

- `OpenWeatherService` that makes a call to the Open Weather Map API to retrieve the weather forecasts and then maps the obtained data to a list of `WeatherForecast`

```csharp
public class OpenWeatherService : IWeatherService
{
    private readonly IOpenWeatherMapApi _openWeatherMapApi;
    private static readonly (double Latitude, double Longitude) BordeauxCoordinates = (44.837789, -0.57918);

    public OpenWeatherService(IOpenWeatherMapApi openWeatherMapApi)
    {
        _openWeatherMapApi = openWeatherMapApi;
    }

    public async Task<WeatherForecast[]> GetWeatherForecasts()
    {
        var weatherApiResponse = await _openWeatherMapApi.GetWeatherForecast(BordeauxCoordinates.Latitude, BordeauxCoordinates.Longitude);
        
        var computeWeatherSummary = (double temperature) =>
            temperature switch
            {
                < 0 => "Freezing",
                >= 0 and < 5 => "Bracing",
                >= 5 and < 12 => "Chilly",
                >= 12 and < 18 => "Cool",
                >= 18 and < 24 => "Mild",
                >= 24 and < 30 => "Warm",
                >= 30 and < 35 => "Balmy",
                >= 35 and < 40 => "Hot",
                >= 40 and < 45 => "Sweltering",
                >= 45 => "Scorching",
                _ => "Warm"
            };
        return weatherApiResponse.List
            .Select(x =>
                new WeatherForecast
                {
                    Date = DateOnly.FromDateTime(DateTimeOffset.FromUnixTimeSeconds(x.Dt).Date),
                    TemperatureC = Convert.ToInt32(x.Main.Temp),
                    Summary = computeWeatherSummary(x.Main.Temp)
                })
            .ToArray();
    }
}

```

The weather forecasts of a specific geolocation are retrieved. Indeed coordinates (corresponding to Bordeaux in France) are passed to the Open Weather Map API call. In C# 12, we can alias any type so we can introduce an alias "Coordinates" for the coordinates tuple:

```csharp
using Coordinates = (double Latitude, double Longitude);

public class OpenWeatherService : IWeatherService
{
    private readonly IOpenWeatherMapApi _openWeatherMapApi;
    private static readonly Coordinates BordeauxCoordinates = (44.837789, -0.57918

```

Once this call is done, results are mapped to the expected model `WeatherForecast`. A lambda expression is used to get the "weather summary" from a temperature. If we want to have a default summary, that's something we can do thanks to the support of [default lambda parameters](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#default-lambda-parameters) in C#12.

```csharp
var computeWeatherSummary = (double temperature, string defaultSummary = "Warm") =>
    temperature switch
    {
        < 0 => "Freezing",
        >= 0 and < 5 => "Bracing",
        >= 5 and < 12 => "Chilly",
        >= 12 and < 18 => "Cool",
        >= 18 and < 24 => "Mild",
        >= 24 and < 30 => "Warm",
        >= 30 and < 35 => "Balmy",
        >= 35 and < 40 => "Hot",
        >= 40 and < 45 => "Sweltering",
        >= 45 => "Scorching",
        _ => defaultSummary
    };

```

`RandomWeatherService` does not have this logic because Summaries are randomly selected from an array containing possible summaries.

```csharp
private static readonly string[] Summaries = new [] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" };

```

With [collection expressions](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#collection-expressions), this array can be defined directly with square brackets.

```csharp
private static readonly string[] Summaries = ["Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

```

It would work with other types of collections as well. If we needed to have another list containing only cold summaries and avoid duplication between the two lists, we could also define the two lists and use the spread operator.

```csharp
 private static readonly IList<string> ColdAdjectives = ["Freezing", "Bracing", "Chilly", "Cool"];
 private static readonly string[] Summaries = [..ColdAdjectives, "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"];

```

The last C# 12 thing we could do in this example is to take advantage of the new class (and structs) primary constructors that were previously limited to records.

The `WeatherForecast` class could become the following:

```csharp
namespace WeatherApi;

public class WeatherForecast(DateOnly date, int temperatureC, string? summary)
{
    public int TemperatureC { get; } = temperatureC;
    
    public DateOnly Date { get; } = date;
    
    public string? Summary { get; } = summary;
    
    public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}

```

> 💬 I'm not sure this is completely relevant here, a record would probably be better but you get the idea.

You can use primary constructors in any class, it will work as well with dependency injection. However, be aware that the services you used to assign to a private read-only field of your class won't be read-only anymore like `weatherService` in this example:

```csharp
public class WeatherForecastController(IWeatherService weatherService, ILogger<WeatherForecastController> logger) : ControllerBase
{
    [HttpGet(Name = "GetWeatherForecast")]
    [ProducesResponseType(typeof(WeatherForecast), StatusCodes.Status200OK)]
    public Task<WeatherForecast[]> Get()
    {
        return weatherService.GetWeatherForecasts();
    }
}

```

Having 2 different implementations of the `IWeatherService` is great, but what if you need one of them in some part of your code? The one you will have injected in your class is the last one registered in the DI container, but that may not be the one you want. You could get all of them by injecting `IEnumerable<IWeatherService>` and selecting the one you need. You could also create a sort of factory to retrieve the correct instance. Yet in .NET 8, you don't need to worry about all that because you have the [keyed DI Services](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#keyed-di-services).

Specifying a key (that can be anything, not necessarily a string) is done when registering the services in the DI container.

```csharp
builder.Services.AddKeyedTransient<IWeatherService, RandomWeatherService>("random");
builder.Services.AddKeyedTransient<IWeatherService, OpenWeatherService>("api");

```

With this key, retrieving a specific implementation becomes easy.

```csharp
    public WeatherForecastController([FromKeyedServices("random")] IWeatherService weatherService, ILogger<WeatherForecastController> logger)
    {
        _logger = logger;
        _weatherService = weatherService;
    }

```

I did not discuss the code that requests the Open Weather Map API. It's quite simple thanks to the uses of [Refit](https://github.com/reactiveui/refit).

```csharp
using Refit;

namespace WeatherApi.Services.OpenWeatherMap;

public interface IOpenWeatherMapApi
{
    [Get("/forecast?lat={latitude}&lon={longitude}&units=metric")]
    Task<WeatherMapResponse> GetWeatherForecast(double latitude, double longitude);
}

public record WeatherMapResponse(IList<WeatherMapForecast> List);

public record WeatherMapForecast(int Dt, WeatherMapMain Main);

public record WeatherMapMain(double Temp);

```

I created an HTTP Message Handler to take care of adding the Open Weather Map API key to the requests. This API key and the URL to the API come from the configuration and are mapped to a configuration object `WeatherMapConfiguration`.

In .NET 8, we can use [data validation attributes](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#data-validation) for data like configuration options. There is also a [source code generator](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#options-validation) that can implement the validation logic:

```csharp
namespace WeatherApi.Services.OpenWeatherMap;

public class WeatherMapConfiguration
{
    [Required]
    public required string ApiKey { get; init; }

    [Required]
    [Url]
    public required string Uri { get; init; }
    
}

[OptionsValidator]
public partial class WeatherMapConfigurationValidator : IValidateOptions<WeatherMapConfiguration>
{
}

```

This way we can make sure that the configuration contains the API Key and the URI that has the `Url` format. The configuration in the `Program.cs` looks like that:

```csharp
builder.Services.Configure<WeatherMapConfiguration>(builder.Configuration.GetSection("WeatherMap"));
builder.Services.AddSingleton<IValidateOptions<WeatherMapConfiguration>, WeatherMapConfigurationValidator>();

builder.Services.AddTransient<ApiKeyHandler>();
builder.Services.AddRefitClient<IOpenWeatherMapApi>()
    .ConfigureHttpClient((provider, client) =>
    {
        var configuration = provider.GetRequiredService<IOptions<WeatherMapConfiguration>>().Value;
        client.BaseAddress = new Uri(configuration.Uri);
    })
    .AddHttpMessageHandler<ApiKeyHandler>();

```

## A few closing words

Here is the recap of what we talked about:

| Feature | Area |
| --- | --- |
| [Support for generic attributes](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0#support-for-generic-attributes) | .NET 8 |
| [Primary constructors](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#primary-constructors) | C# 12 |
| [Collection expressions](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#collection-expressions) | C# 12 |
| [Optional parameters in lambda expressions](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#default-lambda-parameters) | C# 12 |
| [Alias any type](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12#alias-any-type) | C# 12 |
| [Debug customization attributes on ASP.NET Core types      ](https://learn.microsoft.com/en-us/visualstudio/debugger/using-the-debuggerdisplay-attribute) | ASP.NET Core 8 |
| [Options validation](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#options-validation) | .NET 8 |
| [Keyed DI Services](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#keyed-di-services) | .NET 8 |

There are many more interesting features in [C# 12](https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12), [.NET 8](https://learn.microsoft.com/en-us/dotnet/core/whats-new/dotnet-8#keyed-di-services), or [ASP.NET Core 8](https://learn.microsoft.com/en-us/aspnet/core/release-notes/aspnetcore-8.0?view=aspnetcore-8.0). Yet, the ones I introduced in this article are the ones I will probably use the most.

You can find the complete code sample [here](https://github.com/TechWatching/CodeAppAndInfraInDotnet8). The repository also contains a folder `infra` to set up the Azure infrastructure to host this API. 2 IaC solutions that use .NET are shown: one using Azure SDK and one using Pulumi.

This article was published as part of the [C# Advent 2023](https://www.csadvent.christmas/) which is a nice initiative. Make sure to check the other blog articles on the advent calendar.