Skip to content

Commit 8ba7c7b

Browse files
Throw if a component produces an invalid render tree. Fixes #24579 (#24650)
1 parent 3da9c7c commit 8ba7c7b

File tree

4 files changed

+96
-0
lines changed

4 files changed

+96
-0
lines changed

src/Components/Components/src/Rendering/ComponentState.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ public void RenderIntoBatch(RenderBatchBuilder batchBuilder, RenderFragment rend
6969
CurrentRenderTree.Clear();
7070
renderFragment(CurrentRenderTree);
7171

72+
CurrentRenderTree.AssertTreeIsValid(Component);
73+
7274
var diff = RenderTreeDiffBuilder.ComputeDiff(
7375
_renderer,
7476
batchBuilder,

src/Components/Components/src/Rendering/RenderTreeBuilder.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -685,6 +685,17 @@ internal void InsertAttributeExpensive(int insertAtIndex, int sequence, string a
685685
public ArrayRange<RenderTreeFrame> GetFrames() =>
686686
_entries.ToRange();
687687

688+
internal void AssertTreeIsValid(IComponent component)
689+
{
690+
if (_openElementIndices.Count > 0)
691+
{
692+
// It's never valid to leave an element/component/region unclosed. Doing so
693+
// could cause undefined behavior in diffing.
694+
ref var invalidFrame = ref _entries.Buffer[_openElementIndices.Peek()];
695+
throw new InvalidOperationException($"Render output is invalid for component of type '{component.GetType().FullName}'. A frame of type '{invalidFrame.FrameType}' was left unclosed. Do not use try/catch inside rendering logic, because partial output cannot be undone.");
696+
}
697+
}
698+
688699
// Internal for testing
689700
internal void ProcessDuplicateAttributes(int first)
690701
{

src/Components/Components/test/RendererTest.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4001,6 +4001,22 @@ public void CanUseCustomComponentActivatorFromServiceProvider()
40014001
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
40024002
}
40034003

4004+
[Fact]
4005+
public async Task ThrowsIfComponentProducesInvalidRenderTree()
4006+
{
4007+
// Arrange
4008+
var renderer = new TestRenderer();
4009+
var component = new TestComponent(builder =>
4010+
{
4011+
builder.OpenElement(0, "myElem");
4012+
});
4013+
var rootComponentId = renderer.AssignRootComponentId(component);
4014+
4015+
// Act/Assert
4016+
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderRootComponentAsync(rootComponentId));
4017+
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Element' was left unclosed.", ex.Message);
4018+
}
4019+
40044020
private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
40054021
{
40064022
public List<Type> RequestedComponentTypes { get; } = new List<Type>();

src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,6 +1831,69 @@ public void ProcessDuplicateAttributes_CanRemoveOverwrittenAttributes()
18311831
f => AssertFrame.Attribute(f, "3", "see ya"));
18321832
}
18331833

1834+
[Fact]
1835+
public void AcceptsClosedFramesAsValid()
1836+
{
1837+
// Arrange
1838+
var builder = new RenderTreeBuilder();
1839+
var component = new TestComponent();
1840+
builder.OpenElement(0, "myElem");
1841+
builder.OpenRegion(1);
1842+
builder.OpenComponent<OtherComponent>(2);
1843+
builder.CloseComponent();
1844+
builder.CloseRegion();
1845+
builder.CloseElement();
1846+
1847+
// Act/Assert
1848+
// Lack of exception is success
1849+
builder.AssertTreeIsValid(component);
1850+
}
1851+
1852+
[Fact]
1853+
public void ReportsUnclosedElementAsInvalid()
1854+
{
1855+
// Arrange
1856+
var builder = new RenderTreeBuilder();
1857+
var component = new TestComponent();
1858+
builder.OpenElement(0, "outerElem");
1859+
builder.OpenElement(1, "innerElem");
1860+
builder.CloseElement();
1861+
1862+
// Act/Assert
1863+
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
1864+
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Element' was left unclosed.", ex.Message);
1865+
}
1866+
1867+
[Fact]
1868+
public void ReportsUnclosedComponentAsInvalid()
1869+
{
1870+
// Arrange
1871+
var builder = new RenderTreeBuilder();
1872+
var component = new TestComponent();
1873+
builder.OpenComponent<OtherComponent>(0);
1874+
builder.OpenComponent<OtherComponent>(1);
1875+
builder.CloseComponent();
1876+
1877+
// Act/Assert
1878+
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
1879+
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Component' was left unclosed.", ex.Message);
1880+
}
1881+
1882+
[Fact]
1883+
public void ReportsUnclosedRegionAsInvalid()
1884+
{
1885+
// Arrange
1886+
var builder = new RenderTreeBuilder();
1887+
var component = new TestComponent();
1888+
builder.OpenRegion(0);
1889+
builder.OpenRegion(1);
1890+
builder.CloseRegion();
1891+
1892+
// Act/Assert
1893+
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
1894+
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Region' was left unclosed.", ex.Message);
1895+
}
1896+
18341897
private class TestComponent : IComponent
18351898
{
18361899
public void Attach(RenderHandle renderHandle) { }
@@ -1839,6 +1902,10 @@ public Task SetParametersAsync(ParameterView parameters)
18391902
=> throw new NotImplementedException();
18401903
}
18411904

1905+
private class OtherComponent : TestComponent
1906+
{
1907+
}
1908+
18421909
private class TestRenderer : Renderer
18431910
{
18441911
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)

0 commit comments

Comments
 (0)