← Område 5 Guide · Filmarkivet När något går fel

Webbserverprogrammering 1 · område 5 · guide

Filmarkivet – code-first: modell, databas och formulär

Tolv steg som gör listan i koden till rader i en databas. Applikationen ser nästan likadan ut när du är klar. Skillnaden är att det du lägger till nu finns kvar när servern startas om – och att du byter ut hela datalagret utan att röra en enda komponent. En sida är ny: /filmer/ny.

Missade du en lektion?

Guiden går att följa på egen hand. Så här hänger stegen ihop med passen:

  • Lektion 31 (ons 9 dec) – steg 1 till steg 3. Problemet med listan, entiteten får attribut, och DbContext. Bygget går inte igenom efter steg 3 – det är väntat. Läs också begreppen.
  • Lektion 32 (tis 15 dec) – steg 4 till steg 9. Områdets tyngsta pass: paketen, anslutningssträngen, frödata, den första migrationen – och steg 8, där du öppnar tabellen och ser vad attributen faktiskt blev. Sedan byter tjänsten implementation.
  • Lektion 33 (ons 16 dec) – steg 10 till steg 12. EditForm, valideringen som håller när klientvalideringen kringgås, och metoden som sparar. Se också en rad som är hela skillnaden.
  • Lektion 34 (tis 22 dec) – inget nytt. Checklistan, uppgiften med de tre kontrollerna, och terminens fem områden och vårens fyra projekt. Inlämning samma kväll.

Stegen bygger på varandra hela vägen. Steg 8 innehåller ingen ny kod och är ändå det viktigaste – det är där du får veta vad databasen inte gör åt dig.

Ett nytt verktyg, och en ny tidpunkt för fel

dotnet ef är ett eget verktyg vid sidan av dotnet build och dotnet run. Det läser dina klasser och skriver databasen – och det har egna fel, som kommer varken vid bygget eller när sidan öppnas utan när du kör migrations add eller database update. Står du i fel mapp, saknas ett paket eller stämmer inte anslutningssträngen får du veta det där.

Tabellen När något går fel tar upp dem i ordning. Läs felmeddelandet innan du gör något – det säger nästan alltid rakt ut vad som saknas.

Koden går inte att kopiera

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. Terminalkommandon och SQL är undantag – de är figurer, inte kod att öva på.

Guide · Område 5 · Webbserverprogrammering 1

Vad du bygger här

Filmerna slutar vara en lista i koden och blir rader i en databas.

Applikationen ser nästan likadan ut. Skillnaden är att det du lägger till nu finns kvar när servern startas om – och att du för första gången kan lägga till en film utan att öppna en kodfil.

En sida är ny:

Filmarkivet
└── /filmer/ny            Formulär som skapar en film, med validering

Det färdiga projektet ligger i referensprojekt/Filmarkivet.

Det här är området där område 4 betalar tillbaka. Du byter ut hela FilmService mot en som läser ur en databas – och rör inte en enda komponent. Filmer.razor, Filmdetalj.razor och Filmstatistik.razor är exakt de filer de var, och de visar plötsligt databasdata. Håll den bilden framför dig i steg 9.

Ordet för det här arbetssättet är code-first: du skriver klassen, och databasen byggs efter den. Motsatsen – att rita tabellen först och generera klasser ur den – finns också, men code-first är det som gör att modellen, tabellen och formuläret aldrig glider isär.

Vad du ska göra själv efteråt

Samma sak i ditt projekt:

Översättningstabellen

FilmarkivetDitt projekt
Film med attributdin huvudentitet med attribut
FilmarkivetContextDittProjektContext
DbSet<Film> Filmerditt DbSet – pluralnamn, det blir tabellnamnet
filmarkivet.dbdin databasfil
ConnectionStrings:Filmarkivetdin anslutningssträng
SkapaAsync(Film film)din metod som sparar en post
/filmer/nydin sida för att lägga till
NyFilm.razorditt formulär

Fyll i högerkolumnen innan du börjar koda. Namnen följer med dig till område 6, där de får sällskap av en kategorientitet.

Innan du börjar

Ditt projekt från område 4: en tjänst bakom ett gränssnitt, injicerad i minst tre komponenter, med en hårdkodad lista bakom sig.

Kontrollen tar tio sekunder:

dotnet run

Fungerar din listsida och din detaljsida är du redo. Gör de inte det är det område 4 som ska bli klart först – det här området bygger direkt på det.

Du behöver också verktyget som gör migrationer. Kontrollera:

dotnet ef --version

Svarar den inte, installera det en gång:

dotnet tool install --global dotnet-ef

Begreppen i det här momentet

BegreppVad det är
Code-firstKlassen skrivs först, databasen byggs efter den
EntitetEn klass som motsvarar en tabell
Attribut[Required], [Range] – regler skrivna ovanför en egenskap
Data annotationsSamlingsnamnet för de attributen
DbContextKlassen som håller ihop entiteterna och kopplingen till databasen
DbSet<T>En tabell, sedd från C#
PrimärnyckelKolumnen som identifierar raden. Id blir det automatiskt
AnslutningssträngTexten som säger var databasen finns
MigrationEn genererad beskrivning av hur databasen ska ändras
Frödata (seed)Rader som följer med migrationen in i databasen
EditFormBlazors formulär, som kan validera mot en modell
ServersidesvalideringKontrollen som körs på servern och som inte går att stänga av
SQLiteEn databas som är en enda fil. Ingen server att installera

---

Steg 1 – Problemet

Vad vi gör. Ingenting. Vi tittar på vad som är fel.

Öppna din applikation, lägg märke till listan i FilmService, och ställ dig frågan: var tar en ny film vägen?

Det finns bara ett svar i dag: du öppnar FilmService.cs, skriver in en rad till i listan, sparar och startar om. Det betyder tre saker:

Den sista punkten är den viktigaste, och du har redan sett den. Visningslistan i område 4 låg i session och försvann när webbläsaren stängdes. Det var rätt för en visningslista, som hör till besöket. Det är fel för filmerna, som hör till arkivet.

Data som ska finnas kvar i morgon hör hemma i en databas. Allt annat är en förenkling som väntar på att bli ett problem.

I ditt projekt. Titta på din egen tjänst och peka ut listan. Det är den som ska bort, och det är den som är hela området.

Steg 2 – Entiteten

Vad vi gör. Ger modellklassen regler.

Filen. Models/Film.cs:

using System.ComponentModel.DataAnnotations;

namespace Filmarkivet.Models;

// Från och med område 5 är det här en entitet: klassen beskriver både en post i
// databasen och reglerna för vad som får skrivas i formuläret. Attributen skrivs
// en gång och gör båda sakerna.
public class Film
{
    // Id utan attribut blir primärnyckel. EF Core känner igen namnet.
    public int Id { get; set; }

    [Required(ErrorMessage = "Titeln måste fyllas i.")]
    [StringLength(100, ErrorMessage = "Titeln får vara högst 100 tecken.")]
    public string Titel { get; set; } = "";

    [Range(1888, 2100, ErrorMessage = "Året måste ligga mellan 1888 och 2100.")]
    public int Ar { get; set; }

    [Range(1, 500, ErrorMessage = "Längden måste vara mellan 1 och 500 minuter.")]
    public int LangdMinuter { get; set; }

    [Range(1, 5, ErrorMessage = "Betyget är ett tal mellan 1 och 5.")]
    public int Betyg { get; set; }

    [StringLength(500, ErrorMessage = "Beskrivningen får vara högst 500 tecken.")]
    public string Beskrivning { get; set; } = "";

    public bool Tillganglig { get; set; }
}

Varför det ser ut så. Klassen är densamma som i område 2. Det enda nya är raderna inom hakparenteser.

Ett attribut är en regel som sitter fast på egenskapen. Det är inte kod som körs på ett bestämt ställe – det är information som andra delar av systemet läser och rättar sig efter. Här läses den av två helt skilda saker: EF Core när databasen byggs, och EditForm när formuläret skickas.

ErrorMessage är texten användaren får se. Utan den skriver ramverket ett engelskt standardmeddelande, och det duger inte på en svensk sajt.

**Id behöver inget attribut.** EF Core letar efter en egenskap som heter Id eller FilmId och gör den till primärnyckel. Det är en konvention – ramverket gissar rätt så länge du namnger som alla andra.

Kontrollera. dotnet build. Noll fel. Ingenting har hänt ännu – attributen gör inget förrän någon läser dem.

I ditt projekt. Sätt attribut på varje fält som har en regel, och skriv ErrorMessage på svenska. Ett fält utan regler ska inte ha ett attribut bara för att de andra har det.

Steg 3 – `DbContext`

Vad vi gör. Skapar kopplingen mellan klasserna och databasen.

Filen. Data/FilmarkivetContext.cs – ny mapp, ny fil:

using Filmarkivet.Models;
using Microsoft.EntityFrameworkCore;

namespace Filmarkivet.Data;

// Kopplingen mellan klasserna och databasen. Ett DbSet per entitet.
public class FilmarkivetContext : DbContext
{
    public FilmarkivetContext(DbContextOptions<FilmarkivetContext> alternativ)
        : base(alternativ)
    {
    }

    public DbSet<Film> Filmer => Set<Film>();
}

Varför det ser ut så. DbContext är EF Cores klass för "en session mot databasen". Du ärver från den och lägger till ett DbSet per entitet.

DbSet<Film> Filmer betyder ungefär: *det finns en tabell med filmer, och den heter Filmer*. Namnet på egenskapen blir tabellnamnet. Därför står det i plural.

Konstruktorn tar emot DbContextOptions – där ligger anslutningssträngen och valet av databasmotor. Klassen bestämmer alltså inte själv var databasen finns; det säger Program.cs i steg 5. Samma tanke som i område 4: klassen tar emot sina beroenden i stället för att skapa dem.

Kontrollera. Bygget går inte igenom ännu – DbContext finns inte förrän paketen är på plats. Det är nästa steg.

I ditt projekt. En Data-mapp, en context-klass, ett DbSet. Fler entiteter kommer i område 6.

Steg 4 – Paketen

Vad vi gör. Hämtar in EF Core och SQLite.

dotnet add package Microsoft.EntityFrameworkCore.Sqlite
dotnet add package Microsoft.EntityFrameworkCore.Design

Varför det ser ut så. Två paket med olika uppgifter:

SQLite är en enda fil. Ingen server, inget lösenord, inget att installera. Databasen blir filmarkivet.db bredvid projektet. Det gör den perfekt för en kurs – och den är samtidigt en riktig relationsdatabas med riktig SQL.

Kontrollera. dotnet build. Nu ska den gå igenom.

I ditt projekt. Samma två paket. Kontrollera i .csproj att de kom med.

Steg 5 – Registreringen och anslutningssträngen

Vad vi gör. Talar om var databasen finns.

Filen. appsettings.json, en ny sektion bredvid de som finns:

"ConnectionStrings": {
  "Filmarkivet": "Data Source=filmarkivet.db"
}

Filen. Program.cs, bland de andra registreringarna:

// En fabrik i stället för ett DbContext direkt: en komponent kan rendera flera
// gånger och behöver ett eget context per anrop, inte ett delat.
builder.Services.AddDbContextFactory<FilmarkivetContext>(alternativ =>
    alternativ.UseSqlite(builder.Configuration.GetConnectionString("Filmarkivet")));

Varför det ser ut så. Anslutningssträngen ligger i appsettings.json och inte i koden, därför att den är olika på din dator och på servern. I område 8 byts den ut mot en riktig adress utan att en enda kodrad ändras.

AddDbContextFactory är raden som är värd att stanna vid. Det vanliga är AddDbContext, som ger en scoped DbContext. I Blazor är det fel, och skälet kommer direkt ur område 4: **ett DbContext är inte trådsäkert, och en komponent kan rendera flera gånger under samma förfrågan.** Två renderingar som delar ett context kan krocka.

En fabrik löser det: varje metod ber om ett eget context, använder det och kastar det.

using var db = await _fabrik.CreateDbContextAsync();

using gör att det städas bort när metoden är klar. Det är samma using som du sett på filer, i en form du inte behövt förut.

Kontrollera. dotnet run. Servern ska starta. Databasen finns inte ännu, men ingenting har frågat efter den.

I ditt projekt. Din anslutningssträng, din context-typ. Namnet i GetConnectionString måste stämma exakt med nyckeln i appsettings.json.

Steg 6 – Filmerna flyttar in i migrationen

Vad vi gör. Låter den hårdkodade listan bli frödata.

Filen. Data/FilmarkivetContext.cs, en metod till i klassen:

// Filmerna som låg hårdkodade i FilmService i område 4 flyttar hit och
// följer med migrationen in i databasen.
protected override void OnModelCreating(ModelBuilder byggare)
{
    byggare.Entity<Film>().HasData(
        new Film { Id = 1, Titel = "Nyckeln till frihet", Ar = 1994, LangdMinuter = 142, Betyg = 5, Beskrivning = "En bankman döms för ett mord han inte begått och bygger under nitton år i Shawshank en vänskap som bär honom.", Tillganglig = true },
        new Film { Id = 2, Titel = "Sju samurajer", Ar = 1954, LangdMinuter = 207, Betyg = 5, Beskrivning = "En by anlitar sju herrelösa samurajer till försvar mot rövare.", Tillganglig = false },
        new Film { Id = 3, Titel = "Rosemarys baby", Ar = 1968, LangdMinuter = 137, Betyg = 4, Beskrivning = "Ett ungt par flyttar in i ett hus på Manhattan. Grannarna är påfallande vänliga.", Tillganglig = true },
        new Film { Id = 4, Titel = "Såsom i en spegel", Ar = 1961, LangdMinuter = 89, Betyg = 4, Beskrivning = "Fyra människor på en ö under ett dygn.", Tillganglig = true },
        new Film { Id = 5, Titel = "Gökboet", Ar = 1975, LangdMinuter = 133, Betyg = 5, Beskrivning = "McMurphy spelar galen för att slippa straffarbete.", Tillganglig = false },
        new Film { Id = 6, Titel = "Cykeltjuven", Ar = 1948, LangdMinuter = 89, Betyg = 4, Beskrivning = "En man får äntligen arbete, men jobbet kräver en cykel.", Tillganglig = true }
    );
}

Varför det ser ut så. Du kunde ha låtit databasen vara tom och skrivit in filmerna via formuläret i steg 10. HasData är bättre av två skäl: alla som kör projektet får samma innehåll, och raderna blir en del av migrationen – de följer med databasen och går att spåra.

Id måste anges. Vanligtvis sätter databasen id själv, men frödata skrivs innan databasen finns, och EF Core behöver kunna känna igen raderna om de ändras senare.

Kontrollera. Ingenting syns ännu. Frödata blir verklig först i nästa steg.

I ditt projekt. Flytta in din egen lista. Har du sex poster i din hårdkodade lista ska du ha sex rader här.

Steg 7 – Första migrationen

Vad vi gör. Låter EF Core skriva ned hur databasen ska se ut, och bygger den.

dotnet ef migrations add ForstaMigrationen
dotnet ef database update

Varför det ser ut så. Två helt olika saker, och skillnaden är viktig:

KommandoVad det görRör databasen?
migrations addJämför dina klasser med förra migrationen och skriver en ny fil med skillnadenNej
database updateKör de migrationer som inte körtsJa

migrations add skapar en mapp Migrations/ med tre filer. Redigera dem aldrig för hand. De är genererade, precis som elevpaket/guide.html – ändras klassen skriver du en ny migration i stället.

database update skapar filmarkivet.db och kör migrationen mot den. Kör du den igen händer ingenting: EF Core håller reda på vilka migrationer som redan körts, i en egen tabell som heter __EFMigrationsHistory.

Namnet på migrationen betyder något. ForstaMigrationen säger ingenting. LaggTillBetyg säger vad ändringen gjorde. Du kommer att ha fem migrationer innan kursen är slut, och de läses som en historik.

Kontrollera. Filen filmarkivet.db ska ha dykt upp bredvid projektet.

ls filmarkivet.db

I ditt projekt. Samma två kommandon. Lägg till *.db i .gitignore – databasen byggs av migrationen och ska inte checkas in.

Steg 8 – Vad blev av attributen

Vad vi gör. Öppnar databasen och kontrollerar påståendet från steg 2.

Guiden har sagt att attributen styr både kolumnen och formuläret. Nu ska du se efter om det stämmer. Titta på vad migrationen faktiskt skapade:

dotnet ef migrations script

Överst i utskriften står tabellen:

CREATE TABLE "Filmer" (
    "Id" INTEGER NOT NULL CONSTRAINT "PK_Filmer" PRIMARY KEY AUTOINCREMENT,
    "Titel" TEXT NOT NULL,
    "Ar" INTEGER NOT NULL,
    "LangdMinuter" INTEGER NOT NULL,
    "Betyg" INTEGER NOT NULL,
    "Beskrivning" TEXT NOT NULL,
    "Tillganglig" INTEGER NOT NULL
)

Läs den noga, och jämför med klassen. Något saknas.

Id blev primärnyckel med AUTOINCREMENT – databasen räknar upp den åt dig. bool blev INTEGER, för SQLite har ingen egen sanningsvärdestyp: 0 och 1. Alla kolumner är NOT NULL.

Men [StringLength(100)] blev ingenting. Titel är TEXT, utan längd. Och [Range(1, 5)] blev ingenting – det finns inget CHECK på Betyg.

Så här ser det ut på riktigt:

Vad du skrevI databasenI formuläret
public int Idprimärnyckel, AUTOINCREMENT–
public string Titel (icke-nullbar)NOT NULL–
[Required]– (kolumnen är redan NOT NULL)Ja
[StringLength(100)]ingentingJa
[Range(1, 5)]ingentingJa
public bool TillgangligINTEGER NOT NULL–

Varför det ser ut så. SQLite har ingen längd på TEXT, och EF Core skapar inga CHECK-villkor ur [Range]. Mot en annan databasmotor hade [StringLength(100)] blivit nvarchar(100) – men [Range] hade inte blivit någonting där heller.

Det går att bevisa. Öppna databasen och skriv rakt in i den, förbi C# och förbi formuläret:

INSERT INTO Filmer (Titel, Ar, Beskrivning, Betyg, LangdMinuter, Tillganglig)
VALUES ('ett par hundra tecken…', 1900, '', 99, 9999, 1);

Den går igenom. Betyget 99. Längden 9999 minuter. Databasen säger ingenting.

Slutsatsen, och områdets viktigaste mening:

Databasen ser till att fälten finns. Valideringen ser till att de är rimliga. Bara den ena av dem går att lita på för det andra.

Det är därför uppgiften kräver att valideringen håller när klientvalideringen kringgås – och det är därför du inte kan skippa steg 11.

I ditt projekt. Kör dotnet ef migrations script och jämför med dina egna attribut. Skärmbilden du bifogar i inlämningen ska visa den här tabellen.

Steg 9 – Tjänsten byter implementation

Vad vi gör. Låter FilmService läsa ur databasen i stället för ur listan.

Filen. Services/FilmService.cs. Konstruktorn och de två första metoderna:

public class FilmService : IFilmService
{
    private readonly IDbContextFactory<FilmarkivetContext> _fabrik;
    private readonly ILogger<FilmService> _logg;

    public FilmService(IDbContextFactory<FilmarkivetContext> fabrik,
                       ILogger<FilmService> logg)
    {
        _fabrik = fabrik;
        _logg = logg;
    }

    // Task.FromResult är borta. Nu väntar metoden på riktigt.
    public async Task<List<Film>> HamtaAllaAsync()
    {
        using var db = await _fabrik.CreateDbContextAsync();
        return await db.Filmer.ToListAsync();
    }

    public async Task<Film?> HamtaAsync(int id)
    {
        using var db = await _fabrik.CreateDbContextAsync();
        Film? film = await db.Filmer.FindAsync(id);

        if (film is null)
        {
            _logg.LogWarning("Ingen film med id {Id}", id);
        }

        return film;
    }
}

Varför det ser ut så. Lägg märke till vad som inte ändrades:

Det här är hela poängen med område 4. Du bytte ut var datan kommer ifrån, och inget annat i applikationen märkte det. Hade du haft listan direkt i komponenterna – som i område 3 – hade det här varit en dags arbete i stället för tjugo minuter.

Task.FromResult är borta ur HamtaAllaAsync. I område 4 var async kosmetika. Nu väntar metoden på riktigt, och signaturen behövde inte ändras för det. Det var därför den skrevs så från början.

FindAsync letar på primärnyckeln och är kortare än en egen loop.

**SokAsync och HamtaStatistikAsync behåller sina loopar.** De hämtar hela listan med HamtaAllaAsync och går igenom den precis som förut. Det går att skriva om till frågor som körs i databasen – det gör vi i område 6, med LINQ.

Kontrollera. Starta och gå till /filmer. Samma sex filmer som förut. Gå till /filmer/1 och /filmer/99 – 404 ska fortfarande fungera. Och /visningslistan, som hämtar sina filmer genom samma tjänst.

Ta sedan bort listan Alla ur filen. Bygget blir din kontroll på att inget längre använder den.

I ditt projekt. Byt din egen implementation. Kontrollera att gränssnittet är oförändrat – behövde du ändra det är det ett tecken på att något läckt igenom.

Steg 10 – Formuläret

Vad vi gör. Bygger sidan som lägger till en film.

Filen. Components/Pages/NyFilm.razor:

@page "/filmer/ny"
@inject IFilmService Arkivet

<PageTitle>Ny film – Filmarkivet</PageTitle>

<h1>Lägg till en film</h1>

<EditForm Model="Film" OnValidSubmit="Spara" FormName="nyfilm">
    <DataAnnotationsValidator />
    <ValidationSummary />

    <p>
        <label>Titel<br />
            <InputText @bind-Value="Film!.Titel" /></label>
        <ValidationMessage For="() => Film!.Titel" />
    </p>

    <p>
        <label>Betyg 1–5<br />
            <InputNumber @bind-Value="Film!.Betyg" /></label>
        <ValidationMessage For="() => Film!.Betyg" />
    </p>

    <button type="submit">Spara filmen</button>
</EditForm>

@if (sparad)
{
    <p class="tomt">Filmen är sparad.</p>
}

@code {
    // Ingen initierare här. Ramverket fyller egenskapen vid en POST, och en
    // initierare hade kunnat skrivas över med null. Objektet skapas i stället i
    // OnInitialized, som körs före renderingen.
    [SupplyParameterFromForm]
    private Film? Film { get; set; }

    private bool sparad;

    protected override void OnInitialized()
    {
        Film ??= new Film();
    }

    private async Task Spara()
    {
        await Arkivet.SkapaAsync(Film!);
        sparad = true;
        Film = new Film();
    }
}

Fälten för Ar, LangdMinuter, Beskrivning och Tillganglig ser likadana ut. Hela filen ligger i referensprojektet.

Varför det ser ut så. Det här är inte samma sorts formulär som i område 4. Där skrev du <form method="post"> med @formname och <AntiforgeryToken /> för hand. EditForm gör mer:

DelVad den gör
ModelObjektet som fylls i och valideras
FormNameSamma sak som @formname gjorde. Säger vilket formulär som skickades
OnValidSubmitKörs bara om valideringen gick igenom
DataAnnotationsValidatorLäser attributen på klassen. Utan den valideras ingenting
ValidationSummaryAlla fel i en lista, överst
ValidationMessage ForEtt fel intill sitt eget fält
InputText, InputNumberKomponenter som binder mot en egenskap

OnValidSubmit är namnet att lägga på minnet. Det finns också OnSubmit, som körs oavsett – och den som råkar använda den har byggt ett formulär som sparar skräp.

**Om Film? och utropstecknen.** [SupplyParameterFromForm] får inte kombineras med en initierare på egenskapen; ramverket kan skriva över den med null vid en POST, och kompilatorn varnar med BL0008. Därför är egenskapen nullbar och objektet skapas i OnInitialized i stället.

Utropstecknet i Film!.Titel säger åt kompilatorn: jag vet att den inte är null här. Och det stämmer – OnInitialized körde precis före renderingen.

Antiforgery behöver du inte skriva. EditForm lägger in token själv. Öppna sidkällan och leta efter __RequestVerificationToken så ser du den. Skickar någon formuläret utan den svarar servern 400, precis som i område 4.

Kontrollera. Fyll i formuläret och spara. Filmen ska dyka upp på /filmer. Starta sedan om servern och titta igen. Den är kvar. Det är första gången i kursen något överlever en omstart.

I ditt projekt. Ett EditForm för din entitet, med ett fält per egenskap som användaren ska fylla i. Id ska inte vara ett fält – det sätter databasen.

Steg 11 – Valideringen som håller

Vad vi gör. Kringgår vår egen validering och ser om servern klarar sig.

Fyll i formuläret med ett betyg på 99 och tryck spara. Webbläsaren stoppar dig, eller så gör ValidationSummary det. Det bevisar ingenting: det är kontroller som körs i webbläsaren, och webbläsaren är besökarens, inte din.

Gör så här i stället:

  1. Öppna /filmer/ny och tryck F12.
  2. Gå till fliken Elements och leta upp fältet för betyg.
  3. Ändra type="number" till type="text", och ta bort eventuella

min- och max-attribut.

  1. Skriv 99 i fältet och skicka.

Servern svarar med sidan tillbaka, och i ValidationSummary står:

Betyget är ett tal mellan 1 och 5.

Ingen film sparades.

Varför det ser ut så. DataAnnotationsValidator kör attributen på servern, efter att formuläret kommit in. Allt som händer i webbläsaren är bekvämlighet – det gör att användaren slipper vänta på ett svar för att få veta att fältet är tomt. Det är inte ett skydd.

Klientvalidering är för användarens skull. Serversidesvalidering är för systemets. Den som bara har den första har ingen validering alls.

Och kom ihåg steg 8: databasen fångar inte heller felet. Betyg = 99 går rakt in i en INTEGER-kolumn utan protest. Kedjan har exakt ett ställe som håller, och det är DataAnnotationsValidator plus OnValidSubmit.

Prova gärna att ta bort <DataAnnotationsValidator /> och göra om försöket. Nu sparas filmen med betyg 99. Sätt tillbaka raden.

I ditt projekt. Gör exakt det här försöket på ditt eget formulär, på ett fält med [Range] eller [StringLength]. Att du gjort det är ett krav i inlämningen – och det tar två minuter.

Steg 12 – Metoden som sparar

Vad vi gör. Tittar på den sista biten i kedjan.

Filen. Services/FilmService.cs:

public async Task SkapaAsync(Film film)
{
    using var db = await _fabrik.CreateDbContextAsync();
    db.Filmer.Add(film);
    await db.SaveChangesAsync();

    _logg.LogInformation("Ny film sparad: {Titel} (id {Id})", film.Titel, film.Id);
}

Varför det ser ut så. Tre rader, och den mellersta är den som förvånar.

Add sparar ingenting. Den säger till contextet: den här filmen ska läggas till. Ingenting händer i databasen förrän SaveChangesAsync körs. Det är avsiktligt – flera ändringar kan samlas ihop och skickas i en enda omgång, och antingen går allt igenom eller ingenting.

Loggraden skriver ut film.Id efter SaveChangesAsync. Före anropet är den 0; det är databasen som delar ut numret, och EF Core skriver tillbaka det i objektet åt dig.

SkapaAsync är den enda metoden som lagts till i IFilmService i det här området. Den kom av ny funktionalitet, inte av databasen – och det är värt att skilja på.

Kontrollera. Spara en film och läs terminalen. Loggraden ska innehålla ett id som inte är noll.

I ditt projekt. Din egen metod i ditt eget gränssnitt. Add plus SaveChangesAsync, och en loggrad.

---

När något går fel

Slå upp felmeddelandet här innan du fastnar. Skriv en bit av det du ser.

14 rader

FelmeddelandeVad det betyderGör så här
No project was found vid dotnet efDu står i fel mappcd till mappen med .csproj
dotnet ef känns inte igenVerktyget är inte installeratdotnet tool install --global dotnet-ef
Your startup project doesn't reference Microsoft.EntityFrameworkCore.DesignDesign-paketet saknasdotnet add package Microsoft.EntityFrameworkCore.Design
Unable to create a 'DbContext' of typeAnslutningssträngen hittas inteNyckeln i GetConnectionString måste stämma exakt med appsettings.json
SQLite Error 1: 'no such table: Filmer'Migrationen är inte körddotnet ef database update
The model for context has pending changesKlassen ändrad efter senaste migrationendotnet ef migrations add NamnPaAndringen, sedan database update
A second operation was started on this context instanceEtt delat DbContextAnvänd IDbContextFactory och ett eget context per metod
Cannot consume scoped service 'FilmarkivetContext' from singletonContextet injicerat i en singletonSamma fel som i område 4. Använd fabriken
BL0008 vid bygget[SupplyParameterFromForm] med initierareTa bort = new() och skapa objektet i OnInitialized
Formuläret svarar 400Antiforgery-token saknasEditForm lägger in den. Kontrollera att du inte byggt formuläret för hand
Formuläret gör ingentingFormName saknasEditForm behöver den vid statisk serverrendering
Felaktiga värden sparas ändå<DataAnnotationsValidator /> saknasUtan den läses inga attribut
OnValidSubmit körs aldrigValideringen går inte igenomLägg till <ValidationSummary /> och läs vad som står
Sidan visar noll filmerDatabasen är tomKördes migrationen mot rätt fil? Radera .db och kör database update igen

Det som är lätt att missa när du gör det själv

Migrationer redigeras inte för hand. Blev något fel: dotnet ef migrations remove, ändra klassen, och lägg till en ny. Filerna i Migrations/ är genererade.

Databasfilen ska inte checkas in. Lägg *.db i .gitignore. Den som klonar ditt repo kör database update och får samma databas – det är hela poängen med migrationer.

Attributen gör mindre i databasen än du tror. Steg 8 är inte en parentes. [Range] finns bara i valideringen, och den som litar på databasen som sista skyddsnät har inget skyddsnät.

**ErrorMessage på svenska, varje gång.* Utan den får användaren "The field Betyg must be between 1 and 5."* mitt i en svensk sajt.

**OnValidSubmit, inte OnSubmit.** Skillnaden är ett ord och all skillnad.

Gränssnittet ska knappt ändras. Behövde du skriva om IDittService för att databasen kom, har något från datalagret läckt ut i komponenterna. Det är en signal värd att följa upp, inte en detalj.

**Id hör inte hemma i formuläret.** Databasen delar ut det. Ett fält där användaren skriver ett id blir ett problem i område 6.

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