File: CascadingParameterTest.cs
Web Access
Project: src\src\Components\Components\test\Microsoft.AspNetCore.Components.Tests.csproj (Microsoft.AspNetCore.Components.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.AspNetCore.Components.Test.Helpers;
using Microsoft.Extensions.DependencyInjection;
 
namespace Microsoft.AspNetCore.Components.Test;
 
public class CascadingParameterTest
{
    [Fact]
    public void PassesCascadingParametersToNestedComponents()
    {
        // Arrange
        var renderer = new TestRenderer();
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", "Hello");
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
                childBuilder.AddComponentParameter(1, "RegularParameter", "Goodbye");
                childBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act/Assert
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var batch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(batch, out var nestedComponentId);
        var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
 
        // The nested component was rendered with the correct parameters
        Assert.Collection(nestedComponentDiff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    batch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "CascadingParameter=Hello; RegularParameter=Goodbye");
            });
        Assert.Equal(1, nestedComponent.NumRenders);
    }
 
    [Fact]
    public void RetainsCascadingParametersWhenUpdatingDirectParameters()
    {
        // Arrange
        var renderer = new TestRenderer();
        var regularParameterValue = "Initial value";
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", "Hello");
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
                childBuilder.AddComponentParameter(1, "RegularParameter", regularParameterValue);
                childBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act 1: Render in initial state
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
 
        // Capture the nested component so we can verify the update later
        var firstBatch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
        Assert.Equal(1, nestedComponent.NumRenders);
 
        // Act 2: Render again with updated regular parameter
        regularParameterValue = "Changed value";
        component.TriggerRender();
 
        // Assert
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
 
        // The nested component was rendered with the correct parameters
        Assert.Collection(nestedComponentDiff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
                Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
                AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Hello; RegularParameter=Changed value");
            });
        Assert.Equal(2, nestedComponent.NumRenders);
    }
 
    [Fact]
    public void NotifiesDescendantsOfUpdatedCascadingParameterValuesAndPreservesDirectParameters()
    {
        // Arrange
        var providedValue = "Initial value";
        var renderer = new TestRenderer();
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", providedValue);
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
                childBuilder.AddComponentParameter(1, "RegularParameter", "Goodbye");
                childBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act 1: Initial render; capture nested component ID
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var firstBatch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
        Assert.Equal(1, nestedComponent.NumRenders);
 
        // Act 2: Re-render CascadingValue with new value
        providedValue = "Updated value";
        component.TriggerRender();
 
        // Assert: We re-rendered CascadingParameterConsumerComponent
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
 
        // The nested component was rendered with the correct parameters
        Assert.Collection(nestedComponentDiff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
                Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
                AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Updated value; RegularParameter=Goodbye");
            });
        Assert.Equal(2, nestedComponent.NumRenders);
    }
 
    [Fact]
    public void DoesNotNotifyDescendantsIfCascadingParameterValuesAreImmutableAndUnchanged()
    {
        // Arrange
        var renderer = new TestRenderer();
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", "Unchanging value");
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
                childBuilder.AddComponentParameter(1, "RegularParameter", "Goodbye");
                childBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act 1: Initial render
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var firstBatch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out _);
        Assert.Equal(3, firstBatch.DiffsByComponentId.Count); // Root + CascadingValue + nested
        Assert.Equal(1, nestedComponent.NumRenders);
 
        // Act/Assert: Re-render the CascadingValue; observe nested component wasn't re-rendered
        component.TriggerRender();
 
        // Assert: We did not re-render CascadingParameterConsumerComponent
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
        Assert.Equal(1, nestedComponent.NumRenders);
    }
 
    [Fact]
    public void StopsNotifyingDescendantsIfTheyAreRemoved()
    {
        // Arrange
        var providedValue = "Initial value";
        var displayNestedComponent = true;
        var renderer = new TestRenderer();
        var component = new TestComponent(builder =>
        {
            // At the outer level, have an unrelated fixed cascading value to show we can deal with combining both types
            builder.OpenComponent<CascadingValue<int>>(0);
            builder.AddComponentParameter(1, "Value", 123);
            builder.AddComponentParameter(2, "IsFixed", true);
            builder.AddComponentParameter(3, "ChildContent", new RenderFragment(builder2 =>
            {
                // Then also have a non-fixed cascading value so we can show that unsubscription works
                builder2.OpenComponent<CascadingValue<string>>(0);
                builder2.AddComponentParameter(1, "Value", providedValue);
                builder2.AddComponentParameter(2, "ChildContent", new RenderFragment(builder3 =>
                {
                    if (displayNestedComponent)
                    {
                        builder3.OpenComponent<SecondCascadingParameterConsumerComponent<string, int>>(0);
                        builder3.AddComponentParameter(1, "RegularParameter", "Goodbye");
                        builder3.CloseComponent();
                    }
                }));
                builder2.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act 1: Initial render; capture nested component ID
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var firstBatch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
        Assert.Equal(1, nestedComponent.NumSetParametersCalls);
        Assert.Equal(1, nestedComponent.NumRenders);
 
        // Act/Assert 2: Re-render the CascadingValue; observe nested component wasn't re-rendered
        providedValue = "Updated value";
        displayNestedComponent = false; // Remove the nested component
        component.TriggerRender();
 
        // Assert: We did not render the nested component now it's been removed
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        Assert.Equal(1, nestedComponent.NumRenders);
        Assert.Equal(3, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue + CascadingValue, but not nested component
 
        // We *did* send updated params during the first render where it was removed,
        // because the params are sent before the disposal logic runs. We could avoid
        // this by moving the notifications into the OnAfterRender phase, but then we'd
        // often render descendants twice (once because they are descendants and some
        // direct parameter might have changed, then once because a cascading parameter
        // changed). We can't have it both ways, so optimize for the case when the
        // nested component *hasn't* just been removed.
        Assert.Equal(2, nestedComponent.NumSetParametersCalls);
 
        // Act 3: However, after disposal, the subscription is removed, so we won't send
        // updated params on subsequent CascadingValue renders.
        providedValue = "Updated value 2";
        component.TriggerRender();
        Assert.Equal(2, nestedComponent.NumSetParametersCalls);
    }
 
    [Fact]
    public void DoesNotNotifyDescendantsOfUpdatedCascadingParameterValuesWhenFixed()
    {
        // Arrange
        var providedValue = "Initial value";
        var shouldIncludeChild = true;
        var renderer = new TestRenderer();
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", providedValue);
            builder.AddComponentParameter(2, "IsFixed", true);
            builder.AddComponentParameter(3, "ChildContent", new RenderFragment(childBuilder =>
            {
                if (shouldIncludeChild)
                {
                    childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(0);
                    childBuilder.AddComponentParameter(1, "RegularParameter", "Goodbye");
                    childBuilder.CloseComponent();
                }
            }));
            builder.CloseComponent();
        });
 
        // Act 1: Initial render; capture nested component ID
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var firstBatch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<string>>(firstBatch, out var nestedComponentId);
        Assert.Equal(1, nestedComponent.NumRenders);
 
        // Assert: Initial value is supplied to descendant
        var nestedComponentDiff = firstBatch.DiffsByComponentId[nestedComponentId].Single();
        Assert.Collection(nestedComponentDiff.Edits, edit =>
        {
            Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
            AssertFrame.Text(
                firstBatch.ReferenceFrames[edit.ReferenceFrameIndex],
                "CascadingParameter=Initial value; RegularParameter=Goodbye");
        });
 
        // Act 2: Re-render CascadingValue with new value
        providedValue = "Updated value";
        component.TriggerRender();
 
        // Assert: We did not re-render the descendant
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        Assert.Equal(2, secondBatch.DiffsByComponentId.Count); // Root + CascadingValue, but not nested one
        Assert.Equal(1, nestedComponent.NumSetParametersCalls);
        Assert.Equal(1, nestedComponent.NumRenders);
 
        // Act 3: Dispose
        shouldIncludeChild = false;
        component.TriggerRender();
 
        // Assert: Absence of an exception here implies we didn't cause a problem by
        // trying to remove a non-existent subscription
    }
 
    [Fact]
    public void CascadingValueThrowsIfFixedFlagChangesToTrue()
    {
        // Arrange
        var renderer = new TestRenderer();
        var isFixed = false;
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<object>>(0);
            builder.AddComponentParameter(1, "IsFixed", isFixed);
            builder.AddComponentParameter(2, "Value", new object());
            builder.CloseComponent();
        });
        renderer.AssignRootComponentId(component);
        component.TriggerRender();
 
        // Act/Assert
        isFixed = true;
        var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
        Assert.Equal("The value of IsFixed cannot be changed dynamically.", ex.Message);
    }
 
    [Fact]
    public void CascadingValueThrowsIfFixedFlagChangesToFalse()
    {
        // Arrange
        var renderer = new TestRenderer();
        var isFixed = true;
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<object>>(0);
            if (isFixed) // Showing also that "unset" is treated as "false"
            {
                builder.AddComponentParameter(1, "IsFixed", true);
            }
            builder.AddComponentParameter(2, "Value", new object());
            builder.CloseComponent();
        });
        renderer.AssignRootComponentId(component);
        component.TriggerRender();
 
        // Act/Assert
        isFixed = false;
        var ex = Assert.Throws<InvalidOperationException>(() => component.TriggerRender());
        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<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", providedValue);
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<CascadingParameterConsumerComponent<string>>(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<CascadingParameterConsumerComponent<string>>(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<InvalidOperationException>(nestedComponent.AttemptIllegalAccessToLastParameterView);
        Assert.Equal($"The {nameof(ParameterView)} instance can no longer be read because it has expired. {nameof(ParameterView)} can only be read synchronously and must not be stored for later use.", ex.Message);
    }
 
    [Fact]
    public void CanSupplyCascadingValuesForSpecificCascadingParameterAttributeType()
    {
        // Arrange
        var renderer = new TestRenderer();
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CustomCascadingValueProducer<CustomCascadingParameter1Attribute>>(0);
            builder.AddComponentParameter(1, "Value", "Hello 1");
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(builder =>
            {
                builder.OpenComponent<CustomCascadingValueProducer<CustomCascadingParameter2Attribute>>(0);
                builder.AddComponentParameter(1, "Value", "Hello 2");
                builder.AddComponentParameter(2, "ChildContent", new RenderFragment(builder =>
                {
                    builder.OpenComponent<CustomCascadingValueConsumer1>(0);
                    builder.CloseComponent();
                    builder.OpenComponent<CustomCascadingValueConsumer2>(1);
                    builder.CloseComponent();
                }));
                builder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act/Assert
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        var batch = renderer.Batches.Single();
        var nestedComponent1 = FindComponent<CustomCascadingValueConsumer1>(batch, out var nestedComponentId1);
        var nestedComponent2 = FindComponent<CustomCascadingValueConsumer2>(batch, out var nestedComponentId2);
        var nestedComponentDiff1 = batch.DiffsByComponentId[nestedComponentId1].Single();
        var nestedComponentDiff2 = batch.DiffsByComponentId[nestedComponentId2].Single();
 
        // The nested components were rendered with the correct parameters
        Assert.Collection(nestedComponentDiff1.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    batch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "Value 1 is 'Hello 1'.");
            });
        Assert.Collection(nestedComponentDiff2.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    batch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "Value 2 is 'Hello 2'.");
            });
    }
 
    [Fact]
    public void CanSupplyCascadingValueFromServiceProvider()
    {
        // Arrange
        var services = new ServiceCollection();
        var constructionCount = 0;
        services.AddCascadingValue(_ =>
        {
            constructionCount++;
            return new MyParamType("Hello");
        });
        var renderer = new TestRenderer(services.BuildServiceProvider());
 
        // Assert: The value is constructed lazily, so we won't have been asked for it yet, even if some
        // related components were rendered
        var unrelatedComponentId = renderer.AssignRootComponentId(new TestComponent(_ => { }));
        renderer.RenderRootComponent(unrelatedComponentId);
        Assert.Equal(0, constructionCount);
 
        // Act/Assert: Render a component that consumes the value
        var component = new CascadingParameterConsumerComponent<MyParamType> { RegularParameter = "Goodbye" };
        var componentId = renderer.AssignRootComponentId(component);
        Assert.Equal(0, constructionCount);
        renderer.RenderRootComponent(componentId);
        Assert.Equal(1, constructionCount);
        var batch = renderer.Batches.Skip(1).Single();
        var diff = batch.DiffsByComponentId[componentId].Single();
 
        // The component was rendered with the correct parameters
        Assert.Collection(diff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    batch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "CascadingParameter=Hello; RegularParameter=Goodbye");
            });
        Assert.Equal(1, component.NumRenders);
 
        // Act/Assert: Even if another component consumes the value, we don't call the factory again
        var anotherConsumer = new CascadingParameterConsumerComponent<MyParamType> { RegularParameter = "Goodbye" };
        var anotherConsumerComponentId = renderer.AssignRootComponentId(anotherConsumer);
        renderer.RenderRootComponent(anotherConsumerComponentId);
        Assert.Equal(1, constructionCount);
        Assert.Same(component.GetCascadingParameterValue(), anotherConsumer.GetCascadingParameterValue());
    }
 
    [Fact]
    public void CanSupplyCascadingValueFromServiceProviderUsingName()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddCascadingValue("Ignored", _ => new MyParamType("Should be ignored"));
        services.AddCascadingValue("My cascading parameter name", _ => new MyParamType("Should be used"));
        services.AddCascadingValue("Also ignored", _ => new MyParamType("Should also be ignored"));
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new ConsumeNamedCascadingValueComponent();
 
        // Act/Assert
        var componentId = renderer.AssignRootComponentId(component);
        renderer.RenderRootComponent(componentId);
        var batch = renderer.Batches.Single();
        var diff = batch.DiffsByComponentId[componentId].Single();
 
        // The component was rendered with the correct parameters
        Assert.Collection(diff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    batch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "The value is 'Should be used'");
            });
    }
 
    [Fact]
    public void PrefersComponentHierarchyCascadingValuesOverServiceProviderValues()
    {
        // Arrange
        var services = new ServiceCollection();
        services.AddCascadingValue(_ => new MyParamType("Hello from services (this should be overridden)"));
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<MyParamType>>(0);
            builder.AddComponentParameter(1, "Value", new MyParamType("Hello from component hierarchy"));
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(childBuilder =>
            {
                childBuilder.OpenComponent<CascadingParameterConsumerComponent<MyParamType>>(0);
                childBuilder.AddComponentParameter(1, "RegularParameter", "Goodbye");
                childBuilder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act/Assert
        var componentId = renderer.AssignRootComponentId(component);
        renderer.RenderRootComponent(componentId);
        var batch = renderer.Batches.Single();
        var nestedComponent = FindComponent<CascadingParameterConsumerComponent<MyParamType>>(batch, out var nestedComponentId);
        var nestedComponentDiff = batch.DiffsByComponentId[nestedComponentId].Single();
 
        // The nested component was rendered with the correct parameters
        Assert.Collection(nestedComponentDiff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    batch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "CascadingParameter=Hello from component hierarchy; RegularParameter=Goodbye");
            });
        Assert.Equal(1, nestedComponent.NumRenders);
    }
 
    [Fact]
    public void ThrowsIfAttemptingToSubscribeToCascadingValueSourceOutsideSyncContext()
    {
        // Arrange
        var services = new ServiceCollection();
        var cascadingValueSource = new CascadingValueSource<MyParamType>(new MyParamType("Initial value"), isFixed: false);
        services.AddCascadingValue(_ => cascadingValueSource);
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new CascadingParameterConsumerComponent<MyParamType>();
 
        // Act/Assert: Throws because this is where it tries to attach to the CascadingValueSource
        var ex = Assert.Throws<InvalidOperationException>(() => renderer.AssignRootComponentId(component));
        Assert.Contains("The current thread is not associated with the Dispatcher", ex.Message);
    }
 
    [Fact]
    public async Task CanTriggerUpdatesOnCascadingValuesFromServiceProvider()
    {
        // Arrange
        var services = new ServiceCollection();
        var myParamValue = new MyParamType("Initial value");
        var cascadingValueSource = new CascadingValueSource<MyParamType>(myParamValue, isFixed: false);
        services.AddCascadingValue(_ => cascadingValueSource);
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new CascadingParameterConsumerComponent<MyParamType> { RegularParameter = "Goodbye" };
 
        // Act/Assert 1: Initial render
        var componentId = await renderer.Dispatcher.InvokeAsync(() => renderer.AssignRootComponentId(component));
        renderer.RenderRootComponent(componentId);
        var firstBatch = renderer.Batches.Single();
        var diff = firstBatch.DiffsByComponentId[componentId].Single();
        Assert.Collection(diff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    firstBatch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "CascadingParameter=Initial value; RegularParameter=Goodbye");
            });
        Assert.Equal(1, component.NumRenders);
 
        // Act/Assert 2: Notify about a mutation
        myParamValue.ChangeValue("Mutated value");
        await cascadingValueSource.NotifyChangedAsync();
 
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        var diff2 = secondBatch.DiffsByComponentId[componentId].Single();
 
        // The nested component was rendered with the correct parameters
        Assert.Collection(diff2.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
                Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
                AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Mutated value; RegularParameter=Goodbye");
            });
        Assert.Equal(2, component.NumRenders);
 
        // Act/Assert 3: Notify about a completely different object
        await cascadingValueSource.NotifyChangedAsync(new MyParamType("Whole new object"));
        Assert.Equal(3, renderer.Batches.Count);
        var thirdBatch = renderer.Batches[2];
        var diff3 = thirdBatch.DiffsByComponentId[componentId].Single();
 
        // The nested component was rendered with the correct parameters
        Assert.Collection(diff3.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
                Assert.Equal(0, edit.ReferenceFrameIndex); // This is the only change
                AssertFrame.Text(thirdBatch.ReferenceFrames[0], "CascadingParameter=Whole new object; RegularParameter=Goodbye");
            });
        Assert.Equal(3, component.NumRenders);
 
        // Disposing the subscriber does not cause any error
        // We can't really observe any more than this because disposing is what causes unsubscription, and once you're
        // disposed you're not getting notifications anyway, so the most we can say is there was no error
        await renderer.Dispatcher.InvokeAsync(() => renderer.RemoveRootComponent(componentId));
        await cascadingValueSource.NotifyChangedAsync(new MyParamType("Nobody is listening, but this shouldn't be an error"));
    }
 
    [Fact]
    public async Task CanAddSubscriberDuringChangeNotification()
    {
        // Arrange
        var services = new ServiceCollection();
        var paramValue = new MyParamType("Initial value");
        var cascadingValueSource = new CascadingValueSource<MyParamType>(paramValue, isFixed: false);
        services.AddCascadingValue(_ => cascadingValueSource);
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new ConditionallyRenderSubscriberComponent()
        {
            RenderWhenEqualTo = "Final value",
        };
 
        // Act/Assert: Initial render
        var componentId = await renderer.Dispatcher.InvokeAsync(() => renderer.AssignRootComponentId(component));
        renderer.RenderRootComponent(componentId);
        var firstBatch = renderer.Batches.Single();
        var diff = firstBatch.DiffsByComponentId[componentId].Single();
        Assert.Collection(diff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    firstBatch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "CascadingParameter=Initial value");
            });
        Assert.Equal(1, component.NumRenders);
 
        // Act: Second render
        paramValue.ChangeValue("Final value");
        await cascadingValueSource.NotifyChangedAsync();
        var secondBatch = renderer.Batches[1];
        var diff2 = secondBatch.DiffsByComponentId[componentId].Single();
 
        // Assert: Subscriber can get added during change notification and receive the cascading value
        AssertFrame.Text(
            secondBatch.ReferenceFrames[diff2.Edits[0].ReferenceFrameIndex],
            "CascadingParameter=Final value");
        Assert.Equal(2, component.NumRenders);
 
        // Assert: Subscriber can get added during change notification and receive the cascading value
        var nestedComponent = FindComponent<SimpleSubscriberComponent>(secondBatch, out var nestedComponentId);
        var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
        Assert.Collection(nestedComponentDiff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(
                    secondBatch.ReferenceFrames[edit.ReferenceFrameIndex],
                    "CascadingParameter=Final value");
            });
        Assert.Equal(1, nestedComponent.NumRenders);
    }
 
    [Fact]
    public async Task AfterSupplyingValueThroughNotifyChanged_InitialValueFactoryIsNotUsed()
    {
        // Arrange
        var services = new ServiceCollection();
        var cascadingValueSource = new CascadingValueSource<MyParamType>(
            () => throw new InvalidOperationException("This should not be used because NotifyChanged is called with a value first"), isFixed: false);
        services.AddCascadingValue(_ => cascadingValueSource);
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var component = new CascadingParameterConsumerComponent<MyParamType> { RegularParameter = "Goodbye" };
 
        // Act: Supply an update before the value is first consumed
        var updatedValue = new MyParamType("Updated value");
        await cascadingValueSource.NotifyChangedAsync(updatedValue);
 
        // Assert: We see the supplied value, and the factory isn't used (it would have thrown)
        var componentId = await renderer.Dispatcher.InvokeAsync(() => renderer.AssignRootComponentId(component));
        renderer.RenderRootComponent(componentId);
        Assert.Same(updatedValue, component.GetCascadingParameterValue());
    }
 
    [Fact]
    public void OmitsSingleDeliveryCascadingParametersWhenUpdatingDirectParameters()
    {
        // Arrange
        var renderer = new TestRenderer();
        var regularParameterValue = "Initial value";
        var singleDeliveryTextValue = "Initial single delivery value";
        var component = new TestComponent(builder =>
        {
            builder.OpenComponent<CascadingValue<string>>(0);
            builder.AddComponentParameter(1, "Value", "Hello");
            builder.AddComponentParameter(2, "ChildContent", new RenderFragment(builder =>
            {
                builder.OpenComponent<SingleDeliveryCascadingValue>(0);
                builder.AddComponentParameter(1, "Text", singleDeliveryTextValue);
                builder.AddComponentParameter(2, "ChildContent", new RenderFragment(builder =>
                {
                    builder.OpenComponent<SingleDeliveryParameterConsumerComponent>(0);
                    builder.AddComponentParameter(1, "RegularParameter", regularParameterValue);
                    builder.CloseComponent();
                }));
                builder.CloseComponent();
            }));
            builder.CloseComponent();
        });
 
        // Act 1: Render in initial state; see we got the single-delivery parameter
        var componentId = renderer.AssignRootComponentId(component);
        component.TriggerRender();
        singleDeliveryTextValue = "should not appear"; // Make sure it's never read again
 
        var firstBatch = renderer.Batches.Single();
        var nestedComponent = FindComponent<SingleDeliveryParameterConsumerComponent>(firstBatch, out var nestedComponentId);
        Assert.Equal(1, nestedComponent.NumRenders);
        Assert.Equal(3, nestedComponent.LatestParameterView.Count);
        Assert.Contains("RegularParameter", nestedComponent.LatestParameterView.Keys);
        Assert.Contains("CascadingParameter", nestedComponent.LatestParameterView.Keys);
        Assert.Contains("SingleDeliveryCascadingParameter", nestedComponent.LatestParameterView.Keys);
 
        Assert.Collection(firstBatch.GetComponentDiffs<SingleDeliveryParameterConsumerComponent>().Single().Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.PrependFrame, edit.Type);
                AssertFrame.Text(firstBatch.ReferenceFrames[edit.ReferenceFrameIndex], "CascadingParameter=Hello; SingleDeliveryCascadingParameter=Initial single delivery value; RegularParameter=Initial value");
            });
 
        // Act 2: Render again with updated regular parameter
        regularParameterValue = "Changed value";
        component.TriggerRender();
 
        // Assert
        Assert.Equal(2, renderer.Batches.Count);
        var secondBatch = renderer.Batches[1];
        var nestedComponentDiff = secondBatch.DiffsByComponentId[nestedComponentId].Single();
 
        // The nested component was rendered with the correct parameters
        // In particular, it does *not* include SingleDeliveryCascadingParameter, even though
        // it does include the regular parameter and the multi-delivery cascading parameter
        Assert.Equal(2, nestedComponent.NumRenders);
        Assert.Equal(2, nestedComponent.LatestParameterView.Count);
        Assert.Contains("RegularParameter", nestedComponent.LatestParameterView.Keys);
        Assert.Contains("CascadingParameter", nestedComponent.LatestParameterView.Keys);
 
        Assert.Collection(nestedComponentDiff.Edits,
            edit =>
            {
                Assert.Equal(RenderTreeEditType.UpdateText, edit.Type);
                Assert.Equal(0, edit.ReferenceFrameIndex);
                AssertFrame.Text(secondBatch.ReferenceFrames[0], "CascadingParameter=Hello; SingleDeliveryCascadingParameter=Initial single delivery value; RegularParameter=Changed value");
            });
    }
 
    [Fact]
    public void CanUseTryAddPatternForCascadingValuesInServiceCollection_ValueFactory()
    {
        // Arrange
        var services = new ServiceCollection();
 
        // Act
        services.TryAddCascadingValue(_ => new Type1());
        services.TryAddCascadingValue(_ => new Type1());
        services.TryAddCascadingValue(_ => new Type2());
 
        // Assert
        Assert.Equal(2, services.Count());
    }
 
    [Fact]
    public void CanUseTryAddPatternForCascadingValuesInServiceCollection_NamedValueFactory()
    {
        // Arrange
        var services = new ServiceCollection();
 
        // Act
        services.TryAddCascadingValue("Name1", _ => new Type1());
        services.TryAddCascadingValue("Name2", _ => new Type1());
        services.TryAddCascadingValue("Name3", _ => new Type2());
 
        // Assert
        Assert.Equal(2, services.Count());
    }
 
    [Fact]
    public void CanUseTryAddPatternForCascadingValuesInServiceCollection_CascadingValueSource()
    {
        // Arrange
        var services = new ServiceCollection();
 
        // Act
        services.TryAddCascadingValue(_ => new CascadingValueSource<Type1>("Name1", new Type1(), false));
        services.TryAddCascadingValue(_ => new CascadingValueSource<Type1>("Name2", new Type1(), false));
        services.TryAddCascadingValue(_ => new CascadingValueSource<Type2>("Name3", new Type2(), false));
 
        // Assert
        Assert.Equal(2, services.Count());
    }
 
    [Theory]
    [InlineData(0)]
    [InlineData(1)]
    [InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity - 1)]
    [InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity)]
    [InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity + 1)]
    [InlineData(CascadingValueSource<MyParamType>.ComponentStateBuffer.Capacity * 2)]
    public async Task CanHaveManySubscribers(int numSubscribers)
    {
        // Arrange
        var services = new ServiceCollection();
        var paramValue = new MyParamType("Initial value");
        var cascadingValueSource = new CascadingValueSource<MyParamType>(paramValue, isFixed: false);
        services.AddCascadingValue(_ => cascadingValueSource);
        var renderer = new TestRenderer(services.BuildServiceProvider());
        var components = Enumerable.Range(0, numSubscribers).Select(_ => new SimpleSubscriberComponent()).ToArray();
 
        // Act/Assert: Initial render
        foreach (var component in components)
        {
            await renderer.Dispatcher.InvokeAsync(() => renderer.AssignRootComponentId(component));
            component.TriggerRender();
            Assert.Equal(1, component.NumRenders);
        }
 
        // Act/Assert: All components re-render when the cascading value changes
        paramValue.ChangeValue("Final value");
        await cascadingValueSource.NotifyChangedAsync();
        foreach (var component in components)
        {
            Assert.Equal(2, component.NumRenders);
        }
    }
 
    private class SingleDeliveryValue(string text)
    {
        public string Text => text;
    }
 
    private class SingleDeliveryCascadingParameterAttribute : CascadingParameterAttributeBase
    {
        internal override bool SingleDelivery => true;
    }
 
    private class SingleDeliveryCascadingValue : ComponentBase, ICascadingValueSupplier
    {
        [Parameter] public RenderFragment ChildContent { get; set; }
 
        [Parameter] public string Text { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
            => builder.AddContent(0, ChildContent);
 
        public bool IsFixed => true;
 
        public bool CanSupplyValue(in CascadingParameterInfo parameterInfo)
            => parameterInfo.Attribute is SingleDeliveryCascadingParameterAttribute;
 
        public object GetCurrentValue(in CascadingParameterInfo parameterInfo)
            => new SingleDeliveryValue(Text);
 
        public void Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
            => throw new NotImplementedException();
 
        public void Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
            => throw new NotImplementedException();
    }
 
    private static T FindComponent<T>(CapturedBatch batch, out int componentId)
    {
        var componentFrame = batch.ReferenceFrames.Single(
            frame => frame.FrameType == RenderTreeFrameType.Component
                     && frame.Component is T);
        componentId = componentFrame.ComponentId;
        return (T)componentFrame.Component;
    }
 
    class TestComponent : AutoRenderComponent
    {
        private readonly RenderFragment _renderFragment;
 
        public TestComponent(RenderFragment renderFragment)
        {
            _renderFragment = renderFragment;
        }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
            => _renderFragment(builder);
    }
 
    class CascadingParameterConsumerComponent<T> : AutoRenderComponent
    {
        private ParameterView lastParameterView;
 
        public int NumSetParametersCalls { get; private set; }
        public int NumRenders { get; private set; }
 
        [CascadingParameter] T CascadingParameter { get; set; }
        [Parameter] public string RegularParameter { get; set; }
 
        public T GetCascadingParameterValue() => CascadingParameter;
 
        public override async Task SetParametersAsync(ParameterView parameters)
        {
            lastParameterView = parameters;
            NumSetParametersCalls++;
            await base.SetParametersAsync(parameters);
        }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            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<object>("anything", out _);
        }
    }
 
    class ConditionallyRenderSubscriberComponent : AutoRenderComponent
    {
        public int NumRenders { get; private set; }
 
        public SimpleSubscriberComponent NestedSubscriber { get; private set; }
 
        [Parameter] public string RenderWhenEqualTo { get; set; }
 
        [CascadingParameter] MyParamType CascadingParameter { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            NumRenders++;
            builder.AddContent(0, $"CascadingParameter={CascadingParameter}");
 
            if (string.Equals(RenderWhenEqualTo, CascadingParameter.ToString(), StringComparison.OrdinalIgnoreCase))
            {
                builder.OpenComponent<SimpleSubscriberComponent>(1);
                builder.AddComponentReferenceCapture(2, component => NestedSubscriber = component as SimpleSubscriberComponent);
                builder.CloseComponent();
            }
        }
    }
 
    class SimpleSubscriberComponent : AutoRenderComponent
    {
        public int NumRenders { get; private set; }
 
        [CascadingParameter] MyParamType CascadingParameter { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            NumRenders++;
            builder.AddContent(0, $"CascadingParameter={CascadingParameter}");
        }
    }
 
    class SingleDeliveryParameterConsumerComponent : AutoRenderComponent
    {
        public int NumSetParametersCalls { get; private set; }
        public int NumRenders { get; private set; }
        public IReadOnlyDictionary<string, object> LatestParameterView { get; private set; }
 
        [CascadingParameter] string CascadingParameter { get; set; }
        [SingleDeliveryCascadingParameter] SingleDeliveryValue SingleDeliveryCascadingParameter { get; set; }
        [Parameter] public string RegularParameter { get; set; }
 
        public string GetCascadingParameterValue() => CascadingParameter;
 
        public override async Task SetParametersAsync(ParameterView parameters)
        {
            LatestParameterView = parameters.ToDictionary();
            NumSetParametersCalls++;
            await base.SetParametersAsync(parameters);
        }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            NumRenders++;
            builder.AddContent(0, $"CascadingParameter={CascadingParameter}; SingleDeliveryCascadingParameter={SingleDeliveryCascadingParameter.Text}; RegularParameter={RegularParameter}");
        }
    }
 
    class SecondCascadingParameterConsumerComponent<T1, T2> : CascadingParameterConsumerComponent<T1>
    {
        [CascadingParameter] T2 SecondCascadingParameter { get; set; }
    }
 
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    class CustomCascadingParameter1Attribute : CascadingParameterAttributeBase
    {
    }
 
    [AttributeUsage(AttributeTargets.Property, AllowMultiple = false, Inherited = true)]
    class CustomCascadingParameter2Attribute : CascadingParameterAttributeBase
    {
    }
 
    class CustomCascadingValueProducer<TAttribute> : AutoRenderComponent, ICascadingValueSupplier
    {
        [Parameter] public object Value { get; set; }
 
        [Parameter] public RenderFragment ChildContent { get; set; }
 
        bool ICascadingValueSupplier.IsFixed => true;
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, ChildContent);
        }
 
        bool ICascadingValueSupplier.CanSupplyValue(in CascadingParameterInfo parameterInfo)
        {
            if (parameterInfo.Attribute is not TAttribute ||
                parameterInfo.PropertyType != typeof(object) ||
                parameterInfo.PropertyName != nameof(Value))
            {
                return false;
            }
 
            return true;
        }
 
        object ICascadingValueSupplier.GetCurrentValue(in CascadingParameterInfo cascadingParameterState)
        {
            return Value;
        }
 
        void ICascadingValueSupplier.Subscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
        {
            throw new NotImplementedException();
        }
 
        void ICascadingValueSupplier.Unsubscribe(ComponentState subscriber, in CascadingParameterInfo parameterInfo)
        {
            throw new NotImplementedException();
        }
    }
 
    class CustomCascadingValueConsumer1 : AutoRenderComponent
    {
        [CustomCascadingParameter1]
        public object Value { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, $"Value 1 is '{Value}'.");
        }
    }
 
    class CustomCascadingValueConsumer2 : AutoRenderComponent
    {
        [CustomCascadingParameter2]
        public object Value { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, $"Value 2 is '{Value}'.");
        }
    }
 
    class ConsumeNamedCascadingValueComponent : AutoRenderComponent
    {
        [CascadingParameter(Name = "My cascading parameter name")]
        public object Value { get; set; }
 
        protected override void BuildRenderTree(RenderTreeBuilder builder)
        {
            builder.AddContent(0, $"The value is '{Value}'");
        }
    }
 
    class MyParamType(string StringValue)
    {
        public override string ToString() => StringValue;
 
        public void ChangeValue(string newValue)
        {
            StringValue = newValue;
        }
    }
 
    class Type1 { }
    class Type2 { }
}