Læringsmål
- Fortælle hvad DI løser (løs kobling, testbarhed, central konfiguration).
- Vælge mellem Transient, Scoped og Singleton med begrundelse.
- Registrere
HttpClientviaIHttpClientFactoryog typed clients mod OpgaverAPI. - Strukturere services (interface + implementation) og undgå «service locator»-antipattern.
Hvornår bruger I DI? Når mere end én komponent kalder samme API, når I vil mock’e i test, eller når konfiguration (base URL, nøgle-kilde) skal samles ét sted — typisk første uge i et OpgaverAPI-projekt.
Tjek dig selv før du går videre
- Du kan forklare Scoped vs Singleton med to samtidige brugere (favorit-lab).
- Du ved hvornår de ser ens ud (én fane) og hvornår de divergerer (to circuits).
- Du registrerer HttpClient via AddHttpClient — ikke new HttpClient().
- Du har kørt DI-lab for alle tre lifetimes.
Når en Blazor-side skal hente Pokémon fra /go/pokemon og champions fra LoL med X-Api-Key, hører HTTP-logikken ikke hjemme direkte i markup. I opretter fx IPokemonService og ILolService, registrerer dem én gang, og injicerer dem i komponenter — samme idé som i et ASP.NET Web API-projekt.
Program.cs — registrering
I en Blazor Web App er Program.cs composition root: her tilføjer I render modes, Razor Components, og alle services.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddRazorComponents()
.AddInteractiveServerComponents();
// Base URL til OpgaverAPI (miljøspecifik i Docker/Dokploy)
var apiBase = builder.Configuration["OpgaverApi:BaseUrl"]
?? "https://api-opgaver.mercantec.tech";
builder.Services.AddHttpClient<IPokemonService, PokemonService>(client =>
{
client.BaseAddress = new Uri(apiBase);
});
builder.Services.AddScoped<ILolService, LolService>();
builder.Services.AddSingleton<IApiKeyStore, SessionApiKeyStore>();
var app = builder.Build();
// … middleware, MapRazorComponents, Run
AddHttpClient<TClient, TImplementation> kombinerer DI med en korrekt konfigureret HttpClient (handler-pool, DNS, timeouts). Det er Microsofts anbefalede vej — ikke new HttpClient() i hver komponent.
Trin når I tilføjer et nyt API-modul (fx Rick and Morty): definer interface → implementation med HttpClient → AddHttpClient<…> → @inject i page. Composition root er altid Program.cs — ikke static helpers i Razor.
@inject i komponenter
@page "/pokemon-di"
@inject IPokemonService Pokemon
<h1>Pokémon</h1>
@if (loading) { <p>Henter…</p> }
else if (error is not null) { <p class="err">@error</p> }
else
{
<ul>@foreach (var p in list!) { <li>@p.Name</li> }</ul>
}
@code {
List<PokemonDto>? list;
bool loading;
string? error;
protected override async Task OnInitializedAsync()
{
loading = true;
try { list = await Pokemon.GetFirstAsync(24); }
catch (Exception ex) { error = ex.Message; }
finally { loading = false; }
}
}
public interface IPokemonService
{
Task<List<PokemonDto>> GetFirstAsync(int limit, CancellationToken ct = default);
}
public sealed class PokemonService(HttpClient http) : IPokemonService
{
public async Task<List<PokemonDto>> GetFirstAsync(int limit, CancellationToken ct = default)
{
var url = $"/go/pokemon?limit={limit}";
return await http.GetFromJsonAsync<List<PokemonDto>>(url, ct)
?? throw new InvalidOperationException("Tomt svar");
}
}
public record PokemonDto(string Name, int Id);
Hvornår @inject HttpClient direkte? Kun i meget små øvelser. Så snart I har headers (X-Api-Key), fejlhåndtering eller flere endpoints: inject IPokemonService / ILolService i stedet.
Elev-fejl → symptom → fix: @inject HttpClient Http i ti komponenter + manuel BaseAddress i hver — symptom: inkonsistent URL og svær test. Fix: typed client + én registrering.
Lifetimes — hvornår hvad?
Vend kortene
Klik for at afsløre — typiske censor-spørgsmål om DI-lifetimes.
| Lifetime | Betyder | Typisk brug i Blazor |
|---|---|---|
| Transient | Ny instans hver gang den anmodes | Lette, stateless helpers (formatters, validators uden state) |
| Scoped | Én instans pr. scope (ofte pr. HTTP-request / circuit på Server) | Services der cacher per bruger-session, DbContext-lignende state |
| Singleton | Én instans for hele appen | Read-only config, caches med thread-safety, IHttpClientFactory selv |
Lifetime-simulator
Vælg lifetime og «anmod» om service flere gange — loggen viser om I får samme instans-id (scoped/singleton) eller nye (transient).
# Vælg lifetime og tryk Anmod
Faldgrube: At registrere DbContext eller bruger-specifik state som Singleton deler data mellem alle brugere. At registrere HttpClient som Singleton uden factory kan give socket-udmattelse — brug IHttpClientFactory.
Hvornår Transient? Fx en stateless PokemonNameFormatter injiceret mange steder — billigt at oprette. Hvornår Scoped? Brugerens valgte region, indkøbskurv-lignende UI-state, eller cache der må leve i ét Blazor Server-circuit.
Scoped vs Singleton — hvornår er de forskellige?
Hvorfor elever går galt i byen: Med én fane ser Scoped og Singleton ens ud — I får samme instans igen og igen. Forskellen viser sig først når der er flere brugere / circuits (to browsere, underviser + elev, eller to faner på Blazor Server).
Tænk: Scoped = én per bruger-session (circuit). Singleton = én for hele serveren.
| Situation | Scoped | Singleton | Samme resultat? |
|---|---|---|---|
| Én bruger, tre komponenter inject’er samme service | Samme instans i det scope | Samme instans | Ja — I kan ikke se forskel |
| Elev A og Elev B åbner appen samtidig (to circuits) | Hver sin instans → A’s favorit er ikke B’s | Samme instans → A’s valg overskriver B’s UI | Nej — her knækker Singleton |
Service holder SelectedRegion / kurv / «sidst klikket» |
Korrekt: state følger brugeren | Bug: state er fælles for alle | Nej |
| Read-only Pokémon-type-cache (ingen brugerdata) | Virker, men spilder hukommelse (kopi pr. bruger) | Korrekt: én delt cache | Funktionelt OK — Singleton er bedre |
Tæller i felter: RequestCount++ |
Tæller pr. bruger/session | Global tæller for hele appen | Nej — forskellige tal |
To brugere — prøv selv
Vælg lifetime. Lad Elev A og Elev B vælge favorit-Pokémon. Se om B «stjæler» A’s valg.
Favorit: —
Favorit: —
Tre konkrete eksempler I kan huske til censor:
IFavoriteStoremedSelectedId→ Scoped (brugerstate).IPokemonTypeCacheder loader types én gang fra API → Singleton (delt, read-only).ILolRegionPreference(«EUW» / «NA») i memory → Scoped. Samme klasse som Singleton? Så ser Elev B Elev A’s region.
// Samme klasse — lifetime afgør adfærd mellem brugere
public sealed class FavoriteStore : IFavoriteStore
{
public string? SelectedName { get; private set; }
public void Set(string name) => SelectedName = name;
}
// ✅ Isoleret per Blazor Server-circuit / HTTP-scope
builder.Services.AddScoped<IFavoriteStore, FavoriteStore>();
// ❌ Alle samtidige brugere deler SelectedName
// builder.Services.AddSingleton<IFavoriteStore, FavoriteStore>();
// ✅ Singleton er rigtigt når der IKKE er brugerfelter:
builder.Services.AddSingleton<IPokemonTypeCache, PokemonTypeCache>();
Blazor WASM-nuance: I ren WASM kører DI i browseren — Singleton er «pr. fane», ikke «pr. elev på serveren». Scoped vs Singleton betyder stadig noget (nye scopes ved navigation), men den klassiske «Elev A stjæler Elev B»-bug er især Blazor Server / delt ASP.NET-host. På Server: to browsere = to circuits = Scoped adskiller dem; Singleton gør ikke.
IHttpClientFactory og typed client
Typed clients er klasser der tager HttpClient i konstruktoren og kapsler endpoints. Factory’en håndterer levetid af handlers.
builder.Services.AddHttpClient<ILolService, LolService>(client =>
{
client.BaseAddress = new Uri(apiBase);
client.DefaultRequestHeaders.Add("Accept", "application/json");
});
// I LolService tilføjes X-Api-Key per kald fra IApiKeyStore — ikke hardcodet i git
Hvis I både har WASM og Server i samme solution, kan base URL komme fra appsettings.json + miljøvariabler i Docker — præcis som APIer-projektets compose-mønster.
Named clients når I har flere base URLs eller forskellige handlers — fx OpgaverAPI + jeres egen backend med Bearer:
builder.Services.AddHttpClient("opgaver", c =>
c.BaseAddress = new Uri(apiBase));
builder.Services.AddHttpClient("own-api", c =>
{
c.BaseAddress = new Uri(builder.Configuration["OwnApi:BaseUrl"]!);
}).AddHttpMessageHandler<BearerTokenHandler>();
// I service: IHttpClientFactory factory.CreateClient("opgaver")
ConfigurePrimaryHttpMessageHandler bruges sjældent i skoleprojekter (certifikater, proxy) — men factory-mønsteret er det samme: konfigurer i Program.cs, ikke i komponent.
Interfaces og test
Med IPokemonService kan I i en unit test mocke API’et og teste komponentlogik uden netværk. Det er sværere hvis komponenten kalder Http.GetFromJsonAsync direkte overalt.
Hvornår interface? Når I forventer alternativ implementation (mock, offline demo) eller når censor skal kunne skifte datakilde uden at røre Razor. Små lektionsøvelser kan undvære interface — produktionsprojekter bør have det.
Quiz 1
Hvorfor bør I undgå new HttpClient() i hver Blazor-komponent?
Quiz 2
En service der gemmer den aktuelle brugers valgte LoL-region i memory — bedste default-lifetime på Blazor Server?
Scoped vs Singleton — konkret bug
Hvorfor det betyder noget: Lifetime bestemmer om state følger én bruger eller hele app-poolen. I et Pokémon-/LoL-projekt er det forskellen på «min favorit» og «klassens favorit».
Typisk fejl: IFavoriteChampionStore registreres som Singleton, fordi «vi vil kun have én instans». Implementeringen gemmer CurrentChampionId og LastRegion i felter. Elev A vælger Ahri i EUW — Elev B åbner samme side og ser Ahri uden at have klikket.
// BUG: alle Blazor Server circuits deler samme store builder.Services.AddSingleton<IFavoriteChampionStore, FavoriteChampionStore>(); // FIX: scoped til circuit / request — isoleret per bruger-session builder.Services.AddScoped<IFavoriteChampionStore, FavoriteChampionStore>();
Sammenligning i klasseværelset: åbn samme app i to browsere (to brugere). Med Singleton ændrer A’s valg B’s UI; med Scoped gør det ikke.
public sealed class FavoriteChampionStore : IFavoriteChampionStore
{
public string? CurrentChampionId { get; private set; }
public void Select(string id) => CurrentChampionId = id;
}
// Singleton er stadig rigtigt til read-only, thread-safe cache:
builder.Services.AddSingleton<IPokemonTypeCache, PokemonTypeCache>();
Sådan fejler det i praksis: Med én developer-tab ser Singleton «effektivt» ud. Fejlen opdages først ved sideløbende test, demo for censor, eller når underviser og elev deler samme Dokploy-miljø. Symptom: «ghost clicks» — valg skifter uden input.
Mini-opgave
Test ICounterService som Scoped vs Singleton: to komponenter viser delt count kun ved Singleton — dokumentér med screenshot.
Quiz 3
Hvem ejer valgt Pokémon-id for to samtidige brugere?
Læs hos Microsoft
Prøv med API
- Udskil et
PokemonServicei jeres projekt og kald Pokémon-demo’s endpoints — sammenlign med råHttpClienti Network-fanen. - Tilføj LoL med nøgle fra kataloget via en
ILolServiceder læser nøglen fra sessionStorage-wrapper.
Baseret på MAGS Blazor-wiki og Microsoft Learn — Blazor.