Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
13 changes: 13 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,19 @@ src/
│ └── Domain/ # Test domain types using Bogus fakers
```

## Usings and Namespaces

**Do not add `using` directives to library source files.** In this fork they declare their namespaces once in `src/DynamicData/Global.cs`, which both flavours compile: the DynamicData namespaces switch on `REACTIVE_SHIM`, and each library csproj adds its Rx stack (ReactiveUI.Primitives or System.Reactive) as `<Using>` items. `ImplicitUsings` stays enabled. The TUnit test and benchmark projects declare their namespaces as `<Using>` items in their csproj files. If you need a namespace that is not in scope, add it there.

Two DynamicData namespaces are the exception and **must** stay file-level, because importing them globally silently rebinds existing calls:

| Namespace | Why |
|---|---|
| `DynamicData.Alias` | Supplies `Select`, `Where` and `SelectMany` for changeset streams. They are more specific than the Rx operators of the same name, so importing globally silently rebinds calls such as `.Where(_ => flag)` to `Filter`. |
| `DynamicData.Aggregation` | Supplies `Count`, `Sum`, `Avg`, `Maximum` and `Minimum` for changeset streams. `Count()` is more specific than Rx's `Count()` and returns the same type, so importing globally silently turns a count of emitted changesets into a live item count. |

If a global namespace makes a name ambiguous, qualify the call site or add a global alias in `Global.cs`. Do not move the namespace back into the file.

## Operator Architecture Pattern

Most operators follow the same two-part pattern:
Expand Down
8 changes: 4 additions & 4 deletions docs/fork-differences.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@ entry `retired`. Upstream status is as last reviewed; open gaps are tracked as

| ID | What differs | Why | Upstream status |
| --- | --- | --- | --- |
| DD-001 | .NET 10 only; SDK pinned in [global.json](../global.json). | Runic targets .NET 10; one API baseline per flavor. | Upstream multi-targets. |
| DD-002 | Default ReactiveUI.Primitives 9.0.0; `-p:ReactiveUIPrimitivesVersion=8.4.0` keeps ReactiveUI 25 source compatibility. Independent [package versions](../Directory.Packages.props). | Current Runic uses Primitives 9; 8.4 consumers still build. | Based on open PR [#1116](https://github.com/reactivemarbles/DynamicData/pull/1116). |
| DD-003 | Two flavors from shared sources: Primitives (`DynamicData`) and System.Reactive (`DynamicData.Reactive`, `REACTIVE_SHIM`). | Consumers of either Rx stack. | Based on open PR #1116. |
| DD-001 | .NET 10 only; SDK pinned in [global.json](../global.json). No Polyfill package: the BCL provides the APIs upstream polyfills. | Runic targets .NET 10; one API baseline per flavor. | Upstream multi-targets with the Polyfill package (#1201). |
| DD-002 | Default ReactiveUI.Primitives 9.0.0; `-p:ReactiveUIPrimitivesVersion=8.4.0` keeps ReactiveUI 25 source compatibility. Fork-owned central [package versions](../src/Directory.Packages.props) (Primitives via `ReactiveUIPrimitivesVersion`); the [root file](../Directory.Packages.props) keeps CPM off for `eng/` and stops an enclosing workspace's versions from applying. | Current Runic uses Primitives 9; 8.4 consumers still build. | Based on open PR [#1116](https://github.com/reactivemarbles/DynamicData/pull/1116). |
| DD-003 | Two flavors from shared sources: Primitives (`DynamicData`) and System.Reactive (`DynamicData.Reactive`, `REACTIVE_SHIM`). The library [Global.cs](../src/DynamicData/Global.cs) switches DynamicData namespaces on `REACTIVE_SHIM` and leaves Rx imports to each csproj; `ImplicitUsings` stays enabled. | Consumers of either Rx stack. | Based on open PR #1116. |
| DD-004 | Tests use TUnit / Microsoft.Testing.Platform; upstream xUnit tests are translated. | One runner for both flavors. | Upstream uses xUnit. |
| DD-005 | Packages `Runic.DynamicData` and `Runic.DynamicData.Reactive`, released as GitHub release assets; namespaces unchanged, original MIT license and authorship kept. | Avoid clashing with upstream package IDs. | Fork only. |
| DD-006 | List and cache `ExpireAfter` guard callbacks racing subscription/source disposal. | Late timer callbacks after disposal. | Fork fix. |
| DD-007 | `StdDev` uses incremental central moments; integral/decimal selectors use decimal accumulators. | Precision for large offsets ([#1196](https://github.com/reactivemarbles/DynamicData/issues/1196)). | Upstream fixed only the sample divisor (#1191). |
| DD-016 | Queue-serialized operators use gate-free merge helpers that honour the intended Rx selector/empty-input/null-value contracts. | Cross-cache deadlocks ([#1073](https://github.com/reactivemarbles/DynamicData/issues/1073)). | #1097 merged; helper contract repairs are fork only. |
| DD-017 | `RemoveKey` keeps per-subscription key positions. | Equal values lost identity ([#1182](https://github.com/reactivemarbles/DynamicData/issues/1182), #1119). | Open PR [#1192](https://github.com/reactivemarbles/DynamicData/pull/1192). |
| DD-018 | List `Transform`/`TransformAsync`, `EditDiff` and `TransformMany` remove one occurrence at a time (reference, then equality). | Dropped or duplicated rows (#1169, #1072). | Open PR [#1171](https://github.com/reactivemarbles/DynamicData/pull/1171) covers part. |
| DD-018 | List `Transform`/`TransformAsync`, `EditDiff` and `TransformMany` remove one occurrence at a time (reference, then equality). | Dropped or duplicated rows (#1169, #1072). | [#1171](https://github.com/reactivemarbles/DynamicData/pull/1171) merged for list `Transform`/`TransformAsync` only (first equal occurrence); its regressions pass against the fork implementation. |
| DD-019 | Property observation subscribes to the runtime chain target, not only the declared type. | Missed notifications through interfaces (#1156). | Open issue. |
| DD-020 | Cache `AsyncDisposeMany` disposes a replaced distinct reference even when values are equal. | Leaked owned objects. | Fork fix. |
| DD-021 | `BufferInitial` uses one one-shot timer for both collection types. | Spinning with tiny timeouts (#1170). | Open issue. |
Expand Down
10 changes: 2 additions & 8 deletions src/Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,11 @@
<None Include="$(MSBuildThisFileDirectory)..\README.md" Pack="true" PackagePath="\"/>
<None Include="$(MSBuildThisFileDirectory)..\LICENSE" Pack="true" PackagePath="LICENSE" />
</ItemGroup>
<ItemGroup Condition="'$(IsTestProject)' != 'true'">
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="10.0.401" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Nerdbank.GitVersioning" Version="3.10.91" PrivateAssets="all" />
<ItemGroup Condition="!$(IsTestProject)">
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
</ItemGroup>

<ItemGroup Condition="!$(IsTestProject) AND !$(IsBenchmarkProject)">
<PackageReference Include="stylecop.analyzers" Version="1.2.0-beta.556" PrivateAssets="all" />
<PackageReference Include="Roslynator.Analyzers" Version="4.15.0" PrivateAssets="All" />
<AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" Link="stylecop.json" />
</ItemGroup>

Expand Down
42 changes: 42 additions & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>

<Project>
<!-- Runic: this file stops the search for a Directory.Packages.props, so an enclosing
SDK workspace's central versions never apply to the fork. -->
<PropertyGroup>
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>

<!-- ReactiveUIPrimitivesVersion (Directory.Build.props) selects Primitives 9.0.0 or 8.4.0 compatibility. -->
<ItemGroup>
<!-- Referenced from Directory.Build.props, not as a GlobalPackageReference: that would drop the
buildTransitive targets SourceLink needs to embed the repository commit in packages. -->
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="10.0.401" />
<PackageVersion Include="ReactiveUI.Primitives" Version="$(ReactiveUIPrimitivesVersion)" />
<PackageVersion Include="ReactiveUI.Primitives.Reactive" Version="$(ReactiveUIPrimitivesVersion)" />
</ItemGroup>

<!--
Floating versions are allowed only in test and benchmark projects. NuGet rejects any floating PackageVersion visible to a
project without CentralPackageFloatingVersionsEnabled, so these versions are hidden from the shipped library entirely.
-->
<PropertyGroup Condition="$(IsTestProject) OR $(IsBenchmarkProject)">
<CentralPackageFloatingVersionsEnabled>true</CentralPackageFloatingVersionsEnabled>
</PropertyGroup>
<ItemGroup Condition="$(IsTestProject) OR $(IsBenchmarkProject)">
<PackageVersion Include="BenchmarkDotNet" Version="*" />
<PackageVersion Include="Bogus" Version="35.*" />
<PackageVersion Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.11.2" />
<PackageVersion Include="PublicApiGenerator" Version="11.*" />
<PackageVersion Include="TUnit" Version="1.68.4" />
</ItemGroup>

<!-- Build tooling and analyzers, referenced by every matching project as private assets -->
<ItemGroup>
<GlobalPackageReference Include="Nerdbank.GitVersioning" Version="3.10.91" />
</ItemGroup>
<ItemGroup Condition="!$(IsTestProject) AND !$(IsBenchmarkProject)">
<GlobalPackageReference Include="Roslynator.Analyzers" Version="4.15.0" />
<GlobalPackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.556" />
</ItemGroup>
</Project>
6 changes: 3 additions & 3 deletions src/DynamicData.Benchmarks/DynamicData.Benchmarks.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
<TargetFramework>net10.0</TargetFramework>
<PlatformTarget>AnyCPU</PlatformTarget>
<IsPackable>false</IsPackable>
<NoWarn>;1701;1702;1705;CA1822;CA1001</NoWarn>
<NoWarn>$(NoWarn);CA1822;CA1001</NoWarn>
</PropertyGroup>


<ItemGroup>
<PackageReference Include="BenchmarkDotNet" Version="*" />
<PackageReference Include="Bogus" Version="*" />
<PackageReference Include="BenchmarkDotNet" />
<PackageReference Include="Bogus" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFrameworkIdentifier)' == '.NETFramework' ">
<Reference Include="System.Reflection" />
Expand Down
10 changes: 5 additions & 5 deletions src/DynamicData.Reactive.Tests/DynamicData.Reactive.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Bogus" Version="35.*" />
<PackageReference Include="TUnit" Version="1.68.4" />
<PackageReference Include="ReactiveUI.Primitives" Version="$(ReactiveUIPrimitivesVersion)" PrivateAssets="all" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.11.2" />
<PackageReference Include="PublicApiGenerator" Version="11.*" />
<PackageReference Include="Bogus" />
<PackageReference Include="TUnit" />
<PackageReference Include="ReactiveUI.Primitives" PrivateAssets="all" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
<PackageReference Include="PublicApiGenerator" />
</ItemGroup>

<ItemGroup>
Expand Down
21 changes: 1 addition & 20 deletions src/DynamicData.Reactive/DynamicData.Reactive.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,9 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="ReactiveUI.Primitives.Reactive" Version="$(ReactiveUIPrimitivesVersion)" />
<PackageReference Include="ReactiveUI.Primitives.Reactive" />
</ItemGroup>
<ItemGroup>
<Using Include="System" />
<Using Include="System.Collections" />
<Using Include="System.Diagnostics.CodeAnalysis" />
<Using Include="System.ComponentModel" />
<Using Include="System.Runtime.CompilerServices" />
<Using Include="System.Collections.ObjectModel" />
<Using Include="DynamicData.Reactive.Cache" />
<Using Include="DynamicData.Reactive.Kernel" />
<Using Include="DynamicData.Reactive.Internal" />
<Using Include="System.Reactive" />
<Using Include="System.Reactive.Concurrency" />
<Using Include="System.Reactive.Disposables" />
Expand All @@ -52,14 +43,4 @@
<ItemGroup Condition="!$(DefineConstants.Contains('SUPPORTS_ASYNC_DISPOSABLE'))">
<Compile Remove="..\DynamicData\Cache\ObservableCacheEx.AsyncDisposeMany.cs" />
</ItemGroup>

<ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
<Using Include="DynamicData.Reactive.ArgumentExceptionHelper" Alias="ArgumentExceptionHelper" />
<Using Include="DynamicData.Reactive.ArgumentOutOfRangeExceptionHelper" Alias="ArgumentOutOfRangeExceptionHelper" />
</ItemGroup>

<ItemGroup Condition="!$(TargetFramework.StartsWith('net4'))">
<Using Include="System.ArgumentNullException" Alias="ArgumentExceptionHelper" />
<Using Include="System.ArgumentOutOfRangeException" Alias="ArgumentOutOfRangeExceptionHelper" />
</ItemGroup>
</Project>
8 changes: 4 additions & 4 deletions src/DynamicData.Tests/DynamicData.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Bogus" Version="35.*" />
<PackageReference Include="TUnit" Version="1.68.4" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" Version="18.11.2" />
<PackageReference Include="PublicApiGenerator" Version="11.*" />
<PackageReference Include="Bogus" />
<PackageReference Include="TUnit" />
<PackageReference Include="Microsoft.Testing.Extensions.CodeCoverage" />
<PackageReference Include="PublicApiGenerator" />
</ItemGroup>

<ItemGroup>
Expand Down
12 changes: 6 additions & 6 deletions src/DynamicData.Tests/Kernal/EnumerableEx.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ public static class EnumerableEx
{
public static IEnumerable<TResult> CurrentNextZip<T, TResult>(this IEnumerable<T> source, Func<T, T?, TResult> selector)
{
ArgumentExceptionHelper.ThrowIfNull(source);
ArgumentExceptionHelper.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);

var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
Expand All @@ -25,8 +25,8 @@ public static IEnumerable<TResult> CurrentNextZip<T, TResult>(this IEnumerable<T

public static IEnumerable<TResult> PrevCurrentNextZip<T, TResult>(this IEnumerable<T> source, Func<T?, T, T?, TResult> selector)
{
ArgumentExceptionHelper.ThrowIfNull(source);
ArgumentExceptionHelper.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);

var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
Expand All @@ -48,8 +48,8 @@ public static IEnumerable<TResult> PrevCurrentNextZip<T, TResult>(this IEnumerab

public static IEnumerable<TResult> PrevCurrentZip<T, TResult>(this IEnumerable<T> source, Func<T?, T, TResult> selector)
{
ArgumentExceptionHelper.ThrowIfNull(source);
ArgumentExceptionHelper.ThrowIfNull(selector);
ArgumentNullException.ThrowIfNull(source);
ArgumentNullException.ThrowIfNull(selector);

var enumerator = source.GetEnumerator();
if (enumerator.MoveNext())
Expand Down
56 changes: 56 additions & 0 deletions src/DynamicData.Tests/List/TransformAsyncFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,60 @@ public async Task TransformAsyncCancelsTokenOnUnSubscribe()
sub.Dispose();
await Assert.That(tcs.Task.IsCanceled).IsTrue();
}

// Ported from upstream #1171; covers https://github.com/reactivemarbles/DynamicData/issues/1169
[Test]
public async Task RemoveValueWithoutIndex()
{
var source = Enumerable.Empty<IChangeSet<int>>()
.Append(new ChangeSet<int>()
{
new Change<int>(
reason: ListChangeReason.AddRange,
items: new[] { 1, 2, 3, 2, 1 })
})
.Append(new ChangeSet<int>()
{
new Change<int>(reason: ListChangeReason.Remove, current: 2)
})
.ToObservable();

using var subscription = source
.TransformAsync(static item => Task.FromResult(item.ToString()))
.ValidateChangeSets()
.RecordListItems(out var results);

await Assert.That(results.Error).IsNull();
await Assert.That(results.RecordedChangeSets.Count).IsEqualTo(2).Because("2 source operations were performed");
await Assert.That(results.RecordedItems).IsEquivalentTo(new[] { "1", "3", "2", "1" }, TUnit.Assertions.Enums.CollectionOrdering.Matching).Because("The first occurrence of the given item should have been removed");
await Assert.That(results.HasCompleted).IsTrue().Because("the source, and all asynchronous operations, have completed");
}

// Ported from upstream #1171; covers https://github.com/reactivemarbles/DynamicData/issues/1169
[Test]
public async Task RemoveRangeWithoutIndex()
{
var source = Enumerable.Empty<IChangeSet<int>>()
.Append(new ChangeSet<int>()
{
new Change<int>(
reason: ListChangeReason.AddRange,
items: new[] { 1, 2, 3, 4, 5, 4, 3, 2, 1 })
})
.Append(new ChangeSet<int>()
{
new Change<int>(reason: ListChangeReason.RemoveRange, items: new[] { 1, 3, 5 })
})
.ToObservable();

using var subscription = source
.TransformAsync(static item => Task.FromResult(item.ToString()))
.ValidateChangeSets()
.RecordListItems(out var results);

await Assert.That(results.Error).IsNull();
await Assert.That(results.RecordedChangeSets.Count).IsEqualTo(2).Because("2 source operations were performed");
await Assert.That(results.RecordedItems).IsEquivalentTo(new[] { "2", "4", "4", "3", "2", "1" }, TUnit.Assertions.Enums.CollectionOrdering.Matching).Because("The first occurrence of each odd-numbered item in the list should have been removed");
await Assert.That(results.HasCompleted).IsTrue().Because("the source, and all asynchronous operations, have completed");
}
}
Loading
Loading