-
Notifications
You must be signed in to change notification settings - Fork 25
Set data field by expression #1683
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 4 commits
43f09b3
03339cd
dd637ed
53f29af
a11d093
ce5f067
c589a4c
1ea1ece
1f961e4
e9712ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,331 @@ | ||
| 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.DependencyInjection; | ||
| using Microsoft.Extensions.Logging; | ||
| using ComponentContext = Altinn.App.Core.Models.Expressions.ComponentContext; | ||
|
|
||
| namespace Altinn.App.Core.Features.DataProcessing; | ||
|
|
||
| public class DataFieldValueCalculator | ||
|
Check warning on line 14 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| { | ||
| 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; | ||
|
|
||
| public DataFieldValueCalculator( | ||
| ILogger<DataFieldValueCalculator> logger, | ||
| ILayoutEvaluatorStateInitializer layoutEvaluatorStateInitializer, | ||
| IAppResources appResourceService, | ||
| IServiceProvider serviceProvider | ||
| ) | ||
| { | ||
| _logger = logger; | ||
| _appResourceService = appResourceService; | ||
| _layoutEvaluatorStateInitializer = layoutEvaluatorStateInitializer; | ||
| _dataElementAccessChecker = serviceProvider.GetRequiredService<IDataElementAccessChecker>(); | ||
| } | ||
|
|
||
| public async Task Calculate(IInstanceDataAccessor dataAccessor, string taskId) | ||
|
Check warning on line 40 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| { | ||
| foreach (var (dataType, dataElement) in dataAccessor.GetDataElementsWithFormDataForTask(taskId)) | ||
| { | ||
| if (await _dataElementAccessChecker.CanRead(dataAccessor.Instance, dataType) is false) | ||
|
Check warning on line 44 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var calculationConfig = _appResourceService.GetCalculationConfiguration(dataType.Id); | ||
| if (!string.IsNullOrEmpty(calculationConfig)) | ||
| { | ||
| await CalculateFormData(dataAccessor, dataElement, taskId, calculationConfig); | ||
| } | ||
| } | ||
| } | ||
|
Comment on lines
+42
to
+58
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add OpenTelemetry instrumentation for this feature path. This new calculation engine currently logs, but lacks explicit tracing/metrics around calculation lifecycle and failures. Please add spans/counters (e.g., per data element, per calculation, failures, conversion failures). As per coding guidelines " 🤖 Prompt for AI Agents |
||
|
|
||
| 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); | ||
|
|
||
| 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) | ||
| ) | ||
|
Comment on lines
+85
to
+88
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🐛 Proposed fix- if (
- hiddenFields.Exists(d =>
- d.DataElementIdentifier == resolvedField.DataElementIdentifier
- && resolvedField.Field.StartsWith(d.Field, StringComparison.InvariantCulture)
- )
- )
+ if (
+ hiddenFields.Exists(d =>
+ d.DataElementIdentifier == resolvedField.DataElementIdentifier
+ && IsSameOrDescendantField(resolvedField.Field, d.Field)
+ )
+ )
{
continue;
}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] == '[');
}🤖 Prompt for AI Agents |
||
| ) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| var context = new ComponentContext( | ||
| evaluatorState, | ||
| component: null, | ||
| rowIndices: ExpressionHelper.GetRowIndices(resolvedField.Field), | ||
| dataElementIdentifier: resolvedField.DataElementIdentifier | ||
| ); | ||
| var positionalArguments = new object[] { resolvedField.Field }; | ||
| var formDataWrapper = await dataAccessor.GetFormDataWrapper(dataElement); | ||
| 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 140 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| { | ||
| _logger.LogError(e, "Error while evaluating calculation for field {field}", resolvedField.Field); | ||
|
Check warning on line 142 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| throw; | ||
| } | ||
| } | ||
|
|
||
| private Dictionary<string, List<DataFieldCalculation>> ParseDataFieldCalculationConfig( | ||
|
Check failure on line 147 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| string rawCalculationConfig, | ||
| ILogger<DataFieldValueCalculator> logger | ||
| ) | ||
| { | ||
| using var calculationConfigDocument = JsonDocument.Parse(rawCalculationConfig); | ||
| var calculationDefinitions = new Dictionary<string, RawDataFieldValueCalculation>(); | ||
| var hasDefinitions = calculationConfigDocument.RootElement.TryGetProperty( | ||
| "definitions", | ||
| out JsonElement definitionsObject | ||
| ); | ||
| if (hasDefinitions) | ||
| { | ||
| foreach (var definitionProperty in definitionsObject.EnumerateObject()) | ||
| { | ||
| var resolvedDefinition = ResolveCalculationDefinition( | ||
| definitionProperty, | ||
| calculationDefinitions, | ||
| logger | ||
| ); | ||
| if (resolvedDefinition == null) | ||
| { | ||
| logger.LogError("Calculation definition {name} could not be resolved", definitionProperty.Name); | ||
|
Check warning on line 169 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| continue; | ||
| } | ||
| calculationDefinitions[definitionProperty.Name] = resolvedDefinition; | ||
| } | ||
| } | ||
|
|
||
| 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>(); | ||
|
Check warning on line 191 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| dataFieldCalculations[field] = dataFieldCalculation; | ||
| } | ||
| var resolvedDataFieldCalculation = ResolveDataFieldCalculation( | ||
| field, | ||
| calculation, | ||
| calculationDefinitions, | ||
| logger | ||
| ); | ||
| if (resolvedDataFieldCalculation == null) | ||
| { | ||
| logger.LogError("Calculation for field {field} could not be resolved", field); | ||
|
Check warning on line 202 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| continue; | ||
| } | ||
| dataFieldCalculation.Add(resolvedDataFieldCalculation); | ||
| } | ||
| } | ||
| } | ||
| return dataFieldCalculations; | ||
| } | ||
|
|
||
| private DataFieldCalculation? ResolveDataFieldCalculation( | ||
|
Check failure on line 212 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| string field, | ||
| JsonElement definition, | ||
| Dictionary<string, RawDataFieldValueCalculation> resolvedDefinitions, | ||
| 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); | ||
|
Check warning on line 226 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| return null; | ||
| } | ||
|
|
||
| var reference = resolvedDefinitions.GetValueOrDefault(stringReference); | ||
| if (reference == null) | ||
| { | ||
| logger.LogError( | ||
| "Could not resolve reference {stringReference} for calculation for field {field}", | ||
|
Check warning on line 234 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| stringReference, | ||
| field | ||
| ); | ||
| return null; | ||
| } | ||
| rawDataFieldValueCalculation.Condition = reference.Condition; | ||
| } | ||
| 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.Ref != null) | ||
| { | ||
| var reference = resolvedDefinitions.GetValueOrDefault(dataFieldCalculationDefinition.Ref); | ||
| if (reference == null) | ||
| { | ||
| logger.LogError( | ||
| "Could not resolve reference {expressionDefinitionRef} for calculation for field {field}", | ||
| dataFieldCalculationDefinition.Ref, | ||
| field | ||
| ); | ||
| return null; | ||
| } | ||
| rawDataFieldValueCalculation.Condition = reference.Condition; | ||
| } | ||
|
|
||
| if (dataFieldCalculationDefinition.Condition != null) | ||
| { | ||
| rawDataFieldValueCalculation.Condition = dataFieldCalculationDefinition.Condition; | ||
| } | ||
| } | ||
|
|
||
| if (rawDataFieldValueCalculation.Condition == null) | ||
| { | ||
| logger.LogError("Calculation for field {field} is missing condition", field); | ||
|
Check warning on line 276 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| return null; | ||
| } | ||
|
|
||
| var dataFieldCalculation = new DataFieldCalculation | ||
| { | ||
| Condition = rawDataFieldValueCalculation.Condition.Value, | ||
| }; | ||
|
|
||
| return dataFieldCalculation; | ||
| } | ||
|
|
||
| private static RawDataFieldValueCalculation? ResolveCalculationDefinition( | ||
| JsonProperty definitionProperty, | ||
| Dictionary<string, RawDataFieldValueCalculation> resolvedDefinitions, | ||
| ILogger logger | ||
| ) | ||
| { | ||
| var resolvedDefinition = new RawDataFieldValueCalculation(); | ||
| var rawDefinition = definitionProperty.Value.Deserialize<RawDataFieldValueCalculation>(_jsonSerializerOptions); | ||
| if (rawDefinition == null) | ||
| { | ||
| logger.LogError("Calculation definition {name} could not be parsed", definitionProperty.Name); | ||
|
Check warning on line 298 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| return null; | ||
| } | ||
|
|
||
| if (rawDefinition.Ref != null) | ||
| { | ||
| var reference = resolvedDefinitions.GetValueOrDefault(rawDefinition.Ref); | ||
| if (reference == null) | ||
| { | ||
| logger.LogError( | ||
| "Could not resolve reference {rawDefinitionRef} for calculation {name}", | ||
| rawDefinition.Ref, | ||
| definitionProperty.Name | ||
| ); | ||
| return null; | ||
| } | ||
|
|
||
| resolvedDefinition.Condition = reference.Condition; | ||
| } | ||
|
|
||
| if (rawDefinition.Condition != null) | ||
| { | ||
| resolvedDefinition.Condition = rawDefinition.Condition; | ||
| } | ||
|
|
||
| if (resolvedDefinition.Condition == null) | ||
| { | ||
| logger.LogError("Calculation {name} is missing condition", definitionProperty.Name); | ||
|
Check warning on line 325 in src/Altinn.App.Core/Features/DataProcessing/DataFieldValueCalculator.cs
|
||
| return null; | ||
| } | ||
|
|
||
| return resolvedDefinition; | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: Altinn/app-lib-dotnet
Length of output: 10062
Use
AddTransientinstead ofTryAddTransientforIDataWriteProcessor.The code uses
GetAll<IDataWriteProcessor>()to retrieve all registered implementations (InternalPatchService.cs line 269), iterating over each in a loop. WithTryAddTransient, if an app registers its ownIDataWriteProcessor, the core'sDataFieldValueCalculatorProcessorwill be skipped entirely, preventing the default processor from executing alongside app-provided implementations.Change line 185 to
services.AddTransient<IDataWriteProcessor, DataFieldValueCalculatorProcessor>();to ensure the core processor is always included.Additionally,
DataFieldValueCalculatorProcessorshould be markedsealedper coding guidelines unless inheritance is a valid use-case.🤖 Prompt for AI Agents