Skip to content

Commit e91e94d

Browse files
[Blazor] Allow null parameter values to be supplied to interactive components via enhanced page update (#53317) (#53342)
# Allow `null` parameter values to be supplied to interactive components via enhanced page update Backport of #53317 Fixes an issue where a `NullRefrenceException` would be thrown if an enhanced page update supplied a `null` parameter to an interactive root component. ## Description In .NET 8, SSR'd components can supply updated parameters to existing interactive root components. If one of those updated parameters is `null`, an exception currently gets thrown from within the framework. This causes the circuit to crash when using Server interactivity, and it would causes an error to be logged in the browser console when using WebAssembly interactivity. This PR fixes the problem by treating `null` as a valid value for a serialized parameter that gets supplied to an interactive root component. Fixes #52434 ## Customer Impact Without this fix, customers may encounter the unfriendly exception and have a hard time figuring out the underlying cause. We have not yet seen customer reports of the issue, but it's possible that customers have this bug in their apps without knowing it, especially since it only occurs when supplying updated parameters to existing components (not when supplying the initial set of parameters). One workaround would be to use a different value than `null` to specify an empty parameter value, but this may not be possible in cases where the parameter gets supplied by the framework (e.g., via route value), or if the interactive root component's implementation is not under the developer's control. ## Regression? - [ ] Yes - [X] No Only applicable to new scenarios in .NET 8. ## Risk - [ ] High - [ ] Medium - [X] Low The fix is straightforward and well-tested. ## Verification - [x] Manual (required) - [x] Automated ## Packaging changes reviewed? - [ ] Yes - [ ] No - [X] N/A
1 parent 658ddfb commit e91e94d

File tree

6 files changed

+167
-12
lines changed

6 files changed

+167
-12
lines changed

src/Components/Endpoints/test/WebRootComponentParametersTest.cs

+46-9
Original file line numberDiff line numberDiff line change
@@ -100,36 +100,73 @@ public void WebRootComponentParameters_DefinitelyEquals_ReturnsTrue_ForEmptySetO
100100
}
101101

102102
[Fact]
103-
public void WebRootComponentParameters_DefinitelyEquals_Throws_WhenComparingNonJsonElementParameterToJsonElement()
103+
public void WebRootComponentParameters_DefinitelyEquals_ReturnsFalse_WhenComparingNonJsonElementParameterToJsonElement()
104104
{
105105
// Arrange
106106
var parameters1 = CreateParametersWithNonJsonElements(new() { ["First"] = 123 });
107107
var parameters2 = CreateParameters(new() { ["First"] = 456 });
108108

109-
// Act/assert
110-
Assert.Throws<InvalidCastException>(() => parameters1.DefinitelyEquals(parameters2));
109+
// Act
110+
var result = parameters1.DefinitelyEquals(parameters2);
111+
112+
// Assert
113+
Assert.False(result);
111114
}
112115

113116
[Fact]
114-
public void WebRootComponentParameters_DefinitelyEquals_Throws_WhenComparingJsonElementParameterToNonJsonElement()
117+
public void WebRootComponentParameters_DefinitelyEquals_ReturnsFalse_WhenComparingJsonElementParameterToNonJsonElement()
115118
{
116119
// Arrange
117120
var parameters1 = CreateParameters(new() { ["First"] = 123 });
118121
var parameters2 = CreateParametersWithNonJsonElements(new() { ["First"] = 456 });
119122

120-
// Act/assert
121-
Assert.Throws<InvalidCastException>(() => parameters1.DefinitelyEquals(parameters2));
123+
// Act
124+
var result = parameters1.DefinitelyEquals(parameters2);
125+
126+
// Assert
127+
Assert.False(result);
128+
}
129+
130+
[Fact]
131+
public void WebRootComponentParameters_DefinitelyEquals_ReturnsTrue_WhenComparingEqualNonJsonElementParameters()
132+
{
133+
// Arrange
134+
var parameters1 = CreateParametersWithNonJsonElements(new() { ["First"] = 123 });
135+
var parameters2 = CreateParametersWithNonJsonElements(new() { ["First"] = 123 });
136+
137+
// Act
138+
var result = parameters1.DefinitelyEquals(parameters2);
139+
140+
// Assert
141+
Assert.True(result);
122142
}
123143

124144
[Fact]
125-
public void WebRootComponentParameters_DefinitelyEquals_Throws_WhenComparingNonJsonElementParameters()
145+
public void WebRootComponentParameters_DefinitelyEquals_ReturnsFalse_WhenComparingInequalNonJsonElementParameters()
126146
{
127147
// Arrange
128148
var parameters1 = CreateParametersWithNonJsonElements(new() { ["First"] = 123 });
129149
var parameters2 = CreateParametersWithNonJsonElements(new() { ["First"] = 456 });
130150

131-
// Act/assert
132-
Assert.Throws<InvalidCastException>(() => parameters1.DefinitelyEquals(parameters2));
151+
// Act
152+
var result = parameters1.DefinitelyEquals(parameters2);
153+
154+
// Assert
155+
Assert.False(result);
156+
}
157+
158+
[Fact]
159+
public void WebRootComponentParameters_DefinitelyEquals_ReturnsTrue_WhenComparingNullParameters()
160+
{
161+
// Arrange
162+
var parameters1 = CreateParametersWithNonJsonElements(new() { ["First"] = null });
163+
var parameters2 = CreateParametersWithNonJsonElements(new() { ["First"] = null });
164+
165+
// Act
166+
var result = parameters1.DefinitelyEquals(parameters2);
167+
168+
// Assert
169+
Assert.True(result);
133170
}
134171

135172
private static WebRootComponentParameters CreateParameters(Dictionary<string, object> parameters)

src/Components/Shared/src/WebRootComponentParameters.cs

+11-3
Original file line numberDiff line numberDiff line change
@@ -45,9 +45,17 @@ public bool DefinitelyEquals(in WebRootComponentParameters other)
4545
return false;
4646
}
4747

48-
var value = ((JsonElement)_serializedParameterValues[i]).GetRawText();
49-
var otherValue = ((JsonElement)other._serializedParameterValues[i]).GetRawText();
50-
if (!string.Equals(value, otherValue, StringComparison.Ordinal))
48+
// We expect each serialized parameter value to be either a 'JsonElement' or 'null'.
49+
var value = _serializedParameterValues[i];
50+
var otherValue = other._serializedParameterValues[i];
51+
if (value is JsonElement jsonValue && otherValue is JsonElement otherJsonValue)
52+
{
53+
if (!string.Equals(jsonValue.GetRawText(), otherJsonValue.GetRawText(), StringComparison.Ordinal))
54+
{
55+
return false;
56+
}
57+
}
58+
else if (!Equals(value, otherValue))
5159
{
5260
return false;
5361
}

src/Components/test/E2ETest/ServerRenderingTests/EnhancedNavigationTest.cs

+32
Original file line numberDiff line numberDiff line change
@@ -554,6 +554,38 @@ public void LocationChangingEventGetsInvokedOnEnhancedNavigationOnlyForRuntimeTh
554554
Browser.Equal("0", () => Browser.Exists(By.Id($"location-changing-count-{anotherRuntime}")).Text);
555555
}
556556

557+
[Theory]
558+
[InlineData("server")]
559+
[InlineData("wasm")]
560+
public void CanReceiveNullParameterValueOnEnhancedNavigation(string renderMode)
561+
{
562+
// See: https://github.com/dotnet/aspnetcore/issues/52434
563+
Navigate($"{ServerPathBase}/nav");
564+
Browser.Equal("Hello", () => Browser.Exists(By.TagName("h1")).Text);
565+
566+
Browser.Exists(By.TagName("nav")).FindElement(By.LinkText($"Null component parameter ({renderMode})")).Click();
567+
Browser.Equal("Page rendering component with null parameter", () => Browser.Exists(By.TagName("h1")).Text);
568+
Browser.Equal("0", () => Browser.Exists(By.Id("current-count")).Text);
569+
570+
Browser.Exists(By.Id("button-increment")).Click();
571+
Browser.Equal("0", () => Browser.Exists(By.Id("location-changed-count")).Text);
572+
Browser.Equal("1", () => Browser.Exists(By.Id("current-count")).Text);
573+
574+
// This refresh causes the interactive component to receive a 'null' parameter value
575+
Browser.Exists(By.Id("button-refresh")).Click();
576+
Browser.Equal("1", () => Browser.Exists(By.Id("location-changed-count")).Text);
577+
Browser.Equal("1", () => Browser.Exists(By.Id("current-count")).Text);
578+
579+
// Increment the count again to ensure that interactivity still works
580+
Browser.Exists(By.Id("button-increment")).Click();
581+
Browser.Equal("2", () => Browser.Exists(By.Id("current-count")).Text);
582+
583+
// Even if the interactive runtime continues to function (as the WebAssembly runtime might),
584+
// fail the test if any errors were logged to the browser console
585+
var logs = Browser.GetBrowserLogs(LogLevel.Warning);
586+
Assert.DoesNotContain(logs, log => log.Message.Contains("Error"));
587+
}
588+
557589
private void AssertEnhancedUpdateCountEquals(long count)
558590
=> Browser.Equal(count, () => ((IJavaScriptExecutor)Browser).ExecuteScript("return window.enhancedPageUpdateCount;"));
559591

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
@page "/nav/null-parameter/{mode}"
2+
@using TestContentPackage
3+
4+
@* https://github.com/dotnet/aspnetcore/issues/52434 *@
5+
6+
<h1>Page rendering component with null parameter</h1>
7+
8+
@if (Mode == "server")
9+
{
10+
<ComponentAcceptingNullParameter @rendermode="RenderMode.InteractiveServer" Value="@null" />
11+
}
12+
else if (Mode == "wasm")
13+
{
14+
<ComponentAcceptingNullParameter @rendermode="RenderMode.InteractiveWebAssembly" Value="@null" />
15+
}
16+
else
17+
{
18+
<p>Expected a render mode of 'server' or 'wasm', but got '@Mode'.</p>
19+
}
20+
21+
@code {
22+
[Parameter]
23+
public string? Mode { get; set; }
24+
}

src/Components/test/testassets/Components.TestServer/RazorComponents/Shared/EnhancedNavLayout.razor

+2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@
2323
<NavLink href="nav/location-changed/server">LocationChanged/LocationChanging event (server)</NavLink>
2424
<NavLink href="nav/location-changed/wasm">LocationChanged/LocationChanging event (wasm)</NavLink>
2525
<NavLink href="nav/location-changed/server-and-wasm">LocationChanged/LocationChanging event (server-and-wasm)</NavLink>
26+
<NavLink href="nav/null-parameter/server">Null component parameter (server)</NavLink>
27+
<NavLink href="nav/null-parameter/wasm">Null component parameter (wasm)</NavLink>
2628
</nav>
2729
<hr/>
2830
<main>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
@implements IDisposable
2+
@inject NavigationManager NavigationManager
3+
@using Microsoft.AspNetCore.Components.Routing
4+
5+
<p>Value: @(Value ?? "(null)")</p>
6+
7+
@if (_interactive)
8+
{
9+
<button id="button-increment" @onclick="Increment">Count: <span id="current-count">@_count</span></button>
10+
<button id="button-refresh" @onclick="Refresh">Refresh</button>
11+
<p>Location changed count: <span id="location-changed-count">@_locationChangedCount</span></p>
12+
}
13+
14+
@code {
15+
private bool _interactive;
16+
private int _count;
17+
private int _locationChangedCount;
18+
19+
[Parameter]
20+
public string Value { get; set; }
21+
22+
protected override void OnAfterRender(bool firstRender)
23+
{
24+
if (firstRender)
25+
{
26+
NavigationManager.LocationChanged += OnLocationChanged;
27+
_interactive = true;
28+
StateHasChanged();
29+
}
30+
}
31+
32+
private void OnLocationChanged(object sender, LocationChangedEventArgs e)
33+
{
34+
_locationChangedCount++;
35+
StateHasChanged();
36+
}
37+
38+
private void Increment()
39+
{
40+
_count++;
41+
}
42+
43+
private void Refresh()
44+
{
45+
NavigationManager.Refresh();
46+
}
47+
48+
public void Dispose()
49+
{
50+
NavigationManager.LocationChanged -= OnLocationChanged;
51+
}
52+
}

0 commit comments

Comments
 (0)