Skip to content
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
43f09b3
POC
olavsorl Feb 9, 2026
03339cd
Added data processor
olavsorl Feb 10, 2026
dd637ed
Modified logic in DataModelWrapper so that the GetResolvedKeysRecursi…
olavsorl Mar 5, 2026
53f29af
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl Mar 5, 2026
a11d093
Temp fix for unit test
olavsorl Mar 5, 2026
ce5f067
Fix
olavsorl Mar 5, 2026
c589a4c
Implemented suggested changes from code rabbit
olavsorl Mar 10, 2026
1ea1ece
Fixed spacing
olavsorl Mar 10, 2026
1f961e4
Fixed DI
olavsorl Mar 10, 2026
e9712ef
Fixed unit tests
olavsorl Mar 10, 2026
9bd887e
Fixed unit tests
olavsorl Mar 16, 2026
8858280
Implemented suggested changes from Code Rabbit
olavsorl Mar 16, 2026
7b3f5ab
Implemented suggested changes from code rabbit
olavsorl Mar 16, 2026
a86344d
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl Mar 16, 2026
6778191
Implemented suggested change by Sonar Cloud
olavsorl Mar 16, 2026
8275fe0
Implemented suggested changes
olavsorl Mar 16, 2026
6f8d2bd
Cleanup
olavsorl Mar 17, 2026
2816359
Implemented suggested change by code rabbit.
olavsorl Mar 17, 2026
913b06e
Renamed DataFieldValueCalculator to DataModelFieldCalculator
olavsorl Mar 17, 2026
210b986
Implemented suggested changes from code rabbit
olavsorl Mar 17, 2026
7325206
Implemented code rabbit suggestion
olavsorl Mar 17, 2026
0e987e9
Moved json test files outside folder.
olavsorl Mar 17, 2026
30c6eeb
Implemented suggested change by code rabbit
olavsorl Mar 18, 2026
c3c4922
Removed array of expressions
olavsorl Mar 18, 2026
c5103e9
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl Mar 19, 2026
6c3c960
Modified ExpressionValue to support underlayingType of string when js…
olavsorl Mar 19, 2026
ad2d54c
Cleanup in unit tests
olavsorl Mar 20, 2026
df10adf
Implemented suggested change by code rabbit
olavsorl Mar 20, 2026
efdebbe
Implemented suggested changes
olavsorl Mar 20, 2026
7306126
Fixed unit tests
olavsorl Mar 23, 2026
4b54345
Fixed unit tests
olavsorl Mar 23, 2026
a02f2be
Changed to null forgiving initializer
olavsorl Mar 23, 2026
301d08c
Implemented suggested change by code rabbit
olavsorl Mar 23, 2026
b584ccc
Merge branch 'main' into feature/set-data-field-by-expression
olavsorl Mar 23, 2026
5ceb31b
Reverted change to expression value
olavsorl Mar 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions src/Altinn.App.Core/Configuration/AppSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,19 @@ public class AppSettings
{
#pragma warning disable CA1707 // Identifiers should not contain underscores
/// <summary>
/// Constant for the location of json schema file
/// Constant for the suffix on json schema file names
/// </summary>
public const string JSON_SCHEMA_FILENAME = "schema.json";

/// <summary>
/// Constant for the location of validation configuration file
/// Constant for the suffix on validation file names
/// </summary>
public const string VALIDATION_CONFIG_FILENAME = "validation.json";

/// <summary>
/// Constant for the suffix on calculation file names
/// </summary>
public const string CALCULATION_CONFIG_FILENAME = "calculation.json";
#pragma warning restore CA1707 // Identifiers should not contain underscores

/// <summary>
Expand Down Expand Up @@ -95,15 +100,20 @@ public class AppSettings
public string RuleConfigurationJSONFileName { get; set; } = "RuleConfiguration.json";

/// <summary>
/// Gets or sets The JSON schema file name
/// Gets or sets the file names suffix for the json schema files
/// </summary>
public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME;

/// <summary>
/// Gets or sets The JSON schema file name
/// Gets or sets the file names suffix for the validation files
/// </summary>
public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME;

/// <summary>
/// Gets or sets the file names suffix for the calculation files
/// </summary>
public string CalculationConfigurationFileName { get; set; } = CALCULATION_CONFIG_FILENAME;

/// <summary>
/// Gets or sets the filename for application meta data
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -182,13 +182,15 @@
#pragma warning restore CS0618, CS0612 // Type or member is obsolete
services.TryAddTransient<IInstantiationProcessor, NullInstantiationProcessor>();
services.TryAddTransient<IInstantiationValidator, NullInstantiationValidator>();
services.TryAddTransient<DataFieldValueCalculator>();
services.TryAddTransient<IAppModel, DefaultAppModel>();
services.TryAddTransient<DataListsFactory>();
services.TryAddTransient<InstanceDataListsFactory>();
services.TryAddTransient<IDataElementAccessChecker, DataElementAccessChecker>();
services.TryAddTransient<IDataListsService, DataListsService>();
services.TryAddTransient<ILayoutEvaluatorStateInitializer, LayoutEvaluatorStateInitializer>();
services.TryAddTransient<LayoutEvaluatorStateInitializer>();
services.AddTransient<IDataWriteProcessor, DataFieldValueCalculatorProcessor>();
services.AddSingleton<IAuthenticationTokenResolver, AuthenticationTokenResolver>();
services.AddTransient<IDataService, DataService>();
services.AddSingleton<ModelSerializationService>();
Expand Down Expand Up @@ -263,7 +265,7 @@
services.AddTransient<IEventHandlerResolver, EventHandlerResolver>();
services.TryAddSingleton<IEventSecretCodeProvider, KeyVaultEventSecretCodeProvider>();

// TODO: Event subs could be handled by the new automatic Maskinporten auth, once implemented.

Check warning on line 268 in src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this 'TODO' comment.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZy-iQtShgA_KbzevodZ&open=AZy-iQtShgA_KbzevodZ&pullRequest=1683
// The event subscription client depends upon a Maskinporten message handler being
// added to the client during setup. As of now this needs to be done in the apps
// if subscription is to be added. This registration is to prevent the DI container
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
private readonly AppImplementationFactory _appImplementationFactory;

/// <summary>
/// Initializes a new instance of the <see cref="DataListsFactory"/> class.
/// Initializes a new instance of the <see cref="InstanceDataListsFactory"/> class.
/// </summary>
public InstanceDataListsFactory(IServiceProvider serviceProvider)
{
Expand All @@ -26,7 +26,7 @@
public IInstanceDataListProvider GetDataListProvider(string listId)
{
var instanceDataListProviders = _appImplementationFactory.GetAll<IInstanceDataListProvider>();
foreach (var instanceDataListProvider in instanceDataListProviders)

Check warning on line 29 in src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Loops should be simplified using the "Where" LINQ method

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZy-iQsxhgA_Kbzevoc5&open=AZy-iQsxhgA_Kbzevoc5&pullRequest=1683
{
if (instanceDataListProvider.Id.Equals(listId, StringComparison.OrdinalIgnoreCase))
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
using System.Text.Json;
using Altinn.App.Core.Internal.App;
using Altinn.App.Core.Internal.Data;
using Altinn.App.Core.Internal.Expressions;
using Altinn.App.Core.Models;
using Altinn.App.Core.Models.Layout;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.Extensions.Logging;
using ComponentContext = Altinn.App.Core.Models.Expressions.ComponentContext;

namespace Altinn.App.Core.Features.DataProcessing;

internal sealed class DataFieldValueCalculator
{
private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
{
ReadCommentHandling = JsonCommentHandling.Skip,
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};

private readonly ILogger<DataFieldValueCalculator> _logger;
private readonly IAppResources _appResourceService;
private readonly ILayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer;
private readonly IDataElementAccessChecker _dataElementAccessChecker;
private readonly Telemetry? _telemetry;

public DataFieldValueCalculator(
ILogger<DataFieldValueCalculator> logger,
ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer,
IAppResources appResourceService,
IDataElementAccessChecker dataElementAccessChecker,
Telemetry? telemetry = null
)
{
_logger = logger;
_appResourceService = appResourceService;
_layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer;
_dataElementAccessChecker = dataElementAccessChecker;
_telemetry = telemetry;
}

public async Task Calculate(IInstanceDataAccessor dataAccessor, string taskId)
{
using var activity = _telemetry?.StartCalculateActivity(dataAccessor.Instance.Id, taskId);
foreach (var (dataType, dataElement) in dataAccessor.GetDataElementsWithFormDataForTask(taskId))
{
if (await _dataElementAccessChecker.CanRead(dataAccessor.Instance, dataType) is false)
{
continue;
}

var calculationConfig = _appResourceService.GetCalculationConfiguration(dataType.Id);
if (!string.IsNullOrEmpty(calculationConfig))
{
await CalculateFormData(dataAccessor, dataElement, taskId, calculationConfig);
}
}
}

internal async Task CalculateFormData(
IInstanceDataAccessor dataAccessor,
DataElement dataElement,
string taskId,
string rawCalculationConfig
)
{
var evaluatorState = await _layoutEvaluatorStateInitializer.Init(dataAccessor, taskId);
var hiddenFields = await LayoutEvaluator.GetHiddenFieldsForRemoval(
evaluatorState,
evaluateRemoveWhenHidden: false
);
DataElementIdentifier dataElementIdentifier = dataElement;
var dataFieldCalculations = ParseDataFieldCalculationConfig(rawCalculationConfig, _logger);
var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement);

foreach (var (baseField, calculations) in dataFieldCalculations)
{
var resolvedFields = await evaluatorState.GetResolvedKeys(
new DataReference() { Field = baseField, DataElementIdentifier = dataElementIdentifier },
true
);
foreach (var resolvedField in resolvedFields)
{
if (
hiddenFields.Exists(d =>
d.DataElementIdentifier == resolvedField.DataElementIdentifier
&& resolvedField.Field.StartsWith(d.Field, StringComparison.InvariantCulture)
)
)
{
continue;
}

var context = new ComponentContext(
evaluatorState,
component: null,
rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field),
dataElementIdentifier: resolvedField.DataElementIdentifier
);
var positionalArguments = new object[] { resolvedField.Field };
foreach (var calculation in calculations)
{
await RunCalculation(
formDataWrapper,
evaluatorState,
resolvedField,
context,
positionalArguments,
calculation
);
}
}
}
}

private async Task RunCalculation(
IFormDataWrapper formDataWrapper,
LayoutEvaluatorState evaluatorState,
DataReference resolvedField,
ComponentContext context,
object[] positionalArguments,
DataFieldCalculation calculation
)
{
try
{
var calculationResult = await ExpressionEvaluator.EvaluateExpressionToExpressionValue(
evaluatorState,
calculation.Condition,
context,
positionalArguments
);
if (!formDataWrapper.Set(resolvedField.Field, calculationResult))
{
_logger.LogWarning(
"Could not set calculated value for field {Field} in data element {DataElementId}. "
+ "This is because the type conversion failed.",
resolvedField.Field,
resolvedField.DataElementIdentifier.Id
);
}
}
catch (Exception e)

Check warning on line 143 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either log this exception and handle it, or rethrow it with some contextual information.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZy-iQs9hgA_Kbzevoc7&open=AZy-iQs9hgA_Kbzevoc7&pullRequest=1683
{
_logger.LogError(e, "Error while evaluating calculation for field {Field}", resolvedField.Field);
throw;
}
}

private Dictionary<string, List<DataFieldCalculation>> ParseDataFieldCalculationConfig(

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (macos-latest)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (macos-latest)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Run dotnet build and test (ubuntu-latest)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check failure on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View workflow job for this annotation

GitHub Actions / Analyze (csharp)

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static (https://learn.microsoft.com/dotnet/fundamentals/code-analysis/quality-rules/ca1822)

Check warning on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Member 'ParseDataFieldCalculationConfig' does not access instance data and can be marked as static

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZz2bPLtyADDpoK73Jg1&open=AZz2bPLtyADDpoK73Jg1&pullRequest=1683

Check warning on line 150 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Make 'ParseDataFieldCalculationConfig' a static method.

See more on https://sonarcloud.io/project/issues?id=Altinn_app-lib-dotnet&issues=AZz2bPLtyADDpoK73Jg0&open=AZz2bPLtyADDpoK73Jg0&pullRequest=1683
string rawCalculationConfig,
ILogger<DataFieldValueCalculator> logger
)
Comment thread
olavsorl marked this conversation as resolved.
Outdated
{
using var calculationConfigDocument = JsonDocument.Parse(rawCalculationConfig);

var dataFieldCalculations = new Dictionary<string, List<DataFieldCalculation>>();
var hasCalculations = calculationConfigDocument.RootElement.TryGetProperty(
"calculations",
out JsonElement calculationsObject
);
if (hasCalculations)
{
foreach (var calculationArray in calculationsObject.EnumerateObject())
{
var field = calculationArray.Name;
var calculations = calculationArray.Value;
foreach (var calculation in calculations.EnumerateArray())
{
if (!dataFieldCalculations.TryGetValue(field, out var dataFieldCalculation))
{
dataFieldCalculation = new List<DataFieldCalculation>();
dataFieldCalculations[field] = dataFieldCalculation;
}
var resolvedDataFieldCalculation = ResolveDataFieldCalculation(field, calculation, logger);
if (resolvedDataFieldCalculation == null)
{
logger.LogError("Calculation for field {Field} could not be resolved", field);
continue;
}
dataFieldCalculation.Add(resolvedDataFieldCalculation);
}
}
}
return dataFieldCalculations;
}

private static DataFieldCalculation? ResolveDataFieldCalculation(
string field,
JsonElement definition,
ILogger logger
)
{
var rawDataFieldValueCalculation = new RawDataFieldValueCalculation();

if (definition.ValueKind == JsonValueKind.String)
{
var stringReference = definition.GetString();
if (stringReference == null)
{
logger.LogError("Could not resolve null reference for calculation for field {Field}", field);
return null;
}
}
else
{
var dataFieldCalculationDefinition = definition.Deserialize<RawDataFieldValueCalculation>(
_jsonSerializerOptions
);
if (dataFieldCalculationDefinition == null)
{
logger.LogError("Calculation for field {Field} could not be parsed", field);
return null;
}

if (dataFieldCalculationDefinition.Condition != null)
{
rawDataFieldValueCalculation.Condition = dataFieldCalculationDefinition.Condition;
}
}

if (rawDataFieldValueCalculation.Condition == null)
{
logger.LogError("Calculation for field {Field} is missing condition", field);
return null;
}

var dataFieldCalculation = new DataFieldCalculation
{
Condition = rawDataFieldValueCalculation.Condition.Value,
};

return dataFieldCalculation;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using Altinn.App.Core.Models;

namespace Altinn.App.Core.Features.DataProcessing;

/// <summary>
/// Processing data fields values that is calculated by expressions provided in [modelName].calculation.json.
/// </summary>
internal sealed class DataFieldValueCalculatorProcessor : IDataWriteProcessor
{
private readonly DataFieldValueCalculator _dataFieldValueCalculator;

/// <summary>
/// Initializes a new instance of the <see cref="DataFieldValueCalculatorProcessor"/> class.
/// </summary>
/// <param name="dataFieldValueCalculator"></param>
public DataFieldValueCalculatorProcessor(DataFieldValueCalculator dataFieldValueCalculator)
{
_dataFieldValueCalculator = dataFieldValueCalculator;
}

/// <summary>
/// Processes data write operations on properties in the data model.
/// </summary>
/// <param name="instanceDataMutator">Object to fetch data elements not included in changes</param>
/// <param name="taskId">The current task ID</param>
/// <param name="changes">Not used in this context</param>
/// <param name="language">Not used in this context</param>
public async Task ProcessDataWrite(
IInstanceDataMutator instanceDataMutator,
string taskId,
DataElementChanges changes,
string? language
)
{
await _dataFieldValueCalculator.Calculate(instanceDataMutator, taskId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public abstract class GenericDataProcessor<TModel> : IDataProcessor

/// <summary>
/// Do changes to the model before it is written to storage, and report back to frontend.
/// Tyipically used to add calculated values to the model.
/// Typically used to add calculated values to the model.
/// </summary>
public abstract Task ProcessDataWrite(
Instance instance,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ partial class Telemetry
internal Activity? StartGetValidationConfigurationActivity() =>
ActivitySource.StartActivity($"{Prefix}.GetValidationConfiguration");

internal Activity? StartGetCalculationConfigurationActivity() =>
ActivitySource.StartActivity($"{Prefix}.GetCalculationConfiguration");

internal Activity? StartGetLayoutModelActivity() => ActivitySource.StartActivity($"{Prefix}.GetLayoutModel");

internal Activity? StartGetClassRefActivity() => ActivitySource.StartActivity($"{Prefix}.GetClassRef");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using System.Diagnostics;
using static Altinn.App.Core.Features.Telemetry.DataFieldValueCalculator;

namespace Altinn.App.Core.Features;

partial class Telemetry
{
internal Activity? StartCalculateActivity(string instanceId, string taskId)
{
var activity = ActivitySource.StartActivity($"{Prefix}.Calculate");
activity?.SetInstanceId(instanceId);
activity?.SetTaskId(taskId);
return activity;
}

internal static class DataFieldValueCalculator
{
internal const string Prefix = "DataFieldValueCalculator";
}
}
Loading
Loading