Arbeidstilsynet.Common.AltinnApp
2.12.2
Prefix Reserved
dotnet add package Arbeidstilsynet.Common.AltinnApp --version 2.12.2
NuGet\Install-Package Arbeidstilsynet.Common.AltinnApp -Version 2.12.2
<PackageReference Include="Arbeidstilsynet.Common.AltinnApp" Version="2.12.2" />
<PackageVersion Include="Arbeidstilsynet.Common.AltinnApp" Version="2.12.2" />
<PackageReference Include="Arbeidstilsynet.Common.AltinnApp" />
paket add Arbeidstilsynet.Common.AltinnApp --version 2.12.2
#r "nuget: Arbeidstilsynet.Common.AltinnApp, 2.12.2"
#:package Arbeidstilsynet.Common.AltinnApp@2.12.2
#addin nuget:?package=Arbeidstilsynet.Common.AltinnApp&version=2.12.2
#tool nuget:?package=Arbeidstilsynet.Common.AltinnApp&version=2.12.2
Introduction
A collection of common patterns and extensions for cross-cutting concerns in Altinn applications.
📖 Installation
To install the package, you can use the following command in your terminal:
dotnet add package Arbeidstilsynet.Common.AltinnApp
🚀 Features
- Extension Methods for common Altinn operations
- Abstract Data Processors for handling data changes
- Country Code Lookup with country dialing codes and names
- Altinn Options Provider for country dropdowns
- Embedded Resource Utilities for JSON data
🧑💻 Usage
Dependency Injection Setup
Altinn Application Utilities
Add services to your service collection:
public void ConfigureServices(IServiceCollection services)
{
// Add country code lookup functionality
services.AddLandskoder();
// OR add country options for Altinn dropdowns (includes AddLandskoder)
services.AddLandOptions();
}
Structured Data
1. Implement a mapper and register it, specifying the data model and structured data types
The mapper is resolved from the service provider, so it can use constructor injection (e.g. ILandskodeLookup) and be unit tested on its own.
internal class MeldingMapper(ILandskodeLookup landskoder)
: IStructuredDataMapper<MeldingDataModel, MeldingStructuredData>
{
public async Task<MeldingStructuredData> Map(MeldingDataModel dataModel)
{
var land = await landskoder.GetLandskode(dataModel.LandKode);
return new MeldingStructuredData
{
// Property mappings here
};
}
}
// Adds a DataElement to your instance to hold structured data of type MeldingStructuredData.
// Content type will be "application/json".
services.AddStructuredData<MeldingDataModel, MeldingStructuredData, MeldingMapper>(env);
Alternatively, adjust the environment-based defaults (error details are included outside production):
services.AddStructuredData<MeldingDataModel, MeldingStructuredData, MeldingMapper>(
env,
config => config with { TaskIdFilter = ["Task_1"] }
);
The
AddStructuredData<TDataModel, TStructuredData>(Func<TDataModel, TStructuredData> mapFunc, ...)overloads are obsolete. Move the lambda into anIStructuredDataMapperimplementation instead.
2. Add the data element to your App/config/applicationmetadata.json dataTypes
{
"dataTypes": [
{
"id": "structured-data",
"allowedContentTypes": [
"application/json"
],
"allowedContributors": [
"app:owned"
]
},
/// ...Other data types
]
}
3. (Optional) Post-process the instance and data elements
Implement IStructuredDataPostProcessor to run logic from inside the structured data feature at process end. Unlike a plain IProcessEnd, a post-processor is guaranteed that both the structured data element and the main PDF element exist on the instance. The instance is freshly fetched from storage, including framework-created elements such as PDFs. Altinn-managed automatic form-model deletion runs before these post-processors: they must not depend on the form model still being available. Post-processors should be idempotent.
Post-processors are responsible for persisting their own changes (e.g. via IDataClient.Update).
internal class MyPostProcessor(IDataClient dataClient) : IStructuredDataPostProcessor
{
public async Task Process(StructuredDataPostProcessingContext context)
{
var pdf = context.MainPdfElement;
pdf.Metadata ??= [];
pdf.Metadata.Add(new KeyValueEntry { Key = "MyKey", Value = "MyValue" });
await dataClient.Update(context.Instance, pdf);
}
}
services.AddStructuredDataPostProcessor<MyPostProcessor>();
Multiple post-processors run in registration order.
Instance synchronization and process-end validation
Structured data is still mapped and its insertion awaited at task end. Successful package-owned insertions and deletions are reflected in the supplied instance.Data collection, preserving unrelated elements and avoiding duplicate entries. Failed operations propagate errors without recording a mutation that did not succeed; earlier successful deletions are not rolled back.
At process end, the configured structured-data type must exist regardless of the task filter. If it is missing locally, the package fetches the instance from storage once and replaces the supplied instance's data collection. It throws the missing-structured-data exception only if that authoritative recheck also lacks the element. Storage failures propagate instead. There are no delays, polling, or speculative retries.
Post-processors always use fresh storage state, reusing the fallback refresh when one was needed. Without post-processors, an element already present locally requires no additional lookup. Process-end handling does not require a form-model element or delete any data.
Synchronization covers mutations made by this package, not elements created by other framework components. The identified issue is stale in-memory state, not a demonstrated unfinished-write race. In Altinn.App.Core 8.12.11, process-event registration is attempted before app-defined IProcessEnd handlers, so this process-end guard is not a pre-event write barrier. Keep structured-data mapping at task end.
Migrating form-model cleanup ownership
Breaking change in 2.12.1: KeepAppDataModelAfterMapping and package-managed form-model deletion have been removed. Although shipped as a patch to correct the intended cleanup timing, this is a source and behavior change. Apps must explicitly configure Altinn-managed cleanup if the form-model XML should be deleted.
Deleting in IProcessEnd was too late to prevent an event consumer from fetching the form-model XML: process-completed event registration is attempted first. This ordering was not introduced in 8.12.11; it was already present when IProcessEnd was introduced.
To migrate:
Remove assignments to
KeepAppDataModelAfterMappingfrom registration callbacks and tests. Remove anyCS0618suppressions used only for that option.Enable Altinn-managed deletion on the form-model data type, not the structured-data or PDF type:
{ "id": "skjema", "appLogic": { "autoDeleteOnProcessEnd": true } }This is a fragment to merge into the existing data-type configuration in
App/config/applicationmetadata.json. Preserve the existingclassRef, otherappLogicproperties, and data-type settings; use your actual form-model data-type ID.For tests or applications that intentionally retain the model, leave
autoDeleteOnProcessEnddisabled. Retention is now controlled solely by application metadata, not a package option. Adapt form-model-dependent post-processors and recovery before enabling automatic cleanup.
Ordering and failure/retry implications verified against Altinn.App.Core 8.12.11:
DefaultAppEvents.OnEndAppEventawaits automatic data-element deletion, usingDeleteDatawithdelay: true(marking elements as deleted). The removed package-managed form-model cleanup useddelay: false.EndEventEventHandlerinvokes this during process-event handling.ProcessEnginehandles these events, persists process state/events, and attempts event registration before invoking app-definedIProcessEndhandlers. Consequently, framework-managed model deletion occurs before this package's guard and post-processors.- Automatic deletion awaits all deletion tasks. A deletion failure propagates before the app-defined process-end handlers run; successful deletions may already have occurred. There is no transactional rollback. A later guard or post-processor failure cannot undo framework cleanup or the already-persisted process transition.
- Do not assume a failing post-processor will be automatically retried by resubmission. Validate the application's explicit recovery/retry path, and keep post-processors idempotent. With automatic cleanup enabled, post-processors and recovery must not depend on the form model still being available.
Framework-managed cleanup is the idiomatic way to remove the form-model XML before process-completed event registration. The package's guard and post-processors still run later; failures there cannot prevent event registration or preserve a model already deleted by Altinn. Post-processor writes are likewise not guaranteed to precede event consumption. Reverify ordering when upgrading Altinn or overriding its app-event implementation.
Validate the metadata migration and submission-to-backend flow when upgrading, then remove app-local refresh workarounds that are no longer needed. Without the metadata setting the package now retains the form model; do not rely on cleanup to happen implicitly. Keep consumer-side form-model attachment exclusions as defense in depth.
Task-end form-model processing
PreSubmitDataModelProcessor<TDataModel> remains supported. It implements IProcessTaskEnd, so it processes and saves the form model before automatic process-end deletion. Register subclasses as IProcessTaskEnd and use TaskIdFilter to restrict the tasks on which they run.
Task end is not necessarily final submission, and this base class does not establish ordering relative to other task-end handlers. If model mutations must precede specific structured-data mapping or PDF generation, validate that ordering or use an explicitly placed BPMN service task. Use IStructuredDataPostProcessor for structured-data/PDF/attachment post-processing that can run after event registration and automatic form-model deletion, not as a replacement for task-end model mutations.
Extension Methods
Instance Extensions
Easily extract common information from Altinn instances:
using Arbeidstilsynet.Common.AltinnApp.Extensions;
public class MyService
{
public void ProcessInstance(Instance instance)
{
// Extract instance GUID
Guid instanceId = instance.GetInstanceGuid();
// Get application name
string appName = instance.GetAppName();
// Get party ID of instance owner
int partyId = instance.GetInstanceOwnerPartyId();
Console.WriteLine($"Processing {appName} instance {instanceId} for party {partyId}");
}
}
DataClient Extensions
Simplified data operations with Altinn instances:
using Arbeidstilsynet.Common.AltinnApp.Extensions;
public class DataService
{
private readonly IDataClient _dataClient;
public DataService(IDataClient dataClient)
{
_dataClient = dataClient;
}
public async Task<MyDataModel?> GetFormData(Instance instance)
{
// Get form data with default data type "structured-data"
return await _dataClient.GetSkjemaData<MyDataModel>(instance);
// Or specify custom data type
return await _dataClient.GetSkjemaData<MyDataModel>(instance, "customDataType");
}
public async Task CleanupAttachments(Instance instance, List<DataElement> unusedAttachments)
{
foreach (var attachment in unusedAttachments)
{
// Delete data element and remove from instance
await _dataClient.DeleteElement(instance, attachment, delay: true);
}
}
}
Assembly Extensions
Load and deserialize embedded JSON resources:
using Arbeidstilsynet.Common.AltinnApp.Extensions;
public class ConfigurationService
{
public Task<MyResourceType> GetResource()
{
var assembly = Assembly.GetExecutingAssembly();
// Load embedded JSON resource and deserialize
return assembly.GetEmbeddedResource<MyResourceType>("resource.json");
}
}
Country Code Lookup
Use the built-in country code lookup service:
public class AddressService
{
private readonly ILandskodeLookup _landskodeLookup;
public AddressService(ILandskodeLookup landskodeLookup)
{
_landskodeLookup = landskodeLookup;
}
public async Task<string> GetCountryInfo(string countryCode)
{
// Look up specific country by ISO code
var country = await _landskodeLookup.GetLandskode("NOR");
if (country != null)
{
return $"{country.Land} ({country.Kode})"; // "Norway (+47)"
}
return "Unknown country";
}
}
Abstract Data Processors
Create data processors that respond to form data changes:
Simple Data Processor
using Arbeidstilsynet.Common.AltinnApp.Abstract.Processing;
public class MyFormDataProcessor : BaseDataProcessor<MyDataModel>
{
private readonly ILogger<MyFormDataProcessor> _logger;
public MyFormDataProcessor(ILogger<MyFormDataProcessor> logger)
{
_logger = logger;
}
protected override async Task ProcessData(MyDataModel current, MyDataModel? previous)
{
if (previous == null)
{
_logger.LogInformation("New form data created");
return;
}
_logger.LogInformation("Form data updated");
// Your custom processing logic here
}
}
Member-Specific Processor
public class EmailChangeProcessor : MemberProcessor<MyDataModel, string>
{
protected override string? AccessMember(MyDataModel dataModel)
{
return dataModel.Email;
}
protected override async Task ProcessMember(
string? currentEmail,
string? previousEmail,
MyDataModel currentDataModel,
MyDataModel previousDataModel)
{
// Your custom logic to handle the changed member
}
}
List Processor
public class AttachmentListProcessor : ListProcessor<MyDataModel, AttachmentInfo>
{
protected override List<AttachmentInfo>? AccessMember(MyDataModel dataModel)
{
return dataModel.Attachments; // Point to the list we want to process
}
protected override async Task ProcessListChange(
List<AttachmentInfo>? currentList,
List<AttachmentInfo>? previousList,
MyDataModel currentDataModel,
MyDataModel previousDataModel)
{
// Process that attachments have been removed or added
}
protected override async Task ProcessItem(
AttachmentInfo currentItem,
AttachmentInfo previousItem,
MyDataModel currentDataModel,
MyDataModel previousDataModel)
{
// Process individual item changes
}
}
Registration of Data Processors
Register your data processors in dependency injection:
public void ConfigureServices(IServiceCollection services)
{
// Register data processors
services.AddTransient<IDataProcessor, MyFormDataProcessor>();
services.AddTransient<IDataProcessor, EmailChangeProcessor>();
services.AddTransient<IDataProcessor, AttachmentListProcessor>();
}
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Altinn.ApiClients.Dialogporten.ServiceOwner (>= 1.118.10-alpha.1786357013)
- Altinn.App.Api (>= 8.12.11)
- Altinn.App.Core (>= 8.12.11)
- FluentValidation (>= 12.1.1)
- OpenTelemetry.Api (>= 1.19.1)
-
net8.0
- Altinn.ApiClients.Dialogporten.ServiceOwner (>= 1.118.10-alpha.1786357013)
- Altinn.App.Api (>= 8.12.11)
- Altinn.App.Core (>= 8.12.11)
- FluentValidation (>= 12.1.1)
- OpenTelemetry.Api (>= 1.19.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.12.2 | 56 | 10/15/2026 |
| 2.12.1 | 114 | 10/15/2026 |
| 2.12.0 | 79 | 10/14/2026 |
| 2.11.0 | 59 | 10/13/2026 |
| 2.10.0 | 56 | 10/13/2026 |
| 2.9.1 | 74 | 10/5/2026 |
| 2.9.0 | 71 | 10/5/2026 |
| 2.9.0-alpha7 | 163 | 8/14/2026 |
| 2.9.0-alpha6 | 80 | 8/14/2026 |
| 2.9.0-alpha5 | 92 | 8/14/2026 |
| 2.9.0-alpha3 | 80 | 8/13/2026 |
| 2.9.0-alpha | 89 | 8/13/2026 |
| 2.8.1 | 161 | 7/1/2026 |
| 2.8.0 | 135 | 5/22/2026 |
| 2.8.0-alpha1 | 256 | 5/22/2026 |
| 2.7.2 | 279 | 4/29/2026 |
| 2.7.2-alpha | 127 | 4/29/2026 |
| 2.7.1 | 524 | 4/29/2026 |
| 2.7.1-alpha | 133 | 4/28/2026 |
| 2.7.0 | 127 | 4/28/2026 |
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
### Added <!-- for new features. -->
- Add `StructuredDataConfiguration.ExcludedAttachmentDataTypeIds` to exclude additional data types from attachments; the datamodel type is included automatically.
- Validate that `MainPdfDataTypeId` and `StructuredDataTypeId` are non-blank, distinct, and never excluded from attachments (including via the datamodel type).
### Changed <!-- for changes in existing functionality. -->
### Deprecated <!-- for soon-to-be removed features. -->
### Removed <!-- for now removed features. -->
### Fixed <!-- for any bug fixes. -->
### Security <!-- in case of vulnerabilities. -->
## 2.12.2
### Fixed
- Standardized NuGet metadata with Arbeidstilsynet as author, an MIT license expression, an embedded icon and Git repository details.
## 2.12.1
### Fixed
- Synchronize the supplied instance's data collection after successful structured-data insertion and deletion, preserving unrelated elements and avoiding duplicates.
- Verify locally missing structured data against storage once before failing at process end; propagate lookup errors and retain the guard regardless of task filtering or cleanup settings.
- Reuse the fallback refresh for post-processors and preserve fresh framework-created elements, without requiring the form model at process end.
- Reject unsuccessful structured-data deletions without removing their in-memory entries.
### Removed
- **Breaking change:** Remove `KeepAppDataModelAfterMapping` and package-managed form-model deletion. Deletion in `IProcessEnd` could not guarantee cleanup before process-completed event registration. Enable `appLogic.autoDeleteOnProcessEnd` on the existing form-model data type for Altinn-managed cleanup; without it, the form model is retained. See the README migration guide for metadata, test-retention, post-processing, and recovery implications. Process-end validation and post-processors remain supported.
## 2.12.0
### Added
- Add `StructuredDataConfiguration.ExcludedAttachmentDataTypeIds` to exclude additional data types from attachments; the datamodel type is included automatically.
- Validate that `MainPdfDataTypeId` and `StructuredDataTypeId` are non-blank, distinct, and never excluded from attachments (including via the datamodel type).
## 2.11.0
### Added
- feat: Adds `IStructuredDataPostProcessor` and `AddStructuredDataPostProcessor<T>()` for post-processing the instance, structured data element and main PDF element at process end, from inside the structured data feature.
## 2.10.0
### Added
- feat: `IStructuredDataMapper<TDataModel, TStructuredData>` and `AddStructuredData<TDataModel, TStructuredData, TMapper>(env, configure)`. Mappers are resolved from DI (constructor injection, async mapping), and `configure` adjusts the environment-based default `StructuredDataConfiguration`.
### Changed
- Allow publishing with Dialogporten's prerelease client dependency (NU5104).
### Deprecated
- `AddStructuredData<TDataModel, TStructuredData>(Func<TDataModel, TStructuredData>, ...)` overloads. Implement `IStructuredDataMapper` and use the new overload instead.
## 2.9.1
### Changed
- changed(deps): Applied minor and patch updates to dependencies
## 2.9.0
### Added
- feat: Adds a new PatchDialog service task and DI extension to enable Altinn apps to patch Dialogporten dialogs, plus supporting ports and dependencies in the Arbeidstilsynet.Common.AltinnApp NuGet package
## 2.8.1
### Changed
- changed(deps): Applied minor and patch updates to dependencies
## 2.8.0
### Added
- added: `PreSubmitDataModelProcessor<TDataModel>` supports `TaskIdFilter` for limiting pre-submit processing to selected Altinn task IDs
- added: `StructuredDataConfiguration.TaskIdFilter` for limiting structured data generation to selected Altinn task IDs
### Changed
- changed: `StructuredDataManager` now refuses to delete the app data model if no structured data element of the configured type exists on the instance, guarding against data loss on misconfiguration or when structured data mapping did not run for the task.
## 2.7.2
### Fixed
- fixed: `StructuredDataManager` now deletes any pre-existing `structured-data` data elements before inserting the new one, making `CreateStructuredData` idempotent on retries
## 2.7.1
### Fixed
- fixed: `StructuredDataManager<,>` was registered as singleton but depends (via `IStructuredDataValidator<T>`) on FluentValidation's `IValidator<T>`, which is scoped by default. This caused DI scope validation to fail at startup. The manager is now registered as transient.
## 2.7.0
### Added
- added: `ILanguageObserver` interface and `AddLanguageObserver<T>` DI extension for receiving notifications about the user's selected language during `ProcessDataRead`
- added: `SelectedLanguageProcessor` that resolves the current language from the request or falls back to the user's profile preference. Only notifies when the language is supported by the application
- added: `StructuredDataManager` now validates structured data before insertion. Uses registered `IValidator<T>` implementations, or falls back to `DataAnnotations`. Controlled via `StructuredDataConfiguration.DisableValidation`
- added: `FieldsValidator<TDataModel, TField>` base class for field-level validators with expression-based field paths and change detection
- added: `RepeatingGroupValidator<TDataModel, TItem>` base class for validating items within repeating groups (collections), with indexed path generation
- added: `DataTypeValidator<TDataModel, TToValidate>` base class that automatically discovers and validates all instances of a target type within a data model by walking the object graph via reflection, with circular type reference detection
### Changed
- changed: `ILanguageObserver.NotifyCurrentLanguage` now receives the current data model as a parameter (`object dataModel, string language`)
## 2.6.1
### Changed
- changed(deps): Applied minor and patch updates to dependencies
## 2.6.0
### Changed
- changed(deps): Applied minor and patch updates to dependencies
## 2.5.0
### Changed
- ListProcessor no longer emits ProcessList -- only ProcessItem, and items are compared using AltinnRowId. If you need to process the list as a whole, you should use MemberProcessor pointing to the list in question.
## 2.4.1
### Changed
- chore: moved package to nuget.org
## 2.4.0
- changed: Differentiate structured data and main content.
- More configurable, though defaults should for most.
- The altinn app will declare which data elemnts are the main content, and structured data, using data values
## 2.3.0
- added: PreSubmitProcessor to make it easier to implement custom pre-submit logic in AltinnApps.
- added: A few more extension methods for `IApplicationClient` and `IDataClient`
## 2.2.0
- added: Option to disable deletion of app datamodel after mapping structured data. This can be useful for testing and some legacy use cases.
## 2.1.0
### Changed
- changed(deps): Applied minor and patch updates to dependencies
## 2.0.0
### Changed
- changed(deps): Major dotnet updated (v10)
## 1.5.1
- added: Option to include error details related to mapping structured data in the validation response. This intended for development environments.
## 1.5.0
- added: Extension to manage DataElements for structured data. It deletes the xml-based DataModel, and replaces it with application/json of a user-provided type.
## 1.4.1
### Changed
- applied new default skjema type `structured-data` as default
## 1.4.0
- added: More customization options for LandOptions
- Custom order function for country list in LandOptions (default is alphabetical by country name)
- Option for using either alpha2 or alpha3 country codes as value in dropdowns (default is alpha3)
## 1.3.0
### Changed
- changed: Moved all AltinnApp logic (from the Altinn package) to this new dedicated package, only intended to be used by AltinnApps.