[Bug]: ChangeAwareList merges an insert one slot before a pending AddRange, corrupting list order downstream

未关闭
#1,225 0 条评论 0 个 reaction 已指派 0 人 在 GitHub 查看

维护者通常 1 天内回复

@dwcullop 已经在做这个了。

开始于 2026年11月2日。

  • #1227 来自 @dwcullop —— 未关闭

评估

难度
3/5
预计耗时
半天
新手友好度
63/100
Issue 类型
缺陷
描述清晰度
描述清楚
活跃度
活跃
技术栈
csharp
领域
data

调研方向

The bug is in the AddRange branch of InsertItem in src/DynamicData/List/ChangeAwareList.cs, where the minimum bound uses range.Index - 1 instead of range.Index. Start by reading that branch and the clamping code after it, then write a regression test from the issue's repro (source.Items should match the AsObservableList mirror). Done means that test passes and the existing list-change tests still pass. Leave the RangeChange list-aliasing problem for a separate issue.

由索引模型根据 Issue 内容生成。

描述

bug
Describe the bug 🐞

When a ChangeAwareList<T> has a pending AddRange change and an item is inserted one slot before that range, InsertItem folds the item into the range and moves the range start back by one. The two are not contiguous (the element that was at range.Index - 1 now sits between them), so the emitted AddRange places the range items at the wrong positions. Anything that replays indexes (Clone, AsObservableList, Transform, Bind) ends up with a list whose order differs from the source. There is no exception: the downstream data is silently wrong.

Sort is affected independently of the source: it builds its output with its own ChangeAwareList, so a source changeset of [AddRange, Add] whose sorted positions put the single item one slot before the freshly sorted range produces a corrupted sorted output, even when the source changeset itself is correct.

Step to reproduce
using var source = new SourceList<string>();
source.Add("x");

using var mirror = source.Connect().AsObservableList();

source.Edit(list =>
{
    list.AddRange(new[] { "b", "c" }); // appended at index 1
    list.Insert(0, "a");               // index 0 == range.Index - 1
});

// source.Items: a, x, b, c
// emitted:      AddRange index 0 [a, b, c]
// mirror.Items: a, b, c, x   <-- wrong
Reproduction repository

N/A: the repro above is self-contained, and the linked PR adds regression tests.

Expected behavior

mirror.Items equals source.Items (a, x, b, c). The changeset should be [AddRange index 1 [b, c], Add index 0 a], or any other sequence that replays to the same list.

Screenshots 🖼️

N/A

IDE

N/A

Operating system

Any

Version

N/A

Device

N/A

DynamicData Version

9.5.0 and main. The merge window has been unchanged since at least 2019.

Additional information ℹ️

Root cause. src/DynamicData/List/ChangeAwareList.cs, InsertItem, AddRange branch:

var minimum = Math.Max(range.Index - 1, 0);
var maximum = range.Index + range.Count;
var isPartOfRange = index >= minimum && index <= maximum;

A pending range occupies [range.Index, range.Index + range.Count). An inserted item extends it only when it lands inside that span or directly after it, so the valid window is [range.Index, range.Index + range.Count]. The lower bound of range.Index - 1 is off by one, and the clamping and SetStartingIndex code that follows exists only to support that case.

Second case in the same branch. The seeded constructor new ChangeAwareList<T>(items) records an AddRange with index -1. Inserting into that list before CaptureChanges() merges the item one position too far to the right (insertPosition = index - (-1)). The library's own callers (ToObservableChangeSet for ObservableCollection and BindingList) capture immediately after construction, so this is only reachable by custom operators using the public type.

Scope. The other merge branches (Add + insert, Remove + remove, RemoveRange + remove) were checked with exhaustive position tests and a seeded randomized replay test (100k random edit batches) and are correct.

Separate known problem (not addressed by the fix for this issue). RangeChange<T> keeps a reference to the caller's List<T> instead of copying it (items.AsList()). A later single insert that merges into that pending range mutates the caller's list:

var mine = new List<int> { 1, 2 };
source.Edit(list =>
{
    list.AddRange(mine);
    list.Add(3);
});
// mine is now { 1, 2, 3 }

Fixing it means copying on every AddRange, which has a performance cost, so it should be decided and tracked separately.

主要语言
C#
星标
1.9k
派生
194
平均合并
12 分钟
30 天内合并 PR
1

环境准备

这个项目没有提供开发容器、Dockerfile 或贡献指南,环境需要你自己搭建:先看它的 README,通用步骤见我们的新手贡献指南。

从这里开始

  1. 先读完整个 Issue,再读项目的贡献指南。
  2. 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
  3. Fork 仓库,在一个分支上完成修改。
  4. 提交 Pull Request,并在描述里引用这个 Issue 编号。

reactivemarbles/DynamicData 的其他 Issue

查看 reactivemarbles/DynamicData 的全部 Issue

相似的 Issue

更多 C# Issue

把新 issue 发到你的邮箱

精选适合新手参与的 GitHub issue 摘要。