Skip to content

Support lists in expressions#1769

Open
TomasEng wants to merge 10 commits into
mainfrom
support-lists-in-expressions
Open

Support lists in expressions#1769
TomasEng wants to merge 10 commits into
mainfrom
support-lists-in-expressions

Conversation

@TomasEng

@TomasEng TomasEng commented May 27, 2026

Copy link
Copy Markdown
Contributor

Description

This pull request adds support for arrays, along with a list function, in expressions.

  • list function is added
  • Array support is added to ExpressionValue (most of the code was already there, hidden in comments)
  • Made the tests support test files with several test cases and undefined root expression (currently, this is only supported for functions with a simple test file, but that is not the case for dataModel, of which the new file lookup-list.json contain several test cases)

Here is the corresponding change in App Frontend: Altinn/altinn-studio#18974

Related Issue(s)

Verification

  • Your code builds clean without any errors or warnings
  • Not possible to test manually until we have added aggregate functions (without which there is no use case for lists), but the tests should be sufficient
  • Relevant automated test added (if you find this hard, leave it and we'll help out)
  • All tests run green

Documentation

Summary by CodeRabbit

  • New Features

    • Added list expression support so expressions can create, return and evaluate array/list values.
  • Tests

    • Added and expanded fixtures and unit tests covering list creation, empty/nested/mixed lists, lookups, parsing/serialization, evaluation behavior, and related edge cases.

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds end-to-end array/list support: ExpressionValue gains array storage and conversions, JSON (de)serialization handles arrays, ExpressionFunction.list is added and evaluated by ExpressionEvaluator, and tests plus fixtures were added or updated for list behavior and lookups.

Changes

Array Value Support in Expression Evaluator

Layer / File(s) Summary
Array value support in ExpressionValue
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
ExpressionValue adds _arrayValue, constructor accepting JsonArray, implicit conversion operator, public Array property with validation, and extends FromObject/JsonElementToObject to convert enumerables and JSON arrays. ToObject() and ExpressionTypeUnionConverter read/write now handle arrays.
List expression function definition and evaluation
src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs, src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
ExpressionFunction enum gains new list member; ExpressionEvaluator routes ExpressionFunction.list in EvaluateExpression_internal to a new List helper that builds a JsonArray from the evaluated arguments and returns it as the expression result.
Test harness updates for nullable expressions and array handling
test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
ExpressionTestCaseRoot.Expression becomes nullable; test files add using directives and cast test.Expression to Expression with null-forgiving operators. TestFunctions adds List_Theory and updates RunTestCase to skip expression evaluation when test.Expression is null. Invalid-test invocation formatting updated to pass explicit awaited context.
Array value tests and test fixtures
test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs, test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/lookup-list.json, test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json
ExpressionValueTests expanded with JSON array parsing cases (empty, numeric, nested, mixed-type) and asserts that accessing .Array on Undefined/Null throws InvalidCastException. EqualsTests removes unsupported object[]/byte[] nonsense entries. New fixtures exercise list creation syntax, heterogeneous and nested lists, and lookups via data model/component expressions.
Test utilities: DynamicClassBuilder
test/Altinn.App.Core.Tests/LayoutExpressions/TestUtilities/DynamicClassBuilder.cs
DynamicClassBuilder updated to return object for empty or heterogeneous JSON arrays and adds HasDifferentTypes(JsonElement) helper.
Public API surface verification
test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
Public API snapshot records the new ExpressionValue constructor, Array property, implicit conversion operator, and list function enum member.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested labels

other

Suggested reviewers

  • ivarne
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 26.09% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Support lists in expressions' directly and accurately describes the primary objective of the pull request, which adds array/list support to the expression system.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch support-lists-in-expressions

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@TomasEng TomasEng force-pushed the support-lists-in-expressions branch 2 times, most recently from 51411b4 to 3b4b94b Compare May 27, 2026 08:17
@TomasEng TomasEng force-pushed the support-lists-in-expressions branch from 3b4b94b to e084500 Compare May 27, 2026 08:27
@TomasEng TomasEng added backport-ignore This PR is a new feature and should not be cherry-picked onto release branches feature Label Pull requests with new features. Used when generation releasenotes labels May 27, 2026
@TomasEng

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs (1)

651-654: ⚡ Quick win

Deserialize array directly to avoid an extra allocation.

Line 652 deserializes to List<ExpressionValue> and immediately copies with ToArray(). Deserializing directly to ExpressionValue[] avoids one allocation/copy.

Suggested refactor
-        var values =
-            JsonSerializer.Deserialize<List<ExpressionValue>>(ref reader, options)
-            ?? throw new JsonException("Expected EndArray token.");
-        return new ExpressionValue(values.ToArray());
+        var values =
+            JsonSerializer.Deserialize<ExpressionValue[]>(ref reader, options)
+            ?? throw new JsonException("Expected EndArray token.");
+        return new ExpressionValue(values);

As per coding guidelines: "Write efficient code - don't allocate unnecessarily (e.g., avoid calling ToString twice, prefer for loops over LINQ when appropriate)".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs` around lines 651
- 654, The code in ExpressionValue.cs deserializes into List<ExpressionValue>
then calls ToArray(), causing an extra allocation; change the
JsonSerializer.Deserialize call to deserialize directly into ExpressionValue[]
(i.e., JsonSerializer.Deserialize<ExpressionValue[]>(ref reader, options)) and
keep the null-check/JsonException behavior and the return new
ExpressionValue(array) using the deserialized array; update the local variable
name (e.g., values) as needed.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Around line 94-98: Constructor ExpressionValue(ExpressionValue[] value) stores
the caller's array reference into the private field _arrayValue and the array is
also exposed directly elsewhere (when ValueKind == JsonValueKind.Array),
allowing external mutation or a null value to leave the struct in an
inconsistent state; fix by null-guarding and defensively copying: in the
constructor set _arrayValue = (value == null) ? Array.Empty<ExpressionValue>() :
(ExpressionValue[])value.Clone() and ensure ValueKind is set to
JsonValueKind.Array only when the (non-null) copy is assigned, and in the public
accessor that currently exposes the internal array (the member that returns the
array when ValueKind == JsonValueKind.Array) return a copy (e.g.,
_arrayValue.Length == 0 ? Array.Empty<ExpressionValue>() :
(ExpressionValue[])_arrayValue.Clone()) instead of returning the internal
_arrayValue reference.

---

Nitpick comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Around line 651-654: The code in ExpressionValue.cs deserializes into
List<ExpressionValue> then calls ToArray(), causing an extra allocation; change
the JsonSerializer.Deserialize call to deserialize directly into
ExpressionValue[] (i.e., JsonSerializer.Deserialize<ExpressionValue[]>(ref
reader, options)) and keep the null-check/JsonException behavior and the return
new ExpressionValue(array) using the deserialized array; update the local
variable name (e.g., values) as needed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 08d275f1-888a-497c-9f83-05b7c9a6e7ee

📥 Commits

Reviewing files that changed from the base of the PR and between 10da3b0 and e084500.

📒 Files selected for processing (15)
  • src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
  • src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs
  • src/Altinn.App.Core/Models/Expressions/ExpressionFunction.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/ExpressionTestCaseRoot.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestBackendExclusiveFunctions.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestFunctions.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/TestInvalid.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/component/lookup-list.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/array-is-null.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/lookup-list.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/list/list.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/ExpressionValueTests.cs
  • test/Altinn.App.Core.Tests/LayoutExpressions/TestUtilities/DynamicClassBuilder.cs
  • test/Altinn.App.Core.Tests/PublicApiTests.PublicApi_ShouldNotChange_Unintentionally.verified.txt
💤 Files with no reviewable changes (2)
  • test/Altinn.App.Core.Tests/LayoutExpressions/CommonTests/shared-tests/functions/dataModel/array-is-null.json
  • test/Altinn.App.Core.Tests/LayoutExpressions/ExpressionEvaluatorTests/EqualsTests.cs

Comment thread src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs Outdated
@TomasEng

Copy link
Copy Markdown
Contributor Author

@coderabbitai

Deserialize array directly to avoid an extra allocation.

Fixed.

@coderabbitai

coderabbitai Bot commented May 27, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@TomasEng Thanks for the fix!

🐇 ✨


If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@TomasEng

Copy link
Copy Markdown
Contributor Author

The Sonar errors on this pull request seem to be related to existing code only.

@TomasEng TomasEng added the squad/data Issues that belongs to the named squad. label May 27, 2026
@TomasEng TomasEng moved this to 🔎 In review in Team Altinn Studio May 27, 2026
@TomasEng TomasEng marked this pull request as ready for review May 27, 2026 09:57
@TomasEng TomasEng mentioned this pull request May 29, 2026
4 tasks

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs (1)

199-210: ⚠️ Potential issue | 🔴 Critical

Fix/clarify ExpressionValue.ToObject() array return type contract

ExpressionValue.ToObject() maps JsonValueKind.Array to the Array property, and Array is typed as ExpressionValue[], so ToObject() returns the internal wrapper array (not an object?[] of unwrapped CLR element values). Callers that need CLR-shaped arrays must explicitly unwrap elements (e.g., via ExpressionValue[]ToObject() per element), or this behavior needs to be clearly documented as the intended API contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs` around lines 199
- 210, The ToObject() method currently returns the internal ExpressionValue[]
wrapper for JsonValueKind.Array via the Array property, which leaks internal
wrappers; change the JsonValueKind.Array branch in ExpressionValue.ToObject() to
return a CLR object?[] by unwrapping each element (call
ExpressionValue.ToObject() on each item of the Array property and produce an
object?[] result) so callers get a native array of element values rather than
ExpressionValue instances; ensure you still return null/primitive CLR types for
other kinds and keep the method signature as object?.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Around line 199-210: The ToObject() method currently returns the internal
ExpressionValue[] wrapper for JsonValueKind.Array via the Array property, which
leaks internal wrappers; change the JsonValueKind.Array branch in
ExpressionValue.ToObject() to return a CLR object?[] by unwrapping each element
(call ExpressionValue.ToObject() on each item of the Array property and produce
an object?[] result) so callers get a native array of element values rather than
ExpressionValue instances; ensure you still return null/primitive CLR types for
other kinds and keep the method signature as object?.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df09f1b0-d9ab-443c-9b17-dc4762959fcf

📥 Commits

Reviewing files that changed from the base of the PR and between b58a3af and 739cc4e.

📒 Files selected for processing (1)
  • src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

await ExpressionEvaluator.EvaluateExpression(
state,
test.Expression,
(Expression)test.Expression!,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these should be test.Expression.Value

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, fixed!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs (2)

1-1: ⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Remove unused using System.Collections; directive — this is causing the pipeline failure.

The pipeline reports IDE0005: Using directive is unnecessary in this file. After refactoring array storage from ExpressionValue[] to JsonArray, the non-generic System.Collections namespace is no longer used.

🔧 Proposed fix
-using System.Collections;
 using System.Diagnostics;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs` at line 1, The
IDE0005 pipeline failure is caused by the now-unused using directive "using
System.Collections;" in ExpressionValue.cs; remove that using line from the top
of the file so only required namespaces remain (leave references for
JsonArray/ExpressionValue types intact) and rebuild to confirm IDE0005 is
resolved.

286-298: ⚠️ Potential issue | 🟠 Major

Handle JsonValueKind.Array in ExpressionValue string conversions (or block it earlier)

ExpressionValue.ToString(), ToStringForText(), and ExpressionValue.ToStringForEquals() intentionally have the Object/Array serialization commented out and currently throw for JsonValueKind.Array (InvalidOperationException / NotImplementedException) instead (see ExpressionValue.cs around 286-320 and 329-348).

This conflicts with existing expression support: ExpressionEvaluator’s ExpressionFunction.list returns a JsonArray, while functions like concat/contains call ToStringForEquals() on their arguments, so a list result used in those contexts will fail. Similarly, TranslationService renders text resources via fieldValue.ToStringForText(), so list-valued data model fields will fail during text rendering.

Is this deferral intentional until aggregate/list-to-text support is implemented, or should array serialization be added now (e.g., JSON serialization with the existing serializer options) and/or replaced with proper expression type errors instead of throwing?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs` around lines 286
- 298, ExpressionValue.ToString()/ToStringForText()/ToStringForEquals()
currently throw on JsonValueKind.Array/Object causing list-valued expressions
(e.g., ExpressionEvaluator.ExpressionFunction.list) to break; implement proper
serialization instead of throwing: add cases for JsonValueKind.Array and
JsonValueKind.Object in ExpressionValue.ToString(), ToStringForText(), and
ToStringForEquals() that call JsonSerializer.Serialize(Array/Object,
_unsafeSerializerOptionsForSerializingDates) (mirror the String handling) so
arrays and objects are converted to stable JSON for text/equals comparisons;
keep existing serializer options and error branch for unknown kinds and add unit
tests covering list return values used with concat/contains and
TranslationService rendering.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs`:
- Line 1: The IDE0005 pipeline failure is caused by the now-unused using
directive "using System.Collections;" in ExpressionValue.cs; remove that using
line from the top of the file so only required namespaces remain (leave
references for JsonArray/ExpressionValue types intact) and rebuild to confirm
IDE0005 is resolved.
- Around line 286-298:
ExpressionValue.ToString()/ToStringForText()/ToStringForEquals() currently throw
on JsonValueKind.Array/Object causing list-valued expressions (e.g.,
ExpressionEvaluator.ExpressionFunction.list) to break; implement proper
serialization instead of throwing: add cases for JsonValueKind.Array and
JsonValueKind.Object in ExpressionValue.ToString(), ToStringForText(), and
ToStringForEquals() that call JsonSerializer.Serialize(Array/Object,
_unsafeSerializerOptionsForSerializingDates) (mirror the String handling) so
arrays and objects are converted to stable JSON for text/equals comparisons;
keep existing serializer options and error branch for unknown kinds and add unit
tests covering list return values used with concat/contains and
TranslationService rendering.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afe81d00-257b-4270-b229-4b3fe8c515f0

📥 Commits

Reviewing files that changed from the base of the PR and between 739cc4e and f24f67b.

📒 Files selected for processing (2)
  • src/Altinn.App.Core/Internal/Expressions/ExpressionEvaluator.cs
  • src/Altinn.App.Core/Internal/Expressions/ExpressionValue.cs

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
B Maintainability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backport-ignore This PR is a new feature and should not be cherry-picked onto release branches feature Label Pull requests with new features. Used when generation releasenotes squad/data Issues that belongs to the named squad.

Projects

Status: 🔎 In review

Development

Successfully merging this pull request may close these issues.

2 participants