{"id":28951087,"url":"https://github.com/nrkno/dotnetskolen","last_synced_at":"2025-06-23T14:08:07.147Z","repository":{"id":37086134,"uuid":"349064165","full_name":"nrkno/dotnetskolen","owner":"nrkno","description":"Kurs for oppsett av .NET-prosjekter fra bunn","archived":false,"fork":false,"pushed_at":"2025-02-26T16:22:10.000Z","size":1015,"stargazers_count":22,"open_issues_count":3,"forks_count":8,"subscribers_count":25,"default_branch":"main","last_synced_at":"2025-03-05T22:07:47.166Z","etag":null,"topics":["course","dotnet","dotnet-core","dotnetcli","dotnetcore","fsharp","integrationtest","unittest","workshop"],"latest_commit_sha":null,"homepage":"https://nrkno.github.io/dotnetskolen/","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nrkno.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-03-18T12:25:27.000Z","updated_at":"2025-03-03T15:05:07.000Z","dependencies_parsed_at":"2025-02-13T12:37:49.325Z","dependency_job_id":null,"html_url":"https://github.com/nrkno/dotnetskolen","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/nrkno/dotnetskolen","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fdotnetskolen","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fdotnetskolen/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fdotnetskolen/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fdotnetskolen/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nrkno","download_url":"https://codeload.github.com/nrkno/dotnetskolen/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nrkno%2Fdotnetskolen/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":261491816,"owners_count":23166678,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["course","dotnet","dotnet-core","dotnetcli","dotnetcore","fsharp","integrationtest","unittest","workshop"],"created_at":"2025-06-23T14:08:06.232Z","updated_at":"2025-06-23T14:08:07.132Z","avatar_url":"https://github.com/nrkno.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# 🏫 .NET-skolen\n\n## 👋 Innledning\n\nVelkommen til .NET-skolen!\n\nDette 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 å:\n\n- Opprette prosjekter og mappestruktur\n- Legge til NuGet-pakker\n- Sette opp tester\n\nSom 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: \u003chttps://info.nrk.no/presse/tvguide/\u003e\n\n\u003e 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.\n\n### 💻 Fremgangsmåte\n\nVi 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/).\n\nOverordnet kommer mappestrukturen til løsningen vår til å se slik ut:\n\n```txt\n└── docs (kontrakt for web-API-et)\n└── src  (kildekode til web-API-et)\n└── test (kildekode til enhets- og integrasjonstestene)\n```\n\nDet 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.\n\n### 🚀 Kom i gang\n\nFor å 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).\n\n\u003e 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).\n\n\u003e 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).\n\n\u003e Dersom du er helt ny til .NET kan det være nyttig å begynne med å lese:\n\u003e - [Hva .NET er](/docs/hva-er-dotnet.md)\n\u003e - [Hva F# er](/docs/hva-er-fsharp.md)\n\n#### 📍 Alternative startpunkter\n\nDenne 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).\n\nUnder følger noen anbefalinger for alternative startpunkter, avhengig av hvilke temaer du ønsker å lære mer om.\n\n\u003e NB! Dersom du begynner på steg 5 eller senere, må du kjøre `dotnet tool restore` før du fortsetter å følge veiledningen.\n\n##### Oppsett av prosjekter og solution med .NET CLI\n\nDersom du er interessert i å lære mer om hvordan du kan bruke .NET CLI til å opprette prosjekter og solutions, kan følge disse stegene:\n\n- [Steg 1 - Opprette API](#steg-1---opprette-api)\n- [Steg 2 - Opprette testprosjekter](#steg-2---opprette-testprosjekter)\n- [Steg 3 - Opprette solution](#steg-3---opprette-solution)\n\n##### Domenemodellering og enhetstester\n\nVil du lære mer om domenemodellering i F# og tilhørende enhetstester, kan følge disse stegene:\n\n- [Steg 4 - Definere domenemodell](#steg-4---definere-domenemodell)\n- [Steg 5 - Enhetstester for domenemodell](#steg-5---enhetstester-for-domenemodell)\n\n##### API-kontrakter\n\nHvis du vil lære mer om hvordan du kan dokumentere API-et ditt vha. Open API, og modellere kontraktstyper, kan følge disse stegene:\n\n- [Steg 6 - Definere API-kontrakt](#steg-6---definere-api-kontrakt)\n- [Steg 7 - Implementere kontraktstyper](#steg-7---implementere-kontraktstyper)\n\n##### .NET 9 og minimal API\n\nOm 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:\n\n- [Steg 8 - Sette opp skall for API](#steg-8---sette-opp-skall-for-api)\n- [Steg 9 - Implementere web-API](#steg-9---implementere-web-api)\n\n##### Tilleggsoppgaver\n\nTil slutt finnes det noen ekstraoppgaver, hvis du vil ha mer å bryne deg på:\n\n- [Ekstraoppgaver](#ekstraoppgaver)\n  - [Steg 10 - Følge prinsipper i domenedrevet design](#steg-10---følge-prinsipper-i-domenedrevet-design)\n  - [Steg 11 - Grafisk fremstilling av OpenAPI-dokumentasjon](#steg-11---grafisk-fremstilling-av-openapi-dokumentasjon)\n\n### ❓ Spørsmål\n\nLurer du på noe knyttet til kurset? Opprett gjerne en tråd under \"Discussions\" i dette repoet:\n\n- \u003chttps://github.com/nrkno/dotnetskolen/discussions/categories/q-a\u003e\n\n### 💡 Tips og triks\n\nNyttige [tips og triks finner du her](/docs/tips-og-triks.md)\n\n### 🔗 Nyttige lenker\n\n- Microsoft's offisielle dokumentasjon for .NET - \u003chttps://docs.microsoft.com/en-us/dotnet/\u003e\n- F# cheat sheet - \u003chttp://dungpa.github.io/fsharp-cheatsheet/\u003e\n- Innføring i F# - \u003chttps://fsharpforfunandprofit.com/site-contents/#understanding-f\u003e\n- Andre kurs i NRK\n  - F#-skolen, kurs i F# laget av ansatte i NRK TV - \u003chttps://github.com/nrkno/fsharpskolen\u003e\n  - GitHub Actions 101, laget av [@teodor-elstad](https://github.com/teodor-elstad) \u003chttps://github.com/teodor-elstad/github-actions-101\u003e\n\n### 👍👎 Tilbakemeldinger\n\nHar du tilbakemeldinger til kurset? Opprett gjerne en tråd for det her:\n\n- \u003chttps://github.com/nrkno/dotnetskolen/discussions/categories/ideas\u003e\n\n### 👩👨 Medvirkende\n\n- [@thomaswolff](https://github.com/thomaswolff) - Primus motor og forfatter\n- [@heidisu](https://github.com/heidisu) - Idé og kvalitetssikring\n- [@matiasp](https://github.com/matiasp) - Oversettelse og videreutvikling\n- [@teodor-elstad](https://github.com/teodor-elstad) - Oversettelse og videreutvikling\n\n### 🙌 Takk\n\n- Takk til alle som har kommet med konstruktiv kritikk og nyttige tilbakemeldinger til dette kurset.\n\n### 📝 Lisens\n\nAll dokumentasjon (inkludert denne veiledningen) og kildekoden i dette repoet er åpent tilgjengelig under [MIT-lisensen](/LICENCE).\n\n## 📖 Innholdsfortegnelse\n\n- [Steg](#steg)\n  - [Steg 1 - Opprette API](#steg-1---opprette-api)\n  - [Steg 2 - Opprette testprosjekter](#steg-2---opprette-testprosjekter)\n  - [Steg 3 - Opprette solution](#steg-3---opprette-solution)\n  - [Steg 4 - Definere domenemodell](#steg-4---definere-domenemodell)\n  - [Steg 5 - Enhetstester for domenemodell](#steg-5---enhetstester-for-domenemodell)\n  - [Steg 6 - Definere API-kontrakt](#steg-6---definere-api-kontrakt)\n  - [Steg 7 - Implementere kontraktstyper](#steg-7---implementere-kontraktstyper)\n  - [Steg 8 - Sette opp skall for API](#steg-8---sette-opp-skall-for-api)\n  - [Steg 9 - Implementere web-API](#steg-9---implementere-web-api)\n- [Ekstraoppgaver](#ekstraoppgaver)\n  - [Steg 10 - Følge prinsipper i domenedrevet design](#steg-10---følge-prinsipper-i-domenedrevet-design)\n  - [Steg 11 - Grafisk fremstilling av OpenAPI-dokumentasjon](#steg-11---grafisk-fremstilling-av-openapi-dokumentasjon)\n\n## Steg\n\nNå som du har installert alle verktøyene du trenger er du klar til å begynne på selve kurset!\n\n### Steg 1 - Opprette API\n\n**Steg 1 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬇ Neste steg](#steg-2---opprette-testprosjekter)\n\nI dette steget starter vi med en mappe helt uten kode, og bruker .NET CLI til å opprette vårt første prosjekt `NRK.Dotnetskolen.Api`.\n\n#### .NET-versjon\n\nSiden 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:\n\n```json¨\n{\n    \"sdk\": {\n        \"version\": \"9.0.0\",\n        \"rollForward\": \"latestMinor\"\n    }\n}\n```\n\nHer 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.\n\n\u003e Du kan lese mer om `global.json` her: \u003chttps://docs.microsoft.com/en-us/dotnet/core/tools/global-json\u003e\n\n#### .NET-prosjekter\n\nFor å 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\nNår man kompilerer .NET-prosjekter kan man velge mellom to typer output:\n\n- Kjørbar fil (_executable_) - et program som kan kjøres\n- Klassebibliotek (_dynamically linked library_) - en samling med funksjonalitet som kan benyttes av andre programmer\n\n#### Dotnet new\n\nSom 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: \u003chttps://docs.microsoft.com/en-us/dotnet/core/tools/dotnet\u003e\n\n```bash\ndotnet --help\n```\n\n```bash\nUsage: dotnet [runtime-options] [path-to-application] [arguments]\n\nExecute a .NET application.\n\nruntime-options:\n  --additionalprobingpath \u003cpath\u003e   Path containing probing policy and assemblies to probe for.\n  --additional-deps \u003cpath\u003e         Path to additional deps.json file.\n  --depsfile                       Path to \u003capplication\u003e.deps.json file.\n  --fx-version \u003cversion\u003e           Version of the installed Shared Framework to use to run the application.\n  --roll-forward \u003csetting\u003e         Roll forward to framework version  (LatestPatch, Minor, LatestMinor, Major, LatestMajor, Disable).\n  --runtimeconfig                  Path to \u003capplication\u003e.runtimeconfig.json file.\n\npath-to-application:\n  The path to an application .dll file to execute.\n\nUsage: dotnet [sdk-options] [command] [command-options] [arguments]\n\nExecute a .NET SDK command.\n\nsdk-options:\n  -d|--diagnostics  Enable diagnostic output.\n  -h|--help         Show command line help.\n  --info            Display .NET information.\n  --list-runtimes   Display the installed runtimes.\n  --list-sdks       Display the installed SDKs.\n  --version         Display .NET SDK version in use.\n\nSDK commands:\n  add               Add a package or reference to a .NET project.\n  build             Build a .NET project.\n  build-server      Interact with servers started by a build.\n  clean             Clean build outputs of a .NET project.\n  format            Apply style preferences to a project or solution.\n  help              Opens the reference page in a browser for the specified command.\n  list              List packages or references of a .NET project.\n  msbuild           Run Microsoft Build Engine (MSBuild) commands.\n  new               Create a new .NET project or file.\n  nuget             Provides additional NuGet commands.\n  pack              Create a NuGet package.\n  publish           Publish a .NET project for deployment.\n  remove            Remove a package or reference from a .NET project.\n  restore           Restore dependencies specified in a .NET project.\n  run               Build and run a .NET project output.\n  sdk               Manage .NET SDK installation.\n  sln               Modify Visual Studio solution files.\n  store             Store the specified assemblies in the runtime package store.\n  test              Run unit tests using the test runner specified in a .NET project.\n  tool              Install or manage tools that extend the .NET experience.\n  vstest            Run Microsoft Test Engine (VSTest) commands.\n  workload          Manage optional workloads.\n\nAdditional commands from bundled tools:\n  dev-certs         Create and manage development certificates.\n  fsi               Start F# Interactive / execute F# scripts.\n  user-jwts         Manage JSON Web Tokens in development.\n  user-secrets      Manage development user secrets.\n  watch             Start a file watcher that runs a command when files change.\n\nRun 'dotnet [command] --help' for more information on a command.\n```\n\n#### Maler\n\nFor å 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:\n\n```bash\ndotnet new list\n```\n\n```bash\nThese templates matched your input:\n\nTemplate Name                               Short Name                  Language    Tags\n------------------------------------------  --------------------------  ----------  -------------------------------------------------------\n.NET Aspire App Host                        aspire-apphost              [C#]        Common/.NET Aspire/Cloud\n.NET Aspire Empty App                       aspire                      [C#]        Common/.NET Aspire/Cloud/Web/Web API/API/Service\n.NET Aspire Service Defaults                aspire-servicedefaults      [C#]        Common/.NET Aspire/Cloud/Web/Web API/API/Service\n.NET Aspire Starter App                     aspire-starter              [C#]        Common/.NET Aspire/Blazor/Web/Web API/API/Service/Cloud\n.NET Aspire Test Project (MSTest)           aspire-mstest               [C#]        Common/.NET Aspire/Cloud/Web/Web API/API/Service/Test\n.NET Aspire Test Project (NUnit)            aspire-nunit                [C#]        Common/.NET Aspire/Cloud/Web/Web API/API/Service/Test\n.NET Aspire Test Project (xUnit)            aspire-xunit                [C#]        Common/.NET Aspire/Cloud/Web/Web API/API/Service/Test\nAPI Controller                              apicontroller               [C#]        Web/ASP.NET\nASP.NET Core Empty                          web                         [C#],F#     Web/Empty\nASP.NET Core gRPC Service                   grpc                        [C#]        Web/gRPC/API/Service\nASP.NET Core Web API                        webapi                      [C#],F#     Web/WebAPI/Web API/API/Service\nASP.NET Core Web API (native AOT)           webapiaot                   [C#]        Web/Web API/API/Service\nASP.NET Core Web App (Model-View-Contro...  mvc                         [C#],F#     Web/MVC\nASP.NET Core Web App (Razor Pages)          webapp,razor                [C#]        Web/MVC/Razor Pages\nASP.NET Core with Angular                   angular                     [C#]        Web/MVC/SPA\nASP.NET Core with React.js                  react                       [C#]        Web/MVC/SPA\nASP.NET Core with React.js and Redux        reactredux                  [C#]        Web/MVC/SPA\nBlazor Server App                           blazorserver                [C#]        Web/Blazor\nBlazor Server App Empty                     blazorserver-empty          [C#]        Web/Blazor/Empty\nBlazor Web App                              blazor                      [C#]        Web/Blazor/WebAssembly\nBlazor WebAssembly App Empty                blazorwasm-empty            [C#]        Web/Blazor/WebAssembly/PWA/Empty\nBlazor WebAssembly Standalone App           blazorwasm                  [C#]        Web/Blazor/WebAssembly/PWA\nClass Library                               classlib                    [C#],F#,VB  Common/Library\nConsole App                                 console                     [C#],F#,VB  Common/Console\ndotnet gitignore file                       gitignore,.gitignore                    Config\nDotnet local tool manifest file             tool-manifest                           Config\nEditorConfig file                           editorconfig,.editorconfig              Config\nglobal.json file                            globaljson,global.json                  Config\nMSBuild Directory.Build.props file          buildprops                              MSBuild/props\nMSBuild Directory.Build.targets file        buildtargets                            MSBuild/props\nMSBuild Directory.Packages.props file       packagesprops                           MSBuild/packages/props/CPM\nMSTest Playwright Test Project              mstest-playwright           [C#]        Test/MSTest/Playwright/Desktop/Web\nMSTest Test Class                           mstest-class                [C#],F#,VB  Test/MSTest\nMSTest Test Project                         mstest                      [C#],F#,VB  Test/MSTest/Desktop/Web\nMVC Controller                              mvccontroller               [C#]        Web/ASP.NET\nMVC ViewImports                             viewimports                 [C#]        Web/ASP.NET\nMVC ViewStart                               viewstart                   [C#]        Web/ASP.NET\nNuGet Config                                nugetconfig,nuget.config                Config\nNUnit 3 Test Item                           nunit-test                  [C#],F#,VB  Test/NUnit\nNUnit 3 Test Project                        nunit                       [C#],F#,VB  Test/NUnit/Desktop/Web\nNUnit Playwright Test Project               nunit-playwright            [C#]        Test/NUnit/Playwright/Desktop/Web\nProtocol Buffer File                        proto                                   Web/gRPC\nRazor Class Library                         razorclasslib               [C#]        Web/Razor/Library/Razor Class Library\nRazor Component                             razorcomponent              [C#]        Web/ASP.NET\nRazor Page                                  page                        [C#]        Web/ASP.NET\nRazor View                                  view                        [C#]        Web/ASP.NET\nSolution File                               sln,solution                            Solution\nWeb Config                                  webconfig                               Config\nWindows Forms App                           winforms                    [C#],VB     Common/WinForms\nWindows Forms Class Library                 winformslib                 [C#],VB     Common/WinForms\nWindows Forms Control Library               winformscontrollib          [C#],VB     Common/WinForms\nWorker Service                              worker                      [C#],F#     Common/Worker/Web\nWPF Application                             wpf                         [C#],VB     Common/WPF\nWPF Class Library                           wpflib                      [C#],VB     Common/WPF\nWPF Custom Control Library                  wpfcustomcontrollib         [C#],VB     Common/WPF\nWPF User Control Library                    wpfusercontrollib           [C#],VB     Common/WPF\nxUnit Test Project                          xunit                       [C#],F#,VB  Test/xUnit/Desktop/Web\n```\n\nI 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\n\n```bash\ndotnet new --help\n```\n\n```bash\nDescription:\n  Template Instantiation Commands for .NET CLI.\n\nUsage:\n  dotnet new [\u003ctemplate-short-name\u003e [\u003ctemplate-args\u003e...]] [options]\n  dotnet new [command] [options]\n\nArguments:\n  \u003ctemplate-short-name\u003e  A short name of the template to create.\n  \u003ctemplate-args\u003e        Template specific options to use.\n\nOptions:\n  -o, --output \u003coutput\u003e    Location to place the generated output.\n  -n, --name \u003cname\u003e        The name for the output being created. If no name is specified, the name of the output directory is used.\n  --dry-run                Displays a summary of what would happen if the given command line were run if it would result in a template\n                           creation.\n  --force                  Forces content to be generated even if it would change existing files.\n  --no-update-check        Disables checking for the template package updates when instantiating a template.\n  --project \u003cproject\u003e      The project that should be used for context evaluation.\n  -v, --verbosity \u003cLEVEL\u003e  Sets the verbosity level. Allowed values are q[uiet], m[inimal], n[ormal], and diag[nostic]. [default: normal]\n  -d, --diagnostics        Enables diagnostic output.\n  -?, -h, --help           Show command line help.\n\nCommands:\n  create \u003ctemplate-short-name\u003e \u003ctemplate-args\u003e  Instantiates a template with given short name. An alias of 'dotnet new \u003ctemplate name\u003e'.\n  install \u003cpackage\u003e                             Installs a template package.\n  uninstall \u003cpackage\u003e                           Uninstalls a template package.\n  update                                        Checks the currently installed template packages for update, and install the updates.\n  search \u003ctemplate-name\u003e                        Searches for the templates on NuGet.org.\n  list \u003ctemplate-name\u003e                          Lists templates containing the specified template name. If no name is specified, lists all\n                                                templates.\n  details \u003cpackage-identifier\u003e                  Provides the details for specified template package.\n                                                      The command checks if the package is installed locally, if it was not found, it\n                                                searches the configured NuGet feeds.\n```\n\n#### Opprette API-prosjektet\n\nSom 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.\n\nKjør følgende kommando for å opprette API-prosjektet\n\n```bash\ndotnet new console --language F# --output src/api --name NRK.Dotnetskolen.Api\n```\n\n```bash\nThe template \"Console App\" was created successfully.\n\nProcessing post-creation actions...\nRunning 'dotnet restore' on src\\api\\NRK.Dotnetskolen.Api.fsproj...\n  Determining projects to restore...\n  Restored C:\\Dev\\nrkno@github.com\\dotnetskolen\\src\\api\\NRK.Dotnetskolen.Api.fsproj (in 101 ms).\nRestore succeeded.\n```\n\nI 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.\n\n\u003e Merk at istedenfor `--language`, `--output` og `--name`, kunne vi brukt forkortelsene `-lang`, `-o` og `-n`.\n\nDu skal nå ha en mappestruktur som ser slik ut\n\n```txt\nsrc\n└── api\n    └── NRK.Dotnetskolen.Api.fsproj\n    └── Program.fs\n```\n\nSom vi ser av diagrammet over opprettet .NET CLI mappene `src` og `src/api`, med `NRK.Dotnetskolen.Api.fsproj` og `Program.fs` i `src/api`.\n\n\u003e 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.\n\n##### Prosjektfil\n\nÅpne `NRK.Dotnetskolen.Api.fsproj` for å se innholdet til prosjektfilen til prosjektet du nettopp opprettet:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cOutputType\u003eExe\u003c/OutputType\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\nHer ser vi at prosjektet:\n\n- Har outputtypen `exe` - prosjektet kompileres til å bli en kjørbar fil\n- Skal kompileres til .NET 9\n- Består av én fil `Program.fs`\n\n##### Programfilen\n\nFor å se hva programmet gjør kan vi åpne `Program.fs` og se på koden:\n\n```f#\n// For more information see https://aka.ms/fsharp-console-apps\nprintfn \"Hello from F#\"\n```\n\nMalen 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: \u003chttps://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/entry-point#implicit-entry-point\u003e\n\u003e 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: \u003chttps://docs.microsoft.com/en-us/dotnet/standard/design-guidelines/names-of-assemblies-and-dlls\u003e\n\u003e\n\u003e 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.\n\n#### Kjøre API-prosjektet\n\nFor å kjøre prosjektet som ble opprettet over kan du kjøre følgende kommando\n\n```bash\ndotnet run --project src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\nHello world from F#\n```\n\nAlternativt kan du gå til mappen hvor prosjektet ligger, og kjøre `dotnet run` derfra, slik som vist under\n\n```bash\ncd src/api\ndotnet run\n```\n\n```bash\nHello world from F#\n```\n\n#### Lagre endringer i Git (valgfritt)\n\nNå 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.\n\n##### Se endringer\n\nGitt 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:\n\n```bash\ngit status\n```\n\n```bash\nOn branch \u003cbranchnavn\u003e\nUntracked files:\n  (use \"git add \u003cfile\u003e...\" to include in what will be committed)\n        global.json\n        src/\n\nnothing added to commit but untracked files present (use \"git add\" to track)\n```\n\nI 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\").\n\n##### Legg til endringer i Git\n\nFor å få Git til å overvåke filene vi har opprettet, og deretter se status i Git kan du kjøre følgende kommandoer:\n\n```bash\ngit add .\ngit status\n```\n\n```bash\nChanges to be committed:\n  (use \"git restore --staged \u003cfile\u003e...\" to unstage)\n        new file:   global.json\n        new file:   src/api/NRK.Dotnetskolen.Api.fsproj\n        new file:   src/api/Program.fs\n```\n\nNå overvåker Git filene.\n\n##### Lagre endringene\n\nFor å lagre nåværende tilstand av filene i en \"commit\" i Git kan du kjøre følgende kommando:\n\n```bash\ngit commit -m \"Opprettet API-prosjekt\"\n```\n\n```bash\n[\u003cbranchnavn\u003e 00d11c8] Opprettet API-prosjekt\n 2 files changed, 25 insertions(+)\n create mode 100644 src/api/NRK.Dotnetskolen.Api.fsproj\n create mode 100644 src/api/Program.fs\n```\n\n##### Se alle historiske endringer i repoet\n\nFor å se alle commits i nåværende branch i Git, kan du kjøre følgende kommando:\n\n```bash\ngit log\n```\n\n```bash\ncommit 00d11c82d0179f41883a55ce88e147a73ae60ee2 (HEAD -\u003e \u003cbranchnavn\u003e)\nAuthor: Thomas Wolff \u003cthomas.wolff@nrk.no\u003e\nDate:   Fri Apr 16 13:43:40 2021 +0200\n\n    Opprettet API-prosjekt\n...\n```\n\n\u003e 💡 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.\n\n#### Se løsningsforslag\n\nDersom 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:\n\n```bash\ngit checkout steg-1\n```\n\n```bash\nSwitched to branch 'steg-1'\n```\n\n### Steg 2 - Opprette testprosjekter\n\n**Steg 2 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬆ Forrige steg](#steg-1---opprette-api) [⬇ Neste steg](#steg-3---opprette-solution)\n\nTester 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:\n\n- Enhetstester\n- Integrasjonstester\n\nEnhetstester 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.\n\nIntegrasjonstester 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).\n\n#### Dotnet new\n\nI dette steget skal vi opprette to testprosjekter\n\n- Ett for enhetstester - `NRK.Dotnetskolen.UnitTests`\n- Ett for integrasjonstester - `NRK.Dotnetskolen.IntegrationTests`\n\nFor å 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:\n\n- xUnit\n- nUnit\n- MSTest\n\nI 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: \u003chttps://docs.microsoft.com/en-us/dotnet/core/testing/#testing-tools\u003e\n\n#### Opprette enhetstestprosjekt\n\nKjør følgende kommando for å opprette enhetstestprosjektet\n\n```bash\ndotnet new xunit -lang F# -o test/unit -n NRK.Dotnetskolen.UnitTests\n```\n\n```bash\nThe template \"xUnit Test Project\" was created successfully.\n\nProcessing post-creation actions...\nRunning 'dotnet restore' on test/unit/NRK.Dotnetskolen.UnitTests.fsproj...\n  Determining projects to restore...\n  Restored C:\\Dev\\nrkno@github.com\\dotnetskolen\\test\\unit\\NRK.Dotnetskolen.UnitTests.fsproj (in 1.31 sec).\nRestore succeeded.\n```\n\nDu skal nå ha følgende mappestruktur\n\n```txt\nsrc\n└── api\n    └── NRK.Dotnetskolen.Api.fsproj\n    └── Program.fs\ntest\n└── unit\n    └── NRK.Dotnetskolen.UnitTests.fsproj\n    └── Program.fs\n    └── Tests.fs\n```\n\n##### Prosjektfil\n\nÅpne filen `NRK.Dotnetskolen.UnitTests.fsproj`:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n    \u003cIsPackable\u003efalse\u003c/IsPackable\u003e\n    \u003cGenerateProgramFile\u003efalse\u003c/GenerateProgramFile\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Tests.fs\" /\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cPackageReference Include=\"coverlet.collector\" Version=\"6.0.2\" /\u003e\n    \u003cPackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.12.0\" /\u003e\n    \u003cPackageReference Include=\"xunit\" Version=\"2.9.2\" /\u003e\n    \u003cPackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\nI prosjektfilen kan vi se at enhetstestprosjektet:\n\n- Skal kompileres til .NET 9\n- Inneholder to kildekodefiler\n  - `Tests.fs`\n  - `Program.fs`\n- Har referanser til fire NuGet-pakker\n  - `coverlet.collector` - bibliotek for å få code coverage statistikk for prosjekter \u003chttps://docs.microsoft.com/en-us/dotnet/core/testing/unit-testing-code-coverage?tabs=windows\u003e\n  - `Microsoft.NET.Test.Sdk` - Pakke for å bygge .NET testprosjekter\n  - `xunit` - Bibliotek for å skrive enhetstester\n  - `xunit.runner.visualstudio` - Pakke for å kjøre Xunit-tester i \"Test explorer\" i Visual Studio \u003chttps://docs.microsoft.com/en-us/visualstudio/test/run-unit-tests-with-test-explorer?view=vs-2019\u003e\n\n##### Testfilen\n\nÅpne filen `Tests.fs`:\n\n```f#\nmodule Tests\n\nopen System\nopen Xunit\n\n[\u003cFact\u003e]\nlet ``My test`` () =\n    Assert.True(true)\n\n```\n\nØ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 `[\u003cFact\u003e]`. Xunit opererer med to annotasjoner for tester:\n\n- `[\u003cFact\u003e]`\n- `[\u003cTheory\u003e]`\n\nForskjellen på disse blir nærmere forklart i [steget om enhetstester](#steg-5---enhetstester-for-domenemodell).\n\n\u003e Merk at ``` ``\u003cvariabelnavn med mellomrom\u003e`` ``` 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.\n\n##### Kjøre enhetstestprosjektet\n\nFor å kjøre testen i enhetstestprosjektet kan du bruke følgende kommando\n\n```bash\ndotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\nRestore complete (0,3s)\n  NRK.Dotnetskolen.UnitTests succeeded (2,0s) → test\\unit\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.UnitTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.51]   Discovering: NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.54]   Discovered:  NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.54]   Starting:    NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.69]   Finished:    NRK.Dotnetskolen.UnitTests\n  NRK.Dotnetskolen.UnitTests test succeeded (1,8s)\n\nTest summary: total: 1; failed: 0; succeeded: 1; skipped: 0; duration: 1,8s\nBuild succeeded in 4,5s\n```\n\nPå lik linje med `dotnet run`, kan du alternativt gå inn i mappen til enhetstestprosjektet, og kjøre `dotnet test` derfra:\n\n```bash\ncd test/unit\ndotnet test\n```\n\n```bash\nRestore complete (0,3s)\n  NRK.Dotnetskolen.UnitTests succeeded (0,4s) → bin\\Debug\\net9.0\\NRK.Dotnetskolen.UnitTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.50]   Discovering: NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.52]   Discovered:  NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.53]   Starting:    NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.68]   Finished:    NRK.Dotnetskolen.UnitTests\n  NRK.Dotnetskolen.UnitTests test succeeded (1,7s)\n\nTest summary: total: 1; failed: 0; succeeded: 1; skipped: 0; duration: 1,7s\nBuild succeeded in 2,9s\n```\n\n#### Opprette integrasjonstestprosjekt\n\nFor å 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:\n\n```bash\ndotnet new xunit -lang F# -o test/integration -n NRK.Dotnetskolen.IntegrationTests\n```\n\n```bash\nThe template \"xUnit Test Project\" was created successfully.\n\nProcessing post-creation actions...\nRunning 'dotnet restore' on test\\integration\\NRK.Dotnetskolen.IntegrationTests.fsproj...\n  Determining projects to restore...\n  Restored C:\\Dev\\nrkno@github.com\\dotnetskolen\\test\\integration\\NRK.Dotnetskolen.IntegrationTests.fsproj (in 580 ms).\nRestore succeeded.\n```\n\nDu skal nå ha følgende mappestruktur\n\n```txt\nsrc\n└── api\n    └── NRK.Dotnetskolen.Api.fsproj\n    └── Program.fs\ntest\n└── unit\n    └── NRK.Dotnetskolen.UnitTests.fsproj\n    └── Program.fs\n    └── Tests.fs\n└── integration\n    └── NRK.Dotnetskolen.IntegrationTests.fsproj\n    └── Program.fs\n    └── Tests.fs\n```\n\nForelø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).\n\n##### Kjøre integrasjonstester\n\nFor å kjøre testene i integrasjonstestprosjektet kan du bruke følgende kommando\n\n```bash\ndotnet test test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj\n```\n\n```bash\nRestore complete (0,3s)\n  NRK.Dotnetskolen.IntegrationTests succeeded (2,0s) → test\\integration\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.IntegrationTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.54]   Discovering: NRK.Dotnetskolen.IntegrationTests\n[xUnit.net 00:00:00.56]   Discovered:  NRK.Dotnetskolen.IntegrationTests\n[xUnit.net 00:00:00.57]   Starting:    NRK.Dotnetskolen.IntegrationTests\n[xUnit.net 00:00:00.72]   Finished:    NRK.Dotnetskolen.IntegrationTests\n  NRK.Dotnetskolen.IntegrationTests test succeeded (1,8s)\n\nTest summary: total: 1; failed: 0; succeeded: 1; skipped: 0; duration: 1,8s\nBuild succeeded in 4,6s\n```\n\n### Steg 3 - Opprette solution\n\n**Steg 3 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬆ Forrige steg](#steg-2---opprette-testprosjekter) [⬇ Neste steg](#steg-4---definere-domenemodell)\n\nSlik 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.\n\n#### Dotnet sln\n\nFor å opprette en solution med `dotnet` kan du kjøre følgende kommando:\n\n```bash\ndotnet new sln -n Dotnetskolen\n```\n\n```bash\nThe template \"Solution File\" was created successfully.\n```\n\nDu skal nå ha fått filen `Dotnetskolen.sln` slik som vist under\n\n```txt\nsrc\n└── api\n    └── NRK.Dotnetskolen.Api.fsproj\n    └── Program.fs\ntest\n└── unit\n    └── NRK.Dotnetskolen.UnitTests.fsproj\n    └── Program.fs\n    └── Tests.fs\n└── integration\n    └── NRK.Dotnetskolen.IntegrationTests.fsproj\n    └── Program.fs\n    └── Tests.fs\n└── Dotnetskolen.sln\n```\n\nHvis vi ser på innholdet i `Dotnetskolen.sln` ser vi at det ikke er noen referanser til prosjektene våre enda\n\n```txt\n\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.0.31903.59\nMinimumVisualStudioVersion = 10.0.40219.1\nGlobal\n        GlobalSection(SolutionConfigurationPlatforms) = preSolution\n                Debug|Any CPU = Debug|Any CPU\n                Release|Any CPU = Release|Any CPU\n        EndGlobalSection\n        GlobalSection(SolutionProperties) = preSolution\n                HideSolutionNode = FALSE\n        EndGlobalSection\nEndGlobal\n\n```\n\n#### Legge til prosjekter i solution\n\nFor å legge til referanser til prosjektene du har opprettet kan du kjøre følgende kommandoer\n\n##### Legge til API-prosjekt\n\n```bash\ndotnet sln add src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\nProject `src\\api\\NRK.Dotnetskolen.Api.fsproj` added to the solution.\n```\n\n##### Legge til enhetstestprosjekt\n\n```bash\ndotnet sln add test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\nProject `test\\unit\\NRK.Dotnetskolen.UnitTests.fsproj` added to the solution.\n```\n\n##### Legge til integrasjonstestprosjekt\n\n```bash\ndotnet sln add test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj\n```\n\n```bash\nProject `test\\integration\\NRK.Dotnetskolen.IntegrationTests.fsproj` added to the solution.\n```\n\nNå ser vi at `Dotnetskolen.sln` inneholder referanser til prosjektene våre\n\n```txt\n\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 17\nVisualStudioVersion = 17.0.31903.59\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"src\", \"src\", \"{602F7DA2-73CF-4DA2-82E5-D392DE47E0BC}\"\nEndProject\nProject(\"{F2A71F9B-5D33-465A-A702-920D77279786}\") = \"NRK.Dotnetskolen.Api\", \"src\\api\\NRK.Dotnetskolen.Api.fsproj\", \"{618BF895-AEA1-4086-8904-89DD317B2429}\"\nEndProject\nProject(\"{2150E333-8FDC-42A3-9474-1A3956D46DE8}\") = \"test\", \"test\", \"{10963520-731D-442B-B808-DA74BDD9207D}\"\nEndProject\nProject(\"{F2A71F9B-5D33-465A-A702-920D77279786}\") = \"NRK.Dotnetskolen.UnitTests\", \"test\\unit\\NRK.Dotnetskolen.UnitTests.fsproj\", \"{95B87F0E-15B8-4646-98F0-E8DAACA5526D}\"\nEndProject\nProject(\"{F2A71F9B-5D33-465A-A702-920D77279786}\") = \"NRK.Dotnetskolen.IntegrationTests\", \"test\\integration\\NRK.Dotnetskolen.IntegrationTests.fsproj\", \"{391F46FA-9684-460E-B6A2-99EF7363693F}\"\nEndProject\nGlobal\n        GlobalSection(SolutionConfigurationPlatforms) = preSolution\n                Debug|Any CPU = Debug|Any CPU\n                Release|Any CPU = Release|Any CPU\n        EndGlobalSection\n        GlobalSection(SolutionProperties) = preSolution\n                HideSolutionNode = FALSE\n        EndGlobalSection\n        GlobalSection(ProjectConfigurationPlatforms) = postSolution\n                {618BF895-AEA1-4086-8904-89DD317B2429}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n                {618BF895-AEA1-4086-8904-89DD317B2429}.Debug|Any CPU.Build.0 = Debug|Any CPU\n                {618BF895-AEA1-4086-8904-89DD317B2429}.Release|Any CPU.ActiveCfg = Release|Any CPU\n                {618BF895-AEA1-4086-8904-89DD317B2429}.Release|Any CPU.Build.0 = Release|Any CPU\n                {95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n                {95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Debug|Any CPU.Build.0 = Debug|Any CPU\n                {95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Release|Any CPU.ActiveCfg = Release|Any CPU\n                {95B87F0E-15B8-4646-98F0-E8DAACA5526D}.Release|Any CPU.Build.0 = Release|Any CPU\n                {391F46FA-9684-460E-B6A2-99EF7363693F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n                {391F46FA-9684-460E-B6A2-99EF7363693F}.Debug|Any CPU.Build.0 = Debug|Any CPU\n                {391F46FA-9684-460E-B6A2-99EF7363693F}.Release|Any CPU.ActiveCfg = Release|Any CPU\n                {391F46FA-9684-460E-B6A2-99EF7363693F}.Release|Any CPU.Build.0 = Release|Any CPU\n        EndGlobalSection\n        GlobalSection(NestedProjects) = preSolution\n                {618BF895-AEA1-4086-8904-89DD317B2429} = {602F7DA2-73CF-4DA2-82E5-D392DE47E0BC}\n                {95B87F0E-15B8-4646-98F0-E8DAACA5526D} = {10963520-731D-442B-B808-DA74BDD9207D}\n                {391F46FA-9684-460E-B6A2-99EF7363693F} = {10963520-731D-442B-B808-DA74BDD9207D}\n        EndGlobalSection\nEndGlobal\n\n```\n\n#### Solution i Visual Studio\n\nBildet under viser hvordan \"Solution explorer\" i Visual Studio viser løsningen.\n\n![Solution explorer i Visual Studio](./docs/illustrasjoner/solution-explorer.png)\n\n### Steg 4 - Definere domenemodell\n\n**Steg 4 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬆ Forrige steg](#steg-3---opprette-solution) [⬇ Neste steg](#steg-5---enhetstester-for-domenemodell)\n\nVi 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: \u003chttps://info.nrk.no/presse/tvguide/\u003e\n\n\u003e 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.\n\nEn EPG kan sees på som en liste med sendinger, og for vårt eksempel i dette kurset inneholder en sending følgende felter:\n\n- 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: `, . : - !`\n- Kanal - Kanalen sendingen går på. I vårt tilfelle begrenses mulige kanaler til NRK1 og NRK2, og må skrives med store bokstaver.\n- Startdato- og tidspunkt - dato og tidspunkt for når sendingen starter.\n- Sluttdato- og tidspunkt - dato og tidspunkt for når sendingen slutter. Må være etter startdato- og tidspunkt.\n\n#### Domenemodell i F#\n\nNå som vi har spesifisert domenet vårt, kan vi modellere det i F#. Start med å opprett en ny fil `Domain.fs` under `src/api`:\n\n```txt\n└── .config\n    └── ...\nsrc\n└── api\n    └── Domain.fs\n    └── NRK.Dotnetskolen.Api.fsproj\n    └── Program.fs\ntest\n└── ...\n└── Dotnetskolen.sln\n```\n\nLim inn innholdet under i `Domain.fs`:\n\n```f#\nnamespace NRK.Dotnetskolen\n\nmodule Domain =\n\n    open System\n\n    type Sending = {\n        Tittel: string\n        Kanal: string\n        Starttidspunkt: DateTimeOffset\n        Sluttidspunkt: DateTimeOffset\n    }\n\n    type Epg = Sending list\n```\n\nOver definerer vi en F#-modul `Domain` i namespacet `NRK.Dotnetskolen`. I `Domain`-modulen definerer vi domenemodellen vår, som består av to typer:\n\n- `Sending` - modellerer et enkelt innslag i EPG-en, og inneholder feltene som ble definert i forrige seksjon\n  - Tittel\n  - Kanal\n  - Starttidspunkt\n  - Sluttidspunkt\n- `Epg` - en liste med sendinger\n\nVi åpnet også modulen `System` for å få tilgang til typen `DateTimeOffset`.\n\n\u003e Legg merke til innrykket på linjene etter `module Domain =`. Dette inntrykket er påkrevd av F# for at koden skal kompilere riktig.\n\nInkluder `Domain.fs` i api-prosjektet ved å legge til `\u003cCompile Include=\"Domain.fs\" /\u003e` i `src/api/NRK.Dotnetskolen.Api.fsproj` slik som vist under:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cOutputType\u003eExe\u003c/OutputType\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Domain.fs\" /\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\n\u003e 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.\n\u003e\n\u003e Moduler i F# blir kompilert til det samme i CIL som statiske klasser i C#.\n\n#### Opprette en EPG\n\nNå 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:\n\n```f#\nopen System\nopen NRK.Dotnetskolen.Domain\n\nlet epg = [\n    {\n        Tittel = \"Dagsrevyen\"\n        Kanal = \"NRK1\"\n        Starttidspunkt = DateTimeOffset.Parse(\"2021-04-16T19:00:00+02:00\")\n        Sluttidspunkt = DateTimeOffset.Parse(\"2021-04-16T19:30:00+02:00\")\n    }\n]\nprintfn \"%A\" epg\n```\n\nHer oppretter vi en variabel `epg` som er en liste med sendinger, slik vi definerte i `Domain.fs`.\n\nKjør API-prosjektet igjen med følgende kommando, og se at `epg`-verdien blir skrevet til terminalen.\n\n```bash\ndotnet run --project src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\n[{ Tittel = \"Dagsrevyen\"\n   Kanal = \"NRK1\"\n   Starttidspunkt = 16.04.2021 19:00:00 +02:00\n   Sluttidspunkt = 16.04.2021 19:30:00 +02:00 }]\n```\n\n\u003e 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.\n\n### Steg 5 - Enhetstester for domenemodell\n\n**Steg 5 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬆ Forrige steg](#steg-4---definere-domenemodell) [⬇ Neste steg](#steg-6---definere-api-kontrakt)\n\nDomenemodellen 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.\n\n#### Regler i domenet vårt\n\nVi ønsker å verifisere følgende regler fra domenet vårt:\n\n- Tittel\n  - Må bestå av 5-100 tegn (inklusiv)\n  - Kan kun bestå av store og små bokstaver, tall, og følgende spesialtegn: `, . : - !`\n- Kanal\n  - `NRK1` eller `NRK2`.\n  - Kun store bokstaver er lov.\n- Sendetidspunkt\n  - Sluttidspunkt skal være etter starttidspunkt\n\n#### Tittel\n\nLa oss begynne med å verifisere at vi implementerer valideringsreglene for tittel riktig.\n\n##### Enhetstester\n\nEttersom 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.\n\n```f#\nmodule Tests\n\nopen Xunit\n\n[\u003cTheory\u003e]\n[\u003cInlineData(\"abc12\")\u003e]\n[\u003cInlineData(\".,-:!\")\u003e]\n[\u003cInlineData(\"ABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJABCDEFGHIJ\")\u003e]\nlet ``isTittelValid valid tittel returns true`` (tittel: string) =\n    let isTittelValid = isTittelValid tittel\n\n    Assert.True isTittelValid\n\n[\u003cTheory\u003e]\n[\u003cInlineData(\"abcd\")\u003e]\n[\u003cInlineData(\"@$%\u0026/\")\u003e]\n[\u003cInlineData(\"abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghija\")\u003e]\nlet ``isTittelValid invalid tittel returns false`` (tittel: string) =\n    let isTittelValid = isTittelValid tittel\n\n    Assert.False isTittelValid\n```\n\nHer 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 `[\u003cFact\u003e]` eller `[\u003cTheory\u003e]`. Testfunksjoner annotert med `[\u003cFact\u003e]` vil kjøre én gang uten noen inputparametere, mens i testfunksjoner annotert med `[\u003cTheory\u003e]` kan man ta inn parametere, og annotere testfunksjonen med `[\u003cInlineData\u003e]` for å sende inn gitte inputparametere. Da vil testfunksjonen bli kjørt én gang _per_ annotering med `[\u003cInlineData\u003e]`.\n\nHvis 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.\n\n```bash\ndotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\ndotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj     [11:56:17]\nRestore complete (0,4s)\n  NRK.Dotnetskolen.UnitTests failed with 2 error(s) (2,0s)\n    C:\\Dev\\github.com\\nrkno\\dotnetskolen\\test\\unit\\Tests.fs(9,25): error FS0039: The value or constructor 'isTittelValid' is not defined.\n    C:\\Dev\\github.com\\nrkno\\dotnetskolen\\test\\unit\\Tests.fs(18,25): error FS0039: The value or constructor 'isTittelValid' is not defined.\n\nBuild failed with 2 error(s) in 3,0s\n```\n\n##### Implementere isTittelValid\n\nFor å 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`:\n\n```f#\nopen System.Text.RegularExpressions\n```\n\nLim deretter inn følgende kode på slutten av filen:\n\n```f#\n    let isTittelValid (tittel: string) : bool =\n        let tittelRegex = Regex(@\"^[\\p{L}0-9\\.,-:!]{5,100}$\")\n        tittelRegex.IsMatch(tittel)\n```\n\nDet regulære uttrykket lister opp hvilke tegn som er gyldige i en gruppe (tegnene mellom mellom `[` og `]`):\n\n- `\\p{L}` - syntaks for å spesifisere enhver bokstav i Unicode\n- `0-9` - tall\n- `\\.,-:!` - spesialtegnene vi tillater\n\nI tillegg spesifiserer `{5,100}` at vi tillater 5-100 av tegnene i gruppen over.\n\n##### Legge til prosjektreferanse\n\nFor 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:\n\n```bash\ndotnet add ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj reference ./src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\nReference `..\\..\\src\\api\\NRK.Dotnetskolen.Api.fsproj` added to the project.\n```\n\nDu kan se effekten av kommandoen over ved å åpne `test/unit/NRK.Dotnetskolen.UnitTests.fsproj`:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n    \u003cIsPackable\u003efalse\u003c/IsPackable\u003e\n    \u003cGenerateProgramFile\u003efalse\u003c/GenerateProgramFile\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Tests.fs\" /\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cPackageReference Include=\"coverlet.collector\" Version=\"6.0.2\" /\u003e\n    \u003cPackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.12.0\" /\u003e\n    \u003cPackageReference Include=\"xunit\" Version=\"2.9.2\" /\u003e\n    \u003cPackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\" /\u003e\n  \u003c/ItemGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cProjectReference Include=\"..\\..\\src\\api\\NRK.Dotnetskolen.Api.fsproj\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n\n```\n\n##### Åpne modul\n\nI 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`:\n\n```f#\nmodule Tests\n\nopen Xunit\nopen NRK.Dotnetskolen.Domain\n```\n\nNå skal testene kjøre vellykket:\n\n```bash\ndotnet test test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\nRestore complete (1,1s)\n  NRK.Dotnetskolen.Api succeeded (2,9s) → src\\api\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.Api.dll\n  NRK.Dotnetskolen.UnitTests succeeded (2,4s) → test\\unit\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.UnitTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.76]   Discovering: NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.83]   Discovered:  NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.83]   Starting:    NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:01.19]   Finished:    NRK.Dotnetskolen.UnitTests\n  NRK.Dotnetskolen.UnitTests test succeeded (3,2s)\n\nTest summary: total: 6; failed: 0; succeeded: 6; skipped: 0; duration: 3,1s\nBuild succeeded in 10,2s\n```\n\n\u003e 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 `[\u003cInlineData\u003e]`.\n\n#### Kanal\n\nReglene for kanal er ganske enkle ettersom det kun er to gyldige kanaler, og disse kun kan skrives med store bokstaver.\n\n##### Enhetstester\n\nFor å 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:\n\n```f#\n[\u003cTheory\u003e]\n[\u003cInlineData(\"NRK1\")\u003e]\n[\u003cInlineData(\"NRK2\")\u003e]\nlet ``isKanalValid valid kanal returns true`` (kanal: string) =\n    let isKanalValid = isKanalValid kanal\n\n    Assert.True isKanalValid\n\n[\u003cTheory\u003e]\n[\u003cInlineData(\"nrk1\")\u003e]\n[\u003cInlineData(\"NRK3\")\u003e]\nlet ``isKanalValid invalid kanal returns false`` (kanal: string) =\n    let isKanalValid = isKanalValid kanal\n\n    Assert.False isKanalValid\n```\n\n##### Implementasjon av isKanalValid\n\nFør vi kjører testene igjen, definerer vi skallet for `isKanalValid` i `Domain.fs`:\n\n```f#\n    let isKanalValid (kanal: string) : bool =\n    // Implementasjon her\n```\n\n☑️ Implementér `isKanalValid` slik at enhetstestene passerer.\n\n```bash\ndotnet test ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\nRestore complete (0,4s)\n  NRK.Dotnetskolen.Api succeeded (2,2s) → src\\api\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.Api.dll\n  NRK.Dotnetskolen.UnitTests succeeded (2,3s) → test\\unit\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.UnitTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.08]   Discovering: NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.12]   Discovered:  NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.12]   Starting:    NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.26]   Finished:    NRK.Dotnetskolen.UnitTests\n  NRK.Dotnetskolen.UnitTests test succeeded (1,2s)\n\nTest summary: total: 10; failed: 0; succeeded: 10; skipped: 0; duration: 1,1s\nBuild succeeded in 6,7s\n```\n\n#### Sendetidspunkter\n\nDet siste vi skal validere i domenet vårt er at sluttidspunkt er etter starttidspunkt.\n\n##### Enhetstester\n\nUnder følger én enhetstest for validering av sendetidspunkter i `Tests.fs`:\n\n```f#\n[\u003cFact\u003e]\nlet ``areStartAndSluttidspunktValid start before end returns true`` () =\n    let starttidspunkt = DateTimeOffset.Now\n    let sluttidspunkt = starttidspunkt.AddMinutes 30.\n\n    let areStartAndSluttidspunktValid = areStartAndSluttidspunktValid starttidspunkt sluttidspunkt\n\n    Assert.True areStartAndSluttidspunktValid\n```\n\nMerk at du også må legge til følgende `open`-statement i `Tests.fs` for at `DateTimeOffset.Now` fra kodesnutten over skal fungere:\n\n```f#\nopen System\n```\n\n☑️ Legg til flere enhetstester du mener er nødvendig for å verifisere at validering av start- og sluttidspunkt er korrekt.\n\n\u003e Merk at her bruker vi `[\u003cFact\u003e]`-attributtet istedenfor `[\u003cTheory\u003e]`. `[\u003cInlineData\u003e]`-attributtet som man bruker med `[\u003cTheory\u003e]`-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 `[\u003cFact\u003e]`-attributtet.\n\n##### Implementasjon av areStartAndSluttidspunktValid\n\nFunksjonen for å validere sendetidspunktene må undersøke om sluttidspunktet er større enn starttidspunktet. Lim inn skallet til `areStartAndSluttidspunktValid` i `Domain.fs`:\n\n```f#\n    let areStartAndSluttidspunktValid (starttidspunkt: DateTimeOffset) (sluttidspunkt: DateTimeOffset) =\n    // Implementasjon her\n```\n\n☑️ Implementér `areStartAndSluttidspunktValid` og få enhetstestene til å passere.\n\n```bash\ndotnet test ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\nRestore complete (0,4s)\n  NRK.Dotnetskolen.Api succeeded (2,2s) → src\\api\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.Api.dll\n  NRK.Dotnetskolen.UnitTests succeeded (2,2s) → test\\unit\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.UnitTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.08]   Discovering: NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.13]   Discovered:  NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.13]   Starting:    NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.24]   Finished:    NRK.Dotnetskolen.UnitTests\n  NRK.Dotnetskolen.UnitTests test succeeded (1,1s)\n\nTest summary: total: 13; failed: 0; succeeded: 13; skipped: 0; duration: 1,1s\nBuild succeeded in 6,6s\n```\n\n#### Validere en sending\n\nNå som vi har funksjoner for å validere de ulike feltene i en sending, kan vi lage en funksjon som validerer en hel sending.\n\n##### Enhetstester\n\nSiden vi har skrevet enhetstester for valideringsfunksjonene til de ulike delene av en sending, kan enhetstestene for validering av hele sendingen være ganske enkle.\n\n☑️ 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`\n\n##### Implementasjon av isSendingValid\n\nLegg til følgende skall for `isSendingValid` i `Domain.fs`:\n\n```f#\n    let isSendingValid (sending: Sending) : bool =\n    // Implementasjon her\n```\n\n☑️ Implementér `isSendingValid`, og få enhetstestene til å passere:\n\n```bash\ndotnet test ./test/unit/NRK.Dotnetskolen.UnitTests.fsproj\n```\n\n```bash\nRestore complete (0,5s)\n  NRK.Dotnetskolen.Api succeeded (2,3s) → src\\api\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.Api.dll\n  NRK.Dotnetskolen.UnitTests succeeded (2,6s) → test\\unit\\bin\\Debug\\net9.0\\NRK.Dotnetskolen.UnitTests.dll\n[xUnit.net 00:00:00.00] xUnit.net VSTest Adapter v2.8.2+699d445a1a (64-bit .NET 9.0.1)\n[xUnit.net 00:00:00.09]   Discovering: NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.14]   Discovered:  NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.14]   Starting:    NRK.Dotnetskolen.UnitTests\n[xUnit.net 00:00:00.27]   Finished:    NRK.Dotnetskolen.UnitTests\n  NRK.Dotnetskolen.UnitTests test succeeded (1,2s)\n\nTest summary: total: 15; failed: 0; succeeded: 15; skipped: 0; duration: 1,2s\nBuild succeeded in 7,2s\n```\n\n\u003e 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.\n\n### Steg 6 - Definere API-kontrakt\n\n**Steg 6 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬆ Forrige steg](#steg-5---enhetstester-for-domenemodell) [⬇ Neste steg](#steg-7---implementere-kontraktstyper)\n\nFor å 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 (\u003chttps://www.openapis.org/\u003e).\n\n#### Operasjoner\n\nFor å begrense omfanget av API-et vårt skal vi ha kun én operasjon i det:\n\n- Hent EPG på en gitt dato\n\n#### Responser\n\nResponsen til denne operasjonen vil bestå av to lister med sendinger, én for hver kanal i domenet vårt, hvor hver sending har:\n\n- Tittel - tekststreng som følger reglene definert i [domenemodellen vår](#steg-4---definere-domenemodell).\n- Startdato- og tidspunkt - tekststreng som følger datoformatet i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6).\n- 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.\n\n#### JSON Schema\n\nFø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.\n\n```json\n{\n    \"$schema\": \"https://json-schema.org/draft/2020-12/schema\",\n    \"type\": \"object\",\n    \"properties\": {\n        \"nrk1\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"#/components/schemas/Sending\"\n            }\n        },\n        \"nrk2\": {\n            \"type\": \"array\",\n            \"items\": {\n                \"$ref\": \"#/components/schemas/Sending\"\n            }\n        }\n    },\n    \"required\": [\n        \"nrk1\",\n        \"nrk2\"\n    ],\n    \"components\": {\n        \"schemas\": {\n            \"Tittel\": {\n                \"type\": \"string\",\n                \"pattern\": \"^[\\\\p{L}0-9\\\\.,-:!]{5,100}$\",\n                \"example\": \"Dagsrevyen\",\n                \"description\": \"Programtittel\"\n            },\n            \"Sending\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"tittel\": {\n                        \"$ref\": \"#/components/schemas/Tittel\"\n                    },\n                    \"starttidspunkt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\",\n                        \"description\": \"Startdato- og tidspunkt for sendingen.\"\n                    },\n                    \"sluttidspunkt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\",\n                        \"description\": \"Sluttdato- og tidspunkt for sendingen. Er alltid større enn sendingens startdato- og tidspunkt.\"\n                    }\n                },\n                \"required\": [\n                    \"tittel\",\n                    \"starttidspunkt\",\n                    \"sluttidspunkt\"\n                ]\n            }\n        }\n    }\n}\n```\n\nHer 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).\n\nForelø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:\n\n```txt\n└── .config\n    └── ...\n└── docs\n    └── epg.schema.json\n└── src\n    └── ...\n└── test\n    └── ...\n└── Dotnetskolen.sln\n```\n\n#### OpenAPI-kontrakt\n\nNå 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:\n\n```txt\n└── .config\n    └── ...\n└── docs\n    └── epg.schema.json\n    └── openapi.json\n└── src\n    └── ...\ntest\n    └── ...\n└── Dotnetskolen.sln\n```\n\nLa oss begynne med å definere litt metadata for kontrakten vår.\n\nLim inn følgende JSON i `openapi.json`:\n\n```json\n{\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Dotnetskolen EPG-API\",\n        \"description\": \"API for å hente ut EPG for kanalene NRK1 og NRK2 i NRKTV\",\n        \"version\": \"0.0.1\"\n    }\n}\n```\n\nHer 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:\n\n```json\n{\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Dotnetskolen EPG-API\",\n        \"description\": \"API for å hente ut EPG for kanalene NRK1 og NRK2 i NRKTV\",\n        \"version\": \"0.0.1\"\n    },\n    \"paths\": {\n        \"/epg/{dato}\": {\n            \"get\": {\n            }\n        }\n    }\n}\n```\n\nHer 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`:\n\n```json\n{\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Dotnetskolen EPG-API\",\n        \"description\": \"API for å hente ut EPG for kanalene NRK1 og NRK2 i NRKTV\",\n        \"version\": \"0.0.1\"\n    },\n    \"paths\": {\n        \"/epg/{dato}\": {\n            \"get\": {\n                \"parameters\": [\n                    {\n                        \"description\": \"Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.\",\n                        \"in\": \"path\",\n                        \"name\": \"dato\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"type\": \"string\",\n                            \"format\": \"date\"\n                        },\n                        \"example\": \"2021-11-15\"\n                    }\n                ]\n            }\n        }\n    }\n}\n```\n\nHer har vi spesifisert `dato`-parameteret vårt, og sagt at:\n\n- Det er påkrevd\n- At det er en tekststreng som oppfyller formatet `date` i OpenAPI\n- `2021-11-15` er et eksempel på en gyldig dato\n\nNå kan vi legge til hvilke responser endepunktet har: `200 OK` med EPG eller `400 Bad Request` ved ugyldig dato.\n\n```json\n{\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Dotnetskolen EPG-API\",\n        \"description\": \"API for å hente ut EPG for kanalene NRK1 og NRK2 i NRKTV\",\n        \"version\": \"0.0.1\"\n    },\n    \"paths\": {\n        \"/epg/{dato}\": {\n            \"get\": {\n                \"parameters\": [\n                    {\n                        \"description\": \"Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.\",\n                        \"in\": \"path\",\n                        \"name\": \"dato\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"type\": \"string\",\n                            \"format\": \"date\"\n                        },\n                        \"example\": \"2021-11-15\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"content\": {\n                            \"application/json\": {\n                                \"schema\": {\n                                    \"$ref\": \"./epg.schema.json\"\n                                }\n                            }\n                        },\n                        \"description\": \"OK\"\n                    },\n                    \"400\": {\n                        \"content\": {\n                            \"text/plain\": {\n                                \"schema\": {\n                                    \"type\": \"string\",\n                                    \"example\": \"\\\"Ugyldig dato\\\"\"\n                                }\n                            }\n                        },\n                        \"description\": \"Bad Request\"\n                    }\n                }\n            }\n        }\n    }\n}\n```\n\nTil slutt legger vi til en ID for operasjonen, og en tekstlig beskrivelse av den.\n\n```json\n{\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Dotnetskolen EPG-API\",\n        \"description\": \"API for å hente ut EPG for kanalene NRK1 og NRK2 i NRKTV\",\n        \"version\": \"0.0.1\"\n    },\n    \"paths\": {\n        \"/epg/{dato}\": {\n            \"get\": {\n                \"parameters\": [\n                    {\n                        \"description\": \"Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.\",\n                        \"in\": \"path\",\n                        \"name\": \"dato\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"type\": \"string\",\n                            \"format\": \"date\"\n                        },\n                        \"example\": \"2021-11-15\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"content\": {\n                            \"application/json\": {\n                                \"schema\": {\n                                    \"$ref\": \"./epg.schema.json\"\n                                }\n                            }\n                        },\n                        \"description\": \"OK\"\n                    },\n                    \"400\": {\n                        \"content\": {\n                            \"text/plain\": {\n                                \"schema\": {\n                                    \"type\": \"string\",\n                                    \"example\": \"\\\"Ugyldig dato\\\"\"\n                                }\n                            }\n                        },\n                        \"description\": \"Bad Request\"\n                    }\n                },\n                \"operationId\": \"hentEpgPåDato\",\n                \"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.\"\n            }\n        }\n    }\n}\n```\n\n\u003e Kontrakten over er validert ved hjelp av \u003chttps://editor.swagger.io/\u003e\n\u003e\n\u003e 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.\n\n#### Grafisk fremstilling av Open-API-kontrakten\n\nI [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å \u003chttps://editor.swagger.io/\u003e.\n\n\u003e Bare trykk \"OK\" dersom du blir spurt om å gjøre om fra JSON til YAML.\n\n```json\n{\n    \"openapi\": \"3.0.0\",\n    \"info\": {\n        \"title\": \"Dotnetskolen EPG-API\",\n        \"description\": \"API for å hente ut EPG for kanalene NRK1 og NRK2 i NRKTV\",\n        \"version\": \"0.0.1\"\n    },\n    \"paths\": {\n        \"/epg/{dato}\": {\n            \"get\": {\n                \"parameters\": [\n                    {\n                        \"description\": \"Dato slik den er definert i [RFC 3339](https://tools.ietf.org/html/rfc3339#section-5.6). Eksempel: 2021-11-15.\",\n                        \"in\": \"path\",\n                        \"name\": \"dato\",\n                        \"required\": true,\n                        \"schema\": {\n                            \"type\": \"string\",\n                            \"format\": \"date\"\n                        },\n                        \"example\": \"2021-11-15\"\n                    }\n                ],\n                \"responses\": {\n                    \"200\": {\n                        \"content\": {\n                            \"application/json\": {\n                                \"schema\": {\n                                    \"type\": \"object\",\n                                    \"properties\": {\n                                        \"nrk1\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/components/schemas/Sending\"\n                                            }\n                                        },\n                                        \"nrk2\": {\n                                            \"type\": \"array\",\n                                            \"items\": {\n                                                \"$ref\": \"#/components/schemas/Sending\"\n                                            }\n                                        }\n                                    },\n                                    \"required\": [\n                                        \"nrk1\",\n                                        \"nrk2\"\n                                    ]\n                                }\n                            }\n                        },\n                        \"description\": \"OK\"\n                    },\n                    \"400\": {\n                        \"content\": {\n                            \"text/plain\": {\n                                \"schema\": {\n                                    \"type\": \"string\",\n                                    \"example\": \"\\\"Ugyldig dato\\\"\"\n                                }\n                            }\n                        },\n                        \"description\": \"Bad Request\"\n                    }\n                },\n                \"operationId\": \"hentEpgPåDato\",\n                \"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.\"\n            }\n        }\n    },\n    \"components\": {\n        \"schemas\": {\n            \"Tittel\": {\n                \"type\": \"string\",\n                \"pattern\": \"^[\\\\p{L}0-9\\\\.,-:!]{5,100}$\",\n                \"example\": \"Dagsrevyen\",\n                \"description\": \"Programtittel\"\n            },\n            \"Sending\": {\n                \"type\": \"object\",\n                \"properties\": {\n                    \"tittel\": {\n                        \"$ref\": \"#/components/schemas/Tittel\"\n                    },\n                    \"starttidspunkt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\",\n                        \"description\": \"Startdato- og tidspunkt for sendingen.\"\n                    },\n                    \"sluttidspunkt\": {\n                        \"type\": \"string\",\n                        \"format\": \"date-time\",\n                        \"description\": \"Sluttdato- og tidspunkt for sendingen. Er alltid større enn sendingens startdato- og tidspunkt.\"\n                    }\n                },\n                \"required\": [\n                    \"tittel\",\n                    \"starttidspunkt\",\n                    \"sluttidspunkt\"\n                ]\n            }\n        }\n    }\n}\n```\n\n\u003e Merk at \u003chttps://editor.swagger.io/\u003e 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`.\n\n### Steg 7 - Implementere kontraktstyper\n\n**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)\n\nI [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.\n\n1. 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.\n2. 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.\n\nFor 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\".\n\nStart med å opprett en fil `Dto.fs` i mappen `src/api`:\n\n```txt\n└── .config\n    └── ...\n└── docs\n    └── ...\nsrc\n└── api\n    └── Domain.fs\n    └── Dto.fs\n    └── NRK.Dotnetskolen.Api.fsproj\n    └── Program.fs\ntest\n└── ...\n└── Dotnetskolen.sln\n```\n\nLim inn innholdet under i `Dto.fs`:\n\n```f#\nnamespace NRK.Dotnetskolen\n\nmodule Dto =\n\n    type SendingDto = {\n        Tittel: string\n        Starttidspunkt: string\n        Sluttidspunkt: string\n    }\n\n    type EpgDto = {\n        Nrk1: SendingDto list\n        Nrk2: SendingDto list\n    }\n```\n\nPå samme måte som da vi [opprettet domenemodellen](#steg-4---definere-domenemodell), må vi legge til `Dto.fs` i prosjektfilen til API-prosjektet:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cOutputType\u003eExe\u003c/OutputType\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Domain.fs\" /\u003e\n    \u003cCompile Include=\"Dto.fs\" /\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\n### Steg 8 - Sette opp skall for API\n\n**Steg 8 av 9** - [🔝 Gå til toppen](#-net-skolen) [⬆ Forrige steg](#steg-7---implementere-kontraktstyper) [⬇ Neste steg](#steg-9---implementere-web-api)\n\nI 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.\n\n#### Prosjekttyper\n\nFra 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.\n\nÅpne filen `src/api/NRK.Dotnetskolen.Api.fsproj`, og endre `Sdk`-attributtet på `Project`-elementet fra `Microsoft.NET.Sdk` til `Microsoft.NET.Sdk.Web`:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk.Web\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cOutputType\u003eExe\u003c/OutputType\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Domain.fs\" /\u003e\n    \u003cCompile Include=\"Dto.fs\" /\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\nGjenta steget over for `test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj` for å endre SDK-prosjekttypen til integrasjonstestprosjektet:\n\n```xml\n\u003cProject Sdk=\"Microsoft.NET.Sdk.Web\"\u003e\n\n  \u003cPropertyGroup\u003e\n    \u003cTargetFramework\u003enet9.0\u003c/TargetFramework\u003e\n    \u003cIsPackable\u003efalse\u003c/IsPackable\u003e\n    \u003cGenerateProgramFile\u003efalse\u003c/GenerateProgramFile\u003e\n  \u003c/PropertyGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cCompile Include=\"Tests.fs\" /\u003e\n    \u003cCompile Include=\"Program.fs\" /\u003e\n  \u003c/ItemGroup\u003e\n\n  \u003cItemGroup\u003e\n    \u003cPackageReference Include=\"coverlet.collector\" Version=\"6.0.2\" /\u003e\n    \u003cPackageReference Include=\"Microsoft.NET.Test.Sdk\" Version=\"17.12.0\" /\u003e\n    \u003cPackageReference Include=\"xunit\" Version=\"2.9.2\" /\u003e\n    \u003cPackageReference Include=\"xunit.runner.visualstudio\" Version=\"2.8.2\" /\u003e\n  \u003c/ItemGroup\u003e\n\n\u003c/Project\u003e\n```\n\n\u003e Du kan lese mer om de ulike prosjekttypene i .NET her: \u003chttps://docs.microsoft.com/en-us/dotnet/core/project-sdk/overview\u003e\n\n#### Modellen til .NET\n\nFør vi setter opp skallet til web-API-et, skal vi se på noen grunnleggende konsepter som er brukt i .NET for å lage applikasjoner.\n\n##### Host\n\nNå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`.\n\nÅpne `Program.fs` i web-API-prosjektet og erstatt innholdet med følgende:\n\n```f#\nopen Microsoft.Extensions.Hosting\n\nHost.CreateDefaultBuilder().Build().Run()\n```\n\nHer å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\").\n\nTil slutt bygger vi hosten vår, og starter den slik `Host.CreateDefaultBuilder().Build().Run()`.\n\n###### Kjøre host\n\nDu kan kjøre hosten med følgende kommando:\n\n```bash\ndotnet run --project ./src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\ninfo: Microsoft.Hosting.Lifetime[0]\n      Application started. Press Ctrl+C to shut down.\ninfo: Microsoft.Hosting.Lifetime[0]\n      Hosting environment: Production\ninfo: Microsoft.Hosting.Lifetime[0]\n      Content root path: C:\\Dev\\nrkno@github.com\\dotnetskolen\\src\\api\n```\n\nForelø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.\n\nTrykk `Ctrl+C` for å stoppe hosten:\n\n```bash\n// Trykker `Ctrl+C`\n```\n\n```bash\ninfo: Microsoft.Hosting.Lifetime[0]\n      Application is shutting down...\n```\n\n\u003e `Production` er default miljø i .NET med mindre annet er spesifisert. Du kan lese mer om miljøer i .NET her: \u003chttps://docs.microsoft.com/en-us/aspnet/core/fundamentals/environments?view=aspnetcore-9.0\u003e\n\u003e\n\u003e Du kan lese mer om `Host`-konseptet og hva det innebærer her: \u003chttps://docs.microsoft.com/en-us/aspnet/core/fundamentals/host/generic-host?view=aspnetcore-9.0\u003e\n\n##### Middleware pipeline\n\nMicrosoft 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\".\n\n\u003e Du kan se en illustrasjon av hvordan mellomvarer henger sammen i ASP.NET her: \u003chttps://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0#create-a-middleware-pipeline-with-webapplication\u003e\n\nAlle 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.\n\nHosten 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:\n\n```f#\nopen Microsoft.AspNetCore.Builder\n\nWebApplication.CreateBuilder().Build().Run()\n```\n\n`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.\n\n###### Kjøre web host\n\nHvis du nå kjører hosten igjen, vil du se et nytt logginnslag:\n\n```bash\ndotnet run --project ./src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\ninfo: Microsoft.Hosting.Lifetime[0]\n      Now listening on: http://localhost:5000\n...\n```\n\nFra 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 \u003chttp://localhost:5000/\u003e i en nettleser.\n\n\u003e Du kan lese mer om middleware i .NET-web-applikasjoner her: \u003chttps://docs.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-9.0\u003e\n\n#### Ping\n\nNå 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.\n\nI .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.\n\nÅpne `Program.fs` i API-prosjektet, og bytt ut innholdet i filen med koden under:\n\n```f#\nopen System\nopen Microsoft.AspNetCore.Builder\n\nlet app = WebApplication.CreateBuilder().Build()\napp.MapGet(\"/ping\", Func\u003cstring\u003e(fun () -\u003e \"pong\")) |\u003e ignore\napp.Run()\n```\n\nHer 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:\n\n1. En tekststreng som spesifiserer hvilken sti i URL-en som leder til denne funksjonen. I dette tilfellet `ping`.\n2. En funksjon uten parametere som returnerer en tekststreng. I dette tilfellet `pong`.\n\n\u003e Merk at som andre parameter til `MapGet` har vi oppgitt `Func\u003cstring\u003e(fun () -\u003e \"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 () -\u003e \"pong\"`. `\u003cstring\u003e` delen av `Func\u003cstring\u003e` 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\u003cstring\u003e` for det. Dersom den anonyme funksjonen hadde tatt inn et parameter av typen `int`, hadde kallet til `Func` sett slik ut: `Func\u003cint, string\u003e`. Du kan lese mer om delegates i F# her: \u003chttps://docs.microsoft.com/en-us/dotnet/fsharp/language-reference/delegates\u003e\n\u003e\n\u003e Du kan lese mer om \"minimal APIs\" her: \u003chttps://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-9.0\u003e\n\n##### Kjøre API-et\n\nStart API-et med følgende kommando:\n\n```bash\ndotnet run --project ./src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n```bash\ninfo: Microsoft.Hosting.Lifetime[0]\n      Now listening on: https://localhost:5001\ninfo: Microsoft.Hosting.Lifetime[0]\n      Now listening on: http://localhost:5000\ninfo: Microsoft.Hosting.Lifetime[0]\n      Application started. Press Ctrl+C to shut down.\ninfo: Microsoft.Hosting.Lifetime[0]\n      Hosting environment: Development\ninfo: Microsoft.Hosting.Lifetime[0]\n      Content root path: C:\\Dev\\nrkno@github.com\\dotnetskolen\\src\\api\n```\n\nDette starter web-API-et på `http://localhost:5000`. Verifiser at API-et fungerer ved å gå til \u003chttp://localhost:5000/ping\u003e i nettleseren din og se at svaret er `pong`.\n\n#### Integrasjonstester\n\nFø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 å:\n\n1. Kjøre web-API-et vårt på en webserver som kjører i minnet under testen, en såkalt `TestServer`.\n2. Sende forespørsler mot denne testserveren\n3. Verifisere at testserveren svarer med de verdiene vi forventer\n\nSiden 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.\n\n\u003e Webserveren vi skal kjøre i integrasjonstestene er dokumentert her: \u003chttps://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.testhost.testserver?view=aspnetcore-9.0\u003e\n\u003e\n\u003e 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.\n\u003e\n\u003e En liknende metode er også beskrevet i denne artikkelen skrevet av Microsoft: \u003chttps://docs.microsoft.com/en-us/aspnet/core/test/integration-tests?view=aspnetcore-9.0\u003e. 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.\n\n##### Legge til avhengigheter\n\nFor å 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.\n\n###### Microsoft.AspNetCore.Mvc.Testing\n\nFor å få tilgang til testserverem vi skal kjøre under integrasjonstestene er vi avhengig av NuGet-pakken `Microsoft.AspNetCore.Mvc.Testing`.\n\nKjør følgende kommando fra rotenmappen din for å installere pakken:\n\n```bash\ndotnet add ./test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj package Microsoft.AspNetCore.Mvc.Testing\n```\n\n###### Referanse til API-prosjektet\n\nFor å kunne referere til API-et vårt fra testprosjektet må vi legge til en referanse til API-prosjektet fra integrasjonstestprosjektet.\n\nGjør dette ved å kjør følgende kommando fra rotmappen din:\n\n```bash\ndotnet add ./test/integration/NRK.Dotnetskolen.IntegrationTests.fsproj reference ./src/api/NRK.Dotnetskolen.Api.fsproj\n```\n\n##### Klargjøre API-et for testing\n\n###### WebApplicationBuilder\n\nFor å 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.\n\nHusk at `Program.fs` i API-prosjektet nå ser slik ut:\n\n```f#\nopen System\nopen Microsoft.AspNetCore.Builder\n\nlet app = WebApplication.CreateBuilder().Build()\napp.MapGet(\"/ping\", Func\u003cstring\u003e(fun () -\u003e \"pong\")) |\u003e ignore\napp.Run()\n```\n\nFor å få tak i `WebApplicationBuilder`-objektet som `WebApplication.CreateBuilder` returnerer fra integrasjonstesten, trekker vi ut oppretting av `WebApplicationBuilder`-objektet til en egen funksjon `createWebApplicationBuilder` slik:\n\n```f#\nopen System\nopen Microsoft.AspNetCore.Builder\n\nlet createWebApplicationBuilder () =\n    WebApplication.CreateBuilder()\n\nlet app = createWebApplicationBuilder().Build()\napp.MapGet(\"/ping\", Func\u003cstring\u003e(fun () -\u003e \"pong\")) |\u003e ignore\napp.Run()\n```\n\nVed å bruke funksjonen `createWebApplicationBuilder` fra integrasjonstestprosjektet kan vi konfiguerere `WebApplicationBuilder`-objektet til å bruke testserveren når testene kjører.\n\n###### WebApplication\n\nI 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:\n\n```f#\nopen System\nopen Microsoft.AspNetCore.Builder\n\nlet createWebApplicationBuilder () =\n    WebApplication.CreateBuilder()\n\nlet createWebApplication (builder: WebApplicationBuilder) =\n    let app = builder.Build()\n    app.MapGet(\"/ping\", Func\u003cstring\u003e(fun () -\u003e \"pong\")) |\u003e ignore\n    app\n\nlet builder = createWebApplicationBuilder()\nlet app = createWebApplication builder\napp.Run()\n```\n\nVed å 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.\n\n###### Namespace og modul\n\nFor å 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:\n\n```f#\nnamespace NRK.Dotnetskolen.Api\n\nmodule Program =\n\n    open System\n    open Microsoft.AspNetCore.Builder\n\n    let createWebApplicationBuilder () =\n        WebApplication.CreateBuilder()\n\n    let createWebApplication (builder: WebApplicationBuilder) =\n        let app = builder.Build()\n        app.MapGet(\"/ping\", Func\u003cstring\u003e(fun () -\u003e \"pong\")) |\u003e ignore\n        app\n\n    let builder = createWebApplicationBuilder()\n    let app = createWebApplication builder\n    app.Run()\n```\n\n\u003e 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`.\n\n##### Test for ping\n\nNå er vi klare til å kunne sette opp integrasjonstestene. Åpne `Tests.fs` i integrasjonstestprosjektet, og erstatt innholdet i filen med koden under:\n\n```f#\nmodule Tests\n\nopen System.Net.Http\nopen System.Threading.Tasks\nopen Xunit\nopen Microsoft.AspNetCore.TestHost\nopen NRK.Dotnetskolen.Api.Program\n\nlet runWithTestClient (test: HttpClient -\u003e Task\u003cunit\u003e) =\n    task {\n        let builder = createWebApplicationBuilder()\n        builder.WebHost.UseTestServer() |\u003e ignore\n\n        use app = createWebApplication builder\n        do! app.StartAsync()\n\n        let testClient = app.GetTestClient()\n        do! test testClient\n    }\n\n[\u003cFact\u003e]\nlet ``Get \"ping\" returns \"pong\"`` () =\n    runWithTestClient (fun httpClient -\u003e\n        task {\n            let! response = httpClient.GetStringAsync(\"ping\")\n            Ass","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrkno%2Fdotnetskolen","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnrkno%2Fdotnetskolen","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnrkno%2Fdotnetskolen/lists"}