Externe REST services aanroepen met Refit

Dit artikel werd eerder gepubliceerd in het SDN Magazine .

Je kunt je bijna geen applicatie meer voorstellen die geen gebruik maakt van externe gegevens. Vaak komt deze data uit REST Api’s1. Als developer kan het behoorlijk saai zijn om de zoveelste API te moeten ontsluiten maar gelukkig zijn er mogelijkheden om dit een stuk eenvoudiger te maken. Één van die opties is het gebruik van Refit2.

Om te laten zien hoe je met behulp van Refit2 een API aan kunt roepen gebruik ik een een voorbeeeld API met een 5-tal methodes. De voorbeeldcode is ook beschikbaar op GitHub3. De API ziet er zo uit:

Beschikbare NuGet packages

Er zijn twee verschillende NuGet packages beschikbaar, de standaard Refit4 package en Refit.HttpClientFactory5, dezelfde package maar met extra extensies voor Dependency Injection. Voor de meeste voorbeelden maak ik gebruik van een console applicatie met de normale Refit4 nuget package, alleen bij het laatste voorbeeld gebruik ik ook dependency injection.

De implementatie van Refit

Refit2 gebruikt een interface als definitie voor het genereren van code om een API aan te kunnen roepen. In deze interface definieer je welke gegevens je terug verwacht, welke parameters je mee wilt geven en of de API authenticatie gebruikt. Je kunt aangepaste headers meegeven of Bearer Tokens voor authenticatie. In dit artikel laat ik een aantal veelgebruikte functies van Refit2 zien, als je meer wilt weten over de mogelijkheden dan raad ik je aan om de website van Refit2 te lezen.

De interface die nodig is om de bovenstaande API aan te kunnen roepen ziet er zo uit:

public interface IProfessionalsApi
{
    [Get("/requestforsubject/{subject}")]
    Task<Professional> FindProfessionalForQuestion(string subject);

    [Post("/requestforproject")]
    Task SendRequestForProject([Body] ProjectDescription projectDescription);

    [Post("/login")]
    Task<string> Login(UserDto userDto);

    [Post("/addprofessional")]
    Task AddProfessional([Body] Professional professional, [Authorize("Bearer")] string bearerToken);

    [Post("/kortebroeknaarkantoor")]
    Task<string> CanIWearShortsToTheOffice();
}

De eerste methode is een GET request met als parameter Subject. Als resultaat wordt een POCO terug gegeven met gegevens van de gevonden professional. De tweede methode, een POST geeft geen gegevens terug en verzend een ProjectDescription als body. De code om de API daadwerkelijk aan te roepen ziet er zo uit:

// Create an API based on the interface
var api = RestService.For<IProfessionalsApi>("http://localhost:5128");

// Example 1: simple get
var professional = await api.FindProfessionalForQuestion(Subject.Agile.ToString());
Console.WriteLine($"Je kunt met je vragen over {Subject.Agile} terecht bij de deze professional: {professional.FirstName} {professional.LastName}, {professional.Title} <{professional.EmailAddress}>.");

// Example 2: simple post
ProjectDescription projectDescription = new()
{
    CompanyName = "Some Awesome Company",
    Location = "Eindhoven",
    JobTitle = Title.TeamLead,
    Description = "Voor een zelfstandig Agile team van 4 developers en een scrum master zijn we op zoek naar een Team Lead die kan helpen het niveau van het team op een hoger niveau te brengen. De tech-stack is .NET, het niveau van de developers is wisselend"
};
await api.SendRequestForProject(projectDescription);

Authenticatie

Een van de belangrijkste onderdelen van een API is beveiliging. Hebben de gebruikers van de API wel de juiste rechten en zijn ze wie ze zeggen dat ze zijn?

Er zijn twee methodes beschikbaar in de API, een login-methode om een Bearer token op te halen en een methode, AddProfessional, waaraan dit token meegegeven dient te worden, beide methodes zijn ook opgenomen in de interface voor Refit2.

De methode AddProfessional heeft een Authorize attribuut om aan te geven dat deze methode alleen door geauthenticeerde gebruikers mag worden aangeroepen:

// Demo method, authorized post
[HttpPost("addprofessional"), Authorize]
public IActionResult AddProfessional([FromBody] Professional professional)
{
    return Created(string.Empty, professional);
}

In het onderstaande voorbeeld wordt een nieuwe professional aangemaakt door eerst een token op te halen en deze vervolgens mee te sturen naar de API:

// Example 3: login, using bearer token
Professional newProfessional = new()
{
    FirstName = "Dirk",
    LastName = "Alma",
    EmailAddress = "dirk.alma@gmail.com",
    Title = Title.ProductOwner,
    Technologies = new List<string> { Subject.Agile.ToString(), Subject.AzureDevOps.ToString()}
};

var bearerToken = await api.Login(new UserDto("rock", "star123"));
await api.AddProfessional(newProfessional, bearerToken);

Dependency Injection

Voor de laatste methode van de API maakt de applicatie gebruik van een externe API de weersvoorspelling op te halen bij OpenWeatherMap6. Ook voor deze API gebruik ik Refit2:

public interface IOpenWeatherApi
{
    [Get("/data/2.5/weather?lat={lat}&lon={lon}&appid={apiKey}&units=metric")]
    Task<WeatherData> WeatherForLocation(string lat, string lon, string apiKey);
}

Vervolgens registreer ik deze interface:

builder.Services
    .AddRefitClient<IOpenWeatherApi>()
    .ConfigureHttpClient(c =>
        c.BaseAddress = new Uri("https://api.openweathermap.org/"));

Bovenstaande code is voldoende om de externe API aan te kunnen roepen vanuit een controller of service:

[HttpGet("kortebroeknaarkantoor")]
public async Task<IActionResult> KorteBroekNaarKantoor(
    [FromServices] IOpenWeatherApi openWeatherApi,
    [FromServices] WeatherServiceSettings weatherServiceSettings)
{
    var weer = await openWeatherApi.WeatherForLocation(weatherServiceSettings.Latitude, weatherServiceSettings.Longitude, weatherServiceSettings.ApiKey).ConfigureAwait(false);
    return Ok(weer.Temperatures.Maximum >= 15f ?
        $"Prima weer om in je korte broek te gaan, het wordt vandaag {Math.Round(weer.Temperatures.Maximum, 0)} graden!" :
        $"Ik zou het niet doen vandaag, het wordt maar {Math.Round(weer.Temperatures.Maximum, 0)} graden!");
}

Unit Testen

Het gebruik van interfaces heeft nog een bijkomend voordeel: met behulp van bijvoorbeeld Moq is het eenvoudig om code die gebruik maakt van Refit te testen.

[Fact]
public async Task ReturnTheWeatherForTheRequestedLocation()
{
  // ARRANGE
  var mockApi = new Mock<IOpenWeatherApi>();
  mockApi
      .Setup(with => with.WeatherForLocation(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()))
      .ReturnsAsync(new WeatherData {Location = "Test Location"});
  var weatherService = new WeatherService(mockApi.Object);

  // ACT
  var weather = await weatherService.GetWeatherForLocation("testlat", "testlong", "api-key");

  // ASSERT
  mockApi.Verify(apiCall => apiCall.WeatherForLocation(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<string>()), Times.Once);
}

Conclusie

Refit2 is een tool waarmee je eenvoudig en snel API’s kunt ontsluiten, zowel extern als binnen je eigen netwerk. Ik gebruik het zelf al zo lang dat ik bijna niet meer weet hoe ik het zonder zou moeten doen. Er zijn ook alternatieven voor de HttpClient of Refit, ik kom bijvoorbeeld regelmatig Flurl7 tegen.