https://github.com/nrkno/dotnetskolen
Kurs for oppsett av .NET-prosjekter fra bunn
https://github.com/nrkno/dotnetskolen
course dotnet dotnet-core dotnetcli dotnetcore fsharp integrationtest unittest workshop
Last synced: 12 months ago
JSON representation
Kurs for oppsett av .NET-prosjekter fra bunn
- Host: GitHub
- URL: https://github.com/nrkno/dotnetskolen
- Owner: nrkno
- Created: 2021-03-18T12:25:27.000Z (about 5 years ago)
- Default Branch: main
- Last Pushed: 2025-02-26T16:22:10.000Z (over 1 year ago)
- Last Synced: 2025-03-05T22:07:47.166Z (over 1 year ago)
- Topics: course, dotnet, dotnet-core, dotnetcli, dotnetcore, fsharp, integrationtest, unittest, workshop
- Homepage: https://nrkno.github.io/dotnetskolen/
- Size: 991 KB
- Stars: 22
- Watchers: 25
- Forks: 8
- Open Issues: 3
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# đ« .NET-skolen
## đ Innledning
Velkommen til .NET-skolen!
Dette er et kurs hvor du blir tatt gjennom prosessen av Ä sette opp, og implementere, en .NET-lÞsning fra bunnen av, steg for steg. MÄlet med kurset er Ä vise hvordan man kan utfÞre oppgaver som er vanlige i etableringsfasen av et system, som Ä:
- Opprette prosjekter og mappestruktur
- Legge til NuGet-pakker
- Sette opp tester
Som en eksempel-applikasjon skal vi lage et web-API i F# for Ă„ hente ut elektronisk programguide (EPG) for NRK TV, med tilhĂžrende enhets- og integrasjonstester. Tanken er at API-et kunne levert datagrunnlaget til en programguide - f.eks. den som vises her:
> Et sekundÊrt mÄl med dette repoet er at den ferdige eksempel-applikasjonen (som du finner i [branchen ferdig](https://github.com/nrkno/dotnetskolen/tree/ferdig)) kan fungere som et referanse-repo for hvordan Ä sette opp et .NET-prosjekt.
### đ» FremgangsmĂ„te
Vi skal bruke [.NET CLI](https://docs.microsoft.com/en-us/dotnet/core/tools/) til Ä opprette prosjekter, samt kjÞre koden og testene. I tillegg skal vi dokumentere web-API-et vÄrt ved hjelp av [OpenAPI](https://www.openapis.org/).
Overordnet kommer mappestrukturen til lÞsningen vÄr til Ä se slik ut:
```txt
âââ docs (kontrakt for web-API-et)
âââ src (kildekode til web-API-et)
âââ test (kildekode til enhets- og integrasjonstestene)
```
Det anbefales Ä fÞlge denne veiledningen [pÄ GitHub](https://github.com/nrkno/dotnetskolen), da visningen der stÞtter lenkene som er lagt inn, og har innholdsfortegnelse som alltid er synlig oppe til venstre nÄr man blar i veiledningen.
### đ Kom i gang
For Ä gjennomfÞre dette kurset trenger du [.NET 9 SDK](https://dotnet.microsoft.com/download/dotnet), en teksteditor og en terminal. NÄr du har dette, gÄ til [Steg 1 - Opprette API](#steg-1---opprette-api) og fÞlg veiledningen. For alternative startpunkter se [alternative startpunkter](https://github.com/nrkno/dotnetskolen#-alternative-startpunkter).
> Stegene i kurset gir veiledning, steg for steg, med anvisninger for kommandoer du kan kjĂžre og referanseimplementasjon av kode du kan kopiere. Enkelte steder er implementasjonen av koden imidlertid utelatt slik at du kan forsĂžke Ă„ implementere den selv. Disse stedene er markert med âïž. Les mer om hvordan du kan se fullstendig lĂžsningsforslag for hvert steg [her](#se-lĂžsningsforslag).
> Hvis du trenger mer detaljer om hvordan du gjÞr klar maskinen din til Ä gjennomfÞre kurset, se [Detaljer om oppsett pÄ maskinen din](/docs/detaljer-oppsett.md).
> Dersom du er helt ny til .NET kan det vĂŠre nyttig Ă„ begynne med Ă„ lese:
> - [Hva .NET er](/docs/hva-er-dotnet.md)
> - [Hva F# er](/docs/hva-er-fsharp.md)
#### đ Alternative startpunkter
Denne workshopen dekker en del ulike temaer, og det kan ta litt tid Ä fullfÞre alle stegene. Heldigvis finnes det lÞsningsforslag for hvert steg i workshopen, som betyr at du kan starte pÄ et hvilket som helst steg ved Ä sjekke ut branchen med lÞsningsforslaget til steget fÞr du Þnsker Ä begynne pÄ, og fortsette derfra. Les mer om hvordan du kan [klone dette repoet](https://github.com/nrkno/dotnetskolen/blob/main/docs/detaljer-oppsett.md#-lokalt-oppsett-av-koden-valgfritt) og [sjekke ut lÞsningsforslag](https://github.com/nrkno/dotnetskolen#se-l%C3%B8sningsforslag).
Under fĂžlger noen anbefalinger for alternative startpunkter, avhengig av hvilke temaer du Ăžnsker Ă„ lĂŠre mer om.
> NB! Dersom du begynner pÄ steg 5 eller senere, mÄ du kjÞre `dotnet tool restore` fÞr du fortsetter Ä fÞlge veiledningen.
##### Oppsett av prosjekter og solution med .NET CLI
Dersom du er interessert i Ă„ lĂŠre mer om hvordan du kan bruke .NET CLI til Ă„ opprette prosjekter og solutions, kan fĂžlge disse stegene:
- [Steg 1 - Opprette API](#steg-1---opprette-api)
- [Steg 2 - Opprette testprosjekter](#steg-2---opprette-testprosjekter)
- [Steg 3 - Opprette solution](#steg-3---opprette-solution)
##### Domenemodellering og enhetstester
Vil du lĂŠre mer om domenemodellering i F# og tilhĂžrende enhetstester, kan fĂžlge disse stegene:
- [Steg 4 - Definere domenemodell](#steg-4---definere-domenemodell)
- [Steg 5 - Enhetstester for domenemodell](#steg-5---enhetstester-for-domenemodell)
##### API-kontrakter
Hvis du vil lĂŠre mer om hvordan du kan dokumentere API-et ditt vha. Open API, og modellere kontraktstyper, kan fĂžlge disse stegene:
- [Steg 6 - Definere API-kontrakt](#steg-6---definere-api-kontrakt)
- [Steg 7 - Implementere kontraktstyper](#steg-7---implementere-kontraktstyper)
##### .NET 9 og minimal API
Om du er interessert i .NET 9 sin hosting modell, "minimal APIs", og hvordan du kan teste API-et ditt med integrasjonstester, kan fĂžlge disse stegene:
- [Steg 8 - Sette opp skall for API](#steg-8---sette-opp-skall-for-api)
- [Steg 9 - Implementere web-API](#steg-9---implementere-web-api)
##### Tilleggsoppgaver
Til slutt finnes det noen ekstraoppgaver, hvis du vil ha mer Ä bryne deg pÄ:
- [Ekstraoppgaver](#ekstraoppgaver)
- [Steg 10 - FĂžlge prinsipper i domenedrevet design](#steg-10---fĂžlge-prinsipper-i-domenedrevet-design)
- [Steg 11 - Grafisk fremstilling av OpenAPI-dokumentasjon](#steg-11---grafisk-fremstilling-av-openapi-dokumentasjon)
### â SpĂžrsmĂ„l
Lurer du pÄ noe knyttet til kurset? Opprett gjerne en trÄd under "Discussions" i dette repoet:
-
### đĄ Tips og triks
Nyttige [tips og triks finner du her](/docs/tips-og-triks.md)
### đ Nyttige lenker
- Microsoft's offisielle dokumentasjon for .NET -
- F# cheat sheet -
- InnfĂžring i F# -
- Andre kurs i NRK
- F#-skolen, kurs i F# laget av ansatte i NRK TV -
- GitHub Actions 101, laget av [@teodor-elstad](https://github.com/teodor-elstad)
### đđ Tilbakemeldinger
Har du tilbakemeldinger til kurset? Opprett gjerne en trÄd for det her:
-
### đ©đš Medvirkende
- [@thomaswolff](https://github.com/thomaswolff) - Primus motor og forfatter
- [@heidisu](https://github.com/heidisu) - Idé og kvalitetssikring
- [@matiasp](https://github.com/matiasp) - Oversettelse og videreutvikling
- [@teodor-elstad](https://github.com/teodor-elstad) - Oversettelse og videreutvikling
### đ Takk
- Takk til alle som har kommet med konstruktiv kritikk og nyttige tilbakemeldinger til dette kurset.
### đ Lisens
All dokumentasjon (inkludert denne veiledningen) og kildekoden i dette repoet er Äpent tilgjengelig under [MIT-lisensen](/LICENCE).
## đ Innholdsfortegnelse
- [Steg](#steg)
- [Steg 1 - Opprette API](#steg-1---opprette-api)
- [Steg 2 - Opprette testprosjekter](#steg-2---opprette-testprosjekter)
- [Steg 3 - Opprette solution](#steg-3---opprette-solution)
- [Steg 4 - Definere domenemodell](#steg-4---definere-domenemodell)
- [Steg 5 - Enhetstester for domenemodell](#steg-5---enhetstester-for-domenemodell)
- [Steg 6 - Definere API-kontrakt](#steg-6---definere-api-kontrakt)
- [Steg 7 - Implementere kontraktstyper](#steg-7---implementere-kontraktstyper)
- [Steg 8 - Sette opp skall for API](#steg-8---sette-opp-skall-for-api)
- [Steg 9 - Implementere web-API](#steg-9---implementere-web-api)
- [Ekstraoppgaver](#ekstraoppgaver)
- [Steg 10 - FĂžlge prinsipper i domenedrevet design](#steg-10---fĂžlge-prinsipper-i-domenedrevet-design)
- [Steg 11 - Grafisk fremstilling av OpenAPI-dokumentasjon](#steg-11---grafisk-fremstilling-av-openapi-dokumentasjon)
## Steg
NÄ som du har installert alle verktÞyene du trenger er du klar til Ä begynne pÄ selve kurset!
### Steg 1 - Opprette API
**Steg 1 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Neste steg](#steg-2---opprette-testprosjekter)
I dette steget starter vi med en mappe helt uten kode, og bruker .NET CLI til Ä opprette vÄrt fÞrste prosjekt `NRK.Dotnetskolen.Api`.
#### .NET-versjon
Siden denne veiledningen er skrevet for .NET 9, og det er mulig at du har flere .NET-versjoner installert pÄ maskinen din, mÄ vi instruere .NET CLI til Ä benytte .NET 9 nÄr vi kjÞrer kommandoene i veiledningen. Dette gjÞr vi ved Ä opprette en konfigurasjonsfil `global.json` i roten av repoet med fÞlgende innhold:
```jsonš
{
"sdk": {
"version": "9.0.0",
"rollForward": "latestMinor"
}
}
```
Her oppgir vi at vi i utgangspunktet Þnsker Ä bruke version `9.0.0` av .NET SDK. I tillegg sier vi gjennom `rollForward: latestMinor` at vi Þnsker at den hÞyeste tilgjengelige versjonen av .NET 9 pÄ maskinen din skal brukes.
> Du kan lese mer om `global.json` her:
#### .NET-prosjekter
For Ä kunne organisere kode i .NET bruker man _prosjekter_. Et prosjekt er en samling med kildekodefiler, og eventuelle andre ressursfiler, og alle filene som inngÄr i prosjektet er referert til i en _prosjektfil_. For F#-prosjekter har slike prosjektfiler filendelsen `.fsproj`.
NÄr man kompilerer .NET-prosjekter kan man velge mellom to typer output:
- KjĂžrbar fil (_executable_) - et program som kan kjĂžres
- Klassebibliotek (_dynamically linked library_) - en samling med funksjonalitet som kan benyttes av andre programmer
#### Dotnet new
Som nevnt i [innledningen](#-fremgangsmÄte) er .NET CLI et kommandolinjeverktÞy laget for Ä utvikle, bygge, kjÞre og publisere .NET-applikasjoner. .NET CLI kjÞres fra kommandolinjen med kommandoen `dotnet`, og har mange underkommandoer og valg. For Ä se alle kan du kjÞre kommandoen under, eller lese mer her:
```bash
dotnet --help
```
```bash
Usage: dotnet [runtime-options] [path-to-application] [arguments]
Execute a .NET application.
runtime-options:
--additionalprobingpath Path containing probing policy and assemblies to probe for.
--additional-deps Path to additional deps.json file.
--depsfile Path to .deps.json file.
--fx-version Version of the installed Shared Framework to use to run the application.
--roll-forward Roll forward to framework version (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable).
--runtimeconfig Path to .runtimeconfig.json file.
path-to-application:
The path to an application .dll file to execute.
Usage: dotnet [sdk-options] [command] [command-options] [arguments]
Execute a .NET SDK command.
sdk-options:
-d|--diagnostics Enable diagnostic output.
-h|--help Show command line help.
--info Display .NET information.
--list-runtimes Display the installed runtimes.
--list-sdks Display the installed SDKs.
--version Display .NET SDK version in use.
SDK commands:
add Add a package or reference to a .NET project.
build Build a .NET project.
build-server Interact with servers started by a build.
clean Clean build outputs of a .NET project.
format Apply style preferences to a project or solution.
help Opens the reference page in a browser for the specified command.
list List packages or references of a .NET project.
msbuild Run Microsoft Build Engine (MSBuild) commands.
new Create a new .NET project or file.
nuget Provides additional NuGet commands.
pack Create a NuGet package.
publish Publish a .NET project for deployment.
remove Remove a package or reference from a .NET project.
restore Restore dependencies specified in a .NET project.
run Build and run a .NET project output.
sdk Manage .NET SDK installation.
sln Modify Visual Studio solution files.
store Store the specified assemblies in the runtime package store.
test Run unit tests using the test runner specified in a .NET project.
tool Install or manage tools that extend the .NET experience.
vstest Run Microsoft Test Engine (VSTest) commands.
workload Manage optional workloads.
Additional commands from bundled tools:
dev-certs Create and manage development certificates.
fsi Start F# Interactive / execute F# scripts.
user-jwts Manage JSON Web Tokens in development.
user-secrets Manage development user secrets.
watch Start a file watcher that runs a command when files change.
Run 'dotnet [command] --help' for more information on a command.
```
#### Maler
For Ä opprette API-prosjektet skal vi bruke `new`-kommandoen i .NET CLI. `dotnet new` oppretter .NET-prosjekter, og som fÞrste parameter tar `new`-kommandoen inn hva slags mal prosjektet man oppretter skal fÞlge. NÄr man installerer .NET SDK fÄr man med et sett med forhÄndsdefinerte prosjektmaler for vanlige formÄl. For Ä se malene som er installert pÄ din maskin kan du kjÞre `dotnet new --list` slik:
```bash
dotnet new list
```
```bash
These templates matched your input:
Template Name Short Name Language Tags
------------------------------------------ -------------------------- ---------- -------------------------------------------------------
.NET Aspire App Host aspire-apphost [C#] Common/.NET Aspire/Cloud
.NET Aspire Empty App aspire [C#] Common/.NET Aspire/Cloud/Web/Web API/API/Service
.NET Aspire Service Defaults aspire-servicedefaults [C#] Common/.NET Aspire/Cloud/Web/Web API/API/Service
.NET Aspire Starter App aspire-starter [C#] Common/.NET Aspire/Blazor/Web/Web API/API/Service/Cloud
.NET Aspire Test Project (MSTest) aspire-mstest [C#] Common/.NET Aspire/Cloud/Web/Web API/API/Service/Test
.NET Aspire Test Project (NUnit) aspire-nunit [C#] Common/.NET Aspire/Cloud/Web/Web API/API/Service/Test
.NET Aspire Test Project (xUnit) aspire-xunit [C#] Common/.NET Aspire/Cloud/Web/Web API/API/Service/Test
API Controller apicontroller [C#] Web/ASP.NET
ASP.NET Core Empty web [C#],F# Web/Empty
ASP.NET Core gRPC Service grpc [C#] Web/gRPC/API/Service
ASP.NET Core Web API webapi [C#],F# Web/WebAPI/Web API/API/Service
ASP.NET Core Web API (native AOT) webapiaot [C#] Web/Web API/API/Service
ASP.NET Core Web App (Model-View-Contro... mvc [C#],F# Web/MVC
ASP.NET Core Web App (Razor Pages) webapp,razor [C#] Web/MVC/Razor Pages
ASP.NET Core with Angular angular [C#] Web/MVC/SPA
ASP.NET Core with React.js react [C#] Web/MVC/SPA
ASP.NET Core with React.js and Redux reactredux [C#] Web/MVC/SPA
Blazor Server App blazorserver [C#] Web/Blazor
Blazor Server App Empty blazorserver-empty [C#] Web/Blazor/Empty
Blazor Web App blazor [C#] Web/Blazor/WebAssembly
Blazor WebAssembly App Empty blazorwasm-empty [C#] Web/Blazor/WebAssembly/PWA/Empty
Blazor WebAssembly Standalone App blazorwasm [C#] Web/Blazor/WebAssembly/PWA
Class Library classlib [C#],F#,VB Common/Library
Console App console [C#],F#,VB Common/Console
dotnet gitignore file gitignore,.gitignore Config
Dotnet local tool manifest file tool-manifest Config
EditorConfig file editorconfig,.editorconfig Config
global.json file globaljson,global.json Config
MSBuild Directory.Build.props file buildprops MSBuild/props
MSBuild Directory.Build.targets file buildtargets MSBuild/props
MSBuild Directory.Packages.props file packagesprops MSBuild/packages/props/CPM
MSTest Playwright Test Project mstest-playwright [C#] Test/MSTest/Playwright/Desktop/Web
MSTest Test Class mstest-class [C#],F#,VB Test/MSTest
MSTest Test Project mstest [C#],F#,VB Test/MSTest/Desktop/Web
MVC Controller mvccontroller [C#] Web/ASP.NET
MVC ViewImports viewimports [C#] Web/ASP.NET
MVC ViewStart viewstart [C#] Web/ASP.NET
NuGet Config nugetconfig,nuget.config Config
NUnit 3 Test Item nunit-test [C#],F#,VB Test/NUnit
NUnit 3 Test Project nunit [C#],F#,VB Test/NUnit/Desktop/Web
NUnit Playwright Test Project nunit-playwright [C#] Test/NUnit/Playwright/Desktop/Web
Protocol Buffer File proto Web/gRPC
Razor Class Library razorclasslib [C#] Web/Razor/Library/Razor Class Library
Razor Component razorcomponent [C#] Web/ASP.NET
Razor Page page [C#] Web/ASP.NET
Razor View view [C#] Web/ASP.NET
Solution File sln,solution Solution
Web Config webconfig Config
Windows Forms App winforms [C#],VB Common/WinForms
Windows Forms Class Library winformslib [C#],VB Common/WinForms
Windows Forms Control Library winformscontrollib [C#],VB Common/WinForms
Worker Service worker [C#],F# Common/Worker/Web
WPF Application wpf [C#],VB Common/WPF
WPF Class Library wpflib [C#],VB Common/WPF
WPF Custom Control Library wpfcustomcontrollib [C#],VB Common/WPF
WPF User Control Library wpfusercontrollib [C#],VB Common/WPF
xUnit Test Project xunit [C#],F#,VB Test/xUnit/Desktop/Web
```
I tillegg til Ä styre hva slags type prosjekt man vil opprette med `new`-kommandoen, har man mulighet til Ä styre ting som hvilket sprÄk man Þnsker prosjektet skal opprettes for, og i hvilken mappe prosjektet opprettes i. For Ä se alle valgene man har i `dotnet new` kan du kjÞre fÞlgende kommando
```bash
dotnet new --help
```
```bash
Description:
Template Instantiation Commands for .NET CLI.
Usage:
dotnet new [ [...]] [options]
dotnet new [command] [options]
Arguments:
A short name of the template to create.
Template specific options to use.
Options:
-o, --output Location to place the generated output.
-n, --name The name for the output being created. If no name is specified, the name of the output directory is used.
--dry-run Displays a summary of what would happen if the given command line were run if it would result in a template
creation.
--force Forces content to be generated even if it would change existing files.
--no-update-check Disables checking for the template package updates when instantiating a template.
--project The project that should be used for context evaluation.
-v, --verbosity Sets the verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], and diag[nostic]. [default: normal]
-d, --diagnostics Enables diagnostic output.
-?, -h, --help Show command line help.
Commands:
create Instantiates a template with given short name. An alias of 'dotnet new '.
install Installs a template package.
uninstall Uninstalls a template package.
update Checks the currently installed template packages for update, and install the updates.
search Searches for the templates on NuGet.org.
list Lists templates containing the specified template name. If no name is specified, lists all
templates.
details Provides the details for specified template package.
The command checks if the package is installed locally, if it was not found, it
searches the configured NuGet feeds.
```
#### Opprette API-prosjektet
Som du ser av malene som er listet ut over, er det en innebygget mal for web-API som heter `webapi`. For Ä komme raskt i gang med et prosjekt, eller se hvordan et default .NET API er satt opp, kan man bruke `webapi` som mal. Vi kommer imidlertid til Ä opprette API-et vÄrt fra bunnen av ved Ä bruke malen `console` for Ä lÊre mest mulig om de ulike bestanddelene.
KjĂžr fĂžlgende kommando for Ă„ opprette API-prosjektet
```bash
dotnet new console --language F# --output src/api --name NRK.Dotnetskolen.Api
```
```bash
The template "Console App" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on src\api\NRK.Dotnetskolen.Api.fsproj...
Determining projects to restore...
Restored C:\Dev\nrkno@github.com\dotnetskolen\src\api\NRK.Dotnetskolen.Api.fsproj (in 101 ms).
Restore succeeded.
```
I kommandoen over brukte vi `--language`-argumentet for Ä oppgi at vi Þnsket et F#-prosjekt. I tillegg brukte vi `--output` for Ä oppgi hvor vi Þnsket at prosjektet skulle ligge relativt til der vi kjÞrer kommandoen fra, og `--name` til Ä styre navnet pÄ prosjektet.
> Merk at istedenfor `--language`, `--output` og `--name`, kunne vi brukt forkortelsene `-lang`, `-o` og `-n`.
Du skal nÄ ha en mappestruktur som ser slik ut
```txt
src
âââ api
âââ NRK.Dotnetskolen.Api.fsproj
âââ Program.fs
```
Som vi ser av diagrammet over opprettet .NET CLI mappene `src` og `src/api`, med `NRK.Dotnetskolen.Api.fsproj` og `Program.fs` i `src/api`.
> Merk at med mindre noe annet er spesifisert, er alle kommandoene i veiledningen skrevet med forutsetning om at du stÄr i samme mappe nÄr du kjÞrer dem. Dersom du har klonet Git-repoet til kurset er det rotmappen til repoet. Dersom du fÞlger kurset uten Ä bruke Git er det mappen du bestemmer deg for Ä kjÞre kommandoene i.
##### Prosjektfil
Ă
pne `NRK.Dotnetskolen.Api.fsproj` for Ă„ se innholdet til prosjektfilen til prosjektet du nettopp opprettet:
```xml
Exe
net9.0
```
Her ser vi at prosjektet:
- Har outputtypen `exe` - prosjektet kompileres til Ă„ bli en kjĂžrbar fil
- Skal kompileres til .NET 9
- BestÄr av én fil `Program.fs`
##### Programfilen
For Ä se hva programmet gjÞr kan vi Äpne `Program.fs` og se pÄ koden:
```f#
// For more information see https://aka.ms/fsharp-console-apps
printfn "Hello from F#"
```
Malen la inn kun én linje i `Program.fs` som skriver tekststrengen `Hello world from F#` til output. Fra andre programmeringssprÄk er du kanskje vant til Ä se en `main`-funksjon eller liknende, men det ser vi ikke her. Grunnen til det er at F# bruker et implisitt startpunkt som er pÄ toppen av filen. Deretter utfÞres koden linje for linje slik som spesifisert i filen. Det er ogsÄ mulig Ä bruke eksplisitte startpunkter i F#-programmer. Les mer om det her:
> Navnet til prosjektet `NRK.Dotnetskolen.Api.fsproj` fĂžlger Microsoft sin navnekonvensjon for programmer og biblioteker i .NET. For Ă„ lese mer om denne, og andre navnekonvensjoner i .NET, kan du se her:
>
> Mappestrukturen over er ment som et forslag, og de videre stegene i kurset bygger pÄ denne. Hvis du bruker kurset som inspirasjon eller veiledning til Ä opprette ditt eget prosjekt, trenger du ikke fÞlge denne mappestrukturen. Hvordan du strukturerer mappene i ditt system er opp til deg, og er avhengig av aspekter som stÞrrelse pÄ systemet, antall prosjekter, og personlige preferanser.
#### KjĂžre API-prosjektet
For Ă„ kjĂžre prosjektet som ble opprettet over kan du kjĂžre fĂžlgende kommando
```bash
dotnet run --project src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
Hello world from F#
```
Alternativt kan du gÄ til mappen hvor prosjektet ligger, og kjÞre `dotnet run` derfra, slik som vist under
```bash
cd src/api
dotnet run
```
```bash
Hello world from F#
```
#### Lagre endringer i Git (valgfritt)
NÄ som du har fullfÞrt det fÞrste steget i kurset er det en fin anledning til Ä lagre endringene du har gjort sÄ langt i Git.
##### Se endringer
Gitt at du fulgte veiledningen for Ă„ [sette opp koden lokalt](https://github.com/nrkno/dotnetskolen/blob/main/docs/detaljer-oppsett.md#-lokalt-oppsett-av-koden-valgfritt) fĂžr du begynte Ă„ kode, kan du kjĂžre fĂžlgende kommando for Ă„ se hvilke endringer som er gjort i repoet:
```bash
git status
```
```bash
On branch
Untracked files:
(use "git add ..." to include in what will be committed)
global.json
src/
nothing added to commit but untracked files present (use "git add" to track)
```
I outputen over ser vi at Git har oppdaget at det er opprettet en mappe `src` og innhold i den, men Git overvÄker ikke disse per nÄ (filene er "untracked").
##### Legg til endringer i Git
For Ä fÄ Git til Ä overvÄke filene vi har opprettet, og deretter se status i Git kan du kjÞre fÞlgende kommandoer:
```bash
git add .
git status
```
```bash
Changes to be committed:
(use "git restore --staged ..." to unstage)
new file: global.json
new file: src/api/NRK.Dotnetskolen.Api.fsproj
new file: src/api/Program.fs
```
NÄ overvÄker Git filene.
##### Lagre endringene
For Ä lagre nÄvÊrende tilstand av filene i en "commit" i Git kan du kjÞre fÞlgende kommando:
```bash
git commit -m "Opprettet API-prosjekt"
```
```bash
[ 00d11c8] Opprettet API-prosjekt
2 files changed, 25 insertions(+)
create mode 100644 src/api/NRK.Dotnetskolen.Api.fsproj
create mode 100644 src/api/Program.fs
```
##### Se alle historiske endringer i repoet
For Ä se alle commits i nÄvÊrende branch i Git, kan du kjÞre fÞlgende kommando:
```bash
git log
```
```bash
commit 00d11c82d0179f41883a55ce88e147a73ae60ee2 (HEAD -> )
Author: Thomas Wolff
Date: Fri Apr 16 13:43:40 2021 +0200
Opprettet API-prosjekt
...
```
> đĄ Tips! Gjenta de tre stegene over med Ă„ se endringer, legge dem til, og lagre dem etter Ă„ ha fullfĂžrt hvert steg for Ă„ ha bedre oversikt over hva du har vĂŠrt gjennom i kurset.
#### Se lĂžsningsforslag
Dersom du Þnsker Ä se den forventede tilstanden til repoet etter Ä ha utfÞrt de ulike stegene i kurset, kan du sjekke ut branchen med korresponderende navn som seksjonen du Þnsker Ä se pÄ. F.eks. hvis du vil se hvordan repoet ser ut etter fÞrste steg, kan du sjekke ut branchen `steg-1` slik:
```bash
git checkout steg-1
```
```bash
Switched to branch 'steg-1'
```
### Steg 2 - Opprette testprosjekter
**Steg 2 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-1---opprette-api) [⏠Neste steg](#steg-3---opprette-solution)
Tester er en viktig del av systemutvikling fordi de hjelper oss med Ä verifisere at systemet fungerer slik det skal. NÄr man skriver tester for kode opererer man ofte med to typer tester:
- Enhetstester
- Integrasjonstester
Enhetstester verifiserer at smÄ, isolerte deler av koden fungerer slik den skal. Gjerne én og én funksjon. I dette kurset skal vi bruke enhetstester til Ä verifisere valideringsregler i domenet vÄrt.
Integrasjonstester verifiserer at stĂžrre deler av systemet fungerer slik det skal, og kan til og med dekke samspill med andre systemer. I dette kurset skal vi bruke integrasjonstester til Ă„ verifisere at web-API-et oppfĂžrer seg i henhold til [kontrakten vi definerer i steg 6](#steg-6---definere-api-kontrakt).
#### Dotnet new
I dette steget skal vi opprette to testprosjekter
- Ett for enhetstester - `NRK.Dotnetskolen.UnitTests`
- Ett for integrasjonstester - `NRK.Dotnetskolen.IntegrationTests`
For Ä opprette testprosjektene skal vi igjen bruke `dotnet new`-kommandoen, men denne gangen velger vi en annen [mal](#maler) enn da vi opprettet API-prosjektet. NÄr man installerer .NET SDK fÞlger det med flere maler for testprosjekter som korresponderer til ulike rammeverk som finnes for Ä detektere og kjÞre tester:
- xUnit
- nUnit
- MSTest
I dette kurset kommer vi til Ä bruke xUnit. Dette valget er litt vilkÄrlig ettersom alle rammeverkene over vil vÊre tilstrekkelig til formÄlet vÄrt, som er Ä vise hvordan man kan sette opp testprosjekter og komme i gang med Ä skrive tester. Dersom du Þnsker Ä vite mer om de ulike testrammeverkene, kan du lese mer om dem her:
#### Opprette enhetstestprosjekt
KjĂžr fĂžlgende kommando for Ă„ opprette enhetstestprosjektet
```bash
dotnet new xunit -lang F# -o test/unit -n NRK.Dotnetskolen.UnitTests
```
```bash
The template "xUnit Test Project" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on test/unit/NRK.Dotnetskolen.UnitTests.fsproj...
Determining projects to restore...
Restored C:\Dev\nrkno@github.com\dotnetskolen\test\unit\NRK.Dotnetskolen.UnitTests.fsproj (in 1.31 sec).
Restore succeeded.
```
Du skal nÄ ha fÞlgende mappestruktur
```txt
src
âââ api
âââ NRK.Dotnetskolen.Api.fsproj
âââ Program.fs
test
âââ unit
âââ NRK.Dotnetskolen.UnitTests.fsproj
âââ Program.fs
âââ Tests.fs
```
##### Prosjektfil
Ă
pne filen `NRK.Dotnetskolen.UnitTests.fsproj`:
```xml
net9.0
false
false
```
I prosjektfilen kan vi se at enhetstestprosjektet:
- Skal kompileres til .NET 9
- Inneholder to kildekodefiler
- `Tests.fs`
- `Program.fs`
- Har referanser til fire NuGet-pakker
- `coverlet.collector` - bibliotek for Ä fÄ code coverage statistikk for prosjekter
- `Microsoft.NET.Test.Sdk` - Pakke for Ă„ bygge .NET testprosjekter
- `xunit` - Bibliotek for Ă„ skrive enhetstester
- `xunit.runner.visualstudio` - Pakke for Ă„ kjĂžre Xunit-tester i "Test explorer" i Visual Studio
##### Testfilen
Ă
pne filen `Tests.fs`:
```f#
module Tests
open System
open Xunit
[]
let ``My test`` () =
Assert.True(true)
```
Ăverst i filen blir det definert en F#-modul `Tests`. I tillegg blir modulene `System` og `Xunit` Ă„pnet, som kommer fra hhv. basebiblioteket til Microsoft, og biblioteket Xunit. Videre blir det definert en test ``` ``My test`` ```. MĂ„ten vi ser at det er en test pĂ„ er ved Ă„ se at den er annotert med `[]`. Xunit opererer med to annotasjoner for tester:
- `[]`
- `[]`
Forskjellen pÄ disse blir nÊrmere forklart i [steget om enhetstester](#steg-5---enhetstester-for-domenemodell).
> Merk at ``` ```` ``` er brukt for Ä kunne ha et variabelnavn som inneholder mellomrom. PÄ denne mÄten kan man ha et funksjonsnavn som beskriver testen og samtidig er lesbar for mennesker.
##### KjĂžre enhetstestprosjektet
For Ă„ kjĂžre testen i enhetstestprosjektet kan du bruke fĂžlgende kommando
```bash
dotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
Restore complete (0,3s)
NRK.Dotnetskolen.UnitTests succeeded (2,0s) â test\unit\bin\Debug\net9.0\NRK.Dotnetskolen.UnitTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.51] Discovering: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.54] Discovered: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.54] Starting: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.69] Finished: NRK.Dotnetskolen.UnitTests
NRK.Dotnetskolen.UnitTests test succeeded (1,8s)
Test summary: total: 1; failed: 0; succeeded: 1; skipped: 0; duration: 1,8s
Build succeeded in 4,5s
```
PÄ lik linje med `dotnet run`, kan du alternativt gÄ inn i mappen til enhetstestprosjektet, og kjÞre `dotnet test` derfra:
```bash
cd test/unit
dotnet test
```
```bash
Restore complete (0,3s)
NRK.Dotnetskolen.UnitTests succeeded (0,4s) â bin\Debug\net9.0\NRK.Dotnetskolen.UnitTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.50] Discovering: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.52] Discovered: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.53] Starting: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.68] Finished: NRK.Dotnetskolen.UnitTests
NRK.Dotnetskolen.UnitTests test succeeded (1,7s)
Test summary: total: 1; failed: 0; succeeded: 1; skipped: 0; duration: 1,7s
Build succeeded in 2,9s
```
#### Opprette integrasjonstestprosjekt
For Ä opprette integrasjonstestprosjektet, kan du kjÞre samme kommando som da du [opprettet enhetstestprosjektet](#opprette-enhetstestprosjekt), men bytt ut `Unit` med `Integration` i navnet pÄ testprosjektet, som vist under:
```bash
dotnet new xunit -lang F# -o test/integration -n NRK.Dotnetskolen.IntegrationTests
```
```bash
The template "xUnit Test Project" was created successfully.
Processing post-creation actions...
Running 'dotnet restore' on test\integration\NRK.Dotnetskolen.IntegrationTests.fsproj...
Determining projects to restore...
Restored C:\Dev\nrkno@github.com\dotnetskolen\test\integration\NRK.Dotnetskolen.IntegrationTests.fsproj (in 580 ms).
Restore succeeded.
```
Du skal nÄ ha fÞlgende mappestruktur
```txt
src
âââ api
âââ NRK.Dotnetskolen.Api.fsproj
âââ Program.fs
test
âââ unit
âââ NRK.Dotnetskolen.UnitTests.fsproj
âââ Program.fs
âââ Tests.fs
âââ integration
âââ NRK.Dotnetskolen.IntegrationTests.fsproj
âââ Program.fs
âââ Tests.fs
```
ForelÞpig er prosjekt- og test-filene til integrasjonstestprosjektet helt like de fra enhetstestprosjektet (bortsett fra prosjektnavnet). Forskjellen pÄ enhets- og integrasjonstestene blir tydeligere nÄr vi skal skrive testene i hhv. [steg 5](#steg-5---enhetstester-for-domenemodell) og [steg 9](#steg-9---implementere-web-api).
##### KjĂžre integrasjonstester
For Ă„ kjĂžre testene i integrasjonstestprosjektet kan du bruke fĂžlgende kommando
```bash
dotnet test test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj
```
```bash
Restore complete (0,3s)
NRK.Dotnetskolen.IntegrationTests succeeded (2,0s) â test\integration\bin\Debug\net9.0\NRK.Dotnetskolen.IntegrationTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.54] Discovering: NRK.Dotnetskolen.IntegrationTests
[xUnit.net 00:00:00.56] Discovered: NRK.Dotnetskolen.IntegrationTests
[xUnit.net 00:00:00.57] Starting: NRK.Dotnetskolen.IntegrationTests
[xUnit.net 00:00:00.72] Finished: NRK.Dotnetskolen.IntegrationTests
NRK.Dotnetskolen.IntegrationTests test succeeded (1,8s)
Test summary: total: 1; failed: 0; succeeded: 1; skipped: 0; duration: 1,8s
Build succeeded in 4,6s
```
### Steg 3 - Opprette solution
**Steg 3 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-2---opprette-testprosjekter) [⏠Neste steg](#steg-4---definere-domenemodell)
Slik oppsettet er nĂ„, har vi tre prosjekter som er uavhengige av hverandre. Annet enn at de ligger i samme mappe, er det ingenting som kobler dem sammen. For Ă„ kunne gjĂžre operasjoner som Ă„ legge til felles pakker, og kjĂžre alle testene for systemet vĂ„rt, kan vi knytte prosjektene sammen i en og samme lĂžsning (_solution_). Ă
ha alle prosjektene i en og samme lÞsning gir ogsÄ fordelen av at man kan Äpne alle prosjektene samlet i en IDE.
#### Dotnet sln
For Ă„ opprette en solution med `dotnet` kan du kjĂžre fĂžlgende kommando:
```bash
dotnet new sln -n Dotnetskolen
```
```bash
The template "Solution File" was created successfully.
```
Du skal nÄ ha fÄtt filen `Dotnetskolen.sln` slik som vist under
```txt
src
âââ api
âââ NRK.Dotnetskolen.Api.fsproj
âââ Program.fs
test
âââ unit
âââ NRK.Dotnetskolen.UnitTests.fsproj
âââ Program.fs
âââ Tests.fs
âââ integration
âââ NRK.Dotnetskolen.IntegrationTests.fsproj
âââ Program.fs
âââ Tests.fs
âââ Dotnetskolen.sln
```
Hvis vi ser pÄ innholdet i `Dotnetskolen.sln` ser vi at det ikke er noen referanser til prosjektene vÄre enda
```txt
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal
```
#### Legge til prosjekter i solution
For Ă„ legge til referanser til prosjektene du har opprettet kan du kjĂžre fĂžlgende kommandoer
##### Legge til API-prosjekt
```bash
dotnet sln add src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
Project `src\api\NRK.Dotnetskolen.Api.fsproj` added to the solution.
```
##### Legge til enhetstestprosjekt
```bash
dotnet sln add test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
Project `test\unit\NRK.Dotnetskolen.UnitTests.fsproj` added to the solution.
```
##### Legge til integrasjonstestprosjekt
```bash
dotnet sln add test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj
```
```bash
Project `test\integration\NRK.Dotnetskolen.IntegrationTests.fsproj` added to the solution.
```
NÄ ser vi at `Dotnetskolen.sln` inneholder referanser til prosjektene vÄre
```txt
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{602F7DA2-73CF-4DA2-82E5-D392DE47E0BC}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NRK.Dotnetskolen.Api", "src\api\NRK.Dotnetskolen.Api.fsproj", "{618BF895-AEA1-4086-8904-89DD317B2429}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{10963520-731D-442B-B808-DA74BDD9207D}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NRK.Dotnetskolen.UnitTests", "test\unit\NRK.Dotnetskolen.UnitTests.fsproj", "{95B87F0E-15B8-4646-98F0-E8DAACA5526D}"
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "NRK.Dotnetskolen.IntegrationTests", "test\integration\NRK.Dotnetskolen.IntegrationTests.fsproj", "{391F46FA-9684-460E-B6A2-99EF7363693F}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{618BF895-AEA1-4086-8904-89DD317B2429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{618BF895-AEA1-4086-8904-89DD317B2429}.Debug|Any CPU.Build.0 = Debug|Any CPU
{618BF895-AEA1-4086-8904-89DD317B2429}.Release|Any CPU.ActiveCfg = Release|Any CPU
{618BF895-AEA1-4086-8904-89DD317B2429}.Release|Any CPU.Build.0 = Release|Any CPU
{95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Debug|Any CPU.Build.0 = Debug|Any CPU
{95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Release|Any CPU.ActiveCfg = Release|Any CPU
{95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Release|Any CPU.Build.0 = Release|Any CPU
{391F46FA-9684-460E-B6A2-99EF7363693F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{391F46FA-9684-460E-B6A2-99EF7363693F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{391F46FA-9684-460E-B6A2-99EF7363693F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{391F46FA-9684-460E-B6A2-99EF7363693F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{618BF895-AEA1-4086-8904-89DD317B2429} = {602F7DA2-73CF-4DA2-82E5-D392DE47E0BC}
{95B87F0E-15B8-4646-98F0-E8DAACA5526D} = {10963520-731D-442B-B808-DA74BDD9207D}
{391F46FA-9684-460E-B6A2-99EF7363693F} = {10963520-731D-442B-B808-DA74BDD9207D}
EndGlobalSection
EndGlobal
```
#### Solution i Visual Studio
Bildet under viser hvordan "Solution explorer" i Visual Studio viser lĂžsningen.

### Steg 4 - Definere domenemodell
**Steg 4 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-3---opprette-solution) [⏠Neste steg](#steg-5---enhetstester-for-domenemodell)
Vi skal lage et API for Ă„ hente ut en forenklet elektronisk programguide (EPG) for ulike kanaler i NRK TV. Tanken er at dette API-et kunne levert datagrunnlaget til en programguide - f.eks. den som vises her:
> Modellen vi bruker for EPG i dette kurset er forenklet sammenliknet med [den som benyttes i PS API](https://webapp-ps-granitt-api-prod-we.azurewebsites.net/swagger/ui/index#/Epg), og er kun brukt som eksempel.
En EPG kan sees pÄ som en liste med sendinger, og for vÄrt eksempel i dette kurset inneholder en sending fÞlgende felter:
- Tittel - Tittelen til programmet. MÄ vÊre mellom 5 og 100 tegn (inklusiv), og kan kun bestÄ av store og smÄ bokstaver, tall, og enkelte spesialtegn: `, . : - !`
- Kanal - Kanalen sendingen gÄr pÄ. I vÄrt tilfelle begrenses mulige kanaler til NRK1 og NRK2, og mÄ skrives med store bokstaver.
- Startdato- og tidspunkt - dato og tidspunkt for nÄr sendingen starter.
- Sluttdato- og tidspunkt - dato og tidspunkt for nÄr sendingen slutter. MÄ vÊre etter startdato- og tidspunkt.
#### Domenemodell i F#
NÄ som vi har spesifisert domenet vÄrt, kan vi modellere det i F#. Start med Ä opprett en ny fil `Domain.fs` under `src/api`:
```txt
âââ .config
âââ ...
src
âââ api
âââ Domain.fs
âââ NRK.Dotnetskolen.Api.fsproj
âââ Program.fs
test
âââ ...
âââ Dotnetskolen.sln
```
Lim inn innholdet under i `Domain.fs`:
```f#
namespace NRK.Dotnetskolen
module Domain =
open System
type Sending = {
Tittel: string
Kanal: string
Starttidspunkt: DateTimeOffset
Sluttidspunkt: DateTimeOffset
}
type Epg = Sending list
```
Over definerer vi en F#-modul `Domain` i namespacet `NRK.Dotnetskolen`. I `Domain`-modulen definerer vi domenemodellen vÄr, som bestÄr av to typer:
- `Sending` - modellerer et enkelt innslag i EPG-en, og inneholder feltene som ble definert i forrige seksjon
- Tittel
- Kanal
- Starttidspunkt
- Sluttidspunkt
- `Epg` - en liste med sendinger
Vi Äpnet ogsÄ modulen `System` for Ä fÄ tilgang til typen `DateTimeOffset`.
> Legg merke til innrykket pÄ linjene etter `module Domain =`. Dette inntrykket er pÄkrevd av F# for at koden skal kompilere riktig.
Inkluder `Domain.fs` i api-prosjektet ved Ă„ legge til `` i `src/api/NRK.Dotnetskolen.Api.fsproj` slik som vist under:
```xml
Exe
net9.0
```
> Merk at rekkefÞlgen filer blir inkludert i F#-prosjektfiler pÄ har betydning. Dersom `modul A` er definert i `ModulA.fs` og `modul B` er definert i `ModulB.fs`, og `modul A` skal kunne Äpne `modul B` mÄ `ModulB.fs` ligge fÞr `ModulA.fs` i prosjektfilen.
>
> Moduler i F# blir kompilert til det samme i CIL som statiske klasser i C#.
#### Opprette en EPG
NĂ„ som vi har definert domenemodellen vĂ„r, skal vi se hvordan vi kan ta den i bruk. Ă
pne `Program.fs` i web-API-prosjektet og erstatt innholdet med fĂžlgende kode:
```f#
open System
open NRK.Dotnetskolen.Domain
let epg = [
{
Tittel = "Dagsrevyen"
Kanal = "NRK1"
Starttidspunkt = DateTimeOffset.Parse("2021-04-16T19:00:00+02:00")
Sluttidspunkt = DateTimeOffset.Parse("2021-04-16T19:30:00+02:00")
}
]
printfn "%A" epg
```
Her oppretter vi en variabel `epg` som er en liste med sendinger, slik vi definerte i `Domain.fs`.
KjĂžr API-prosjektet igjen med fĂžlgende kommando, og se at `epg`-verdien blir skrevet til terminalen.
```bash
dotnet run --project src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
[{ Tittel = "Dagsrevyen"
Kanal = "NRK1"
Starttidspunkt = 16.04.2021 19:00:00 +02:00
Sluttidspunkt = 16.04.2021 19:30:00 +02:00 }]
```
> Merk at noen har rapportert om problemer med feilmeldinger i Rider etter Ä ha lagt til linjen `open NRK.Dotnetskolen.Domain`. Dersom du opplever det samme kan du hÞyreklikke pÄ "Solution"-noden i Rider, og klikke pÄ "Unload" etterfulgt av "Reload". Dette skal forhÄpentligvis rette opp i problemet.
### Steg 5 - Enhetstester for domenemodell
**Steg 5 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-4---definere-domenemodell) [⏠Neste steg](#steg-6---definere-api-kontrakt)
Domenemodellen som ble innfÞrt i [forrige steg](#steg-4---definere-domenemodell) inneholder bÄde strukturen til EPG-en, og valideringsreglene knyttet til dem. SÄ langt har vi kun modellert strukturen til domenemodellen i F# (at EPG bestÄr av en liste med sendinger, og hvilke felter hver sending inneholder). I dette steget skal vi implementere valideringsreglene i F#, og verifisere at vi har implementert dem riktig ved hjelp av enhetstester.
#### Regler i domenet vÄrt
Vi Þnsker Ä verifisere fÞlgende regler fra domenet vÄrt:
- Tittel
- MÄ bestÄ av 5-100 tegn (inklusiv)
- Kan kun bestÄ av store og smÄ bokstaver, tall, og fÞlgende spesialtegn: `, . : - !`
- Kanal
- `NRK1` eller `NRK2`.
- Kun store bokstaver er lov.
- Sendetidspunkt
- Sluttidspunkt skal vĂŠre etter starttidspunkt
#### Tittel
La oss begynne med Ă„ verifisere at vi implementerer valideringsreglene for tittel riktig.
##### Enhetstester
Ettersom tittel har lengdebegrensninger er det viktig Ă„ teste grensetilfellene til lengden. I tillegg er det viktig Ă„ teste at kun gyldige tegn er lov. Erstatt den eksisterende testen i `Tests.fs` i enhetstestprosjektet med testene under.
```f#
module Tests
open Xunit
[]
[]
[]
[]
let ``isTittelValid valid tittel returns true`` (tittel: string) =
let isTittelValid = isTittelValid tittel
Assert.True isTittelValid
[]
[]
[]
[]
let ``isTittelValid invalid tittel returns false`` (tittel: string) =
let isTittelValid = isTittelValid tittel
Assert.False isTittelValid
```
Her har vi definert to enhetstester som begge tester funksjonen `isTittelValid`. Den fÞrste testen verifiserer at `isTittelValid` returnerer `true` nÄr tittelen _er_ gyldig, mens den andre verifiserer det motsatte tilfellet. I xUnit annoterer man testfunksjoner med enten `[]` eller `[]`. Testfunksjoner annotert med `[]` vil kjÞre én gang uten noen inputparametere, mens i testfunksjoner annotert med `[]` kan man ta inn parametere, og annotere testfunksjonen med `[]` for Ä sende inn gitte inputparametere. Da vil testfunksjonen bli kjÞrt én gang _per_ annotering med `[]`.
Hvis du forsÞker Ä kjÞre testene, vil du se at testprosjektet ikke kompilerer fordi vi verken har referanse til API-prosjektet (hvor domenet vÄrt er definert) eller har definert funksjonen `isTittelValid` enda.
```bash
dotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
dotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj [11:56:17]
Restore complete (0,4s)
NRK.Dotnetskolen.UnitTests failed with 2 error(s) (2,0s)
C:\Dev\github.com\nrkno\dotnetskolen\test\unit\Tests.fs(9,25): error FS0039: The value or constructor 'isTittelValid' is not defined.
C:\Dev\github.com\nrkno\dotnetskolen\test\unit\Tests.fs(18,25): error FS0039: The value or constructor 'isTittelValid' is not defined.
Build failed with 2 error(s) in 3,0s
```
##### Implementere isTittelValid
For Ă„ validere en tittel bruker vi et regulĂŠrt uttrykk som reflekterer reglene i domenet vĂ„rt. Ă
pne filen `Domain.fs` i API-prosjektet, og legg til fĂžlgende `open`-statement under `open system`:
```f#
open System.Text.RegularExpressions
```
Lim deretter inn fÞlgende kode pÄ slutten av filen:
```f#
let isTittelValid (tittel: string) : bool =
let tittelRegex = Regex(@"^[\p{L}0-9\.,-:!]{5,100}$")
tittelRegex.IsMatch(tittel)
```
Det regulĂŠre uttrykket lister opp hvilke tegn som er gyldige i en gruppe (tegnene mellom mellom `[` og `]`):
- `\p{L}` - syntaks for Ă„ spesifisere enhver bokstav i Unicode
- `0-9` - tall
- `\.,-:!` - spesialtegnene vi tillater
I tillegg spesifiserer `{5,100}` at vi tillater 5-100 av tegnene i gruppen over.
##### Legge til prosjektreferanse
For at enhetstestprosjektet skal fÄ tilgang til funksjonen vi nettopp definerte i `Domain.fs` mÄ vi legge til en prosjektreferanse til API-prosjektet i enhetstestprosjektet. Det kan vi gjÞre vha. .NET CLI med fÞlgende kommando:
```bash
dotnet add ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj reference ./src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
Reference `..\..\src\api\NRK.Dotnetskolen.Api.fsproj` added to the project.
```
Du kan se effekten av kommandoen over ved Ä Äpne `test/unit/NRK.Dotnetskolen.UnitTests.fsproj`:
```xml
net9.0
false
false
```
##### Ă
pne modul
I tillegg til Ä legge til en referanse til API-prosjektet i enhetstestprosjektet, mÄ vi Äpne `NRK.Dotnetskolen.Domain`-modulen i `Tests.fs`. Det kan du gjÞre ved Ä legge til `open NRK.Dotnetskolen.Domain` under `open Xunit` i `Tests.fs`:
```f#
module Tests
open Xunit
open NRK.Dotnetskolen.Domain
```
NĂ„ skal testene kjĂžre vellykket:
```bash
dotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
Restore complete (1,1s)
NRK.Dotnetskolen.Api succeeded (2,9s) â src\api\bin\Debug\net9.0\NRK.Dotnetskolen.Api.dll
NRK.Dotnetskolen.UnitTests succeeded (2,4s) â test\unit\bin\Debug\net9.0\NRK.Dotnetskolen.UnitTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.76] Discovering: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.83] Discovered: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.83] Starting: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:01.19] Finished: NRK.Dotnetskolen.UnitTests
NRK.Dotnetskolen.UnitTests test succeeded (3,2s)
Test summary: total: 6; failed: 0; succeeded: 6; skipped: 0; duration: 3,1s
Build succeeded in 10,2s
```
> Legg merke til at testrapporten viser at seks tester ble kjĂžrt. ForelĂžpig har vi kun definert to tester. Dette illustrerer at `xUnit` kjĂžrer tester en gang per annotasjon med `[]`.
#### Kanal
Reglene for kanal er ganske enkle ettersom det kun er to gyldige kanaler, og disse kun kan skrives med store bokstaver.
##### Enhetstester
For Ä teste valideringsreglen for kanal trenger vi én positiv test per gyldige kanal, en negativ test for en kanal med smÄ bokstaver, og en negativ test for en ugyldig kanal. Utvid `Tests.fs` i med fÞlgende tester for kanal:
```f#
[]
[]
[]
let ``isKanalValid valid kanal returns true`` (kanal: string) =
let isKanalValid = isKanalValid kanal
Assert.True isKanalValid
[]
[]
[]
let ``isKanalValid invalid kanal returns false`` (kanal: string) =
let isKanalValid = isKanalValid kanal
Assert.False isKanalValid
```
##### Implementasjon av isKanalValid
FĂžr vi kjĂžrer testene igjen, definerer vi skallet for `isKanalValid` i `Domain.fs`:
```f#
let isKanalValid (kanal: string) : bool =
// Implementasjon her
```
âïž ImplementĂ©r `isKanalValid` slik at enhetstestene passerer.
```bash
dotnet test ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
Restore complete (0,4s)
NRK.Dotnetskolen.Api succeeded (2,2s) â src\api\bin\Debug\net9.0\NRK.Dotnetskolen.Api.dll
NRK.Dotnetskolen.UnitTests succeeded (2,3s) â test\unit\bin\Debug\net9.0\NRK.Dotnetskolen.UnitTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.08] Discovering: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.12] Discovered: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.12] Starting: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.26] Finished: NRK.Dotnetskolen.UnitTests
NRK.Dotnetskolen.UnitTests test succeeded (1,2s)
Test summary: total: 10; failed: 0; succeeded: 10; skipped: 0; duration: 1,1s
Build succeeded in 6,7s
```
#### Sendetidspunkter
Det siste vi skal validere i domenet vÄrt er at sluttidspunkt er etter starttidspunkt.
##### Enhetstester
Under fÞlger én enhetstest for validering av sendetidspunkter i `Tests.fs`:
```f#
[]
let ``areStartAndSluttidspunktValid start before end returns true`` () =
let starttidspunkt = DateTimeOffset.Now
let sluttidspunkt = starttidspunkt.AddMinutes 30.
let areStartAndSluttidspunktValid = areStartAndSluttidspunktValid starttidspunkt sluttidspunkt
Assert.True areStartAndSluttidspunktValid
```
Merk at du ogsÄ mÄ legge til fÞlgende `open`-statement i `Tests.fs` for at `DateTimeOffset.Now` fra kodesnutten over skal fungere:
```f#
open System
```
âïž Legg til flere enhetstester du mener er nĂždvendig for Ă„ verifisere at validering av start- og sluttidspunkt er korrekt.
> Merk at her bruker vi `[]`-attributtet istedenfor `[]`. `[]`-attributtet som man bruker med `[]`-attributtet krever verdier som er konstante ved kompilering. Ettersom vi benytter `DateTimeOffset`-objekter (som ikke er konstante ved kompilering) som input til `areStartAndSluttidspunktValid`, bruker vi derfor `[]`-attributtet.
##### Implementasjon av areStartAndSluttidspunktValid
Funksjonen for Ä validere sendetidspunktene mÄ undersÞke om sluttidspunktet er stÞrre enn starttidspunktet. Lim inn skallet til `areStartAndSluttidspunktValid` i `Domain.fs`:
```f#
let areStartAndSluttidspunktValid (starttidspunkt: DateTimeOffset) (sluttidspunkt: DateTimeOffset) =
// Implementasjon her
```
âïž ImplementĂ©r `areStartAndSluttidspunktValid` og fĂ„ enhetstestene til Ă„ passere.
```bash
dotnet test ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
Restore complete (0,4s)
NRK.Dotnetskolen.Api succeeded (2,2s) â src\api\bin\Debug\net9.0\NRK.Dotnetskolen.Api.dll
NRK.Dotnetskolen.UnitTests succeeded (2,2s) â test\unit\bin\Debug\net9.0\NRK.Dotnetskolen.UnitTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.08] Discovering: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.13] Discovered: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.13] Starting: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.24] Finished: NRK.Dotnetskolen.UnitTests
NRK.Dotnetskolen.UnitTests test succeeded (1,1s)
Test summary: total: 13; failed: 0; succeeded: 13; skipped: 0; duration: 1,1s
Build succeeded in 6,6s
```
#### Validere en sending
NĂ„ som vi har funksjoner for Ă„ validere de ulike feltene i en sending, kan vi lage en funksjon som validerer en hel sending.
##### Enhetstester
Siden vi har skrevet enhetstester for valideringsfunksjonene til de ulike delene av en sending, kan enhetstestene for validering av hele sendingen vĂŠre ganske enkle.
âïž Skriv Ă©n positiv test for en gyldig sending, og Ă©n negativ test for en ugyldig sending i `Tests.fs` som antar at det finnes en funksjon `isSendingValid` i `Domain.fs`
##### Implementasjon av isSendingValid
Legg til fĂžlgende skall for `isSendingValid` i `Domain.fs`:
```f#
let isSendingValid (sending: Sending) : bool =
// Implementasjon her
```
âïž ImplementĂ©r `isSendingValid`, og fĂ„ enhetstestene til Ă„ passere:
```bash
dotnet test ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj
```
```bash
Restore complete (0,5s)
NRK.Dotnetskolen.Api succeeded (2,3s) â src\api\bin\Debug\net9.0\NRK.Dotnetskolen.Api.dll
NRK.Dotnetskolen.UnitTests succeeded (2,6s) â test\unit\bin\Debug\net9.0\NRK.Dotnetskolen.UnitTests.dll
[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)
[xUnit.net 00:00:00.09] Discovering: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.14] Discovered: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.14] Starting: NRK.Dotnetskolen.UnitTests
[xUnit.net 00:00:00.27] Finished: NRK.Dotnetskolen.UnitTests
NRK.Dotnetskolen.UnitTests test succeeded (1,2s)
Test summary: total: 15; failed: 0; succeeded: 15; skipped: 0; duration: 1,2s
Build succeeded in 7,2s
```
> Merk at domenemodellen, slik den er implementert i [steg 4](#steg-4---definere-domenemodell) og [steg 5](#steg-5---enhetstester-for-domenemodell), har en svakhet i at man kan opprette en `Sending`-verdi som er ugyldig. Vi har implementert `isSendingValid`, men det er ingenting som hindrer oss i Ä opprette en `Sending`-verdi uten Ä bruke `isSendingValid`. I ekstraoppgaven i [steg 10](#steg-10---fÞlge-prinsipper-i-domenedrevet-design) blir en alternativ tilnÊrming som bruker prinsipper fra [domenedrevet design](https://en.wikipedia.org/wiki/Domain-driven_design) presentert. De resterende stegene i dette kurset frem til og med steg 10 kommer til Ä basere seg pÄ domenemodellen slik den er definert her i [steg 4](#steg-4---definere-domenemodell) og [steg 5](#steg-5---enhetstester-for-domenemodell) for Ä ikke innfÞre for mange prinsipper pÄ en gang, og holde fokus pÄ det kurset er ment for. Dersom du Þnsker mÄ du gjerne gÄ videre til [steg 10](#steg-10---fÞlge-prinsipper-i-domenedrevet-design) nÄ for Ä se hvordan det er gjort der. Husk at steg 11 er skrevet med forutsetning av at man har gjennomfÞrt kurset til og med steg 10 fÞrst.
### Steg 6 - Definere API-kontrakt
**Steg 6 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-5---enhetstester-for-domenemodell) [⏠Neste steg](#steg-7---implementere-kontraktstyper)
For Ä dokumentere hva API-et vÄrt tilbyr av operasjoner og responser skal vi lage en API-kontrakt. I NRK TV og NRK Radio definerer vi API-kontrakter ved bruk av OpenAPI ().
#### Operasjoner
For Ä begrense omfanget av API-et vÄrt skal vi ha kun én operasjon i det:
- Hent EPG pÄ en gitt dato
#### Responser
Responsen til denne operasjonen vil bestÄ av to lister med sendinger, én for hver kanal i domenet vÄrt, hvor hver sending har:
- Tittel - tekststreng som fÞlger reglene definert i [domenemodellen vÄr](#steg-4---definere-domenemodell).
- Startdato- og tidspunkt - tekststreng som fĂžlger datoformatet i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6).
- Sluttdato- og tidspunkt - tekststreng som fĂžlger datoformatet i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Er garantert Ă„ vĂŠre stĂžrre enn startdato- og tidspunkt.
#### JSON Schema
FÞr vi definerer selve kontrakten til API-et i en OpenAPI-spesifikasjon, skal vi definere et [JSON Schema](https://json-schema.org/) for innholdet i responsen til operasjonen i API-et vÄrt. Dette er vist under.
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"nrk1": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Sending"
}
},
"nrk2": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Sending"
}
}
},
"required": [
"nrk1",
"nrk2"
],
"components": {
"schemas": {
"Tittel": {
"type": "string",
"pattern": "^[\\p{L}0-9\\.,-:!]{5,100}$",
"example": "Dagsrevyen",
"description": "Programtittel"
},
"Sending": {
"type": "object",
"properties": {
"tittel": {
"$ref": "#/components/schemas/Tittel"
},
"starttidspunkt": {
"type": "string",
"format": "date-time",
"description": "Startdato- og tidspunkt for sendingen."
},
"sluttidspunkt": {
"type": "string",
"format": "date-time",
"description": "Sluttdato- og tidspunkt for sendingen. Er alltid stĂžrre enn sendingens startdato- og tidspunkt."
}
},
"required": [
"tittel",
"starttidspunkt",
"sluttidspunkt"
]
}
}
}
}
```
Her ser vi at responsen bestÄr av et objekt med to felter: `nrk1` og `nrk2`, som begge er en liste med sendingene pÄ de respektive kanalene. Hver sending inneholder en tittel, samt start- og sluttidspunkt. Hver av feltene er tekststrenger som fÞlger valideringsreglene vi har definert i domenet vÄrt. `Tittel` har `pattern` lik det regulÊre uttrykket vi benyttet i `isTittelValid` i `Domain.fs`. `Starttidspunkt` og `Sluttidspunkt` har `format: "date-time"`, som fÞlger datoformatet i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6).
ForelÞpig skal vi ikke gjÞre noe mer med JSON schemaet enn Ä ha det som dokumentasjon pÄ API-et vÄrt. Lag en ny mappe `docs` i rotmappen din med en ny fil `epg.schema.json` hvor du limer inn JSON schemaet over. Du skal nÄ ha fÞlgende mappehierarki:
```txt
âââ .config
âââ ...
âââ docs
âââ epg.schema.json
âââ src
âââ ...
âââ test
âââ ...
âââ Dotnetskolen.sln
```
#### OpenAPI-kontrakt
NÄ som vi har formatet pÄ innholdet i responsen vÄr, kan vi definere Open API-spesifikasjonen for API-et vÄrt. La oss starte med Ä opprett en ny fil `openapi.json` i `docs`-mappen. Du skal nÄ ha fÞlgende mappehierarki:
```txt
âââ .config
âââ ...
âââ docs
âââ epg.schema.json
âââ openapi.json
âââ src
âââ ...
test
âââ ...
âââ Dotnetskolen.sln
```
La oss begynne med Ä definere litt metadata for kontrakten vÄr.
Lim inn fĂžlgende JSON i `openapi.json`:
```json
{
"openapi": "3.0.0",
"info": {
"title": "Dotnetskolen EPG-API",
"description": "API for Ă„ hente ut EPG for kanalene NRK1 og NRK2 i NRKTV",
"version": "0.0.1"
}
}
```
Her oppgir vi hvilken versjon av OpenAPI vi benytter, og litt metadata om API-et vÄrt. Fortsett med Ä legg til definisjon av hvilke URL-er som er eksponert i API-et vÄrt:
```json
{
"openapi": "3.0.0",
"info": {
"title": "Dotnetskolen EPG-API",
"description": "API for Ă„ hente ut EPG for kanalene NRK1 og NRK2 i NRKTV",
"version": "0.0.1"
},
"paths": {
"/epg/{dato}": {
"get": {
}
}
}
}
```
Her har vi spesifisert at API-et vÄrt eksponerer URL-en `/epg/{dato}` for HTTP `GET`-forespÞrsler. La oss fortsette med Ä spesifisere parameteret `dato`:
```json
{
"openapi": "3.0.0",
"info": {
"title": "Dotnetskolen EPG-API",
"description": "API for Ă„ hente ut EPG for kanalene NRK1 og NRK2 i NRKTV",
"version": "0.0.1"
},
"paths": {
"/epg/{dato}": {
"get": {
"parameters": [
{
"description": "Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.",
"in": "path",
"name": "dato",
"required": true,
"schema": {
"type": "string",
"format": "date"
},
"example": "2021-11-15"
}
]
}
}
}
}
```
Her har vi spesifisert `dato`-parameteret vÄrt, og sagt at:
- Det er pÄkrevd
- At det er en tekststreng som oppfyller formatet `date` i OpenAPI
- `2021-11-15` er et eksempel pÄ en gyldig dato
NĂ„ kan vi legge til hvilke responser endepunktet har: `200 OK` med EPG eller `400 Bad Request` ved ugyldig dato.
```json
{
"openapi": "3.0.0",
"info": {
"title": "Dotnetskolen EPG-API",
"description": "API for Ă„ hente ut EPG for kanalene NRK1 og NRK2 i NRKTV",
"version": "0.0.1"
},
"paths": {
"/epg/{dato}": {
"get": {
"parameters": [
{
"description": "Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.",
"in": "path",
"name": "dato",
"required": true,
"schema": {
"type": "string",
"format": "date"
},
"example": "2021-11-15"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "./epg.schema.json"
}
}
},
"description": "OK"
},
"400": {
"content": {
"text/plain": {
"schema": {
"type": "string",
"example": "\"Ugyldig dato\""
}
}
},
"description": "Bad Request"
}
}
}
}
}
}
```
Til slutt legger vi til en ID for operasjonen, og en tekstlig beskrivelse av den.
```json
{
"openapi": "3.0.0",
"info": {
"title": "Dotnetskolen EPG-API",
"description": "API for Ă„ hente ut EPG for kanalene NRK1 og NRK2 i NRKTV",
"version": "0.0.1"
},
"paths": {
"/epg/{dato}": {
"get": {
"parameters": [
{
"description": "Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.",
"in": "path",
"name": "dato",
"required": true,
"schema": {
"type": "string",
"format": "date"
},
"example": "2021-11-15"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"$ref": "./epg.schema.json"
}
}
},
"description": "OK"
},
"400": {
"content": {
"text/plain": {
"schema": {
"type": "string",
"example": "\"Ugyldig dato\""
}
}
},
"description": "Bad Request"
}
},
"operationId": "hentEpgPÄDato",
"description": "Henter EPG for NRK1 og NRK 2 pÄ den oppgitte datoen. Returnerer 400 dersom dato er ugyldig. Listen med sendinger for en kanal er tom dersom det ikke finnes noen sendinger pÄ den gitte dagen."
}
}
}
}
```
> Kontrakten over er validert ved hjelp av
>
> Merk at i OpenAPI-kontrakten over benytter vi versjon `3.0.0` av OpenAPI. I denne versjonen er det ikke full stÞtte for JSON Schema. Man kan derfor ikke bruke alle features i JSON Schema i OpenAPI-kontrakten. Kontrakten vÄr bruker imidlertid kun features i JSON Schema som er stÞttet. `OpenAPI 3.1.0` ble lansert 16. februar 2021, som _har_ full stÞtte for alle features i JSON Schema. Det vil imidlertid ta noe tid fÞr det er stÞtte for denne i tooling som `ReDoc` (brukt i [steg 11](#steg-11---grafisk-fremstilling-av-openapi-dokumentasjon)) `WebGUI` og linting. Takk til [@laat](https://github.com/laat) som poengterte det.
#### Grafisk fremstilling av Open-API-kontrakten
I [steg 11](#steg-11---grafisk-fremstilling-av-openapi-dokumentasjon) ser vi pÄ hvordan man kan sette opp en grafisk fremstilling av OpenAPI-dokumentasjonen som en egen HTML-side i API-et,. Merk at det forutsetter at du har utfÞrt steg 1-10 fÞrst. Dersom du Þnsker Ä se en grafisk fremstilling nÄ kan du lime inn koden under pÄ .
> Bare trykk "OK" dersom du blir spurt om Ă„ gjĂžre om fra JSON til YAML.
```json
{
"openapi": "3.0.0",
"info": {
"title": "Dotnetskolen EPG-API",
"description": "API for Ă„ hente ut EPG for kanalene NRK1 og NRK2 i NRKTV",
"version": "0.0.1"
},
"paths": {
"/epg/{dato}": {
"get": {
"parameters": [
{
"description": "Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.",
"in": "path",
"name": "dato",
"required": true,
"schema": {
"type": "string",
"format": "date"
},
"example": "2021-11-15"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"nrk1": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Sending"
}
},
"nrk2": {
"type": "array",
"items": {
"$ref": "#/components/schemas/Sending"
}
}
},
"required": [
"nrk1",
"nrk2"
]
}
}
},
"description": "OK"
},
"400": {
"content": {
"text/plain": {
"schema": {
"type": "string",
"example": "\"Ugyldig dato\""
}
}
},
"description": "Bad Request"
}
},
"operationId": "hentEpgPÄDato",
"description": "Henter EPG for NRK1 og NRK 2 pÄ den oppgitte datoen. Returnerer 400 dersom dato er ugyldig. Listen med sendinger for en kanal er tom dersom det ikke finnes noen sendinger pÄ den gitte dagen."
}
}
},
"components": {
"schemas": {
"Tittel": {
"type": "string",
"pattern": "^[\\p{L}0-9\\.,-:!]{5,100}$",
"example": "Dagsrevyen",
"description": "Programtittel"
},
"Sending": {
"type": "object",
"properties": {
"tittel": {
"$ref": "#/components/schemas/Tittel"
},
"starttidspunkt": {
"type": "string",
"format": "date-time",
"description": "Startdato- og tidspunkt for sendingen."
},
"sluttidspunkt": {
"type": "string",
"format": "date-time",
"description": "Sluttdato- og tidspunkt for sendingen. Er alltid stĂžrre enn sendingens startdato- og tidspunkt."
}
},
"required": [
"tittel",
"starttidspunkt",
"sluttidspunkt"
]
}
}
}
}
```
> Merk at ikke stÞtter at JSON Schema og Open-API-kontrakt er definert i to forskjellige filer. Derfor er kontrakten over en sammenslÄing av `epg.schema.json` og `openapi.json`.
### Steg 7 - Implementere kontraktstyper
**Steg 7 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-6---definere-api-kontrakt) [⏠Neste steg](#steg-8---sette-opp-skall-for-api)
I [steg-4](#steg-4---definere-domenemodell) definerte vi domenemodellen vÄr som en F#-type. Domenemodellen representerer EPG-en slik vi konseptuelt tenker pÄ den, bÄde nÄr det gjelder struktur og regler for gyldige tilstander. API-kontrakter er ikke nÞdvendigvis en-til-en med domenemodeller.
1. For det fÞrste kan strukturen til typene i API-et vÊre annerledes enn i domenemodellen. Dette ser vi i vÄrt tilfelle hvor domenemodellen har alle sendinger, pÄ tvers av kanaler, i én liste, mens API-kontrakten har én liste med sendinger per kanal.
2. I tillegg er vi begrenset til Ä representere data med tekst i API-et ettersom HTTP er en tekstbasert protokoll. For eksempel benytter vi en `DateTimeOffset` til Ä representere start- og sluttidspunkt i domenemodellen vÄr, mens vi benytter `string` i OpenAPI-kontrakten vÄr.
For at vi skal kunne oversette domenemodellen til OpenAPI-kontrakten skal vi innfÞre en egen F#-type som reflekterer typene i OpenAPI-kontrakten vÄr. Generelt blir typer som representerer dataene vÄre slik vi kommuniserer med andre systemer pÄ kalt "data transfer objects", eller "DTO".
Start med Ă„ opprett en fil `Dto.fs` i mappen `src/api`:
```txt
âââ .config
âââ ...
âââ docs
âââ ...
src
âââ api
âââ Domain.fs
âââ Dto.fs
âââ NRK.Dotnetskolen.Api.fsproj
âââ Program.fs
test
âââ ...
âââ Dotnetskolen.sln
```
Lim inn innholdet under i `Dto.fs`:
```f#
namespace NRK.Dotnetskolen
module Dto =
type SendingDto = {
Tittel: string
Starttidspunkt: string
Sluttidspunkt: string
}
type EpgDto = {
Nrk1: SendingDto list
Nrk2: SendingDto list
}
```
PÄ samme mÄte som da vi [opprettet domenemodellen](#steg-4---definere-domenemodell), mÄ vi legge til `Dto.fs` i prosjektfilen til API-prosjektet:
```xml
Exe
net9.0
```
### Steg 8 - Sette opp skall for API
**Steg 8 av 9** - [đ GĂ„ til toppen](#-net-skolen) [⏠Forrige steg](#steg-7---implementere-kontraktstyper) [⏠Neste steg](#steg-9---implementere-web-api)
I dette steget skal vi sette opp et skall for web-API-et, og verifisere at vi nÄr API-et ved Ä skrive en integrasjonstest. FÞr vi begynner Ä kode skal vi se pÄ et par relevante konsepter i .NET.
#### Prosjekttyper
Fra og med .NET Core opererer .NET med ulike SDK-prosjekttyper avhengig av hva slags type applikasjon man Þnsker Ä utvikle. Via de ulike prosjekttype fÄr man tilgang til forskjellig funksjonalitet knyttet til kompilering og publisering av prosjektene. Da vi opprettet API- og testprosjektene fikk vi prosjekter med den grunnleggende prosjekttypen `.NET SDK`. Siden vi i dette steget er avhengig av funksjonalitet som finnes i `.NET Web SDK` skal vi endre prosjekttypene til API- og integrasjonstestprosjektene.
Ă
pne filen `src/api/NRK.Dotnetskolen.Api.fsproj`, og endre `Sdk`-attributtet pÄ `Project`-elementet fra `Microsoft.NET.Sdk` til `Microsoft.NET.Sdk.Web`:
```xml
Exe
net9.0
```
Gjenta steget over for `test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj` for Ă„ endre SDK-prosjekttypen til integrasjonstestprosjektet:
```xml
net9.0
false
false
```
> Du kan lese mer om de ulike prosjekttypene i .NET her:
#### Modellen til .NET
FÞr vi setter opp skallet til web-API-et, skal vi se pÄ noen grunnleggende konsepter som er brukt i .NET for Ä lage applikasjoner.
##### Host
NÄr vi utvikler og kjÞrer en applikasjon har vi behov for tilgang til felles ressurser som konfigurasjon, avhengigheter og logging. I tillegg Þnsker vi Ä ha kontroll pÄ hvordan prosessen til applikasjonen vÄr starter og slutter. Microsoft tilbyr et objekt, `IHost`, som holder styr pÄ disse tingene for oss. Typisk bygger man opp og starter et `IHost`-objekt i `Program.fs`. Det skal vi gjÞre nÄ ved Ä kalle en innebygd funksjon i Microsoft sitt bibliotek `Host.CreateDefaultBuilder`.
Ă
pne `Program.fs` i web-API-prosjektet og erstatt innholdet med fĂžlgende:
```f#
open Microsoft.Extensions.Hosting
Host.CreateDefaultBuilder().Build().Run()
```
Her Äpner vi `Microsoft.Extensions.Hosting` for Ä fÄ tilgang til `CreateDefaultBuilder`. `CreateDefaultBuilder` kommer fra biblioteket til Microsoft, og sÞrger for Ä lese konfigurasjon, sette opp grunnleggende logging, og setter filstien til ressursfilene til applikasjonen (ogsÄ kalt "content root").
Til slutt bygger vi hosten vÄr, og starter den slik `Host.CreateDefaultBuilder().Build().Run()`.
###### KjĂžre host
Du kan kjĂžre hosten med fĂžlgende kommando:
```bash
dotnet run --project ./src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Dev\nrkno@github.com\dotnetskolen\src\api
```
ForelÞpig gjÞr ikke hosten vÄr noen ting. Den bare starter, og kjÞrer helt til vi avslutter den ved Ä trykke `Ctrl+C` i terminalen. I outputen over ser vi imidlertid tre logginnslag av typen `info` som er blitt skrevet av hosten. Dette illustrerer at `CreateDefaultBuilder` har satt opp logging til konsoll. Logginnslagene forteller at applikasjonen har startet, at miljÞet er `Production`, og hva filstien til "content root" er.
Trykk `Ctrl+C` for Ă„ stoppe hosten:
```bash
// Trykker `Ctrl+C`
```
```bash
info: Microsoft.Hosting.Lifetime[0]
Application is shutting down...
```
> `Production` er default miljĂž i .NET med mindre annet er spesifisert. Du kan lese mer om miljĂžer i .NET her:
>
> Du kan lese mer om `Host`-konseptet og hva det innebĂŠrer her:
##### Middleware pipeline
Microsoft har laget et rammeverk for web-applikasjoner i .NET, ASP.NET (ASP stÄr for "active server pages"). Web-applikasjoner i ASP.NET er konfigurerbare og modulÊre, og gjennom Ä konfigurere modulene i den har man kontroll pÄ hvordan HTTP-forespÞrsler blir prosessert helt fra de kommer inn til serveren, og til HTTP-responsen blir sendt tilbake til klienten. Modulene i denne sammenhengen kalles mellomvare (eller "middleware" pÄ engelsk), og de henger sammen i en lenket liste hvor HTTP-forespÞrselen blir prosessert suksessivt av mellomvarene i listen. Denne lenkede listen blir omtalt som "middleware pipeline".
> Du kan se en illustrasjon av hvordan mellomvarer henger sammen i ASP.NET her:
Alle mellomvarer har i utgangspunktet anledning til Ä prosessere HTTP-forespÞrselen bÄde fÞr og etter den neste mellomvaren i listen prosesserer den, og kan pÄ den mÄten vÊre med Ä pÄvirke responsen som blir sendt tilbake til klienten. Enhver mellomvare har ansvar for Ä kalle den neste mellomvaren. PÄ denne mÄten kan en mellomvare stoppe videre prosessering av forespÞrselen ogsÄ. Et eksempel pÄ en slik mellomvare er autentisering, hvor man ikke sender forespÞrselen videre i pipelinen dersom den ikke er tilstrekkelig autentisert. Pga. denne kortslutningen ligger autentisering tidlig i listen over mellomvarer.
Hosten vi opprettet i forrige avsnitt er et utgangspunkt for hvilken som helst applikasjon. Det kan bli f.eks. en bakgrunnstjeneste eller en web-applikasjon. Siden vi skal lage et web-API skal vi gÄ videre med Ä tilpasse hosten til Ä bli en web-server. Microsoft har laget en spesiell funksjon akkurat til dette formÄlet: `WebApplication.CreateBuilder`. Denne likner pÄ `Host.CreateDefaultBuilder` som vi brukte i tidligere i avsnittet om [host](#host), bare at hosten den lager er en web-server som har mulighet til Ä konfigurere en "middleware pipeline". For Ä lage en web-applikasjon istedenfor en generisk applikasjon, Äpne `Microsoft.AspNetCore.Builder`, og bytt ut linjen `Host.CreateDefaultBuilder().Build().Run()` med `WebApplication.CreateBuilder().Build().Run()` slik at `Program.fs` i API-prosjektet nÄ ser slik ut:
```f#
open Microsoft.AspNetCore.Builder
WebApplication.CreateBuilder().Build().Run()
```
`WebApplication.CreateBuilder` sÞrger bl.a. for Ä sette opp [Kestrel](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/servers/kestrel?view=aspnetcore-9.0) som web-server for applikasjonen vÄr. I tillegg returnerer den et objekt av typen `WebApplicationBuilder` som vi kan bruke til Ä konfigurere web-applikasjonen etter vÄre behov. Vi kaller umiddelbart pÄ `WebApplicationBuilder` sin funksjon `Build` for Ä bygge web-applikasjonen vÄr. `Build` returnerer et objekt av typen `WebApplication`, og vi kaller til slutt `Run`-metoden pÄ `WebApplication`-objektet for Ä starte web-applikasjonen.
###### KjĂžre web host
Hvis du nÄ kjÞrer hosten igjen, vil du se et nytt logginnslag:
```bash
dotnet run --project ./src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
...
```
Fra logginnslaget over ser vi at hosten vÄr nÄ lytter pÄ HTTP-forespÞrsler pÄ port `5000`. I og med at vi ikke har lagt til noen middlewares i pipelinen vÄr enda, svarer API-et med `404 Not Found` pÄ alle forespÞrsler. Det kan du verifisere ved Ä Äpne i en nettleser.
> Du kan lese mer om middleware i .NET-web-applikasjoner her:
#### Ping
NÄ som vi har blitt kjent med noen grunnleggende konsepter i .NET-applikasjoner, kan vi starte Ä sette sammen vÄrt eget web-API. For Ä gjÞre det trenger vi en middleware pipeline som kan behandle HTTP-forespÞrslene som kommer inn til API-et vÄrt.
I .NET 6 innfÞrte Microsoft "minimal APIs" som er en rekke metoder som gjÞr det enklere Ä komme i gang med Ä definere oppfÞrslen til en host. For web-applikasjoner har Microsoft laget "minimal APIs" som gjÞr det enkelt Ä legge til funksjoner i "middleware pipelinen" til en web-applikasjon som hÄndterer innkommende HTTP-forespÞrsler for en gitt sti. Dette kan vi bruke for Ä lage et "ping"-endepunkt.
Ă
pne `Program.fs` i API-prosjektet, og bytt ut innholdet i filen med koden under:
```f#
open System
open Microsoft.AspNetCore.Builder
let app = WebApplication.CreateBuilder().Build()
app.MapGet("/ping", Func(fun () -> "pong")) |> ignore
app.Run()
```
Her har vi tatt vare pÄ `WebApplication`-objektet, som `WebApplication.CreateBuilder().Build()` returnerer, i en egen variabel `app`. Dette har vi gjort for Ä fÄ tilgang til "minimal API"-metodene Microsoft har definert for `WebApplication`. Videre har vi brukt én av dem, nemlig `MapGet`, som tar inn to argumenter:
1. En tekststreng som spesifiserer hvilken sti i URL-en som leder til denne funksjonen. I dette tilfellet `ping`.
2. En funksjon uten parametere som returnerer en tekststreng. I dette tilfellet `pong`.
> Merk at som andre parameter til `MapGet` har vi oppgitt `Func(fun () -> "pong")` som strengt tatt ikke er en funksjon. `Func` er .NET sin mÄte Ä opprette et `Delegate` pÄ. Delegates er .NET sin mÄte Ä pakke inn funksjonskall som objekter pÄ. Siden "Minimal APIs" er skrevet for Ä fungere for hvilket som helst programmeringssprÄk i .NET, har Microsoft vÊrt nÞdt til Ä velge en modell som passer bÄde for bÄde det objektorienterte programmeringsparadigmet sÄ vel som det funksjonelle programmeringsparadigmet. Dermed tar `MapGet` strengt tatt inn et `Delegate`-objekt som andre parameter, og mÄten man oppretter et `Delegate`-objekt i F# pÄ er ved Ä kalle `Func` sin konstruktÞr. I konstruktÞren til `Func` sender vi inn den anonyme F#-funksjonen `fun () -> "pong"`. `` delen av `Func` definerer hva slags type returverdien til den anonyme funksjonen har. Ettersom den anonyme funksjonen ikke tar inn noen parametere er det ikke spesifisert noe mer i `Func` for det. Dersom den anonyme funksjonen hadde tatt inn et parameter av typen `int`, hadde kallet til `Func` sett slik ut: `Func`. Du kan lese mer om delegates i F# her:
>
> Du kan lese mer om "minimal APIs" her:
##### KjĂžre API-et
Start API-et med fĂžlgende kommando:
```bash
dotnet run --project ./src/api/NRK.Dotnetskolen.Api.fsproj
```
```bash
info: Microsoft.Hosting.Lifetime[0]
Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\Dev\nrkno@github.com\dotnetskolen\src\api
```
Dette starter web-API-et pÄ `http://localhost:5000`. Verifiser at API-et fungerer ved Ä gÄ til i nettleseren din og se at svaret er `pong`.
#### Integrasjonstester
FÞr vi fortsetter med Ä implementere web-API-et skal vi sette opp en integrasjonstest som verifiserer at API-et er oppe og kjÞrer, og at det svarer pÄ HTTP-forespÞrsler. Det skal vi gjÞre ved Ä:
1. KjÞre web-API-et vÄrt pÄ en webserver som kjÞrer i minnet under testen, en sÄkalt `TestServer`.
2. Sende forespĂžrsler mot denne testserveren
3. Verifisere at testserveren svarer med de verdiene vi forventer
Siden vi gir hele web-API-et vÄrt som input til testserveren er responsene vi fÄr tilsvarende de web-API-et svarer med i et deployet miljÞ, og dermed kan vi vÊre trygge pÄ at API-et oppfyller kontrakten vi har definert ogsÄ nÄr det deployes.
> Webserveren vi skal kjĂžre i integrasjonstestene er dokumentert her:
>
> Inspirasjonen til Ä skrive integrasjonstestene pÄ mÄten beskrevet over er fra [et kurs](https://github.com/erikly/FagkveldTesthost/tree/CompleteWithTestHost) som [@erikly](https://github.com/erikly) har arrangert.
>
> En liknende metode er ogsÄ beskrevet i denne artikkelen skrevet av Microsoft: . Artikkelen belager seg imidlertid pÄ konsepter fra objektorientert programmering, og siden dette kurset fokuserer pÄ F# og funksjonell programmering er det valgt Ä skrive integrasjonstestene med en mer funksjonell tilnÊrming.
##### Legge til avhengigheter
For Ä kunne kjÞre integrasjonstestene vÄre er vi avhengig av et par NuGet-pakker og en prosjektreferanse til web-API-et. De fÞlgende avsnittene forklarer hvordan du legger dem til.
###### Microsoft.AspNetCore.Mvc.Testing
For Ä fÄ tilgang til testserverem vi skal kjÞre under integrasjonstestene er vi avhengig av NuGet-pakken `Microsoft.AspNetCore.Mvc.Testing`.
KjĂžr fĂžlgende kommando fra rotenmappen din for Ă„ installere pakken:
```bash
dotnet add ./test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj package Microsoft.AspNetCore.Mvc.Testing
```
###### Referanse til API-prosjektet
For Ä kunne referere til API-et vÄrt fra testprosjektet mÄ vi legge til en referanse til API-prosjektet fra integrasjonstestprosjektet.
GjĂžr dette ved Ă„ kjĂžr fĂžlgende kommando fra rotmappen din:
```bash
dotnet add ./test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj reference ./src/api/NRK.Dotnetskolen.Api.fsproj
```
##### KlargjĂžre API-et for testing
###### WebApplicationBuilder
For Ä kunne lage en testserver som representerer API-et vÄrt nÄr vi kjÞrer testene mÄ vi konfiguerere API-et vÄrt til Ä bruke en testserver, men kun nÄr vi faktisk kjÞrer testene, og ikke nÄr API-et kjÞrer ellers. For Ä fÄ til dette mÄ vi kalle en funksjon pÄ `WebApplicationBuilder`-objektet (som vi oppretter i `main`-funksjonen i `Program.fs` i API-prosjektet) nÄr vi setter opp testserveren i testene.
Husk at `Program.fs` i API-prosjektet nÄ ser slik ut:
```f#
open System
open Microsoft.AspNetCore.Builder
let app = WebApplication.CreateBuilder().Build()
app.MapGet("/ping", Func(fun () -> "pong")) |> ignore
app.Run()
```
For Ä fÄ tak i `WebApplicationBuilder`-objektet som `WebApplication.CreateBuilder` returnerer fra integrasjonstesten, trekker vi ut oppretting av `WebApplicationBuilder`-objektet til en egen funksjon `createWebApplicationBuilder` slik:
```f#
open System
open Microsoft.AspNetCore.Builder
let createWebApplicationBuilder () =
WebApplication.CreateBuilder()
let app = createWebApplicationBuilder().Build()
app.MapGet("/ping", Func(fun () -> "pong")) |> ignore
app.Run()
```
Ved Ä bruke funksjonen `createWebApplicationBuilder` fra integrasjonstestprosjektet kan vi konfiguerere `WebApplicationBuilder`-objektet til Ä bruke testserveren nÄr testene kjÞrer.
###### WebApplication
I tillegg til Ä konfigurere `WebApplicationBuilder`-objektet til Ä bruke en testserver trenger vi Ä fÄ tak i `app`-objektet fra `main`-funksjonen i API-prosjektet for Ä opprette en HTTP-klient som sender HTTP-forespÞrsler til testserveren. For Ä fÄ til dette trekker vi ut koden som oppretter og konfigurerer `WebApplication`-objektet i API-et slik:
```f#
open System
open Microsoft.AspNetCore.Builder
let createWebApplicationBuilder () =
WebApplication.CreateBuilder()
let createWebApplication (builder: WebApplicationBuilder) =
let app = builder.Build()
app.MapGet("/ping", Func(fun () -> "pong")) |> ignore
app
let builder = createWebApplicationBuilder()
let app = createWebApplication builder
app.Run()
```
Ved Ä bruke funksjonen `createWebApplication` fra integrasjonstestprosjektet kan vi hente ut `WebApplication`-objektet som representerer hele web-API-et vÄrt, og sende HTTP-forespÞrsler mot det fra integrasjonstestene vÄre.
###### Namespace og modul
For Ä kunne referere til de to nye funksjonene vi lagde i API-prosjektet, `createWebApplicationBuilder` og `createWebApplication`, fra integrasjonstestprosjektet mÄ vi legge dem i en egen modul, slik:
```f#
namespace NRK.Dotnetskolen.Api
module Program =
open System
open Microsoft.AspNetCore.Builder
let createWebApplicationBuilder () =
WebApplication.CreateBuilder()
let createWebApplication (builder: WebApplicationBuilder) =
let app = builder.Build()
app.MapGet("/ping", Func(fun () -> "pong")) |> ignore
app
let builder = createWebApplicationBuilder()
let app = createWebApplication builder
app.Run()
```
> Merk at vi her ogsÄ la til linjen `namespace NRK.Dotnetskolen.Api` Þverst. Dette setter modulen `Program` i kontekst av `NRK.Dotnetskolen.Api`, og gjÞr at nÄr vi skal referere til funksjonene `createWebApplicationBuilder` og `createWebApplication` mÄ vi Äpne `NRK.Dotnetskolen.Api.Program`.
##### Test for ping
NĂ„ er vi klare til Ă„ kunne sette opp integrasjonstestene. Ă
pne `Tests.fs` i integrasjonstestprosjektet, og erstatt innholdet i filen med koden under:
```f#
module Tests
open System.Net.Http
open System.Threading.Tasks
open Xunit
open Microsoft.AspNetCore.TestHost
open NRK.Dotnetskolen.Api.Program
let runWithTestClient (test: HttpClient -> Task) =
task {
let builder = createWebApplicationBuilder()
builder.WebHost.UseTestServer() |> ignore
use app = createWebApplication builder
do! app.StartAsync()
let testClient = app.GetTestClient()
do! test testClient
}
[]
let ``Get "ping" returns "pong"`` () =
runWithTestClient (fun httpClient ->
task {
let! response = httpClient.GetStringAsync("ping")
Ass