Skip to content

Improve Avalonia Support#562

Merged
Daniel-Svensson merged 12 commits intoOpenRIAServices:mainfrom
Daniel-Svensson:feature/avaolonia
Apr 8, 2026
Merged

Improve Avalonia Support#562
Daniel-Svensson merged 12 commits intoOpenRIAServices:mainfrom
Daniel-Svensson:feature/avaolonia

Conversation

@Daniel-Svensson
Copy link
Copy Markdown
Member

@Daniel-Svensson Daniel-Svensson commented Apr 6, 2026

When binding collections avalonia requires any collection that does implement INotifyCollectionChanged to also implement IList.

  • Implement IList and IRadOnlyList for EntitySet and EntityCollection
  • Make Contains(entity) public
  • Add public indexer to EntityCollection and EnriryResult
  • Various small fixes

Summary by CodeRabbit

  • New Features

    • Collections and load results now expose indexed access and implement standard list/read-only list interfaces, including IList-compatible behavior.
  • Bug Fixes / Behavior

    • Removal and change notifications are more precise: per-item remove notifications use stable indexes and membership checks.
    • Lookup/Contains and IList operations are more consistent and reliably report presence/removal results.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

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

EntityCollection, EntitySet, and LoadResult were changed to expose indexable interfaces and indexers (IList / IReadOnlyList), made removals index-aware (TryRemoveEntity / RemoveEntity(entity, index)), and adjusted collection-change notification flows for per-item remove and batched add notifications.

Changes

Cohort / File(s) Summary
EntityCollection core & API
src/OpenRiaServices.Client/Framework/EntityCollection.cs
Now implements IList and IReadOnlyList<TEntity>; adds public TEntity this[int index]; explicit non-generic IList/ICollection members added; IEntityCollection.Entities returns this.
EntityCollection removal & notifications
src/OpenRiaServices.Client/Framework/EntityCollection.cs
Replaced RemoveEntity(TEntity) with RemoveEntity(TEntity, int index) and added TryRemoveEntity(TEntity, out int index). Removal call sites updated to use index-aware removal. Source removal handling now removes per-item and raises per-entity remove notifications. Add handling materializes with ToList() and computes start index from Entities.Count for batched add notifications.
EntitySet surface & typed improvements
src/OpenRiaServices.Client/Framework/EntitySet.cs
EntitySet now explicitly implements IList (explicit members delegate or throw). EntitySet<TEntity> implements IReadOnlyList<TEntity> with an indexer; introduced typed List<TEntity> view, made Contains(Entity) public, and switched entity creation to Activator.CreateInstance<TEntity>().
LoadResult indexing
src/OpenRiaServices.Client/Framework/LoadResult.cs
LoadResult<TEntity> changed from IReadOnlyCollection to IReadOnlyList<TEntity], adds public TEntity this[int index] and uses Data.ReadOnlyObservableLoaderCollection<TEntity> as backing collection; Count made expression-bodied.
CollectionView proxy adjustments
src/OpenRiaServices.Client/Framework/...ListCollectionViewProxy<T>
Simplified proxy IList implementations to delegate to underlying EntitySet/its List; removed proxy-specific casting/validation and adjusted Remove/RemoveAt to act on source list.

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Coll as EntityCollection<T>
    participant Source as SourceSet
    participant Events as NotificationSystem

    rect rgba(100,150,200,0.5)
        Note over Client,Coll: Client requests removal
        Client->>Coll: Remove(entity)
        Coll->>Coll: TryRemoveEntity(entity, out idx)
        alt removed (idx >= 0)
            Coll->>Events: Notify Remove at idx
        else not present
            Coll->>Events: (no notification)
        end
    end

    rect rgba(150,100,200,0.5)
        Note over Source,Coll: SourceSet collection changed
        Source->>Coll: SourceSet_CollectionChanged(args)
        alt items added
            Coll->>Coll: materialize new items (ToList), compute newStartingIdx = Entities.Count
            Coll->>Events: Notify Add range starting at newStartingIdx
        else items removed
            loop per removed entity
                Coll->>Coll: TryRemoveEntity(entity, out idx)
                alt removed (idx >= 0)
                    Coll->>Events: Notify Remove at idx for that entity
                end
            end
        end
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 I hopped through lists both new and old,
I counted friends by index, brave and bold,
Each removal noted, numbered just so,
Adds arrive in order — now watch them grow,
A rabbit cheers for tidy rows 🌷

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The PR title 'Improve Avalonia Support' is vague and overly broad, lacking specific details about the actual changes made to the codebase. Consider a more specific title that captures the main technical change, such as 'Implement IList and IReadOnlyList interfaces for entity collections' or 'Add IList implementation to EntitySet and EntityCollection for Avalonia compatibility'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

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

⚠️ Outside diff range comments (1)
src/OpenRiaServices.Client/Framework/EntityCollection.cs (1)

1018-1051: 🧹 Nitpick | 🔵 Trivial

IReadOnlyList<TEntity> indexer does not call Load().

Line 1019: The indexer accesses Entities[index] directly without calling Load(). If accessed before Count (which triggers Load()), this could return incorrect results or throw. Consider calling Load() for safety, or document that Count must be accessed first.

♻️ Option to add Load() call for safety
         `#region` ICollection<TEntity>, IReadOnlyList<TEntity> Members
-        TEntity IReadOnlyList<TEntity>.this[int index] => Entities[index];
+        TEntity IReadOnlyList<TEntity>.this[int index]
+        {
+            get
+            {
+                Load();
+                return Entities[index];
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs` around lines 1018 -
1051, The IReadOnlyList<TEntity> indexer getter currently returns
Entities[index] directly (IReadOnlyList<TEntity>.this[int index] =>
Entities[index]); modify this getter to call Load() before accessing Entities to
ensure the collection is initialized (i.e., call this.Load() then return
Entities[index]); update any related tests or callers if necessary to account
for potential lazy loading side-effects.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs`:
- Around line 1116-1119: The IList.Remove implementation in EntityCollection
currently casts the incoming object directly and will throw InvalidCastException
for non-TEntity values; update the IList.Remove method to first check the type
(e.g., use "is TEntity" or "as TEntity") and only call Remove(TEntity) when the
value is a TEntity, otherwise do nothing (mirror the safe behavior of
IList.Contains) to avoid exceptions for wrong types.

In `@src/OpenRiaServices.Client/Framework/EntitySet.cs`:
- Around line 985-988: The NotSupportedException in IList.RemoveAt (in class
EntitySet) has a typo in its message ("RemoteAt"); update the format argument to
"RemoveAt" so the Resource.IsNotSupported message correctly references the
RemoveAt operation (i.e., change the string passed into the string.Format call
from "RemoteAt" to "RemoveAt" in the IList.RemoveAt method).
- Around line 1271-1272: The List property currently references itself causing
infinite recursion; change its getter to return the base class list cast to
List<TEntity> (i.e., use base.List) so the property reads the inherited
collection instead of calling itself; update the property declared as "private
new List<TEntity> List" to return (List<TEntity>)base.List to fix
StackOverflowException when accessing List in EntitySet or the List property
accessor.
- Around line 11-12: Remove the unused using directive "using
System.Diagnostics.Contracts;" from the top of the file; locate the EntitySet
class in src/OpenRiaServices.Client/Framework/EntitySet.cs and delete that using
line so only actually referenced namespaces remain, ensuring no other code in
EntitySet (constructors, methods, properties) depends on
System.Diagnostics.Contracts.

---

Outside diff comments:
In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs`:
- Around line 1018-1051: The IReadOnlyList<TEntity> indexer getter currently
returns Entities[index] directly (IReadOnlyList<TEntity>.this[int index] =>
Entities[index]); modify this getter to call Load() before accessing Entities to
ensure the collection is initialized (i.e., call this.Load() then return
Entities[index]); update any related tests or callers if necessary to account
for potential lazy loading side-effects.
🪄 Autofix (Beta)

❌ Autofix failed (check again to retry)

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: ASSERTIVE

Plan: Pro

Run ID: ee3e524b-b64f-4539-83bf-d2f17b0ef8bf

📥 Commits

Reviewing files that changed from the base of the PR and between 84dd62c and a08aa46.

📒 Files selected for processing (2)
  • src/OpenRiaServices.Client/Framework/EntityCollection.cs
  • src/OpenRiaServices.Client/Framework/EntitySet.cs

@Daniel-Svensson
Copy link
Copy Markdown
Member Author

Todo: look at LoadOperation and LoadResult

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

Note

Autofix is a beta feature. Expect some limitations and changes as we gather feedback and continue to improve it.

❌ Failed to clone repository into sandbox. Please try again.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OpenRiaServices.Client/Framework/EntitySet.cs`:
- Line 21: The IList capability flags on EntitySet are inconsistent with
ListCollectionViewProxy<T>; change the IList implementations on EntitySet so
IList.IsFixedSize and IList.IsReadOnly mirror the underlying add/remove
capabilities the same way the proxy does: compute both properties as
!(this.CanAdd || this.CanRemove) (or equivalent using the EntitySet's
CanAdd/CanRemove members) rather than hardcoding IsFixedSize=false and relying
on IsReadOnly fallback; update the IList explicit interface implementations in
EntitySet to return that computed value so direct IList callers see the same
behavior as ListCollectionViewProxy<T>.
- Around line 955-960: IList.Add(object) currently assumes only one item is
added and returns -1 when Add(Entity) (which may call AddAttachInferrer.Infer())
expands the set by more than one; change the implementation so if Count !=
countBefore + 1 it returns the actual index of the inserted entity by calling
IndexOf((Entity)value) (or similar) instead of -1, otherwise keep returning
countBefore; update the IList.Add(object) method to use IndexOf when infer-add
expanded the collection.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: 38e05d74-91e8-4043-b060-6c969c8a7281

📥 Commits

Reviewing files that changed from the base of the PR and between a08aa46 and c089bf7.

📒 Files selected for processing (1)
  • src/OpenRiaServices.Client/Framework/EntitySet.cs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/OpenRiaServices.Client/Framework/EntitySet.cs (1)

946-950: ⚠️ Potential issue | 🟠 Major

Advertise fixed-size correctly on read-only sets.

When neither add nor remove is supported, IList.IsReadOnly becomes true but IList.IsFixedSize stays false. ListCollectionViewProxy<T> now forwards these flags from EntitySet, so view code still sees immutable sets as resizable. Mirror the same capability check in both properties.

♻️ Proposed fix
-bool IList.IsFixedSize => false;
-
-bool IList.IsReadOnly => (_supportedOperations & (EntitySetOperations.Remove | EntitySetOperations.Add)) == 0;
+bool IList.IsFixedSize => !(this.CanAdd || this.CanRemove);
+
+bool IList.IsReadOnly => !(this.CanAdd || this.CanRemove);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntitySet.cs` around lines 946 - 950,
IList.IsFixedSize is incorrectly always false while IList.IsReadOnly correctly
checks supported operations; make IsFixedSize mirror the same capability check
as IsReadOnly by returning true when neither EntitySetOperations.Add nor
EntitySetOperations.Remove is supported. Update the IList.IsFixedSize getter in
EntitySet (and keep IList.IsReadOnly as-is) to use the same bitmask check
against _supportedOperations (EntitySetOperations.Remove |
EntitySetOperations.Add) so immutable sets advertise fixed size consistently for
ListCollectionViewProxy<T> and other consumers.
src/OpenRiaServices.Client/Framework/EntityCollection.cs (1)

1116-1119: ⚠️ Potential issue | 🟠 Major

Make direct IList.Remove tolerant of probe calls.

This still casts object directly and then delegates to the strict Remove(TEntity) path. That turns wrong-type or already-removed values into exceptions on the new IList surface instead of a no-op.

♻️ Proposed fix
 void IList.Remove(object value)
 {
-    Remove((TEntity)value);
+    if (value is TEntity entity)
+    {
+        ((ICollection<TEntity>)this).Remove(entity);
+    }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs` around lines 1116 -
1119, The IList.Remove implementation should be tolerant of probe/wrong-type
calls: instead of unconditionally casting object to TEntity, first check the
runtime type and only call Remove(TEntity) when value is a TEntity (e.g., use
"if (value is TEntity entity) Remove(entity);" or equivalent); otherwise
return/no-op so wrong-type or already-removed/null probe calls do not throw on
the IList surface.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs`:
- Around line 1084-1090: IList.Add currently skips validation and returns
countBefore when it assumes exactly one item added; instead mirror
EntityCollection<TEntity>.ListCollectionViewProxy<T>.Add by validating/casting
the input (throwing the same ArgumentException/InvalidCast as Add(TEntity)
does), call the collection add logic (Add((TEntity)value) or underlying
Source.Add), then call IndexOf on the added entity to determine and return the
actual insertion index (or -1 if not found) so side-effects that add multiple
items still yield the correct index; use the same validation and IndexOf lookup
code paths as the proxy Add to keep behavior consistent.
- Around line 1018-1020: The public indexer TEntity this[int index] currently
returns Entities[index] without ensuring the collection is loaded; modify the
indexer to call Load() before accessing Entities to guarantee the cache is
populated (matching Count and GetEnumerator behavior) and update its comment to
document that Load() is invoked; apply the same change to the other public
indexer occurrences around lines referenced (e.g., the IList indexer already
documents expectation—mirror that behavior by invoking Load() in the public
indexer and any similar accessors such as the one at 1074-1082).

In `@src/OpenRiaServices.Client/Framework/EntitySet.cs`:
- Around line 985-988: IList.Remove(object) currently casts value to Entity and
can throw InvalidCastException; change its implementation to mirror
Contains/IndexOf by first checking "value is Entity e" and only call Remove(e)
when true, otherwise do nothing (silently return) so incompatible types or null
don't cause exceptions—update the IList.Remove(object) method that currently
calls Remove((Entity)value) to use the safe type-check pattern.

---

Duplicate comments:
In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs`:
- Around line 1116-1119: The IList.Remove implementation should be tolerant of
probe/wrong-type calls: instead of unconditionally casting object to TEntity,
first check the runtime type and only call Remove(TEntity) when value is a
TEntity (e.g., use "if (value is TEntity entity) Remove(entity);" or
equivalent); otherwise return/no-op so wrong-type or already-removed/null probe
calls do not throw on the IList surface.

In `@src/OpenRiaServices.Client/Framework/EntitySet.cs`:
- Around line 946-950: IList.IsFixedSize is incorrectly always false while
IList.IsReadOnly correctly checks supported operations; make IsFixedSize mirror
the same capability check as IsReadOnly by returning true when neither
EntitySetOperations.Add nor EntitySetOperations.Remove is supported. Update the
IList.IsFixedSize getter in EntitySet (and keep IList.IsReadOnly as-is) to use
the same bitmask check against _supportedOperations (EntitySetOperations.Remove
| EntitySetOperations.Add) so immutable sets advertise fixed size consistently
for ListCollectionViewProxy<T> and other consumers.
🪄 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: ASSERTIVE

Plan: Pro

Run ID: ffcc4c47-7b9f-4bb7-9755-aaea82d1e515

📥 Commits

Reviewing files that changed from the base of the PR and between c089bf7 and dcc1b20.

📒 Files selected for processing (3)
  • src/OpenRiaServices.Client/Framework/EntityCollection.cs
  • src/OpenRiaServices.Client/Framework/EntitySet.cs
  • src/OpenRiaServices.Client/Framework/LoadResult.cs

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (3)
src/OpenRiaServices.Client/Framework/EntitySet.cs (1)

985-995: ⚠️ Potential issue | 🟡 Minor

Short-circuit incompatible or missing items before IList.Remove.

value is Entity still lets a different entity subtype fall through to Remove, which then throws ArgumentException even though Contains/IndexOf treat that case as a miss. A containment guard also lets you drop the localized-message exception filter.

♻️ Suggested fix
         void IList.Remove(object value)
         {
-            try
-            {
-                if (value is Entity entity)
-                    Remove(entity);
-            }
-            catch (InvalidOperationException ioe) when (ioe.Message == Resource.EntitySet_EntityNotInSet)
-            {
-                // Don't throw if item was not in the collection
-            }
+            if (value is Entity entity && this.Contains(entity))
+            {
+                this.Remove(entity);
+            }
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntitySet.cs` around lines 985 - 995,
The IList.Remove implementation should short-circuit non-Entity or non-contained
items instead of calling Remove and relying on an exception filter; update the
IList.Remove method so it first checks that value is an Entity and that
this.Contains((Entity)value) (or this.IndexOf((Entity)value) != -1) before
calling Remove(entity), and simply return/no-op for null or incompatible types —
drop the localized InvalidOperationException filter and related catch block
because containment check prevents the exception path; reference the
IList.Remove implementation, the Remove(Entity) method and Contains/IndexOf
helpers when making the change.
src/OpenRiaServices.Client/Framework/EntityCollection.cs (2)

1018-1024: ⚠️ Potential issue | 🟠 Major

Load before serving the new indexer.

Entities only materializes the backing list; it does not populate it. On an unloaded association, collection[index] can therefore throw or expose a partial view until some other call happens to trigger Load(). The explicit IList.this[int] getter now forwards here too, so both indexer surfaces inherit the same fragile precondition.

♻️ Suggested fix
-        /// <remarks>**Important**: Make sure to check <see cref="Count"/> first to ensure the collection is initialized</remarks>
         /// <param name="index">The zero-based index of the entity to retrieve.</param>
         /// <returns>The entity located at the specified index.</returns>
-        public TEntity this[int index] => Entities[index];
+        public TEntity this[int index]
+        {
+            get
+            {
+                this.Load();
+                return this.Entities[index];
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs` around lines 1018 -
1024, The indexer getter (public TEntity this[int index] => Entities[index])
must ensure the collection is loaded before accessing the backing list; modify
the TEntity indexer and any IList.this[int] forwarding to call the collection's
load/ensure method (e.g., Load() or EnsureLoaded()) first and only then return
Entities[index], so an unloaded association no longer exposes a partial view or
throws.

1086-1096: ⚠️ Potential issue | 🟡 Minor

Guard the direct IList object paths instead of casting blindly.

IList.Add and IList.Remove still cast value straight to TEntity. On the new object-based list surface, that means incompatible inputs can blow up with InvalidCastException, even though IList.Contains and ListCollectionViewProxy<T>.Add already do type-aware handling.

♻️ Suggested fix
         int IList.Add(object value)
         {
+            if (value is null)
+            {
+                throw new ArgumentNullException(nameof(value));
+            }
+            if (value is not TEntity entity)
+            {
+                throw new ArgumentException(
+                    string.Format(CultureInfo.CurrentCulture, Resource.MustBeAnEntity, nameof(value)),
+                    nameof(value));
+            }
+
             int countBefore = this.Count;
-            Add((TEntity)value);
+            this.Add(entity);
 
             if (this.Count == countBefore + 1)
                 return countBefore;
             else if (this.Count == countBefore)
                 return -1;
             else
-                return Entities.IndexOf((TEntity)value, countBefore);
+                return this.Entities.IndexOf(entity, countBefore);
         }
         void IList.Remove(object value)
         {
-            Remove((TEntity)value);
+            if (value is TEntity entity)
+            {
+                this.Remove(entity);
+            }
         }

Also applies to: 1123-1125

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs` around lines 1086 -
1096, IList.Add and IList.Remove currently cast value directly to TEntity which
can throw InvalidCastException for incompatible inputs; change both to
type-guard before casting: in IList.Add, check "if (value is TEntity entity) {
Add(entity); /* existing return logic */ } else throw new
ArgumentException(\"Value is of incorrect type\", nameof(value));" and in
IList.Remove, check "if (value is TEntity entity) return Remove(entity); else
return false;"; mirror the type-aware pattern used by IList.Contains and
ListCollectionViewProxy<T>.Add and keep the existing post-add count/return logic
in Add (use the same Entities.IndexOf path when added).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs`:
- Around line 1018-1024: The indexer getter (public TEntity this[int index] =>
Entities[index]) must ensure the collection is loaded before accessing the
backing list; modify the TEntity indexer and any IList.this[int] forwarding to
call the collection's load/ensure method (e.g., Load() or EnsureLoaded()) first
and only then return Entities[index], so an unloaded association no longer
exposes a partial view or throws.
- Around line 1086-1096: IList.Add and IList.Remove currently cast value
directly to TEntity which can throw InvalidCastException for incompatible
inputs; change both to type-guard before casting: in IList.Add, check "if (value
is TEntity entity) { Add(entity); /* existing return logic */ } else throw new
ArgumentException(\"Value is of incorrect type\", nameof(value));" and in
IList.Remove, check "if (value is TEntity entity) return Remove(entity); else
return false;"; mirror the type-aware pattern used by IList.Contains and
ListCollectionViewProxy<T>.Add and keep the existing post-add count/return logic
in Add (use the same Entities.IndexOf path when added).

In `@src/OpenRiaServices.Client/Framework/EntitySet.cs`:
- Around line 985-995: The IList.Remove implementation should short-circuit
non-Entity or non-contained items instead of calling Remove and relying on an
exception filter; update the IList.Remove method so it first checks that value
is an Entity and that this.Contains((Entity)value) (or
this.IndexOf((Entity)value) != -1) before calling Remove(entity), and simply
return/no-op for null or incompatible types — drop the localized
InvalidOperationException filter and related catch block because containment
check prevents the exception path; reference the IList.Remove implementation,
the Remove(Entity) method and Contains/IndexOf helpers when making the change.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f742f43a-f819-4f0a-8d67-933a202ec32d

📥 Commits

Reviewing files that changed from the base of the PR and between dcc1b20 and 91aa923.

📒 Files selected for processing (3)
  • src/OpenRiaServices.Client/Framework/EntityCollection.cs
  • src/OpenRiaServices.Client/Framework/EntitySet.cs
  • src/OpenRiaServices.Client/Framework/LoadResult.cs

…k if items are allowed to be added or removed by that instance (not if somebody else can change the list)
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

♻️ Duplicate comments (2)
src/OpenRiaServices.Client/Framework/EntityCollection.cs (2)

1018-1024: ⚠️ Potential issue | 🟠 Major

Load before serving the new indexer.

Line 1024 returns Entities[index] without calling Load(). On an unloaded collection, Entities just materializes an empty cache, so collection[index] can fail even though Count and enumeration would have loaded the source set. Because Lines 1080-1083 delegate here, both new indexers inherit it.

Proposed fix
-public TEntity this[int index] => Entities[index];
+public TEntity this[int index]
+{
+    get
+    {
+        this.Load();
+        return this.Entities[index];
+    }
+}

Also applies to: 1079-1083

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs` around lines 1018 -
1024, The indexer this[int index] currently returns Entities[index] without
ensuring the collection is loaded; change it to call Load() first (i.e., ensure
the collection is initialized before accessing the Entities cache) so that an
unloaded collection won't produce an empty materialized cache and fail on index
access; update the indexer implementation (and any other delegating indexers at
the same code paths around the methods referencing Entities and Lines 1079–1083)
to invoke Load() prior to reading Entities or Count.

1086-1097: ⚠️ Potential issue | 🟠 Major

Route the direct IList mutators through the existing safe paths.

Lines 1089 and 1125 still direct-cast object. That drops the validation already present in ListCollectionViewProxy<T>.Add at Lines 850-865 and bypasses the non-throwing ICollection<TEntity>.Remove path at Lines 1047-1056, so the new Avalonia-facing IList surface can still throw on wrong types or non-members.

Proposed fix
 int IList.Add(object value)
 {
-    int countBefore = this.Count;
-    Add((TEntity)value);
+    TEntity entity = value as TEntity;
+    if (entity == null)
+    {
+        throw new ArgumentException(
+            string.Format(CultureInfo.CurrentCulture, Resource.MustBeAnEntity, "value"),
+            nameof(value));
+    }
+
+    int countBefore = this.Count;
+    Add(entity);
 
     if (this.Count == countBefore + 1)
         return countBefore;
     else if (this.Count == countBefore)
         return -1;
     else
-        return Entities.IndexOf((TEntity)value, countBefore);
+        return Entities.IndexOf(entity, countBefore);
 }
 
 void IList.Remove(object value)
 {
-    Remove((TEntity)value);
+    TEntity entity = value as TEntity;
+    if (entity != null)
+    {
+        ((ICollection<TEntity>)this).Remove(entity);
+    }
 }

Run the following script to verify the mismatch against the safe paths already present in this file:

#!/bin/bash
sed -n '850,865p' src/OpenRiaServices.Client/Framework/EntityCollection.cs
printf '\n---\n'
sed -n '1047,1056p' src/OpenRiaServices.Client/Framework/EntityCollection.cs
printf '\n---\n'
sed -n '1086,1126p' src/OpenRiaServices.Client/Framework/EntityCollection.cs

Expected result: the proxy Add validates value, ICollection<TEntity>.Remove returns false when the item is absent, and the direct IList mutators still cast directly.

Also applies to: 1123-1126

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs` around lines 1086 -
1097, The IList mutators are bypassing existing validation and non-throwing
behavior by direct-casting object values; change IList.Add(object) and the
IList.Remove(object) path to route through the safe helpers already in this
file: call the existing ListCollectionViewProxy<T>.Add validation path (the
Add(TEntity) / proxy Add logic around lines ~850-865) before computing the
return index so wrong-type inputs get the same validation/exception behavior,
and for removal route through ICollection<TEntity>.Remove (the non-throwing path
around lines ~1047-1056) instead of casting and removing directly; only use
Entities.IndexOf(...) after the safe Add completed to determine the return
index.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@src/OpenRiaServices.Client/Framework/EntityCollection.cs`:
- Around line 1018-1024: The indexer this[int index] currently returns
Entities[index] without ensuring the collection is loaded; change it to call
Load() first (i.e., ensure the collection is initialized before accessing the
Entities cache) so that an unloaded collection won't produce an empty
materialized cache and fail on index access; update the indexer implementation
(and any other delegating indexers at the same code paths around the methods
referencing Entities and Lines 1079–1083) to invoke Load() prior to reading
Entities or Count.
- Around line 1086-1097: The IList mutators are bypassing existing validation
and non-throwing behavior by direct-casting object values; change
IList.Add(object) and the IList.Remove(object) path to route through the safe
helpers already in this file: call the existing ListCollectionViewProxy<T>.Add
validation path (the Add(TEntity) / proxy Add logic around lines ~850-865)
before computing the return index so wrong-type inputs get the same
validation/exception behavior, and for removal route through
ICollection<TEntity>.Remove (the non-throwing path around lines ~1047-1056)
instead of casting and removing directly; only use Entities.IndexOf(...) after
the safe Add completed to determine the return index.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 078bb1dc-09ef-4aa9-b585-462fb71e20a2

📥 Commits

Reviewing files that changed from the base of the PR and between 91aa923 and 4e07d06.

📒 Files selected for processing (3)
  • src/OpenRiaServices.Client/Framework/EntityCollection.cs
  • src/OpenRiaServices.Client/Framework/EntitySet.cs
  • src/OpenRiaServices.Client/Framework/LoadResult.cs

@OpenRIAServices OpenRIAServices deleted a comment from coderabbitai bot Apr 7, 2026
@sonarqubecloud
Copy link
Copy Markdown

sonarqubecloud bot commented Apr 8, 2026

@Daniel-Svensson Daniel-Svensson merged commit 4b365cf into OpenRIAServices:main Apr 8, 2026
7 checks passed
@Daniel-Svensson Daniel-Svensson deleted the feature/avaolonia branch April 8, 2026 13:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants