Stateless app (innsynstjeneste)
Slik konfigurerer du en stateless app
Introduksjon til stateless apper
En stateless app, eller tilstandsløs app, skiller seg fra vanlige apper ved at den ikke lagrer noe data - hverken skjemadata eller metadata om instanser av appen. Appen havner heller ikke i innboksen til brukeren. En stateless app tilsvarer en innsynstjeneste i Altinn 2.
Stateless apper passer godt som innsynstjenester der en bruker eller et system gjør et oppslag mot en ressurs, eller presenterer data fra en tredjepart basert på identiteten til brukeren. Du kan også konfigurere en stateless app til å tillate anonyme brukere, det vil si brukere som ikke er pålogget.
Konfigurasjon
Du kan styre hvordan appen oppfører seg ved oppstart, og konfigurere den som en stateless app. Dette gjør du i filen applicationmetadata.json.
Eksempel på konfigurasjon:
App/config/applicationmetadata.json{
"id": "ttd/stateless-app-demo",
"org": "ttd",
"title": {
"nb": "Stateless App Demo"
},
"dataTypes": [
{
"id": "ref-data-as-pdf",
"allowedContentTypes": [
"application/pdf"
],
"maxCount": 0,
"minCount": 0
},
{
"id": "Stateless-model",
"allowedContentTypes": [
"application/xml"
],
"appLogic": {
"autoCreate": true,
"classRef": "Altinn.App.Models.StatelessV1"
},
"maxCount": 1,
"minCount": 1
}
],
...
"onEntry": { "show": "stateless" } // legg til denne linjen
}
I feltet onEntry.show oppgir du navnet på mappen under App/ui som skal vises når appen starter.
Opprett mappen App/ui/stateless, og legg til Settings.json. Sett defaultDataType til datamodellen som stateless-visningen skal bruke:
App/ui/stateless/Settings.json{
"$schema": "https://altinncdn.no/schemas/json/layout/layoutSettings.schema.v1.json",
"defaultDataType": "Stateless-model",
"pages": {
"order": ["{page}"]
}
}
Eksempel app-struktur for en app som er satt opp på denne måten:
├───App
├───config
├───logic
├───models
│ Stateless-model.cs
│ Stateless-model.metadata.json
│ Stateless-model.schema.json
│ Stateless-model.xsd
└───ui
└───stateless
│ Settings.json
│
└───layouts
{page}.json
{page}.json kan settes opp på samme måte som en vanlig side i appen, og støtter alle komponenter med unntak av:
- Filopplaster
- Knapp
Appens frontend leser konfigurasjonen fra applicationmetadata.json og forstår at den ikke skal opprette en instans. I stedet henter den layout-filene og tilhørende datamodeller, og presenterer dem for brukeren.
Konfigurere tilgang uten innlogging
For å tillate bruk av appen for brukere som ikke er innlogget, følger du stegene beskrevet over. I tillegg må du angi at datatypen som er satt opp til å brukes for stateless-visningen, tillater anonym (ikke innlogget) bruk. Dette gjør du ved å endre det aktuelle dataType-elementet i applicationMetadata.json. Datatypens appLogic-objekt må få en ny innstilling: "allowAnonymousOnStateless": true. Se eksempel under:
App/config/applicationmetadata.json{
"id": "ttd/stateless-app-demo",
"org": "ttd",
"title": {
"nb": "Stateless App Demo"
},
"dataTypes": [
{
"id": "ref-data-as-pdf",
"allowedContentTypes": [
"application/pdf"
],
"maxCount": 0,
"minCount": 0
},
{
"id": "Stateless-model",
"allowedContentTypes": [
"application/xml"
],
"appLogic": {
"autoCreate": true,
"classRef": "Altinn.App.Models.StatelessV1",
"allowAnonymousOnStateless": true
},
"maxCount": 1,
"minCount": 1
}
],
...
"onEntry": { "show": "stateless" }
}
Fylle ut data
Når du bruker en stateless datatype, kan du fylle ut datamodellen når app-frontend henter skjemadataen.
Første gang frontend henter data (GET), skjer dette i to steg:
På påfølgende oppdateringer av samme skjemadata (POST) kjøres ikke forhåndsutfyllingen på nytt, men kalkuleringen trigges. Dette gjør det mulig å endre dataene basert på det brukeren skriver inn, selv i en stateless tilstand.
Eksempel på en kalkulering som fyller ut datamodellen nevnt i eksempelet over:
public async Task<bool> ProcessDataRead(Instance instance, Guid? dataId, object data)
{
if (data.GetType() == typeof(StatelessV1))
{
StatelessV1 form = (StatelessV1) data;
// Her kan du gjøre det du ønsker, for eksempel et API-kall
// hvis tjenesten skal oppføre seg som en innsynstjeneste.
form.Fornavn = "Test";
form.Etternavn = "Testesten";
return true
}
return false;
}
Autorisasjon med tredjepartsløsninger
Tilgangsstyring for stateless apper kan løses med standard app-autorisasjon, der du ved hjelp av Altinn-roller definerer hvem som har tilgang til å bruke tjenesten. Hvis du har behov for ytterligere sikring av tjenesten, kan du skrive logikk for autorisasjon av brukere med tredjepartsløsninger. Dette kan være API-er som er eksponert innenfor egen virksomhet, eller åpne API fra andre tilbydere.
Eksempelet nedenfor bruker Finanstilsynets API til å fastslå om virksomheten som en bruker representerer i Altinn, har tilstrekkelige lisenser til å bruke tjenesten.

Se kildekoden til appen som eksempelet er basert på (krever bruker i Altinn Studio).
Videre i eksempelet vil betegnelsen bruker være synonymt med en virksomhet representert av en person i Altinn.
Utvid datamodellen med felter for autorisasjon
I tillegg til et felt for inndata fra brukeren og et felt for å vise resultatet, har vi i dette eksempelet et felt for å holde på informasjon om hvorvidt brukeren er autentisert, og et felt for å holde på en dynamisk feilmelding.
App/models/{model}.xsd<xs:sequence> <xs:element name="searchString" type="xs:string" /> <xs:element name="result" type="xs:string" /> <xs:element name="userAuthorized" type="xs:boolean" /> <xs:element name="errorMessage" type="xs:string" /> </xs:sequence>Hopp til steg 4 hvis appen kun skal brukes via API.
Legg til felt for å vise feilmelding i brukergrensesnittet
I brukergrensesnittet til appen er det tre komponenter: Et søkefelt der brukeren skriver inn søkeord, et tekstfelt dedikert til å vise søkeresultatet, og en paragraf som er reservert for feilmeldinger.
Komponentene er koblet til datamodell og tekstressurs på følgende måte i
{page}.json:App/ui/stateless/layouts/{page}.json"layout": [ { "id": "sokeBoks", "type": "Input", "textResourceBindings": { "title": "SearchString" }, "dataModelBindings": { "simpleBinding": "searchString" }, "hidden": ["equals", ["dataModel", "userAuthorized"], false], "required": false, "readOnly": false }, { "id": "resultatBoks", "type": "TextArea", "textResourceBindings": { "title": "Result" }, "dataModelBindings": { "simpleBinding": "result" }, "hidden": ["equals", ["dataModel", "userAuthorized"], false], "required": false, "readOnly": true }, { "id": "errorBoks", "type": "Paragraph", "textResourceBindings": { "title": "ErrorMessage" }, "hidden": [ "or", ["equals", ["dataModel", "userAuthorized"], true], ["equals", ["dataModel", "userAuthorized"], null] ], "required": false, "readOnly": true } ]Vis eller skjul felter med uttrykk
Komponentenes
hidden-egenskap styrer hvilke felter brukeren ser. FeltetuserAuthorizedformidler resultatet av kontrollen som er beskrevet i steg 5. Feltet og uttrykkene styrer bare visningen og gir ikke i seg selv tilgang til tjenesten.- Søkefeltet og resultatfeltet skjules når
userAuthorizederfalse. - Feilmeldingen skjules når
userAuthorizedertrueeller ikke har fått en verdi.
Dermed er søke- og resultatfeltene synlige som standard. Hvis autorisasjonen mislykkes, skjules disse feltene, og feilmeldingen vises. Se dokumentasjonen for uttrykk for flere eksempler.
- Søkefeltet og resultatfeltet skjules når
Legg til tekstressurser
I tillegg til navnet på tjenesten er det lagt inn tre tekstressurser.
Tekstressursen for feilmelding inneholder en plassholder for navnet på brukeren. Variabelen
errorMessagelegges inn i datamodellen når det registreres at en bruker ikke er autorisert til å bruke tjenesten.App/config/texts/resource.{language}.json{ "id": "ErrorMessage", "value": "{0} mangler rettigheter til å bruke denne tjenesten.", "variables": [ { "key": "errorMessage", "dataSource": "dataModel.lookup" } ] }, { "id": "Result", "value": "Resultat" }, { "id": "SearchString", "value": "Legg inn søkeord her:" },Legg til autorisasjonslogikk
All behandling av data for stateless apper ligger i filen
App/logic/DataProcessing/DataProcessingHandler.cs, og det er her autorisasjonslogikken skal plasseres.Logikk for å slå opp data og autorisere brukeren ligger i metoden
ProcessDataRead. Denne kalles hver gang en bruker åpner appen eller sender inn input-data.public async Task<bool> ProcessDataRead(Instance instance, Guid? dataId, object data) { lookup lookup = (lookup)data; // Check if user is authorized to use service Party party = await _register.GetParty(int.Parse(instance.InstanceOwner.PartyId)); if (string.IsNullOrEmpty(party.OrgNumber) || !await _finanstilsynet.HasReqiuiredLicence(_settings.LicenseCode, party.OrgNumber)) { lookup.userAuthorized = false; lookup.errorMessage = $"{party.Name}"; return true; } // logic for looking up data if (!string.IsNullOrEmpty(lookup.searchString)) { lookup.result = $"You typed \"{lookup.searchString}\". This is the result."; return true; } return false; }Metoden starter med logikk for å hente ut skjemadataen slik at denne kan brukes videre i metoden.
lookup lookup = (lookup)dataVidere kommer logikken for å sjekke om brukeren er autorisert.
// Check if user is authorized to use service Party party = await _register.GetParty(int.Parse(instance.InstanceOwner.PartyId)) if (string.IsNullOrEmpty(party.OrgNumber) || !await _finanstilsynet.HasReqiuiredLicence(_settings.LicenseCode, party.OrgNumber)) { lookup.userAuthorized = false; lookup.errorMessage = $"{party.Name}"; return true; }For å vite hvem brukeren er, bruker vi identifikatoren
instance.InstanceOwner.PartyId, som metoden mottar som parameter. Vi slår opp i Altinns register for å hente ut party-objektet som representerer brukeren. Dette kan inneholde en organisasjon eller en person.Party party = await _register.GetParty(int.Parse(instance.InstanceOwner.PartyId))Det gjøres to sjekker for å avgjøre om en bruker er autorisert eller ikke. Først verifiseres det at party-objektet har definert et organisasjonsnummer. Hvis dette ikke er tilfellet, er brukeren en person, og dermed ikke autorisert.
Den andre sjekken kaller
_finanstilsynet.HasReqiuiredLicence(), en metode som slår opp i Finanstilsynets API for å avgjøre om organisasjonen har en gitt lisens. Implementasjonen av servicen er tilgjengelig her.Hvis ingen av sjekkene er vellykkede, fyller metoden to felter i datamodellen:
- en indikator på at brukeren ikke er autorisert
- en feilmelding, her kun navnet til brukeren
og metoden returnerer
truefor å indikere at dataverdiene er oppdatert.lookup.userAuthorized = false; lookup.errorMessage = $"{party.Name}"; return true;Helt til slutt kommer logikken for å vise et resultat basert på søkestrengen.
// logic for looking up data if (!string.IsNullOrEmpty(lookup.searchString)) { lookup.result = $"You typed \"{lookup.searchString}\". This is the result."; return true; } return false;lookup.resultfylles med verdien av oppslaget. I dette tilfellet skriver vi bare søkestrengen tilbake til brukeren. Igjen returnerer metodentruefor å indikere at en dataverdi er endret, ogfalsehvis dette ikke er tilfellet.
Starte instans fra et stateless skjema
Fra en stateless app kan du bruke InstantiationButton-komponenten til å starte en instans. Foreløpig støtter vi kun å starte en instans innad i samme appen som stateless-skjemaet vises i. Muligheten til å starte en instans i en annen app kommer senere.
Det er laget en eksempelapp som er satt opp som en innsynstjeneste hvor brukeren kan velge å starte en instans på den aktuelle appen. Denne kan brukes til inspirasjon for videre utvikling. Se appen med kildekode.
Instansiere med forhåndsutfylling
Et bruksområde for å starte en instans fra en stateless visning kan være at du først ønsker at appen skal oppføre seg som en innsynstjeneste der brukeren blir presentert for aktuelle data. Fra disse dataene kan brukeren velge å gå videre, og appen går da over til en vanlig innsendingstjeneste.
For å få til en slik flyt må du først sette opp appen som en stateless app som beskrevet under konfigurasjon. Når dette er gjort, kan du utvide stateless-visningen til å inkludere InstantiationButton, som starter en ny instans når brukeren klikker på knappen. Knappen sender bare med data til den nye instansen hvis du ber om det. Du velger ut feltene fra datamodellen i stateless-steget ved å legge til queryParameters på InstantiationButton-komponenten. Hver verdi er et uttrykk. For eksempel:
App/ui/stateless/layouts/{page}.json {
"id": "instantiation-button",
"type": "InstantiationButton",
"textResourceBindings": {
"title": "Start instans"
},
"queryParameters": {
"name": ["dataModel", "some.source.field"],
"id": ["dataModel", "some.other.field"]
}
}
Når brukeren velger å starte en instans, henter app-frontend ut feltene some.source.field og some.other.field fra datamodellen i stateless-steget, og sender dem med som name og id i feltet prefill i instansieringskallet. Eksempel på request som går mot backend:
{
"prefill": {
"name": "Ola Nordmann",
"id": "12345"
},
...
}
Hvis nøklene i queryParameters er stier til felt i datamodellen til innsendingsdelen, for eksempel "Sender.Name", fyller appen ut feltene automatisk når instansen starter. Da trenger du ikke å skrive kode.
Hvis du trenger mer kontroll, kan du bruke verdiene for forhåndsutfylling i metoden DataCreation i en klasse som implementerer IInstantiationProcessor. Der fyller du ut feltene du trenger i datamodellen til innsendingsdelen av appen. Eksempel:
using Altinn.App.Core.Features;
using Altinn.App.Models.model; // Navnerommet til datamodellen din, vanligvis Altinn.App.Models.<modellnavn>
using Altinn.Platform.Storage.Interface.Models;
namespace Altinn.App.Logic;
public class InstantiationProcessor : IInstantiationProcessor
{
public async Task DataCreation(Instance instance, object data, Dictionary<string, string>? prefill)
{
if (data is MessageV1 skjema && prefill is not null)
{
if (prefill.TryGetValue("name", out var name))
{
skjema.Sender = name;
}
if (prefill.TryGetValue("id", out var id))
{
skjema.Reference = id;
}
}
await Task.CompletedTask;
}
}
Husk å registrere klassen i metoden RegisterCustomAppServices i Program.cs, og legg til using for navnerommet til klassen øverst i filen:
App/Program.csusing Altinn.App.Logic;
// ...
services.AddTransient<IInstantiationProcessor, InstantiationProcessor>();
Instansiere fra en repeterende gruppe
Hvis du i stateless-steget ønsker at brukeren for eksempel velger et element fra en repeterende gruppe og jobber videre på et gitt element, kan du sette opp InstantiationButton-komponenten som en del av den repeterende gruppen. Uttrykkene i queryParameters henter da verdiene fra raden brukeren starter instansen fra. Du trenger ikke å oppgi indeksen til raden. Eksempel:
App/ui/stateless/layouts/{page}.json {
"id": "instantiation-button",
"type": "InstantiationButton",
"textResourceBindings": {
"title": "Start ny instans"
},
"queryParameters": {
"name": ["dataModel", "people.name"],
"age": ["dataModel", "people.age"]
}
}

