Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7e7ae24
Wait for quiescence in MergeManyChangeSets stress tests (#1100)
dwcullop May 28, 2026
f3e4c04
Make SizeLimit tests deterministic by deduplicating generator output …
dwcullop May 29, 2026
7885e3b
Rewrote testing for the cache variant of the AutoRefresh() and AutoRe…
JakenVeina May 30, 2026
03ced2d
Split ObservableListEx.cs into per-family partial classes (#1096)
dwcullop Jun 6, 2026
d6dd784
Split ObservableCacheEx.cs into per-family partial classes (#1095)
dwcullop Jun 6, 2026
fc6c09e
WhenPropertyChanged: don't drop events fired during subscribe (#1111)
dwcullop Jun 15, 2026
41ee2ec
Fix FilterImmutable Update->Remove to carry the previous value as Cur…
dwcullop Jun 17, 2026
0916d96
test(sum): add more sum tests (#1071)
giard-alexandre Jun 18, 2026
b51dcd5
Update dotnet/nbgv action to v0.5.2 (#1102)
renovate[bot] Jun 20, 2026
7868341
Update actions/setup-dotnet action to v5.3.0 (#1058)
renovate[bot] Jun 20, 2026
b42d8f9
ci: adopt GitReleaseNoteGenerator and nbgv CLI tool (#1125)
glennawatson Jun 30, 2026
09ce424
Fix Exception in list static filter index assumptions (#1120)
jcummings2 Jul 4, 2026
437ca1f
Added support for automatic nesting of source files with hierarchal n…
JakenVeina Jul 9, 2026
9d74b4a
Feature: Cancellation support for TransformAsync (#1135)
glen-nicol Jul 20, 2026
3f1b070
Fixed #1131 - Race condition between `SuspendNotifications()` and `.R…
JakenVeina Jul 27, 2026
bc35ad6
Make every BatchIf overload shape resolve (#1153)
dwcullop Jul 30, 2026
ea4dad2
Added documentation for clarity on the shadowing behavior of DynamicD…
JakenVeina Jul 30, 2026
7b77850
Report a source failure as a failure to deferred subscriptions (#1141)
dwcullop Jul 30, 2026
374a2ed
Fix that the cache Switch operator never completes (#1137)
dwcullop Jul 30, 2026
ebc341c
Added tests for `ObservableCacheEx.ToCollection()`, in preparation fo…
JakenVeina Jul 31, 2026
9ebefa3
Removed extraneous internal access grants, from old, long-removed, pr…
JakenVeina Jul 31, 2026
d2a7a5a
Fixed that .WhenPropertyChanged() did not support a variety of types …
JakenVeina Jul 31, 2026
3d76872
Feat: sum benchmarks (#1118)
giard-alexandre Aug 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 14 additions & 6 deletions .github/workflows/ci-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ jobs:
lfs: true

- name: Setup .NET (With cache)
uses: actions/setup-dotnet@v5.0.1
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: |
6.0.x
Expand All @@ -37,12 +37,20 @@ jobs:
**/global.json
**/nuget.config

- name: NBGV
- name: Install (or update) nbgv tool
run: dotnet tool update --global nbgv

- name: Set NBGV cloud variables
run: nbgv cloud -a

- name: Expose NBGV version as step outputs
id: nbgv
uses: dotnet/nbgv@v0.5.1
with:
setAllVars: true

shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
"SemVer2=$env:NBGV_SemVer2" >> $env:GITHUB_OUTPUT
"PrereleaseVersion=$env:NBGV_PrereleaseVersion" >> $env:GITHUB_OUTPUT

- name: NuGet Restore
run: dotnet restore DynamicData.sln
working-directory: src
Expand Down
35 changes: 23 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
Write-Host "OK: publishing from '$env:REF_NAME'."

- name: Setup .NET (With cache)
uses: actions/setup-dotnet@v5.0.1
uses: actions/setup-dotnet@v5.3.0
with:
dotnet-version: |
6.0.x
Expand All @@ -56,11 +56,19 @@ jobs:
**/global.json
**/nuget.config

- name: NBGV
- name: Install (or update) nbgv tool
run: dotnet tool update --global nbgv

- name: Set NBGV cloud variables
run: nbgv cloud -a

- name: Expose NBGV version as step outputs
id: nbgv
uses: dotnet/nbgv@v0.5.1
with:
setAllVars: true
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
"SemVer2=$env:NBGV_SemVer2" >> $env:GITHUB_OUTPUT
"PrereleaseVersion=$env:NBGV_PrereleaseVersion" >> $env:GITHUB_OUTPUT

- name: Verify version matches branch policy
shell: pwsh
Expand Down Expand Up @@ -126,21 +134,24 @@ jobs:
if ($LASTEXITCODE -ne 0) { throw "dotnet nuget push failed for $($pkg.Name) (exit $LASTEXITCODE)." }
}

- name: Changelog
uses: glennawatson/ChangeLog@0464dd89b26f61fecf24b41d675f8ffdb11c4c3f # v1
id: changelog
- name: Install GitReleaseNoteGenerator
run: dotnet tool install -g GitReleaseNoteGenerator

- name: Generate release notes
env:
GITHUB_TOKEN: ${{ github.token }}
RELEASE_VERSION: ${{ steps.nbgv.outputs.SemVer2 }}
shell: pwsh
run: git-release-notes --release-version "$env:RELEASE_VERSION" --output-file release-notes.md

- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ steps.nbgv.outputs.SemVer2 }}
IS_PRERELEASE: ${{ steps.nbgv.outputs.PrereleaseVersion != '' }}
BODY: ${{ steps.changelog.outputs.commitLog }}
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$notesPath = Join-Path $env:RUNNER_TEMP 'release-notes.md'
Set-Content -Path $notesPath -Value $env:BODY -Encoding utf8 -NoNewline
$cmd = @('release', 'create', $env:TAG, '--title', $env:TAG, '--notes-file', $notesPath, '--target', $env:GITHUB_SHA)
$cmd = @('release', 'create', $env:TAG, '--title', $env:TAG, '--notes-file', 'release-notes.md', '--target', $env:GITHUB_SHA)
if ($env:IS_PRERELEASE -eq 'true') { $cmd += '--prerelease' }
gh @cmd
7 changes: 7 additions & 0 deletions src/Directory.Build.targets
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,13 @@
<Product>$(AssemblyName) ($(TargetFramework))</Product>
</PropertyGroup>

<ItemGroup>
<!-- Automatic file nesting, for .cs files-->
<Compile Update="**\*.*.cs">
<DependentUpon>$([System.Text.RegularExpressions.Regex]::Replace(%(Filename), '\.[^\.]+$', '.cs'))</DependentUpon>
</Compile>
</ItemGroup>

<PropertyGroup Condition="$(TargetFramework.StartsWith('net4'))">
<DefineConstants>$(DefineConstants);P_LINQ;SUPPORTS_BINDINGLIST</DefineConstants>
</PropertyGroup>
Expand Down
134 changes: 134 additions & 0 deletions src/DynamicData.Benchmarks/Cache/Sum_Cache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;

using BenchmarkDotNet.Attributes;

using DynamicData.Aggregation;

namespace DynamicData.Benchmarks.Cache;

[MemoryDiagnoser]
[MarkdownExporterAttribute.GitHub]
public class Sum_Cache
{
private IReadOnlyList<IChangeSet<Item, int>> _addChangeSets = null!;
private IReadOnlyList<IChangeSet<Item, int>> _replaceChangeSets = null!;
private IReadOnlyList<IChangeSet<Item, int>> _removeChangeSets = null!;
private IReadOnlyList<IChangeSet<Item, int>> _refreshChangeSets = null!;

private IChangeSet<Item, int> _seedAfterAdds = null!;
private IChangeSet<Item, int> _seedAfterReplaces = null!;

[Params(100, 500, 1_000, 10_000)]
public int Count { get; set; }

[GlobalSetup]
public void Setup()
{
var source = new ChangeAwareCache<Item, int>(capacity: Count);
var items = new Item[Count + 1];

var addChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
{
var item = new Item()
{
Id = id,
Value = id
};
items[id] = item;
source.Add(item, key: id);
addChangeSets.Add(source.CaptureChanges());
}
_addChangeSets = addChangeSets;

var addedItems = (Item[])items.Clone();

var replaceChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
{
var replacement = new Item()
{
Id = id,
Value = id * 2
};
items[id] = replacement;
source.AddOrUpdate(replacement, key: id);
replaceChangeSets.Add(source.CaptureChanges());
}
_replaceChangeSets = replaceChangeSets;

var refreshChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
{
// Mutate in place, then refresh - the scenario stateless aggregation cannot currently observe.
items[id].Value += 1;
source.Refresh(id);
refreshChangeSets.Add(source.CaptureChanges());
}
_refreshChangeSets = refreshChangeSets;

var removeChangeSets = new List<IChangeSet<Item, int>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
{
source.Remove(id);
removeChangeSets.Add(source.CaptureChanges());
}
_removeChangeSets = removeChangeSets;

// Replaces, refreshes, and removes only form a valid sequence for an operator that has already
// seen the items they refer to, so each of those runs gets seeded with the population as it stood
// beforehand. Collapsing the seed into a single change set keeps its cost off the measurement as
// far as possible: replaces follow on from the items that were added, while refreshes and removes
// follow on from the items that replaced them.
_seedAfterAdds = BuildSeed(addedItems);
_seedAfterReplaces = BuildSeed(items);
}

[Benchmark]
public void Adds() => Run(seed: null, _addChangeSets);

[Benchmark]
public void Replaces() => Run(_seedAfterAdds, _replaceChangeSets);

[Benchmark]
public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets);

[Benchmark]
public void Removes() => Run(_seedAfterReplaces, _removeChangeSets);

private static IChangeSet<Item, int> BuildSeed(Item[] items)
{
var seed = new ChangeAwareCache<Item, int>(capacity: items.Length - 1);

for (var id = 1; id < items.Length; ++id)
seed.Add(items[id], key: id);

return seed.CaptureChanges();
}

private static void Run(IChangeSet<Item, int>? seed, IReadOnlyList<IChangeSet<Item, int>> changeSets)
{
using var source = new Subject<IChangeSet<Item, int>>();

using var subscription = source
.Sum(static item => item.Value)
.Subscribe();

if (seed is not null)
source.OnNext(seed);

foreach (var changeSet in changeSets)
source.OnNext(changeSet);

source.OnCompleted();
}

private sealed class Item
{
public required int Id { get; init; }

public int Value { get; set; }
}
}
131 changes: 131 additions & 0 deletions src/DynamicData.Benchmarks/List/Sum_List.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Reactive.Subjects;

using BenchmarkDotNet.Attributes;

using DynamicData.Aggregation;

namespace DynamicData.Benchmarks.List;

[MemoryDiagnoser]
[MarkdownExporterAttribute.GitHub]
public class Sum_List
{
private IReadOnlyList<IChangeSet<Item>> _addChangeSets = null!;
private IReadOnlyList<IChangeSet<Item>> _replaceChangeSets = null!;
private IReadOnlyList<IChangeSet<Item>> _removeChangeSets = null!;
private IReadOnlyList<IChangeSet<Item>> _refreshChangeSets = null!;

private IChangeSet<Item> _seedAfterAdds = null!;
private IChangeSet<Item> _seedAfterReplaces = null!;

[Params(100, 500, 1_000, 10_000)]
public int Count { get; set; }

[GlobalSetup]
public void Setup()
{
var source = new ChangeAwareList<Item>(capacity: Count);
var items = new Item[Count];

var addChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var index = 0; index < Count; ++index)
{
items[index] = new Item()
{
Id = index + 1,
Value = index + 1
};
source.Add(items[index]);
addChangeSets.Add(source.CaptureChanges());
}
_addChangeSets = addChangeSets;

var addedItems = (Item[])items.Clone();

var replaceChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var index = 0; index < Count; ++index)
{
items[index] = new Item()
{
Id = index + 1,
Value = (index + 1) * 2
};
source[index] = items[index];
replaceChangeSets.Add(source.CaptureChanges());
}
_replaceChangeSets = replaceChangeSets;

var refreshChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var index = 0; index < Count; ++index)
{
// Mutate in place, then refresh - the scenario stateless aggregation cannot currently observe.
items[index].Value += 1;
source.RefreshAt(index);
refreshChangeSets.Add(source.CaptureChanges());
}
_refreshChangeSets = refreshChangeSets;

var removeChangeSets = new List<IChangeSet<Item>>(capacity: Count);
for (var id = 1; id <= Count; ++id)
{
source.RemoveAt(source.Count - 1);
removeChangeSets.Add(source.CaptureChanges());
}
_removeChangeSets = removeChangeSets;

// Replaces, refreshes, and removes only form a valid sequence for an operator that has already
// seen the items they refer to, so each of those runs gets seeded with the population as it stood
// beforehand. Collapsing the seed into a single change set keeps its cost off the measurement as
// far as possible: replaces follow on from the items that were added, while refreshes and removes
// follow on from the items that replaced them.
_seedAfterAdds = BuildSeed(addedItems);
_seedAfterReplaces = BuildSeed(items);
}

[Benchmark]
public void Adds() => Run(seed: null, _addChangeSets);

[Benchmark]
public void Replaces() => Run(_seedAfterAdds, _replaceChangeSets);

[Benchmark]
public void Refreshes() => Run(_seedAfterReplaces, _refreshChangeSets);

[Benchmark]
public void Removes() => Run(_seedAfterReplaces, _removeChangeSets);

private static IChangeSet<Item> BuildSeed(Item[] items)
{
var seed = new ChangeAwareList<Item>(capacity: items.Length);

seed.AddRange(items);

return seed.CaptureChanges();
}

private static void Run(IChangeSet<Item>? seed, IReadOnlyList<IChangeSet<Item>> changeSets)
{
using var source = new Subject<IChangeSet<Item>>();

using var subscription = source
.Sum(static item => item.Value)
.Subscribe();

if (seed is not null)
source.OnNext(seed);

foreach (var changeSet in changeSets)
source.OnNext(changeSet);

source.OnCompleted();
}

private sealed class Item
{
public required int Id { get; init; }

public int Value { get; set; }
}
}
Loading
Loading