diff --git a/src/Altinn.App.Core/Configuration/AppSettings.cs b/src/Altinn.App.Core/Configuration/AppSettings.cs
index 2b98c9be97..0a69702862 100644
--- a/src/Altinn.App.Core/Configuration/AppSettings.cs
+++ b/src/Altinn.App.Core/Configuration/AppSettings.cs
@@ -8,14 +8,19 @@ public class AppSettings
{
#pragma warning disable CA1707 // Identifiers should not contain underscores
///
- /// Constant for the location of json schema file
+ /// Constant for the suffix on json schema file names
///
public const string JSON_SCHEMA_FILENAME = "schema.json";
///
- /// Constant for the location of validation configuration file
+ /// Constant for the suffix on validation file names
///
public const string VALIDATION_CONFIG_FILENAME = "validation.json";
+
+ ///
+ /// Constant for the suffix on calculation file names
+ ///
+ public const string CALCULATION_CONFIG_FILENAME = "calculation.json";
#pragma warning restore CA1707 // Identifiers should not contain underscores
///
@@ -95,15 +100,20 @@ public class AppSettings
public string RuleConfigurationJSONFileName { get; set; } = "RuleConfiguration.json";
///
- /// Gets or sets The JSON schema file name
+ /// Gets or sets the file names suffix for the json schema files
///
public string JsonSchemaFileName { get; set; } = JSON_SCHEMA_FILENAME;
///
- /// Gets or sets The JSON schema file name
+ /// Gets or sets the file names suffix for the validation files
///
public string ValidationConfigurationFileName { get; set; } = VALIDATION_CONFIG_FILENAME;
+ ///
+ /// Gets or sets the file names suffix for the calculation files
+ ///
+ public string CalculationConfigurationFileName { get; set; } = CALCULATION_CONFIG_FILENAME;
+
///
/// Gets or sets the filename for application meta data
///
diff --git a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
index ea5b298ea1..0238718fd7 100644
--- a/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
+++ b/src/Altinn.App.Core/Extensions/ServiceCollectionExtensions.cs
@@ -185,6 +185,7 @@ IWebHostEnvironment env
#pragma warning restore CS0618, CS0612 // Type or member is obsolete
services.TryAddTransient();
services.TryAddTransient();
+ services.TryAddTransient();
services.TryAddTransient();
services.TryAddTransient();
services.TryAddTransient();
@@ -192,6 +193,7 @@ IWebHostEnvironment env
services.TryAddTransient();
services.TryAddTransient();
services.TryAddTransient();
+ services.AddTransient();
services.AddSingleton();
services.AddTransient();
services.AddSingleton();
diff --git a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs
index f04d517c45..a78f276089 100644
--- a/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs
+++ b/src/Altinn.App.Core/Features/DataLists/InstanceDataListsFactory.cs
@@ -11,7 +11,7 @@ public class InstanceDataListsFactory
private readonly AppImplementationFactory _appImplementationFactory;
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
public InstanceDataListsFactory(IServiceProvider serviceProvider)
{
diff --git a/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
new file mode 100644
index 0000000000..13bba02d07
--- /dev/null
+++ b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculator.cs
@@ -0,0 +1,227 @@
+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 DataModelFieldCalculator
+{
+ private static readonly JsonSerializerOptions _jsonSerializerOptions = new()
+ {
+ ReadCommentHandling = JsonCommentHandling.Skip,
+ PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
+ };
+
+ private readonly ILogger _logger;
+ private readonly IAppResources _appResourceService;
+ private readonly ILayoutEvaluatorStateInitializer _layoutEvaluatorStateInitializer;
+ private readonly IDataElementAccessChecker _dataElementAccessChecker;
+ private readonly Telemetry? _telemetry;
+
+ public DataModelFieldCalculator(
+ ILogger 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 dataModelFieldCalculations = ParseDataModelFieldCalculationConfig(rawCalculationConfig);
+ var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement);
+
+ foreach (var (baseField, calculation) in dataModelFieldCalculations)
+ {
+ 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
+ && IsSameOrDescendantField(resolvedField.Field, d.Field)
+ )
+ )
+ {
+ continue;
+ }
+
+ var context = new ComponentContext(
+ evaluatorState,
+ component: null,
+ rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field),
+ dataElementIdentifier: resolvedField.DataElementIdentifier
+ );
+ var positionalArguments = new object[] { resolvedField.Field };
+
+ await RunCalculation(
+ formDataWrapper,
+ evaluatorState,
+ resolvedField,
+ context,
+ positionalArguments,
+ calculation
+ );
+ }
+ }
+ }
+
+ private async Task RunCalculation(
+ IFormDataWrapper formDataWrapper,
+ LayoutEvaluatorState evaluatorState,
+ DataReference resolvedField,
+ ComponentContext context,
+ object[] positionalArguments,
+ DataModelFieldCalculation calculation
+ )
+ {
+ try
+ {
+ var calculationResult = await ExpressionEvaluator.EvaluateExpressionToExpressionValue(
+ evaluatorState,
+ calculation.Expression,
+ 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)
+ {
+ _logger.LogError(e, "Error while evaluating calculation for field {Field}", resolvedField.Field);
+ throw;
+ }
+ }
+
+ private Dictionary ParseDataModelFieldCalculationConfig(
+ string rawCalculationConfig
+ )
+ {
+ JsonDocument calculationConfigDocument;
+ try
+ {
+ calculationConfigDocument = JsonDocument.Parse(
+ rawCalculationConfig,
+ new JsonDocumentOptions { CommentHandling = JsonCommentHandling.Skip }
+ );
+ }
+ catch (JsonException e)
+ {
+ _logger.LogError(e, "Failed to parse calculation configuration JSON");
+ return new Dictionary();
+ }
+ using (calculationConfigDocument)
+ {
+ var dataModelFieldCalculations = new Dictionary();
+ var hasCalculations = calculationConfigDocument.RootElement.TryGetProperty(
+ "calculations",
+ out JsonElement calculationsObject
+ );
+ if (hasCalculations)
+ {
+ foreach (var calculationArray in calculationsObject.EnumerateObject())
+ {
+ var field = calculationArray.Name;
+ var calculation = calculationArray.Value;
+ var resolvedDataModelFieldCalculation = ResolveDataModelFieldCalculation(field, calculation);
+ if (resolvedDataModelFieldCalculation == null)
+ {
+ _logger.LogError("Calculation for field {Field} could not be resolved", field);
+ continue;
+ }
+ dataModelFieldCalculations[field] = resolvedDataModelFieldCalculation;
+ }
+ }
+ return dataModelFieldCalculations;
+ }
+ }
+
+ private DataModelFieldCalculation? ResolveDataModelFieldCalculation(string field, JsonElement definition)
+ {
+ var dataModelFieldCalculationDefinition = definition.Deserialize(
+ _jsonSerializerOptions
+ );
+ if (dataModelFieldCalculationDefinition == null)
+ {
+ _logger.LogError("Calculation for field {Field} could not be parsed", field);
+ return null;
+ }
+
+ if (dataModelFieldCalculationDefinition.Expression == null)
+ {
+ _logger.LogError("Calculation for field {Field} is missing expression", field);
+ return null;
+ }
+
+ var dataModelFieldCalculation = new DataModelFieldCalculation
+ {
+ Expression = dataModelFieldCalculationDefinition.Expression.Value,
+ };
+
+ return dataModelFieldCalculation;
+ }
+
+ private static bool IsSameOrDescendantField(string candidate, string hiddenField)
+ {
+ if (candidate.Equals(hiddenField, StringComparison.Ordinal))
+ {
+ return true;
+ }
+
+ return candidate.StartsWith(hiddenField, StringComparison.Ordinal)
+ && candidate.Length > hiddenField.Length
+ && (candidate[hiddenField.Length] == '.' || candidate[hiddenField.Length] == '[');
+ }
+}
diff --git a/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs
new file mode 100644
index 0000000000..2491a5c484
--- /dev/null
+++ b/src/Altinn.App.Core/Features/DataProcessing/DataModelFieldCalculatorProcessor.cs
@@ -0,0 +1,37 @@
+using Altinn.App.Core.Models;
+
+namespace Altinn.App.Core.Features.DataProcessing;
+
+///
+/// Processing data model fields that is calculated by expressions provided in [modelName].calculation.json.
+///
+internal sealed class DataModelFieldCalculatorProcessor : IDataWriteProcessor
+{
+ private readonly DataModelFieldCalculator _dataModelFieldCalculator;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ ///
+ public DataModelFieldCalculatorProcessor(DataModelFieldCalculator dataModelFieldCalculator)
+ {
+ _dataModelFieldCalculator = dataModelFieldCalculator;
+ }
+
+ ///
+ /// Processes data write operations on properties in the data model.
+ ///
+ /// Object to fetch data elements not included in changes
+ /// The current task ID
+ /// Not used in this context
+ /// Not used in this context
+ public async Task ProcessDataWrite(
+ IInstanceDataMutator instanceDataMutator,
+ string taskId,
+ DataElementChanges changes,
+ string? language
+ )
+ {
+ await _dataModelFieldCalculator.Calculate(instanceDataMutator, taskId);
+ }
+}
diff --git a/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs
index 317191959c..acc961d743 100644
--- a/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs
+++ b/src/Altinn.App.Core/Features/DataProcessing/GenericDataProcessor.cs
@@ -16,7 +16,7 @@ public abstract class GenericDataProcessor : IDataProcessor
///
/// 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.
///
public abstract Task ProcessDataWrite(
Instance instance,
diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs
index 399cb752f8..ed7a7ae451 100644
--- a/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs
+++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.ApplicationMetadata.Service.cs
@@ -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");
diff --git a/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs b/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs
new file mode 100644
index 0000000000..f320c85535
--- /dev/null
+++ b/src/Altinn.App.Core/Features/Telemetry/Telemetry.DataFieldValueCalculator.cs
@@ -0,0 +1,20 @@
+using System.Diagnostics;
+using static Altinn.App.Core.Features.Telemetry.DataModelFieldCalculator;
+
+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 DataModelFieldCalculator
+ {
+ internal const string Prefix = "DataModelFieldCalculator";
+ }
+}
diff --git a/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs
index 3defcd9177..899b05ecfb 100644
--- a/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs
+++ b/src/Altinn.App.Core/Features/Validation/Default/ExpressionValidator.cs
@@ -147,7 +147,7 @@ internal async Task> ValidateFormData(
var context = new ComponentContext(
evaluatorState,
component: null,
- rowIndices: GetRowIndices(resolvedField.Field),
+ rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field),
dataElementIdentifier: resolvedField.DataElementIdentifier
);
var positionalArguments = new object[] { resolvedField.Field };
@@ -168,44 +168,6 @@ await RunValidation(
return validationIssues;
}
- private static int[]? GetRowIndices(string field)
- {
- Span rowIndicesSpan = stackalloc int[200]; // Assuming max 200 indices for simplicity recursion will never go deeper than 3-4
- int count = 0;
- for (int index = 0; index < field.Length; index++)
- {
- if (field[index] == '[')
- {
- int startIndex = index + 1;
- int endIndex = field.IndexOf(']', startIndex);
- if (endIndex == -1)
- {
- throw new InvalidOperationException($"Unpaired [ character in field: {field}");
- }
- string indexString = field[startIndex..endIndex];
- if (int.TryParse(indexString, out int rowIndex))
- {
- rowIndicesSpan[count] = rowIndex;
- count++;
- index = endIndex; // Move index to the end of the current bracket
- }
- else
- {
- throw new InvalidOperationException(
- $"Invalid row index in field: {field} at position {startIndex}"
- );
- }
- }
- }
- if (count == 0)
- {
- return null; // No indices found
- }
- int[] rowIndices = new int[count];
- rowIndicesSpan[..count].CopyTo(rowIndices);
- return rowIndices;
- }
-
private async Task RunValidation(
LayoutEvaluatorState evaluatorState,
List validationIssues,
diff --git a/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs b/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs
index 6c2fa1b89a..ab535df7d2 100644
--- a/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs
+++ b/src/Altinn.App.Core/Helpers/DataModel/DataModelWrapper.cs
@@ -122,6 +122,22 @@ ReadOnlySpan rowIndexes
/// ]
///
public string[] GetResolvedKeys(string field)
+ {
+ return GetResolvedKeys(field, isCalculating: false);
+ }
+
+ ///
+ /// Get all valid indexed keys for the field, depending on the number of rows in repeating groups
+ ///
+ ///
+ /// GetResolvedKeys("data.bedrifter.styre.medlemmer") =>
+ /// [
+ /// "data.bedrifter[0].styre.medlemmer",
+ /// "data.bedrifter[1].styre.medlemmer"
+ /// ...
+ /// ]
+ ///
+ public string[] GetResolvedKeys(string field, bool isCalculating)
{
if (_dataModel is null)
{
@@ -129,7 +145,7 @@ public string[] GetResolvedKeys(string field)
}
var fieldParts = field.Split('.');
- return GetResolvedKeysRecursive(fieldParts, _dataModel);
+ return GetResolvedKeysRecursive(fieldParts, _dataModel, _dataModel.GetType(), isCalculating: isCalculating);
}
private static string JoinFieldKeyParts(string? currentKey, string? key)
@@ -148,61 +164,111 @@ private static string JoinFieldKeyParts(string? currentKey, string? key)
private static string[] GetResolvedKeysRecursive(
string[] keyParts,
- object currentModel,
+ object? currentModel,
+ Type currentType,
int currentIndex = 0,
- string currentKey = ""
+ string currentKey = "",
+ bool isCalculating = false
)
{
- if (currentModel is null)
- {
- return [];
- }
-
if (currentIndex == keyParts.Length)
{
return [currentKey];
}
var (key, groupIndex) = ParseKeyPart(keyParts[currentIndex]);
- var prop = Array.Find(currentModel.GetType().GetProperties(), p => IsPropertyWithJsonName(p, key));
- var childModel = prop?.GetValue(currentModel);
- if (childModel is null)
+ var lookupType = currentModel?.GetType() ?? currentType;
+ var prop = Array.Find(lookupType.GetProperties(), p => IsPropertyWithJsonName(p, key));
+ if (prop is null)
{
return [];
}
- if (childModel is not string && childModel is System.Collections.IEnumerable childModelList)
+ var childType = prop.PropertyType;
+ // Check if the property is a collection type (but not string)
+ if (childType != typeof(string) && childType.IsAssignableTo(typeof(System.Collections.IEnumerable)))
{
- // childModel is a list
- if (groupIndex is null)
+ // For collections, we need the actual object to enumerate
+ var childModel = currentModel is not null ? prop.GetValue(currentModel) : null;
+ if (childModel is System.Collections.IEnumerable childModelList)
{
+ // Get the element type of the collection
+ var elementType = childType.IsArray
+ ? childType.GetElementType() ?? typeof(object)
+ : childType.GetGenericArguments().FirstOrDefault() ?? typeof(object);
// Index not specified, recurse on all elements
- int i = 0;
- var resolvedKeys = new List();
- foreach (var child in childModelList)
+ if (groupIndex is null)
+ {
+ int i = 0;
+ var resolvedKeys = new List();
+ foreach (var child in childModelList)
+ {
+ if (child is null)
+ {
+ i++;
+ continue;
+ }
+ var newResolvedKeys = GetResolvedKeysRecursive(
+ keyParts,
+ child,
+ elementType,
+ currentIndex + 1,
+ JoinFieldKeyParts(currentKey, $"{key}[{i}]"),
+ isCalculating
+ );
+ resolvedKeys.AddRange(newResolvedKeys);
+ i++;
+ }
+ return resolvedKeys.ToArray();
+ }
+ // Index specified, recurse on that element
+ var elementAt = GetElementAt(childModelList, groupIndex.Value);
+ if (elementAt is null)
{
- var newResolvedKeys = GetResolvedKeysRecursive(
- keyParts,
- child,
- currentIndex + 1,
- JoinFieldKeyParts(currentKey, key + "[" + i + "]")
- );
- resolvedKeys.AddRange(newResolvedKeys);
- i++;
+ return [];
}
- return resolvedKeys.ToArray();
+ return GetResolvedKeysRecursive(
+ keyParts,
+ elementAt,
+ elementType,
+ currentIndex + 1,
+ JoinFieldKeyParts(currentKey, $"{key}[{groupIndex}]"),
+ isCalculating
+ );
+ }
+
+ if (isCalculating && currentIndex == keyParts.Length - 1)
+ {
+ return [JoinFieldKeyParts(currentKey, key)];
}
- // Index specified, recurse on that element
- return GetResolvedKeysRecursive(
- keyParts,
- childModel,
- currentIndex + 1,
- JoinFieldKeyParts(currentKey, key + "[" + groupIndex + "]")
- );
+ return [];
}
- // Otherwise, just recurse
- return GetResolvedKeysRecursive(keyParts, childModel, currentIndex + 1, JoinFieldKeyParts(currentKey, key));
+ // For non-collection properties, we can work with just the type
+ // Get the child object for further traversal
+ var childValue = currentModel is not null ? prop.GetValue(currentModel) : null;
+ // If this is the last key part
+ if (currentIndex == keyParts.Length - 1)
+ {
+ // Return the key if value exists, or if calculating (to allow null fields during calculation)
+ return childValue is not null || isCalculating ? [JoinFieldKeyParts(currentKey, key)] : [];
+ }
+
+ // If child is null and we're not calculating, we can't continue
+ if (childValue is null && !isCalculating)
+ {
+ return [];
+ }
+
+ // Continue recursion using type information (childValue may be null if isCalculating=true)
+ return GetResolvedKeysRecursive(
+ keyParts,
+ childValue,
+ childType,
+ currentIndex + 1,
+ JoinFieldKeyParts(currentKey, key),
+ isCalculating
+ );
}
private static object? GetElementAt(System.Collections.IEnumerable enumerable, int index)
diff --git a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs
index 95be38aa60..2d36cb2524 100644
--- a/src/Altinn.App.Core/Implementation/AppResourcesSI.cs
+++ b/src/Altinn.App.Core/Implementation/AppResourcesSI.cs
@@ -523,4 +523,21 @@ private static byte[] ReadFileContentsFromLegalPath(string legalPath, string fil
return filedata;
}
+
+ ///
+ public string? GetCalculationConfiguration(string dataTypeId)
+ {
+ using var activity = _telemetry?.StartGetCalculationConfigurationActivity();
+ string legalPath = Path.Join(_settings.AppBasePath, _settings.ModelsFolder);
+ string filename = Path.Join(legalPath, $"{dataTypeId}.{_settings.CalculationConfigurationFileName}");
+ PathHelper.EnsureLegalPath(legalPath, filename);
+
+ string? fileData = null;
+ if (File.Exists(filename))
+ {
+ fileData = File.ReadAllText(filename, Encoding.UTF8);
+ }
+
+ return fileData;
+ }
}
diff --git a/src/Altinn.App.Core/Internal/App/IAppResources.cs b/src/Altinn.App.Core/Internal/App/IAppResources.cs
index 6a08382f28..d6fd507522 100644
--- a/src/Altinn.App.Core/Internal/App/IAppResources.cs
+++ b/src/Altinn.App.Core/Internal/App/IAppResources.cs
@@ -166,4 +166,10 @@ public interface IAppResources
/// Gets the validation configuration for a given data type
///
string? GetValidationConfiguration(string dataTypeId);
+
+ ///
+ /// Gets the calculation configuration for a given data type
+ ///
+ /// The calculation configuration in JSON format represented as string
+ string? GetCalculationConfiguration(string dataTypeId);
}
diff --git a/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs b/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
index be0cda83e4..9696087d89 100644
--- a/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
+++ b/src/Altinn.App.Core/Internal/Data/IFormDataWrapper.cs
@@ -285,7 +285,11 @@ private static int InvokeReturnIntOrError(MethodInfo info, object instance)
/// group[1].name
/// group[1].age
///
- public static DataReference[] GetResolvedKeys(this IFormDataWrapper formDataWrapper, DataReference reference)
+ public static DataReference[] GetResolvedKeys(
+ this IFormDataWrapper formDataWrapper,
+ DataReference reference,
+ bool isCalculating = false
+ )
{
//TODO: write more efficient code that uses the formDataWrapper to resolve keys instead of reflection in DataModelWrapper
var data = formDataWrapper.BackingData