Webbserverprogrammering 1 · område 4 · guide
Filmarkivet – tjänster, dependency injection och tillstånd
Sjutton steg som flyttar all logik ut ur komponenterna.
Applikationen ser likadan ut när du är klar – det är avsikten. Skillnaden syns
bara i koden: ingen komponent vet längre var filmerna kommer ifrån. De ber om
dem, och något annat svarar. Två sidor är nya:
/visningslistan och /livstider.
Guiden går att följa på egen hand. Så här hänger stegen ihop med passen:
- Lektion 23 (ons 11 nov) – steg 1. Problemet: tre sidor som behöver samma lista. Läs också begreppen, de används utan förklaring efter det här passet.
- Lektion 24 (tis 17 nov) – steg 2 till
steg 7. Områdets tyngsta pass: gränssnittet,
implementationen, registreringen i
Program.csoch@inject. Öva sedan i övning 51–56. - Lektion 25 (ons 18 nov) – inget nytt. Eget arbete med övning 57–62.
- Lektion 26 (tis 24 nov) – steg 8 till
steg 10. Tre livstider, demot på
/livstideroch hur man väljer. Öva i övning 63–69. - Lektion 27 (ons 25 nov) – steg 11 till steg 14. HTTP minns ingenting, session, cookien och formuläret som lägger till.
- Lektion 28 (tis 1 dec) – steg 15 till
steg 17.
Task, loggning ochtry/catchpå rätt nivå. Öva i övning 70–72. - Lektion 29 (ons 2 dec) – inget nytt. Checklistan, uppgiften och sex fel som flyttar sig. Inlämning mån 7 dec.
Stegen bygger på varandra hela vägen. Hoppa inte över steg 1 för att det inte innehåller någon ny kod – det är där hela området får sitt skäl.
I område 3 kom felen när sidan öppnades. Här tillkommer en tidpunkt till:
registreringarna i Program.cs kontrolleras när servern startar.
En scoped tjänst i en singleton, eller AddSession utan cache, gör
att dotnet run avbryts – med ett bygge som gick igenom utan en
enda varning.
Fyra tidpunkter gäller från och med nu: bygget, serverstarten, sidvisningen – och det fjärde, som inte säger något alls. Sex fel som flyttar sig tar ett av varje.
Kodblocken på den här sidan går inte att markera, och det är med flit. Att skriva av koden för hand är där en stor del av inlärningen sker: du läser raden, håller den i huvudet och upptäcker vid tangentbordet vad du inte förstod. Klistrar du in den hoppar du över precis det steget.
Guide · Område 4 · Webbserverprogrammering 1
Vad du bygger här
Applikationen ser likadan ut när du är klar. Det är avsikten.
Skillnaden syns bara i koden: ingen komponent vet längre var filmerna kommer ifrån. De ber om dem, och något annat svarar. Uträkningarna som låg i Filmstatistik har flyttat. Den statiska Filmdata från område 3 är borta – den lovade att försvinna här, och den gör det.
Två saker är nya på skärmen:
Filmarkivet
├── /visningslistan Filmer du lagt undan. Överlever sidbyten
└── /livstider Demo: transient, scoped och singleton bredvid varandra
Det färdiga projektet ligger i referensprojekt/Filmarkivet.
Det här är områdets svåraste sak att se poängen med, eftersom belöningen kommer först i område 5. Då byter du ut hela FilmService mot en som läser ur en databas – och rör inte en enda komponent. Håll den bilden framför dig medan du flyttar kod som redan fungerade.
Vad du ska göra själv efteråt
Samma sak i ditt projekt:
- Ett gränssnitt och en implementation för din huvudentitet
- Registrering i
Program.csoch@injecti komponenterna - All datalogik ur komponenterna. Ingen komponent hämtar sin egen data
- En samling som överlever sidvisningar – din motsvarighet till visningslistan
- En skriftlig motivering av vald livstid
- En kort text om skillnaden mellan scoped, singleton och cookie
Översättningstabellen
| Filmarkivet | Ditt projekt |
|---|---|
IFilmService | IDittService – gränssnittet |
FilmService | implementationen med din hårdkodade lista |
HamtaAllaAsync | din metod som hämtar allt |
HamtaAsync(int id) | din uppslagning på id |
SokAsync(string? sok) | din sökning |
Arkivstatistik | ditt svar som håller ihop flera uträknade värden |
IVisningslista / Visningslista | din samling: önskelista, veckomeny, spellista |
/visningslistan | din sida för samlingen |
Arkivet (injektionsnamnet) | ditt namn – inte samma som sidans filnamn |
Samlingen valde du redan i område 3, när du bockade av domänkraven. Nu byggs den.
Innan du börjar
Ditt projekt från område 3: komponenter med parametrar, en listsida och en detaljsida som hänger ihop, och en hårdkodad lista i en egen fil.
Kontrollen tar tio sekunder:
dotnet run
Gå till din detaljsida och klicka dig dit från listan. Fungerar det är du redo.
Begreppen i det här momentet
| Begrepp | Vad det är |
|---|---|
| Lager | En nivå i applikationen med ett eget ansvar: visa, beräkna, lagra |
| Presentationslager | Komponenterna. Visar och tar emot – beräknar inte |
| Tjänst | En klass som gör arbetet: hämtar, räknar, sparar |
| Gränssnitt (interface) | Ett löfte om vilka metoder som finns, utan att säga hur de gör |
| Implementation | Klassen som uppfyller löftet |
| Dependency injection | Att be ramverket om en tjänst i stället för att skapa den själv |
| Registrering | Raden i Program.cs som säger vilken klass som svarar på ett gränssnitt |
| Konstruktorinjektion | Att ta emot sina beroenden som parametrar till konstruktorn |
| Livstid (lifetime) | Hur länge ett tjänsteobjekt lever: transient, scoped, singleton |
| Tillstånd (state) | Data som ska finnas kvar mellan två sidvisningar |
| Session | Serverns minne per besökare. Nyckeln ligger i en cookie |
| Cookie | En liten text webbläsaren skickar med varje förfrågan till samma sajt |
| Antiforgery | Skydd mot att en annan sajt skickar ett formulär i besökarens namn |
Task<T> | Ett svar som kanske inte är färdigt än |
| Logging | Att skriva vad som hände, för utvecklaren – inte för användaren |
Steg 1 – Problemet
Vad vi gör. Ingenting. Vi tittar på vad som redan är fel.
Öppna ditt projekt från område 3 och leta efter namnet på din datafil – Filmdata i Filmarkivet. Räkna ställena.
I Filmarkivet är de fyra:
Components/Pages/Home.razor Filmdata.Alla (tre gånger)
Components/Pages/Filmer.razor Filmdata.Alla
Components/Pages/Filmdetalj.razor Filmdata.Hamta, Filmdata.Alla
Components/Filmer/Filmstatistik.razor räknar själv på listan den fick
Varför det är ett problem. Inte för att det är fult. För att varje sådant ställe är ett ställe som måste ändras den dag datan kommer någon annanstans ifrån – och den dagen är område 5.
Ställ tre frågor till din egen kod:
- Om listan i morgon läses ur en databas, hur många filer ändras?
- Om sökningen ska bli okänslig för å, ä och ö, var ändrar du?
- Om två sidor räknar snittbetyg, var ligger uträkningen?
Svaren i område 3 är: fyra, på två ställen, och i komponenten. Efter det här området är svaren: en, på ett ställe, och i tjänsten.
Ansvarsfördelningen som resten av kursen vilar på:
| Lager | Vad det gör | I ditt projekt |
|---|---|---|
| Presentation | Visar, tar emot klick och adresser | Components/ |
| Applikationslogik | Räknar, väljer, avgör | Services/ |
| Data | Hämtar och sparar | Services/, sedan databasen i område 5 |
I ditt projekt. Skriv upp dina egna ställen på papper innan du börjar flytta. Listan är din checklista genom hela området.
Steg 2 – Gränssnittet
Vad vi gör. Skriver löftet, innan vi skriver något som uppfyller det.
Filen. Services/IFilmService.cs – ny mapp, ny fil:
using Filmarkivet.Models;
namespace Filmarkivet.Services;
public interface IFilmService
{
Task<List<Film>> HamtaAllaAsync();
Task<Film?> HamtaAsync(int id);
Task<List<Film>> SokAsync(string? sok);
Task<Arkivstatistik> HamtaStatistikAsync();
}
Varför det ser ut så. Ett interface innehåller metodernas namn, parametrar och returtyper – ingen kod. Det säger vad man kan be om, inte hur svaret tas fram.
I först i namnet är C#-praxis för gränssnitt. Det är inte ett krav i språket, men alla gör det, och kodningsstandard är eget centralt innehåll i kursen.
Metoderna slutar på Async och svarar med Task<T>. Just nu finns ingenting att vänta på – listan ligger i minnet. Men databasen i område 5 kommer att kräva det, och signaturen ska inte behöva ändras då. Steg 15 går igenom vad Task betyder.
Skriv gränssnittet först, före implementationen. Det tvingar dig att bestämma vad tjänsten ska kunna innan du börjar tänka på hur.
Filen. Models/Arkivstatistik.cs, för svaret som håller ihop fyra värden:
namespace Filmarkivet.Models;
public class Arkivstatistik
{
public int Antal { get; set; }
public double Snittbetyg { get; set; }
public string LangstaTitel { get; set; } = "";
public int LangstaMinuter { get; set; }
public int AntalTillgangliga { get; set; }
}
Kontrollera. Projektet bygger. Ingenting har hänt på skärmen.
I ditt projekt. Fyra till sex metoder räcker. Ta med det dina sidor faktiskt gör i dag – inte det du tror att du kommer att behöva.
Steg 3 – Implementationen
Vad vi gör. Flyttar listan och uppslagningen ur Models/Filmdata.cs in i en klass som uppfyller löftet.
Filen. Services/FilmService.cs:
using Filmarkivet.Models;
namespace Filmarkivet.Services;
public class FilmService : IFilmService
{
private static readonly List<Film> Alla = new()
{
new Film { Id = 1, Titel = "Nyckeln till frihet", Ar = 1994, LangdMinuter = 142, Betyg = 5, Beskrivning = "…", Tillganglig = true },
// … resten av filmerna, flyttade ur Filmdata
};
public Task<List<Film>> HamtaAllaAsync()
{
return Task.FromResult(new List<Film>(Alla));
}
public Task<Film?> HamtaAsync(int id)
{
foreach (var film in Alla)
{
if (film.Id == id)
{
return Task.FromResult<Film?>(film);
}
}
return Task.FromResult<Film?>(null);
}
}
Hela filen med alla fyra metoderna ligger i referensprojektet.
Varför det ser ut så. : IFilmService efter klassnamnet är löftet. Kompilatorn kontrollerar att varje metod i gränssnittet finns här, med exakt rätt signatur. Glömmer du en får du CS0535 – och det är ett kompileringsfel, till skillnad från nästan allt i område 3.
new List<Film>(Alla) och inte Alla rakt av: den som får listan får en kopia, och kan inte råka ändra i arkivet. Det kostar nästan ingenting och tar bort en hel sorts bugg.
Task.FromResult(...) gör ett färdigt värde till ett Task. Steg 15 förklarar det.
Kontrollera. Projektet bygger fortfarande. Ta inte bort Filmdata ännu – komponenterna använder den.
I ditt projekt. Klipp ut, klistra in, byt namn. Det är flyttarbete, inte nyskrivning. Motstå frestelsen att förbättra logiken samtidigt – flytta först, förbättra sedan.
Steg 4 – Registreringen
Vad vi gör. Talar om för ramverket vem som svarar på IFilmService.
Filen. Program.cs, före var app = builder.Build();:
using Filmarkivet.Services;
builder.Services.AddScoped<IFilmService, FilmService>();
Varför det ser ut så. Raden läses: *ber någon om IFilmService, ge dem en FilmService.* Vinkelparenteserna har två typer, och ordningen spelar roll – gränssnittet först, klassen sedan.
AddScoped är livstiden. Ett objekt per förfrågan. Steg 8 till 10 går igenom vad alternativen betyder och varför just den här är rätt här.
**Det här är den enda raden i hela projektet som nämner FilmService.** Sök gärna på klassnamnet när du är klar med området: det ska finnas på två ställen, i sin egen fil och här.
Kontrollera. Projektet startar. Fortfarande ingen synlig skillnad.
I ditt projekt. En rad per tjänst. De ligger tillsammans, i toppen av Program.cs, inte utspridda.
Steg 5 – Injektionen
Vad vi gör. Låter en komponent be om tjänsten i stället för att gå till listan.
Filen. Components/Pages/Filmer.razor:
@page "/filmer"
@inject IFilmService Arkivet
@code {
[SupplyParameterFromQuery]
public string? Sok { get; set; }
private List<Film> traffar = new();
protected override async Task OnParametersSetAsync()
{
traffar = await Arkivet.SokAsync(Sok);
}
}
Varför det ser ut så. @inject är samma sak som @inject NavigationManager i område 3 – du har redan använt dependency injection utan att det hette något. Skillnaden är att tjänsten nu är din egen.
Komponenten nämner bara gränssnittet. Den vet inte att FilmService finns, och ska inte veta det.
OnParametersSetAsync i stället för OnParametersSet, och await framför anropet. Metoden svarar med Task, och await betyder "vänta tills svaret är klart, och ge mig innehållet".
Namnet efter typen är ditt val, och det finns en fälla. Sidan heter Filmer.razor, alltså blir klassen Filmer. Skriver du @inject IFilmService Filmer får du:
CS0542: 'Filmer': member names cannot be the same as their enclosing type
Därför heter injektionen Arkivet. Samma fälla väntar dig om din sida heter samma sak som det den visar – vilket den ofta gör.
Kontrollera. Gå till /filmer. Allt ser likadant ut. Sök på något – sökningen fungerar, men den utförs inte längre här.
I ditt projekt. Börja med en sida. Få den att fungera. Ta nästa sedan.
Steg 6 – Resten av komponenterna, och `Filmdata` försvinner
Vad vi gör. Gör om samma sak på de andra sidorna, och tar bort den gamla filen.
Components/Pages/Home.razor:
@page "/"
@inject IFilmService Arkivet
@code {
private List<Film> topplistan = new();
private Film veckansFilm = new();
private Arkivstatistik statistik = new();
protected override async Task OnInitializedAsync()
{
List<Film> alla = await Arkivet.HamtaAllaAsync();
veckansFilm = alla[0];
foreach (var film in alla)
{
if (film.Betyg == 5)
{
topplistan.Add(film);
}
if (film.LangdMinuter < veckansFilm.LangdMinuter)
{
veckansFilm = film;
}
}
statistik = await Arkivet.HamtaStatistikAsync();
}
}
Components/Pages/Filmdetalj.razor byter Filmdata.Hamta(Id) mot await Arkivet.HamtaAsync(Id) och OnParametersSet mot OnParametersSetAsync.
**Radera sedan Models/Filmdata.cs.**
Varför det ser ut så. Att ta bort filen är inte städning – det är kontrollen. Så länge den finns kvar kan en sida smyga tillbaka till den. Bygger projektet efter att den är borta vet du att ingen gör det.
Lägg märke till att OnInitializedAsync fortfarande gör ett urval – topplistan och veckans film räknas fram i komponenten. Det är medvetet kvar så länge, och steg 7 tar hand om statistiken. Var gränsen går är en av områdets svåraste frågor, och uppgiften ställer den.
Kontrollera. Projektet bygger, och Filmdata finns inte kvar någonstans. Alla sidor svarar som förut.
I ditt projekt. Samma sak, och radera din datafil. Om något går sönder pekar felmeddelandet på exakt den sida du missat.
Steg 7 – Statistiken flyttar in i tjänsten
Vad vi gör. Låter komponenten som heter Filmstatistik sluta räkna.
Filen. Services/FilmService.cs, den fjärde metoden:
public Task<Arkivstatistik> HamtaStatistikAsync()
{
int summa = 0;
int tillgangliga = 0;
Film langst = Alla[0];
foreach (var film in Alla)
{
summa = summa + film.Betyg;
if (film.Tillganglig)
{
tillgangliga++;
}
if (film.LangdMinuter > langst.LangdMinuter)
{
langst = film;
}
}
Arkivstatistik svar = new()
{
Antal = Alla.Count,
Snittbetyg = (double)summa / Alla.Count,
LangstaTitel = langst.Titel,
LangstaMinuter = langst.LangdMinuter,
AntalTillgangliga = tillgangliga
};
return Task.FromResult(svar);
}
Filen. Components/Filmer/Filmstatistik.razor – hela filen, efteråt:
<ul class="statistik">
<li>Antal filmer: @Statistik.Antal</li>
<li>Snittbetyg: @Statistik.Snittbetyg.ToString("0.0")</li>
<li>Längsta filmen: @Statistik.LangstaTitel, @Statistik.LangstaMinuter minuter</li>
<li>Tillgängliga just nu: @Statistik.AntalTillgangliga av @Statistik.Antal</li>
</ul>
@code {
[Parameter]
[EditorRequired]
public Arkivstatistik Statistik { get; set; } = new();
}
Varför det ser ut så. Jämför med samma fil i område 3: den innehöll tre metoder och trettio rader logik. Nu innehåller den noll. Komponenten visar, tjänsten vet.
Fyra loopar blev en. Det är en bonus, inte poängen – men det är typiskt för vad som händer när uträkningar samlas där de hör hemma.
Arkivstatistik är ett litet objekt vars enda uppgift är att bära ett svar. Det behöver inga metoder.
Kontrollera. Startsidan visar samma fyra rader som förut. Snittet ska stå som 4,5 – blir det 4 har (double) fallit bort i flytten.
I ditt projekt. Har du en komponent som räknar? Flytta uträkningen och låt komponenten ta emot svaret. Det är C-kravet, formulerat som kod.
Steg 8 – Tre livstider
Vad vi gör. Bygger demot som gör livstiderna synliga.
Filen. Services/Raknare.cs:
namespace Filmarkivet.Services;
public interface ITransientRaknare
{
int Nummer { get; }
}
public interface IScopedRaknare
{
int Nummer { get; }
}
public interface ISingletonRaknare
{
int Nummer { get; }
}
public class Raknare : ITransientRaknare, IScopedRaknare, ISingletonRaknare
{
private static int _nasta = 0;
public int Nummer { get; }
public Raknare()
{
Nummer = System.Threading.Interlocked.Increment(ref _nasta);
}
}
Filen. Program.cs:
builder.Services.AddTransient<ITransientRaknare, Raknare>();
builder.Services.AddScoped<IScopedRaknare, Raknare>();
builder.Services.AddSingleton<ISingletonRaknare, Raknare>();
Filen. Components/Delade/Raknarrad.razor:
@inject ITransientRaknare Transient
@inject IScopedRaknare Scoped
@inject ISingletonRaknare Singleton
<tr>
<th scope="row">@Var</th>
<td>@Transient.Nummer</td>
<td>@Scoped.Nummer</td>
<td>@Singleton.Nummer</td>
</tr>
@code {
[Parameter]
[EditorRequired]
public string Var { get; set; } = "";
}
Sidan Components/Pages/Livstider.razor renderar tre <Raknarrad> i en tabell. Hela filen ligger i referensprojektet.
Varför det ser ut så. En enda klass bakom alla tre gränssnitten. Koden är identisk. Allt som skiljer är vilket Add-anrop som användes. Det är hela lektionen, och den går inte att se på klassen – bara på registreringen.
Interlocked.Increment i stället för _nasta++: två besökare kan komma samtidigt, och då skulle en vanlig ökning kunna tappa bort ett nummer. Du behöver inte kunna det – men materialet ska inte innehålla en bugg med flit.
Kontrollera. Gå till /livstider.
I ditt projekt. Lägg in demot som det är. Det är inget du behöver hitta på själv – det är en mätare, inte en funktion.
Steg 9 – Vad tabellen visar
Vad vi gör. Läser av mätaren.
Så här såg det ut vid två laddningar i rad i referensprojektet:
Laddning A Transient Scoped Singleton
Komponent 1 23 22 1
Komponent 2 24 22 1
Komponent 3 25 22 1
Laddning B Transient Scoped Singleton
Komponent 1 27 26 1
Komponent 2 28 26 1
Komponent 3 29 26 1
Varför det ser ut så. Läs tabellen i två riktningar.
Nedåt, inom en laddning: transient ger tre olika nummer – tre objekt, ett per fråga. Scoped ger samma nummer tre gånger – ett objekt, delat av alla som frågar under den förfrågan. Singleton likaså.
Mellan laddningarna: transient och scoped får nya nummer. Singleton står still. Den siffran, en etta som vägrar röra sig, är områdets tydligaste bild.
| Inom en förfrågan | Vid omladdning | För en annan besökare | |
|---|---|---|---|
AddTransient | nytt objekt varje gång | nya | nya |
AddScoped | ett objekt | nytt | eget |
AddSingleton | ett objekt | samma | samma |
Kontrollera. Ladda om fem gånger och följ singletonkolumnen. Öppna sedan sidan i ett privat fönster: singleton står kvar på samma nummer, för en helt annan besökare.
Vad betyder det att två besökare delar ett objekt?
Det är frågan steg 10 svarar på, och den är viktigare än siffrorna.
I ditt projekt. Kör demot, och skriv ned de tre raderna du ser. Du kommer att behöva dem till motiveringen i uppgiften.
Steg 10 – Att välja livstid
Vad vi gör. Bestämmer, och skriver ned varför.
Regeln, i den ordning du ska pröva den:
- **
AddScopedsom förval.** En förfrågan, ett objekt. Det är rätt för nästan allt
som rör en besökare, och det är vad FilmService och Visningslista använder.
- **
AddSingletonbara för sådant som ska delas av alla** och som inte innehåller
någons personliga data: en cache, ett konfigurationsvärde, en räknare över hela sajten.
- **
AddTransientnär objektet är litet, billigt och inte ska minnas något.**
Behövs sällan i den här kursen.
Varför det är farligt att välja fel. Lägg besökarens visningslista i en singleton, och alla besökare delar samma lista. Din lärare lägger till en film, och den syns hos hela klassen. Det är inte en teoretisk risk – det är den vanligaste allvarliga buggen i det här området, och den syns inte när man testar ensam.
Åt andra hållet: en singleton som håller något dyrt att skapa – en anslutning, en inläst fil – är precis rätt, och att göra den scoped kostar prestanda i onödan.
**Vad scoped faktiskt betyder här.** Vid statisk serverrendering är en scope samma sak som en HTTP-förfrågan. Varje klick är en ny förfrågan, alltså ett nytt objekt. I ett interaktivt renderingsläge är en scope i stället hela den uppkoppling besökaren håller öppen, och då lever ett scopat objekt mycket längre. Kursen håller sig på serversidan, så räkna med förfrågan – men vet att det finns ett till svar.
Kontrollera. Öppna din Program.cs och läs varje registrering högt med motiveringen. Kan du inte motivera en av dem är det den du ska tänka om.
I ditt projekt. Skriv motiveringen nu, i README.md, medan du minns varför. Den är ett krav i uppgiften och tar två minuter i dag och tjugo i december.
Steg 11 – HTTP minns ingenting
Vad vi gör. Ingenting. Vi konstaterar problemet innan vi löser det.
Prova i ditt projekt: lägg ett vanligt fält i en komponent, öka det, ladda om.
Det är noll igen. Varje förfrågan bygger ett nytt objekt av komponenten – det såg du redan på /livscykel i område 3, där räknaren bara steg för att den var static.
Varför det ser ut så. HTTP är tillståndslöst. Servern får en förfrågan, svarar, och glömmer. Nästa förfrågan från samma besökare ser likadan ut som en förfrågan från någon helt annan.
Så hur vet en sajt att du är inloggad?
Tre svar, och de är olika saker:
| Var det ligger | Vad det är | Vem ser det |
|---|---|---|
| Cookie | En liten text som webbläsaren skickar med varje förfrågan | Besökaren, och alla som ser trafiken |
| Session | Data på servern. Cookien innehåller bara nyckeln till den | Bara servern |
| Singleton | Data på servern, delad av alla besökare | Alla, samtidigt |
Kontrollera. Öppna DevTools på en sajt du känner till, fliken Application och Cookies. Titta på namn, värde och Expires. De flesta värden är obegripliga – det är meningen, det är nycklar och inte innehåll.
I ditt projekt. Ingenting ännu. Nästa steg bygger.
Steg 12 – Session och cookien
Vad vi gör. Slår på serverns minne per besökare.
Filen. Program.cs:
builder.Services.AddHttpContextAccessor();
builder.Services.AddDistributedMemoryCache();
builder.Services.AddSession(alternativ =>
{
alternativ.IdleTimeout = TimeSpan.FromMinutes(30);
alternativ.Cookie.HttpOnly = true;
alternativ.Cookie.IsEssential = true;
});
och, bland app-raderna, före app.MapRazorComponents<App>():
app.UseSession();
Varför det ser ut så. Fyra rader, och tre av dem är fällor.
AddDistributedMemoryCache() – session lagrar sina data i en cache men drar inte in någon själv. Utan raden startar programmet inte alls:
Unable to resolve service for type 'IDistributedCache'
Felet kommer när servern startar, inte när projektet byggs. Bygget går igenom.
AddHttpContextAccessor() behövs för att en tjänst ska nå den aktuella förfrågan. En komponent kan få HttpContext på andra sätt, men Visningslista är ingen komponent.
app.UseSession() måste finnas. Saknas raden svarar sidan 500 med Session has not been configured for this application or request. Placeringen relativt MapRazorComponents spelar däremot ingen roll: den middleware som kör själva sidan läggs alltid till sist, så UseSession hinner före oavsett var raden står. Skriv den ändå bland de övriga app.Use-raderna – det är där en läsare letar.
HttpOnly = true betyder att JavaScript inte kommer åt cookien. IsEssential = true säger att den behövs för att sajten ska fungera, och därför inte kräver samtycke.
Kontrollera. Starta. Startar servern över huvud taget har du klarat den vanligaste fällan.
I ditt projekt. Samma fyra rader, i samma ordning.
Steg 13 – Samlingen som tjänst
Vad vi gör. Bygger visningslistan bakom ett gränssnitt, precis som filmerna.
Filen. Services/IVisningslista.cs:
using Filmarkivet.Models;
namespace Filmarkivet.Services;
public interface IVisningslista
{
Task<List<Film>> HamtaAsync();
int Antal { get; }
void LaggTill(int filmId);
void TaBort(int filmId);
void Rensa();
}
Filen. Services/Visningslista.cs – kärnan:
public class Visningslista : IVisningslista
{
private const string Nyckel = "visningslista";
private readonly IHttpContextAccessor _http;
private readonly IFilmService _filmer;
public Visningslista(IHttpContextAccessor http, IFilmService filmer)
{
_http = http;
_filmer = filmer;
}
private ISession? Session => _http.HttpContext?.Session;
private List<int> Idn()
{
List<int> idn = new();
string? text = Session?.GetString(Nyckel);
if (string.IsNullOrEmpty(text))
{
return idn;
}
foreach (var del in text.Split(','))
{
if (int.TryParse(del, out int id))
{
idn.Add(id);
}
}
return idn;
}
private void Spara(List<int> idn)
{
Session?.SetString(Nyckel, string.Join(",", idn));
}
}
Hela filen ligger i referensprojektet. Registrera den:
builder.Services.AddScoped<IVisningslista, Visningslista>();
Varför det ser ut så. Konstruktorinjektion: klassen tar emot sina två beroenden som parametrar och sparar dem i readonly-fält. Den skapar ingenting själv – exakt samma princip som @inject i en komponent, fast i vanlig C#.
Att IFilmService injiceras i Visningslista visar att tjänster får använda varandra. Beroendet går mot gränssnittet även här.
Session lagrar text, inte objekt. Därför sparas id:na som "1,4,6" och tolkas tillbaka med int.TryParse – samma metod som i område 2, och av samma skäl: värdet kommer utifrån och kan vara skräp.
Bara id:n sparas, inte hela filmer. Sessionen ska vara liten, och filmen kan alltid hämtas på nytt. HamtaAsync slår upp dem via IFilmService.
Kontrollera. Projektet bygger. Ingen sida använder tjänsten ännu.
I ditt projekt. Din samling, ditt gränssnitt. Spara id:n, inte objekt.
Steg 14 – Formuläret som lägger till
Vad vi gör. Ger detaljsidan en knapp som faktiskt gör något.
Filen. Components/Pages/Filmdetalj.razor:
@inject IVisningslista Listan
<form method="post" @formname="visningslistan" @onsubmit="LaggTill" class="sokruta">
<AntiforgeryToken />
<button type="submit">Lägg till i visningslistan</button>
<a href="visningslistan">Visa listan (@Listan.Antal)</a>
</form>
@if (tillagd)
{
<p class="tomt">@film.Titel ligger nu i din visningslista.</p>
}
@code {
private bool tillagd;
private void LaggTill()
{
Listan.LaggTill(Id);
tillagd = true;
}
}
Varför det ser ut så. I område 3 gjorde du sökningen med <form method="get">, och en get ändrar ingenting – den bara frågar. Att lägga till i en lista ändrar något, och då är post rätt metod. Det är samma skillnad som du lärde dig i område 1, nu använd på riktigt.
@formname säger vilket formulär på sidan som skickades. Har sidan två formulär behöver servern veta vilket – och Visningslistan.razor har ett till, för att rensa.
@onsubmit pekar ut metoden som körs. Det här är inget klick. Webbläsaren skickar en helt vanlig POST, servern kör metoden och renderar om sidan. Ett @onclick hade fortfarande inte gjort någonting alls.
<AntiforgeryToken /> lägger in ett dolt fält med en engångsnyckel. Utan det svarar servern 400. Skyddet finns för att en annan sajt annars skulle kunna få din webbläsare att skicka formuläret i ditt namn, med dina cookies – cross-site request forgery. Servern satte en cookie med samma nyckel när sidan hämtades, och jämför de två.
Det här är inte EditForm. Den kommer i område 5, tillsammans med validering. Här är det ren HTML plus två Razor-attribut.
Kontrollera. Fyra saker, i ordning:
| Steg | Ska ge |
|---|---|
| Öppna en film och tryck på knappen | "…ligger nu i din visningslista" och räknaren blir 1 |
Gå till /visningslistan | Filmen står där |
| Lägg till en till, gå tillbaka | Två filmer |
| Öppna sajten i ett privat fönster | Tom lista |
Det sista är beviset för att sessionen hör till besökaren och inte till servern som helhet. Är listan full även där har du lagt den i en singleton.
I ditt projekt. Din knapp, din samling. Och gör den sista kontrollen – den tar tio sekunder och är skillnaden mellan att tro och att veta.
Steg 15 – Varför tjänsten svarar med `Task`
Vad vi gör. Förstår async och await innan vi behöver dem på riktigt.
Titta på en av metoderna igen:
public Task<List<Film>> HamtaAllaAsync()
{
return Task.FromResult(new List<Film>(Alla));
}
och på anropet:
protected override async Task OnInitializedAsync()
{
List<Film> alla = await Arkivet.HamtaAllaAsync();
}
Varför det ser ut så. Task<T> är ett löfte om ett svar som kanske inte är färdigt än. await betyder: vänta tills det är klart, och ge mig innehållet. En metod som innehåller await måste själv vara async.
Just nu väntar ingenting på någonting. Listan ligger i minnet, svaret är klart direkt, och Task.FromResult gör om ett färdigt värde till ett Task för att signaturen kräver det. Det ser onödigt ut, och är det – i dag.
Skälet står i område 5. Att läsa ur en databas tar tid, och under den tiden får servern inte stå still: den ska hinna svara andra besökare. Därför är all I/O asynkron i ASP.NET Core – databas, filer, nätverk.
Och skulle du skriva metoderna synkrona nu, måste varje anropande komponent skrivas om när databasen kommer. Det är precis den sortens ändring hela området handlar om att slippa.
En metod som verkligen väntar finns redan: Visningslista.HamtaAsync gör await inuti en loop, mot IFilmService. Där är async inte kosmetika.
Kontrollera. Ta bort ett await någonstans och läs felmeddelandet. Kompilatorn förklarar oftast exakt vad som saknas.
I ditt projekt. Async hela vägen: Task<T> i gränssnittet, async Task i komponentens livscykelmetod. Blanda inte – en synkron metod som anropar en asynkron med .Result låser sig, och den buggen är svår att hitta.
Steg 16 – Loggning
Vad vi gör. Låter tjänsten berätta vad som hände, för utvecklaren.
Filen. Services/FilmService.cs:
public class FilmService : IFilmService
{
private readonly ILogger<FilmService> _logg;
public FilmService(ILogger<FilmService> logg)
{
_logg = logg;
}
public Task<Film?> HamtaAsync(int id)
{
foreach (var film in Alla)
{
if (film.Id == id)
{
return Task.FromResult<Film?>(film);
}
}
_logg.LogWarning("Ingen film med id {Id}", id);
return Task.FromResult<Film?>(null);
}
}
Varför det ser ut så. ILogger<FilmService> injiceras i konstruktorn. Du registrerar den inte – ramverket har den, precis som NavigationManager.
Typparametern blir loggpostens kategori, så du ser i terminalen vilken klass som skrev raden.
"Ingen film med id {Id}", id och inte $"Ingen film med id {id}". Klammern är en platshållare, och värdet skickas separat. Det gör att loggen går att söka i på "alla poster där Id var 99" i stället för att bara vara text. Vänj dig vid det nu.
Nivåerna, från lugnast:
| Nivå | När |
|---|---|
LogInformation | Det som hände och som är normalt |
LogWarning | Något oväntat, men applikationen klarar sig |
LogError | Något gick fel och någon behöver titta på det |
Kontrollera. Gå till /filmer/99 och titta i terminalen där dotnet run körs. Där ska stå warn: med klassnamnet och id:t.
I ditt projekt. Minst en loggrad någonstans där något kan gå snett. Loggen är till för dig, inte för besökaren.
Steg 17 – Felhantering på rätt nivå
Vad vi gör. Bestämmer vem som tar hand om ett fel, och vad besökaren får se.
Två tjänster som gör samma anrop mot samma opålitliga källa. Filerna ligger i exempel/felhantering/.
Utan felhantering:
public class FelNivaService
{
private readonly Utlaningskalla _kalla = new();
public int Antal()
{
return _kalla.AntalUtlanade();
}
}
/fel-niva svarar 500. Hela sidan är borta för att en siffra saknades.
Med felhantering i tjänsten:
public class RattNivaService
{
private readonly Utlaningskalla _kalla = new();
private readonly ILogger<RattNivaService> _logg;
public RattNivaService(ILogger<RattNivaService> logg)
{
_logg = logg;
}
public int? Antal()
{
try
{
return _kalla.AntalUtlanade();
}
catch (Exception fel)
{
_logg.LogError(fel, "Kunde inte läsa utlåningsregistret");
return null;
}
}
}
/ratt-niva svarar 200, visar "Utlåningssiffran är inte tillgänglig just nu", och resten av sidan fungerar. I terminalen ligger hela undantaget.
Varför det ser ut så. Tre saker att ta med sig.
Undantaget är till för utvecklaren, meddelandet för användaren. Logga hela felet – _logg.LogError(fel, ...) med undantaget som första argument tar med stackspåret. Visa något som går att förstå.
Returtypen bär informationen. int? och inte int. Hade metoden svarat 0 när registret var nere hade sidan påstått att noll filmer är utlånade – ett faktapåstående som råkar vara falskt. null betyder "vet inte", och det är sant. Det här är exemplets viktigaste rad och den lättaste att gå förbi.
**try/catch hör hemma i tjänsten.** Komponenten vet inte vad ett utlåningsregister är och kan därför inte avgöra vad ett rimligt svar är när det fattas. Låg felhanteringen i komponenten skulle varje sida som använder tjänsten behöva upprepa den – samma argument som hela området vilar på.
Kontrollera. Kör båda adresserna med terminalen synlig bredvid webbläsaren. Det är loggen som är poängen, och den syns inte på sidan.
I ditt projekt. Minst ett ställe där något kan gå fel, hanterat i tjänsten, med en loggpost och ett begripligt meddelande.
När något går fel
Slå upp felmeddelandet här innan du fastnar. Skriv en bit av det du ser.
12 rader
| Felmeddelande | Vad det betyder | Gör så här |
|---|---|---|
Unable to resolve service for type 'IDistributedCache' | AddSession() utan cache. Servern startar inte | Lägg till builder.Services.AddDistributedMemoryCache(); före |
Unable to resolve service for type 'IFilmService' | Tjänsten är inte registrerad | En AddScoped-rad i Program.cs saknas, eller ligger efter builder.Build() |
CS0535: does not implement interface member | Klassen saknar en metod ur gränssnittet | Kopiera signaturen exakt, inklusive Task<> |
CS0542: member names cannot be the same as their enclosing type | Injektionsnamnet är samma som filnamnet | Döp om injektionen: Arkivet, inte Filmer |
CS4033/CS4032 kring await | Metoden är inte async – 4033 om den är void, 4032 om den redan ger Task | async Task på metoden, Task<T> på tjänstens |
CS1061: 'Task<…>' does not contain a definition for … | Resultatet används utan await | await före anropet |
Cannot consume scoped service 'X' from singleton 'Y' – servern startar inte | En scoped tjänst injicerad i en singleton | Singletonen lever längre än scopet. Gör den scoped, eller be om tjänsten när den behövs |
| Sidan svarar 400 på formuläret | <AntiforgeryToken /> saknas | Lägg in den innanför <form> |
| Formuläret gör ingenting | @formname eller @onsubmit saknas | Båda behövs. Sidan visas om utan att metoden körts |
InvalidOperationException: Session has not been configured | app.UseSession() saknas | Lägg till raden bland app.Use-raderna |
| Alla besökare ser samma lista | Tjänsten är registrerad som singleton | AddScoped. Kontrollera i ett privat fönster |
NullReferenceException i tjänsten | HttpContext är null utanför en förfrågan | _http.HttpContext?.Session med frågetecken |
Det som är lätt att missa när du gör det själv
Gränssnittet är inte en formalitet. En tjänst utan gränssnitt fungerar, och missar hela poängen: område 5 ska kunna byta implementation utan att röra komponenterna. Kravet är IDittService, inte bara DittService.
"Ingen datalogik i komponenten" gäller på riktigt. Ett foreach som filtrerar en lista i OnInitializedAsync är datalogik. Gränsen går ungefär så här: väljer koden ut vilka poster som ska visas hör den till tjänsten; bestämmer den hur de ska se ut hör den till komponenten.
Livstiden ska motiveras skriftligt. Det är ett eget krav, inte en bisats. Två meningar per tjänst räcker, men de ska säga varför just den livstiden – inte vad livstiden heter.
Kontrollera i ett privat fönster. Det är den enda kontrollen som avslöjar en samling som råkat bli delad mellan alla besökare, och den tar tio sekunder.
Sessionen är inte en databas. Den försvinner efter trettio minuters inaktivitet och när webbläsaren stängs. Det är rätt för en visningslista och fel för allt som ska finnas kvar i morgon. Det kommer i område 5.
**Filmdata ska bort.** Så länge den gamla datafilen finns kvar kan en sida smyga tillbaka till den. Radera filen – bygget blir din kontroll.
Checklista
Guidens egen checklista. Den är inte betygskraven – de står i uppgiften och är uppdelade på E, C och A. Den här listan säger bara om du gjort klart det guiden går igenom.
Klart när allt är ikryssat
Kryssen sparas i din webbläsare.
Vill du mer
- Dependency injection in ASP.NET Core – vad behållaren gör, och varför man inte skapar sina egna tjänster med
new. - Service lifetimes – transient, scoped och singleton, med de fällor som uppstår när de blandas.
- Blazor dependency injection –
@inject, och vad en scope omfattar i olika renderingslägen. - Session and state management – session, cookies, och vad man inte ska lägga i dem.
- Static SSR form handling –
@formname,@onsubmitoch antiforgery utanEditForm. - Logging in .NET –
ILogger, nivåerna, och varför man skriver{Id}i stället för$"{id}". - Asynchronous programming with async and await –
Task,awaitoch varför I/O ska vara asynkron.