Throw if a component produces an invalid render tree. Fixes #24579 (#24650)

This commit is contained in:
Steve Sanderson 2020-08-07 16:41:08 +01:00 committed by GitHub
parent 3da9c7cff7
commit 8ba7c7b457
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 96 additions and 0 deletions

View File

@ -69,6 +69,8 @@ namespace Microsoft.AspNetCore.Components.Rendering
CurrentRenderTree.Clear();
renderFragment(CurrentRenderTree);
CurrentRenderTree.AssertTreeIsValid(Component);
var diff = RenderTreeDiffBuilder.ComputeDiff(
_renderer,
batchBuilder,

View File

@ -685,6 +685,17 @@ namespace Microsoft.AspNetCore.Components.Rendering
public ArrayRange<RenderTreeFrame> GetFrames() =>
_entries.ToRange();
internal void AssertTreeIsValid(IComponent component)
{
if (_openElementIndices.Count > 0)
{
// It's never valid to leave an element/component/region unclosed. Doing so
// could cause undefined behavior in diffing.
ref var invalidFrame = ref _entries.Buffer[_openElementIndices.Peek()];
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.");
}
}
// Internal for testing
internal void ProcessDuplicateAttributes(int first)
{

View File

@ -4001,6 +4001,22 @@ namespace Microsoft.AspNetCore.Components.Test
requestedType => Assert.Equal(typeof(TestComponent), requestedType));
}
[Fact]
public async Task ThrowsIfComponentProducesInvalidRenderTree()
{
// Arrange
var renderer = new TestRenderer();
var component = new TestComponent(builder =>
{
builder.OpenElement(0, "myElem");
});
var rootComponentId = renderer.AssignRootComponentId(component);
// Act/Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => renderer.RenderRootComponentAsync(rootComponentId));
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Element' was left unclosed.", ex.Message);
}
private class TestComponentActivator<TResult> : IComponentActivator where TResult : IComponent, new()
{
public List<Type> RequestedComponentTypes { get; } = new List<Type>();

View File

@ -1831,6 +1831,69 @@ namespace Microsoft.AspNetCore.Components.Rendering
f => AssertFrame.Attribute(f, "3", "see ya"));
}
[Fact]
public void AcceptsClosedFramesAsValid()
{
// Arrange
var builder = new RenderTreeBuilder();
var component = new TestComponent();
builder.OpenElement(0, "myElem");
builder.OpenRegion(1);
builder.OpenComponent<OtherComponent>(2);
builder.CloseComponent();
builder.CloseRegion();
builder.CloseElement();
// Act/Assert
// Lack of exception is success
builder.AssertTreeIsValid(component);
}
[Fact]
public void ReportsUnclosedElementAsInvalid()
{
// Arrange
var builder = new RenderTreeBuilder();
var component = new TestComponent();
builder.OpenElement(0, "outerElem");
builder.OpenElement(1, "innerElem");
builder.CloseElement();
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Element' was left unclosed.", ex.Message);
}
[Fact]
public void ReportsUnclosedComponentAsInvalid()
{
// Arrange
var builder = new RenderTreeBuilder();
var component = new TestComponent();
builder.OpenComponent<OtherComponent>(0);
builder.OpenComponent<OtherComponent>(1);
builder.CloseComponent();
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Component' was left unclosed.", ex.Message);
}
[Fact]
public void ReportsUnclosedRegionAsInvalid()
{
// Arrange
var builder = new RenderTreeBuilder();
var component = new TestComponent();
builder.OpenRegion(0);
builder.OpenRegion(1);
builder.CloseRegion();
// Act/Assert
var ex = Assert.Throws<InvalidOperationException>(() => builder.AssertTreeIsValid(component));
Assert.StartsWith($"Render output is invalid for component of type '{typeof(TestComponent).FullName}'. A frame of type 'Region' was left unclosed.", ex.Message);
}
private class TestComponent : IComponent
{
public void Attach(RenderHandle renderHandle) { }
@ -1839,6 +1902,10 @@ namespace Microsoft.AspNetCore.Components.Rendering
=> throw new NotImplementedException();
}
private class OtherComponent : TestComponent
{
}
private class TestRenderer : Renderer
{
public TestRenderer() : base(new TestServiceProvider(), NullLoggerFactory.Instance)