diff --git a/src/Components/Components/test/CascadingParameterTest.cs b/src/Components/Components/test/CascadingParameterTest.cs index aba1199d9b..0716474c30 100644 --- a/src/Components/Components/test/CascadingParameterTest.cs +++ b/src/Components/Components/test/CascadingParameterTest.cs @@ -354,6 +354,40 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message); } + [Fact] + public void ParameterViewSuppliedWithCascadingParametersCannotBeUsedAfterSynchronousReturn() + { + // Arrange + var providedValue = "Initial value"; + var renderer = new TestRenderer(); + var component = new TestComponent(builder => + { + builder.OpenComponent>(0); + builder.AddAttribute(1, "Value", providedValue); + builder.AddAttribute(2, "ChildContent", new RenderFragment(childBuilder => + { + childBuilder.OpenComponent>(0); + childBuilder.CloseComponent(); + })); + builder.CloseComponent(); + }); + + // Initial render; capture nested component + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + var firstBatch = renderer.Batches.Single(); + var nestedComponent = FindComponent>(firstBatch, out var nestedComponentId); + + // Re-render CascadingValue with new value, so it gets a new ParameterView + providedValue = "Updated value"; + component.TriggerRender(); + Assert.Equal(2, renderer.Batches.Count); + + // It's no longer able to access anything in the ParameterView it just received + var ex = Assert.Throws(nestedComponent.AttemptIllegalAccessToLastParameterView); + Assert.Equal("blah", ex.Message); + } + private static T FindComponent(CapturedBatch batch, out int componentId) { var componentFrame = batch.ReferenceFrames.Single( @@ -378,6 +412,8 @@ namespace Microsoft.AspNetCore.Components.Test class CascadingParameterConsumerComponent : AutoRenderComponent { + private ParameterView lastParameterView; + public int NumSetParametersCalls { get; private set; } public int NumRenders { get; private set; } @@ -386,6 +422,7 @@ namespace Microsoft.AspNetCore.Components.Test public override async Task SetParametersAsync(ParameterView parameters) { + lastParameterView = parameters; NumSetParametersCalls++; await base.SetParametersAsync(parameters); } @@ -395,6 +432,13 @@ namespace Microsoft.AspNetCore.Components.Test NumRenders++; builder.AddContent(0, $"CascadingParameter={CascadingParameter}; RegularParameter={RegularParameter}"); } + + public void AttemptIllegalAccessToLastParameterView() + { + // You're not allowed to hold onto a ParameterView and access it later, + // so this should throw + lastParameterView.TryGetValue("anything", out _); + } } class SecondCascadingParameterConsumerComponent : CascadingParameterConsumerComponent diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index a48cda6939..eac2d1b13a 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -3701,6 +3701,24 @@ namespace Microsoft.AspNetCore.Components.Test Assert.Contains("Cannot start a batch when one is already in progress.", ex.Message); } + [Fact] + public void CannotAccessParameterViewAfterSynchronousReturn() + { + // Arrange + var renderer = new TestRenderer(); + var component = new ParameterViewIllegalCapturingComponent(); + var componentId = renderer.AssignRootComponentId(component); + renderer.RenderRootComponentAsync(componentId); + + // Act/Assert + var ex = Assert.Throws(() => + { + // TODO: check other types of access too + component.CapturedParameterView.TryGetValue("anything", out _); + }); + Assert.Equal("blah", ex.Message); + } + private class NoOpRenderer : Renderer { public NoOpRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance) @@ -4443,5 +4461,23 @@ namespace Microsoft.AspNetCore.Components.Test public new void ProcessPendingRender() => base.ProcessPendingRender(); } + + class ParameterViewIllegalCapturingComponent : IComponent + { + public ParameterView CapturedParameterView { get; private set; } + + public void Attach(RenderHandle renderHandle) + { + } + + public Task SetParametersAsync(ParameterView parameters) + { + CapturedParameterView = parameters; + + // Return a task that never completes to show that access is forbidded + // after the synchronous return, not just after the returned task completes + return new TaskCompletionSource().Task; + } + } } }