From 1225d3e8e36a34a32cd113e729af5fd338374ebd Mon Sep 17 00:00:00 2001 From: NaCl-HCl Date: Sun, 18 Oct 2026 05:53:28 -0400 Subject: [PATCH 1/2] fix: prevent null WrappedElement during concurrent component re-renders Use null-coalescing assignment in WrapperBase.WrappedElement getter to ensure the field is only read once during retrieval, avoiding race conditions where OnElementReplaced clears the field before the return. Closes #1937 --- CHANGELOG.md | 1 + .../Web.AngleSharp/WrapperBase.cs | 8 +--- ...orTest.Generator#WrapperBase.g.verified.cs | 10 +--- .../RefreshingWrappedElementTest.cs | 47 +++++++++++++++++++ 4 files changed, 51 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c9769a6f7..a0c0cacf8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to **bUnit** will be documented in this file. The project ad ### Fixed +- `WrapperBase.WrappedElement` could return `null` and throw `NullReferenceException` when an element was accessed concurrently with component re-renders. Reported by [@psu-prog](https://github.com/psu-prog) in #1937. Fixed by [@Nacl-HCl](https://github.com/Nacl-HCl). - `DisposeComponentsAsync()` could dispose nothing when the renderer's dispatcher was busy, e.g. with a timer calling `InvokeAsync`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1939. Fixed by [@linkdotnet](https://github.com/linkdotnet). ## [2.11.3] - 2026-09-13 diff --git a/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs b/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs index 8dd4d5963..46bcc12f2 100644 --- a/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs +++ b/src/bunit.generators.internal/Web.AngleSharp/WrapperBase.cs @@ -24,13 +24,7 @@ public abstract class WrapperBase : IElementWrapper public TElement WrappedElement { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - if (element is null) - element = elementFactory.GetElement(); - - return element; - } + get => element ??= elementFactory.GetElement(); } /// diff --git a/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs b/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs index 2f8cc5e05..6135001c7 100644 --- a/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs +++ b/tests/bunit.generators.tests/Web.AngleSharp/WrapperElementsGeneratorTest.Generator#WrapperBase.g.verified.cs @@ -1,4 +1,4 @@ -//HintName: WrapperBase.g.cs +//HintName: WrapperBase.g.cs #nullable enable using System.CodeDom.Compiler; using System.Diagnostics; @@ -25,13 +25,7 @@ public abstract class WrapperBase : IElementWrapper public TElement WrappedElement { [MethodImpl(MethodImplOptions.AggressiveInlining)] - get - { - if (element is null) - element = elementFactory.GetElement(); - - return element; - } + get => element ??= elementFactory.GetElement(); } /// diff --git a/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs b/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs index 4f1602435..e432b470b 100644 --- a/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs +++ b/tests/bunit.tests/Extensions/RefreshingWrappedElementTest.cs @@ -76,6 +76,53 @@ public void Test031() Should.Throw(() => btn.TextContent); } + [Fact(DisplayName = "Found element is never null when accessed concurrently with component re-renders")] + public async Task Test040() + { + var cut = Render(); + var elm = cut.Find("span"); + using var cts = CancellationTokenSource.CreateLinkedTokenSource(Xunit.TestContext.Current.CancellationToken); + cts.CancelAfter(TimeSpan.FromSeconds(1)); + var renders = Task.Run(async () => + { + while (!cts.IsCancellationRequested) + { + await cut.InvokeAsync(cut.Instance.Increment); + } + }, Xunit.TestContext.Current.CancellationToken); + + try + { + while (!cts.IsCancellationRequested) + { + elm.TextContent.ShouldNotBeNull(); + } + } + finally + { + await cts.CancelAsync(); + await renders; + } + } + + private sealed class Counter : ComponentBase + { + private int count; + + public void Increment() + { + count++; + StateHasChanged(); + } + + protected override void BuildRenderTree(RenderTreeBuilder builder) + { + builder.OpenElement(0, "span"); + builder.AddContent(1, count); + builder.CloseElement(); + } + } + private sealed class Markup : ComponentBase { [Parameter] public string Base { get; set; } = string.Empty; From 02bc1ee1afec06a506777264c1bad2f4ebeba581 Mon Sep 17 00:00:00 2001 From: NaCl-HCl Date: Mon, 19 Oct 2026 06:05:33 -0400 Subject: [PATCH 2/2] docs: remove generic type brackets in CHANGELOG to prevent malformed nuspec --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0c0cacf8..59b0cd618 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ All notable changes to **bUnit** will be documented in this file. The project ad ### Fixed -- `WrapperBase.WrappedElement` could return `null` and throw `NullReferenceException` when an element was accessed concurrently with component re-renders. Reported by [@psu-prog](https://github.com/psu-prog) in #1937. Fixed by [@Nacl-HCl](https://github.com/Nacl-HCl). +- `WrapperBase.WrappedElement` could return `null` and throw `NullReferenceException` when an element was accessed concurrently with component re-renders. Reported by [@psu-prog](https://github.com/psu-prog) in #1937. Fixed by [@Nacl-HCl](https://github.com/Nacl-HCl). - `DisposeComponentsAsync()` could dispose nothing when the renderer's dispatcher was busy, e.g. with a timer calling `InvokeAsync`. Reported by [@xfunc-tim-schneider](https://github.com/xfunc-tim-schneider) in #1939. Fixed by [@linkdotnet](https://github.com/linkdotnet). ## [2.11.3] - 2026-09-13