GET · POST · X-Api-Key · async

HttpClient og API-kald

Kald OpgaverAPI asynkront med HttpClient — CRUD over HTTP, DTO’er, typed clients og robust fejlhåndtering.

Læringsmål

Hvornår bruger I det? Når UI skal vise data uden for jeres egen database — Pokémon-lister, LoL-champions, Rick and Morty-episoder. HttpClient er broen mellem Blazor-state og OpgaverAPI; DI-lektionen viser hvor I registrerer klienten.

Tjek dig selv før du går videre

  • Du håndterer IsSuccessStatusCode og viser fejl i UI.
  • Du kan læse ProblemDetails/title fra API-svar.
  • Du har afspillet async-lab og HTTP-sim.

Typisk elev-fejl: alt virker lokalt, men i Dokploy får I 404 — symptom: tom liste, ingen fejltekst. Fix: tjek BaseAddress + relative path (én konvention for slash) og at nginx proxyer /api til rigtig backend.

CRUD over HTTP: POST create, GET read, PUT/PATCH update, DELETE delete. I Blazor er kaldene async — UI-tråden blokeres ikke mens I venter på Pokémon-JSON.

Se HttpClient i aktion

Samme request din Blazor-app sender — afspil GET, POST eller 401 uden nøgle.

KlientHttpClient
ServerOpgaverAPI
# Vælg scenarie og tryk Afspil

Async-tidslinje

Se rækkefølgen: UI viser loading → await HTTP → UI opdateres med resultat.

  1. Bruger klikker «Hent Pokémon»
  2. loading = true (render)
  3. await GetFromJsonAsync — tråd frigives
  4. JSON deserialiseres til List<PokeDto>
  5. loading = false, liste vises
CRUD til HTTP-metoder
CRUD ↔ HTTP-metoder.
@page "/pokemon"
@inject HttpClient Http

<button @onclick="Load" disabled="@loading">Hent Pokémon</button>
@if (loading) { <p>Henter…</p> }
@if (error is not null) { <p class="err">@error</p> }
@if (items is not null)
{
  <ul>@foreach (var p in items) { <li>@p.Name</li> }</ul>
}

@code {
  List<PokeDto>? items;
  bool loading;
  string? error;

  async Task Load()
  {
    loading = true;
    error = null;
    try
    {
      items = await Http.GetFromJsonAsync<List<PokeDto>>("/go/pokemon?limit=20");
    }
    catch (HttpRequestException ex)
    {
      error = $"API fejl: {ex.StatusCode}";
    }
    finally { loading = false; }
  }

  public record PokeDto(string Name, int Id);
}

DTO og JSON-shape

Property-navne skal matche JSON (case-insensitive default). Brug [JsonPropertyName("id")] ved behov. Records er gode til read-only API-data.

public record ChampionDto(string Id, string Name);

// Explicit check
var response = await Http.GetAsync("/LeagueOfLegends/Champions");
if (!response.IsSuccessStatusCode)
{
    var body = await response.Content.ReadAsStringAsync();
    throw new InvalidOperationException($"{response.StatusCode}: {body}");
}
var champs = await response.Content.ReadFromJsonAsync<ChampionDto[]>();

Hvornår bruger I explicit ReadFromJsonAsync? Når I skal læse body ved fejl (ProblemDetails), logge rå JSON i DevTools, eller håndtere 204 No Content uden exception. GetFromJsonAsync er kortere til «happy path» GET — men kaster ved 4xx/5xx.

// OpgaverAPI returnerer camelCase — default JsonSerializer er case-insensitive
public record PokeDto(int Id, string Name);

// Hvis API skifter til snake_case, brug [JsonPropertyName] — ikke dynamic
[JsonPropertyName("champion_id")]
public string ChampionId { get; init; } = "";

Elev-fejl → symptom → fix: DTO har Id, JSON har id — alt er 0/null. Fix: tjek Network → Response; tilpas property-navne eller attributter; undgå at «gætte» med JsonDocument i produktion.

X-Api-Key og headers

using var request = new HttpRequestMessage(HttpMethod.Get, "/LeagueOfLegends/Champions");
request.Headers.Add("X-Api-Key", apiKey);
var response = await Http.SendAsync(request);

Bedre: encapsuler i ILolService så komponenter ikke kender header-detaljer.

Hvornår hvilken header? X-Api-Key til personlig OpgaverAPI-nøgle (LoL m.fl.). Authorization: Bearer når jeres egen backend eller Mercantec Auth JWT skal med. Pokémon-endpoints uden nøgle: ingen header — men stadig Accept: application/json via factory.

  1. Hent nøgle efter login på kataloget — gem i sessionStorage.
  2. ILolService læser nøgle via IApiKeyStore og sætter header på hvert kald.
  3. Ved 401: vis «Log ind og opret nøgle» — ikke raw exception i UI.

Typed client

builder.Services.AddHttpClient<IPokemonApi, PokemonApi>(c =>
    c.BaseAddress = new Uri("https://api-opgaver.mercantec.tech"));

Hvornår typed client frem for rå HttpClient i komponent? Så snart I har mere end ét endpoint (liste + detail + søgning). Pokémon-demo med kun GET kan starte med @inject HttpClient; LoL med nøgle og flere routes bør have ILolService registreret i Program.cs.

Faldgrube: At læse JSON ind i dynamic eller JsonDocument overalt — det skalerer dårligt. Definér DTO’er; fang mismatches tidligt med tests eller tydelige fejl i UI.

Faldgrube: Glemt trailing slash i BaseAddress + absolute path — dobbelte eller manglende slashes giver 404. Følg én konvention (base med slash, relative paths uden leading slash).

Quiz 1

Hvilken HTTP-metode svarer typisk til «read/list» i REST?

Quiz 2

LoL-endpoint returnerer 401 — mest sandsynlige årsag?

ProblemDetails og statuskoder

Hvorfor det betyder noget: OpgaverAPI svarer med meningsfulde statuskoder — 401 (manglende nøgle), 404 (forkert route), 503 (nedetid). Brugeren og censor skal se en handlingsbar besked, ikke en hvid skærm.

Klik en statuskode

Se hvad I bør gøre i Blazor-UI — samme koder I møder i Network-fanen.

public async Task<(List<Mon>? Data, string? Error)> GetPokemonAsync(CancellationToken ct)
{
    using var response = await _http.GetAsync("/go/pokemon?limit=20", ct);
    if (response.IsSuccessStatusCode)
        return (await response.Content.ReadFromJsonAsync<List<Mon>>(ct), null);

    ProblemDetails? problem = null;
    try { problem = await response.Content.ReadFromJsonAsync<ProblemDetails>(ct); }
    catch { /* body var ikke JSON */ }

    var title = problem?.Title ?? response.ReasonPhrase ?? "Ukendt fejl";
    return (null, $"{(int)response.StatusCode}: {title}");
}

I UI: vis Error i rød tekst og tilbyd «Prøv igen» — særligt ved timeout på skolens WiFi.

StatusTypisk på OpgaverAPIHandling i UI
401Manglende/ugyldig X-Api-Key (LoL)Link til katalog + nøgle; ikke retry i loop
404Forkert path eller id«Ikke fundet» + tjek route/DTO
503Nedetid / gatewayKort besked + retry-knap
200 + tom bodySjældentTom-state, ikke crash på deserialize

Sådan fejler det i praksis: GetFromJsonAsync kaster ved 401 — uncaught exception i Blazor Server. Eller I viser hele JSON-body i @error inkl. HTML — farligt hvis API ændres. Parse ProblemDetails, vis kort dansk tekst.

Timeout og CancellationToken

builder.Services.AddHttpClient<IPokemonService, PokemonService>(c =>
{
    c.BaseAddress = new Uri(apiBase);
    c.Timeout = TimeSpan.FromSeconds(15);
});

// I komponent — send token videre fra livscyklus
mons = await Api.GetPokemonAsync(cancellationToken);

Fang TaskCanceledException når brugeren navigerer væk eller netværket dør — skeln mellem timeout og bruger-cancel.

catch (TaskCanceledException ex) when (!ct.IsCancellationRequested)
{
    error = "OpgaverAPI svarede ikke i tide — prøv igen.";
}
catch (OperationCanceledException)
{
    return; // bruger forlod siden — vis ikke fejl
}

Hvornår sende CancellationToken? Altid videre fra OnParametersSetAsync / OnInitializedAsync til service-metoder — især ved LoL-detail når brugeren klikker hurtigt gennem champion-listen.

Mini-opgave

GET /go/pokemon?limit=5 med fejl-UI ved forkert path; vis status + title fra ProblemDetails.

Quiz 3

503 ProblemDetails — hvad vises?

Læs hos Microsoft

Prøv med API

16476 #116476 #1616475 #15

Baseret på MAGS Blazor-wiki og Microsoft Learn — Blazor.