diff --git a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs index f3990f5e1070..e0db0c46914d 100644 --- a/src/Components/Authorization/test/AuthorizeRouteViewTest.cs +++ b/src/Components/Authorization/test/AuthorizeRouteViewTest.cs @@ -35,8 +35,8 @@ public AuthorizeRouteViewTest() var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); - var componentFactory = new ComponentFactory(new DefaultComponentActivator()); - _authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView)); + var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer); + _authorizeRouteViewComponent = (AuthorizeRouteView)componentFactory.InstantiateComponent(services, typeof(AuthorizeRouteView), null); _authorizeRouteViewComponentId = _renderer.AssignRootComponentId(_authorizeRouteViewComponent); } diff --git a/src/Components/Components/src/ComponentFactory.cs b/src/Components/Components/src/ComponentFactory.cs index 5cbf330db9b4..e71c61d909e9 100644 --- a/src/Components/Components/src/ComponentFactory.cs +++ b/src/Components/Components/src/ComponentFactory.cs @@ -5,6 +5,7 @@ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Microsoft.AspNetCore.Components.Reflection; +using Microsoft.AspNetCore.Components.RenderTree; using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components; @@ -14,46 +15,70 @@ internal sealed class ComponentFactory private const BindingFlags _injectablePropertyBindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic; - private static readonly ConcurrentDictionary> _cachedInitializers = new(); + private static readonly ConcurrentDictionary _cachedComponentTypeInfo = new(); private readonly IComponentActivator _componentActivator; + private readonly Renderer _renderer; - public ComponentFactory(IComponentActivator componentActivator) + public ComponentFactory(IComponentActivator componentActivator, Renderer renderer) { _componentActivator = componentActivator ?? throw new ArgumentNullException(nameof(componentActivator)); + _renderer = renderer ?? throw new ArgumentNullException(nameof(renderer)); } - public static void ClearCache() => _cachedInitializers.Clear(); + public static void ClearCache() => _cachedComponentTypeInfo.Clear(); - public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType) + private static ComponentTypeInfoCacheEntry GetComponentTypeInfo([DynamicallyAccessedMembers(Component)] Type componentType) { - var component = _componentActivator.CreateInstance(componentType); + // Unfortunately we can't use 'GetOrAdd' here because the DynamicallyAccessedMembers annotation doesn't flow through to the + // callback, so it becomes an IL2111 warning. The following is equivalent and thread-safe because it's a ConcurrentDictionary + // and it doesn't matter if we build a cache entry more than once. + if (!_cachedComponentTypeInfo.TryGetValue(componentType, out var cacheEntry)) + { + var componentTypeRenderMode = componentType.GetCustomAttribute()?.Mode; + cacheEntry = new ComponentTypeInfoCacheEntry( + componentTypeRenderMode, + CreatePropertyInjector(componentType)); + _cachedComponentTypeInfo.TryAdd(componentType, cacheEntry); + } + + return cacheEntry; + } + + public IComponent InstantiateComponent(IServiceProvider serviceProvider, [DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId) + { + var componentTypeInfo = GetComponentTypeInfo(componentType); + var component = componentTypeInfo.ComponentTypeRenderMode is null + ? _componentActivator.CreateInstance(componentType) + : _renderer.ResolveComponentForRenderMode(componentType, parentComponentId, _componentActivator, componentTypeInfo.ComponentTypeRenderMode); + if (component is null) { - // The default activator will never do this, but an externally-supplied one might + // The default activator/resolver will never do this, but an externally-supplied one might throw new InvalidOperationException($"The component activator returned a null value for a component of type {componentType.FullName}."); } - PerformPropertyInjection(serviceProvider, component); + if (component.GetType() == componentType) + { + // Fast, common case: use the cached data we already looked up + componentTypeInfo.PerformPropertyInjection(serviceProvider, component); + } + else + { + // Uncommon case where the activator/resolver returned a different type. Needs an extra cache lookup. + PerformPropertyInjection(serviceProvider, component); + } + return component; } private static void PerformPropertyInjection(IServiceProvider serviceProvider, IComponent instance) { - // This is thread-safe because _cachedInitializers is a ConcurrentDictionary. - // We might generate the initializer more than once for a given type, but would - // still produce the correct result. - var instanceType = instance.GetType(); - if (!_cachedInitializers.TryGetValue(instanceType, out var initializer)) - { - initializer = CreateInitializer(instanceType); - _cachedInitializers.TryAdd(instanceType, initializer); - } - - initializer(serviceProvider, instance); + var componentTypeInfo = GetComponentTypeInfo(instance.GetType()); + componentTypeInfo.PerformPropertyInjection(serviceProvider, instance); } - private static Action CreateInitializer([DynamicallyAccessedMembers(Component)] Type type) + private static Action CreatePropertyInjector([DynamicallyAccessedMembers(Component)] Type type) { // Do all the reflection up front List<(string name, Type propertyType, PropertySetter setter)>? injectables = null; @@ -93,4 +118,9 @@ void Initialize(IServiceProvider serviceProvider, IComponent component) } } } + + // Tracks information about a specific component type that ComponentFactory uses + private record class ComponentTypeInfoCacheEntry( + IComponentRenderMode? ComponentTypeRenderMode, + Action PerformPropertyInjection); } diff --git a/src/Components/Components/src/IComponentRenderMode.cs b/src/Components/Components/src/IComponentRenderMode.cs new file mode 100644 index 000000000000..dd1f53f3d87b --- /dev/null +++ b/src/Components/Components/src/IComponentRenderMode.cs @@ -0,0 +1,11 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// Represents a render mode for a component. +/// +public interface IComponentRenderMode +{ +} diff --git a/src/Components/Components/src/ParameterView.cs b/src/Components/Components/src/ParameterView.cs index 544ef27ccc28..37ba9c2dfbcd 100644 --- a/src/Components/Components/src/ParameterView.cs +++ b/src/Components/Components/src/ParameterView.cs @@ -103,9 +103,9 @@ public TValue GetValueOrDefault(string parameterName, TValue defaultValu /// Returns a dictionary populated with the contents of the . /// /// A dictionary populated with the contents of the . - public IReadOnlyDictionary ToDictionary() + public IReadOnlyDictionary ToDictionary() { - var result = new Dictionary(); + var result = new Dictionary(); foreach (var entry in this) { result[entry.Name] = entry.Value; diff --git a/src/Components/Components/src/PublicAPI.Unshipped.txt b/src/Components/Components/src/PublicAPI.Unshipped.txt index 374824fbd3a3..6048b3a28d79 100644 --- a/src/Components/Components/src/PublicAPI.Unshipped.txt +++ b/src/Components/Components/src/PublicAPI.Unshipped.txt @@ -1,4 +1,5 @@ #nullable enable +abstract Microsoft.AspNetCore.Components.RenderModeAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! Microsoft.AspNetCore.Components.CascadingModelBinder Microsoft.AspNetCore.Components.CascadingModelBinder.CascadingModelBinder() -> void Microsoft.AspNetCore.Components.CascadingModelBinder.ChildContent.get -> Microsoft.AspNetCore.Components.RenderFragment! @@ -8,10 +9,13 @@ Microsoft.AspNetCore.Components.CascadingModelBinder.IsFixed.set -> void Microsoft.AspNetCore.Components.CascadingModelBinder.Name.get -> string! Microsoft.AspNetCore.Components.CascadingModelBinder.Name.set -> void Microsoft.AspNetCore.Components.ComponentBase.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! +Microsoft.AspNetCore.Components.IComponentRenderMode Microsoft.AspNetCore.Components.Infrastructure.ComponentStatePersistenceManager.PersistStateAsync(Microsoft.AspNetCore.Components.IPersistentComponentStateStore! store, Microsoft.AspNetCore.Components.Dispatcher! dispatcher) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.ModelBindingContext Microsoft.AspNetCore.Components.ModelBindingContext.BindingContextId.get -> string! Microsoft.AspNetCore.Components.ModelBindingContext.Name.get -> string! +Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary! +*REMOVED*Microsoft.AspNetCore.Components.ParameterView.ToDictionary() -> System.Collections.Generic.IReadOnlyDictionary! Microsoft.AspNetCore.Components.RenderHandle.DispatchExceptionAsync(System.Exception! exception) -> System.Threading.Tasks.Task! *REMOVED*Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string! relativeUri) -> System.Uri! Microsoft.AspNetCore.Components.NavigationManager.ToAbsoluteUri(string? relativeUri) -> System.Uri! @@ -22,6 +26,8 @@ Microsoft.AspNetCore.Components.RouteData.RouteData(System.Type! pageType, Syste Microsoft.AspNetCore.Components.RouteData.RouteValues.get -> System.Collections.Generic.IReadOnlyDictionary! Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider Microsoft.AspNetCore.Components.Routing.IRoutingStateProvider.RouteData.get -> Microsoft.AspNetCore.Components.RouteData? +Microsoft.AspNetCore.Components.RenderModeAttribute +Microsoft.AspNetCore.Components.RenderModeAttribute.RenderModeAttribute() -> void Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash Microsoft.AspNetCore.Components.Routing.IScrollToLocationHash.RefreshScrollPositionForHash(string! locationAbsolute) -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Rendering.ComponentState @@ -58,5 +64,6 @@ override Microsoft.AspNetCore.Components.EventCallback.Equals(object? ob virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.AddPendingTask(Microsoft.AspNetCore.Components.Rendering.ComponentState? componentState, System.Threading.Tasks.Task! task) -> void virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.CreateComponentState(int componentId, Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.Rendering.ComponentState? parentComponentState) -> Microsoft.AspNetCore.Components.Rendering.ComponentState! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.DispatchEventAsync(ulong eventHandlerId, Microsoft.AspNetCore.Components.RenderTree.EventFieldInfo? fieldInfo, System.EventArgs! eventArgs, bool quiesce) -> System.Threading.Tasks.Task! +virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ResolveComponentForRenderMode(System.Type! componentType, int? parentComponentId, Microsoft.AspNetCore.Components.IComponentActivator! componentActivator, Microsoft.AspNetCore.Components.IComponentRenderMode! componentTypeRenderMode) -> Microsoft.AspNetCore.Components.IComponent! virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.ShouldTrackNamedEventHandlers() -> bool virtual Microsoft.AspNetCore.Components.RenderTree.Renderer.TrackNamedEventId(ulong eventHandlerId, int componentId, string! eventHandlerName) -> void diff --git a/src/Components/Components/src/RenderModeAttribute.cs b/src/Components/Components/src/RenderModeAttribute.cs new file mode 100644 index 000000000000..d9d7ff286270 --- /dev/null +++ b/src/Components/Components/src/RenderModeAttribute.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components; + +/// +/// Specifies a fixed rendering mode for a component type. +/// +/// Where possible, components should not specify any render mode this way, and should +/// be implemented to work across all render modes. Component authors should only specify +/// a fixed rendering mode when the component is incapable of running in other modes. +/// +[AttributeUsage(AttributeTargets.Class)] +public abstract class RenderModeAttribute : Attribute +{ + /// + /// Gets the fixed rendering mode for a component type. + /// + public abstract IComponentRenderMode Mode { get; } +} diff --git a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs index 77800e8d0bb8..f51431100e37 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeDiffBuilder.cs @@ -952,15 +952,8 @@ private static void InitializeNewComponentFrame(ref DiffContext diffContext, int { var frames = diffContext.NewTree; ref var frame = ref frames[frameIndex]; - - if (frame.ComponentStateField != null) - { - throw new InvalidOperationException($"Child component already exists during {nameof(InitializeNewComponentFrame)}"); - } - var parentComponentId = diffContext.ComponentId; - diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId); - var childComponentState = frame.ComponentStateField; + var childComponentState = diffContext.Renderer.InstantiateChildComponentOnFrame(ref frame, parentComponentId); // Set initial parameters var initialParametersLifetime = new ParameterViewLifetime(diffContext.BatchBuilder); diff --git a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs index 2a7edd70e08c..136c41ed3359 100644 --- a/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs +++ b/src/Components/Components/src/RenderTree/RenderTreeFrameType.cs @@ -59,4 +59,3 @@ public enum RenderTreeFrameType : short /// Markup = 8, } - diff --git a/src/Components/Components/src/RenderTree/Renderer.cs b/src/Components/Components/src/RenderTree/Renderer.cs index 0fc9a3266ff7..001454f09b02 100644 --- a/src/Components/Components/src/RenderTree/Renderer.cs +++ b/src/Components/Components/src/RenderTree/Renderer.cs @@ -86,7 +86,7 @@ public Renderer(IServiceProvider serviceProvider, ILoggerFactory loggerFactory, // has always taken ILoggerFactory so to avoid the per-instance string allocation of the logger name we just pass the // logger name in here as a string literal. _logger = loggerFactory.CreateLogger("Microsoft.AspNetCore.Components.RenderTree.Renderer"); - _componentFactory = new ComponentFactory(componentActivator); + _componentFactory = new ComponentFactory(componentActivator, this); } internal HotReloadManager HotReloadManager { get; set; } = HotReloadManager.Default; @@ -162,9 +162,7 @@ await Dispatcher.InvokeAsync(() => /// The type of the component to instantiate. /// The component instance. protected IComponent InstantiateComponent([DynamicallyAccessedMembers(Component)] Type componentType) - { - return _componentFactory.InstantiateComponent(_serviceProvider, componentType); - } + => _componentFactory.InstantiateComponent(_serviceProvider, componentType, null); /// /// Associates the with the , assigning @@ -475,7 +473,7 @@ public Type GetEventArgsType(ulong eventHandlerId) : EventArgsTypeCache.GetEventArgsType(methodInfo); } - internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId) + internal ComponentState InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int parentComponentId) { if (frame.FrameTypeField != RenderTreeFrameType.Component) { @@ -487,10 +485,12 @@ internal void InstantiateChildComponentOnFrame(ref RenderTreeFrame frame, int pa throw new ArgumentException($"The frame already has a non-null component instance", nameof(frame)); } - var newComponent = InstantiateComponent(frame.ComponentTypeField); + var newComponent = _componentFactory.InstantiateComponent(_serviceProvider, frame.ComponentTypeField, parentComponentId); var newComponentState = AttachAndInitComponent(newComponent, parentComponentId); frame.ComponentStateField = newComponentState; frame.ComponentIdField = newComponentState.ComponentId; + + return newComponentState; } internal void AddToPendingTasksWithErrorHandling(Task task, ComponentState? owningComponentState) @@ -1159,6 +1159,27 @@ void NotifyExceptions(List exceptions) } } + /// + /// Determines how to handle an when obtaining a component instance. + /// This is only called for components that have specified a render mode. Subclasses may override this + /// method to return a component of a different type, or throw, depending on whether the renderer + /// supports the render mode and how it implements that support. + /// + /// The type of component that was requested. + /// The parent component ID, or null if it is a root component. + /// An that should be used when instantiating component objects. + /// The declared on . + /// An instance. + protected internal virtual IComponent ResolveComponentForRenderMode( + [DynamicallyAccessedMembers(Component)] Type componentType, + int? parentComponentId, + IComponentActivator componentActivator, + IComponentRenderMode componentTypeRenderMode) + { + // Nothing is supported by default. Subclasses must override this to opt into supporting specific render modes. + throw new NotSupportedException($"Cannot supply a component of type '{componentType}' because the current platform does not support the render mode '{componentTypeRenderMode}'."); + } + /// /// Releases all resources currently used by this instance. /// diff --git a/src/Components/Components/test/ComponentFactoryTest.cs b/src/Components/Components/test/ComponentFactoryTest.cs index 653ed8d1e7e3..6c8ed10fcf28 100644 --- a/src/Components/Components/test/ComponentFactoryTest.cs +++ b/src/Components/Components/test/ComponentFactoryTest.cs @@ -1,7 +1,11 @@ // 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; +using Microsoft.Extensions.Logging; namespace Microsoft.AspNetCore.Components; @@ -12,10 +16,10 @@ public void InstantiateComponent_CreatesInstance() { // Arrange var componentType = typeof(EmptyComponent); - var factory = new ComponentFactory(new DefaultComponentActivator()); + var factory = new ComponentFactory(new DefaultComponentActivator(), new TestRenderer()); // Act - var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); + var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null); // Assert Assert.NotNull(instance); @@ -27,10 +31,10 @@ public void InstantiateComponent_CreatesInstance_NonComponent() { // Arrange var componentType = typeof(List); - var factory = new ComponentFactory(new DefaultComponentActivator()); + var factory = new ComponentFactory(new DefaultComponentActivator(), new TestRenderer()); // Assert - var ex = Assert.Throws(() => factory.InstantiateComponent(GetServiceProvider(), componentType)); + var ex = Assert.Throws(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null)); Assert.StartsWith($"The type {componentType.FullName} does not implement {nameof(IComponent)}.", ex.Message); } @@ -39,10 +43,10 @@ public void InstantiateComponent_CreatesInstance_WithCustomActivator() { // Arrange var componentType = typeof(EmptyComponent); - var factory = new ComponentFactory(new CustomComponentActivator()); + var factory = new ComponentFactory(new CustomComponentActivator(), new TestRenderer()); // Act - var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); + var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null); // Assert Assert.NotNull(instance); @@ -60,10 +64,10 @@ public void InstantiateComponent_ThrowsForNullInstance() { // Arrange var componentType = typeof(EmptyComponent); - var factory = new ComponentFactory(new NullResultComponentActivator()); + var factory = new ComponentFactory(new NullResultComponentActivator(), new TestRenderer()); // Act - var ex = Assert.Throws(() => factory.InstantiateComponent(GetServiceProvider(), componentType)); + var ex = Assert.Throws(() => factory.InstantiateComponent(GetServiceProvider(), componentType, null)); Assert.Equal($"The component activator returned a null value for a component of type {componentType.FullName}.", ex.Message); } @@ -72,10 +76,10 @@ public void InstantiateComponent_AssignsPropertiesWithInjectAttributeOnBaseType( { // Arrange var componentType = typeof(DerivedComponent); - var factory = new ComponentFactory(new CustomComponentActivator()); + var factory = new ComponentFactory(new CustomComponentActivator(), new TestRenderer()); // Act - var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); + var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null); // Assert Assert.NotNull(instance); @@ -95,10 +99,10 @@ public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute() { // Arrange var componentType = typeof(ComponentWithNonInjectableProperties); - var factory = new ComponentFactory(new DefaultComponentActivator()); + var factory = new ComponentFactory(new DefaultComponentActivator(), new TestRenderer()); // Act - var instance = factory.InstantiateComponent(GetServiceProvider(), componentType); + var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null); // Assert Assert.NotNull(instance); @@ -108,6 +112,46 @@ public void InstantiateComponent_IgnoresPropertiesWithoutInjectAttribute() Assert.Null(component.Property2); } + [Fact] + public void InstantiateComponent_WithNoRenderMode_DoesNotUseRenderModeResolver() + { + // Arrange + var componentType = typeof(ComponentWithInjectProperties); + var renderer = new RendererWithResolveComponentForRenderMode( + /* won't be used */ new ComponentWithRenderMode()); + var factory = new ComponentFactory(new DefaultComponentActivator(), renderer); + + // Act + var instance = factory.InstantiateComponent(GetServiceProvider(), componentType, null); + + // Assert + Assert.IsType(instance); + Assert.False(renderer.ResolverWasCalled); + } + + [Fact] + public void InstantiateComponent_WithRenderModeOnComponent_UsesRenderModeResolver() + { + // Arrange + var resolvedComponent = new ComponentWithInjectProperties(); + var componentType = typeof(ComponentWithRenderMode); + var renderer = new RendererWithResolveComponentForRenderMode(resolvedComponent); + var componentActivator = new DefaultComponentActivator(); + var factory = new ComponentFactory(componentActivator, renderer); + + // Act + var instance = (ComponentWithInjectProperties)factory.InstantiateComponent(GetServiceProvider(), componentType, 1234); + + // Assert + Assert.True(renderer.ResolverWasCalled); + Assert.Same(resolvedComponent, instance); + Assert.NotNull(instance.Property1); + Assert.Equal(componentType, renderer.RequestedComponentType); + Assert.Equal(1234, renderer.SuppliedParentComponentId); + Assert.Same(componentActivator, renderer.SuppliedActivator); + Assert.IsType(renderer.SuppliedComponentTypeRenderMode); + } + private static IServiceProvider GetServiceProvider() { return new ServiceCollection() @@ -200,4 +244,63 @@ public IComponent CreateInstance(Type componentType) return null; } } + + private class TestRenderMode : IComponentRenderMode { } + + [OwnRenderMode] + private class ComponentWithRenderMode : IComponent + { + public void Attach(RenderHandle renderHandle) + { + throw new NotImplementedException(); + } + + public Task SetParametersAsync(ParameterView parameters) + { + throw new NotImplementedException(); + } + + class OwnRenderMode : RenderModeAttribute + { + public override IComponentRenderMode Mode => new TestRenderMode(); + } + } + + private class RendererWithResolveComponentForRenderMode : TestRenderer + { + private readonly IComponent _componentToReturn; + + public RendererWithResolveComponentForRenderMode(IComponent componentToReturn) : base() + { + _componentToReturn = componentToReturn; + } + + public bool ResolverWasCalled { get; private set; } + public Type RequestedComponentType { get; private set; } + public int? SuppliedParentComponentId { get; private set; } + public IComponentActivator SuppliedActivator { get; private set; } + public IComponentRenderMode SuppliedComponentTypeRenderMode { get; private set; } + + public override Dispatcher Dispatcher => throw new NotImplementedException(); + + protected override void HandleException(Exception exception) + { + throw new NotImplementedException(); + } + + protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) + { + throw new NotImplementedException(); + } + + protected internal override IComponent ResolveComponentForRenderMode(Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode componentTypeRenderMode) + { + ResolverWasCalled = true; + RequestedComponentType = componentType; + SuppliedParentComponentId = parentComponentId; + SuppliedActivator = componentActivator; + SuppliedComponentTypeRenderMode = componentTypeRenderMode; + return _componentToReturn; + } + } } diff --git a/src/Components/Components/test/RendererTest.cs b/src/Components/Components/test/RendererTest.cs index 92ffaeae8b6e..29dd39f23805 100644 --- a/src/Components/Components/test/RendererTest.cs +++ b/src/Components/Components/test/RendererTest.cs @@ -5217,6 +5217,87 @@ public void DuplicateNamedEventHandlersOnElementThrows() Assert.Equal("An event handler 'MyFormSubmit' is already defined in this component.", exception.Message); } + [Fact] + public void ThrowsForUnknownRenderMode_OnComponentType() + { + // Arrange + var renderer = new TestRenderer(); + var component = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.CloseComponent(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + var ex = Assert.Throws(() => component.TriggerRender()); + Assert.Contains($"Cannot supply a component of type '{typeof(ComponentWithUnknownRenderMode)}' because the current platform does not support the render mode '{typeof(ComponentWithUnknownRenderMode.UnknownRenderMode)}'.", ex.Message); + } + + [Fact] + public void RenderModeResolverCanSupplyComponent_WithComponentTypeRenderMode() + { + // Arrange + var renderer = new RendererWithRenderModeResolver(); + + var component = new TestComponent(builder => + { + builder.OpenComponent(0); + builder.AddComponentParameter(1, nameof(MessageComponent.Message), "Some message"); + builder.CloseComponent(); + }); + + // Act + var componentId = renderer.AssignRootComponentId(component); + component.TriggerRender(); + + // Assert + var batch = renderer.Batches.Single(); + var componentFrames = batch.GetComponentFrames(); + var resolvedComponent = (MessageComponent)componentFrames.Single().Component; + Assert.Equal("Some message", resolvedComponent.Message); + } + + [HasSubstituteComponentRenderMode] + private class ComponentWithRenderMode : IComponent + { + public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + + public class HasSubstituteComponentRenderMode : RenderModeAttribute + { + public override IComponentRenderMode Mode => new SubstituteComponentRenderMode(); + } + } + + [HasUnknownRenderMode] + private class ComponentWithUnknownRenderMode : IComponent + { + public void Attach(RenderHandle renderHandle) => throw new NotImplementedException(); + public Task SetParametersAsync(ParameterView parameters) => throw new NotImplementedException(); + + public class HasUnknownRenderMode : RenderModeAttribute + { + public override IComponentRenderMode Mode => new UnknownRenderMode(); + } + + public class UnknownRenderMode : IComponentRenderMode { } + } + + private class RendererWithRenderModeResolver : TestRenderer + { + protected internal override IComponent ResolveComponentForRenderMode(Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode componentTypeRenderMode) + { + return componentTypeRenderMode switch + { + SubstituteComponentRenderMode => componentActivator.CreateInstance(typeof(MessageComponent)), + var other => throw new NotSupportedException($"{nameof(RendererWithRenderModeResolver)} should not have received rendermode {other}"), + }; + } + } + + private class SubstituteComponentRenderMode : IComponentRenderMode { } + private class TestComponentActivator : IComponentActivator where TResult : IComponent, new() { public List RequestedComponentTypes { get; } = new List(); diff --git a/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs index 2f4ae8d3f734..4a927561c38f 100644 --- a/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs +++ b/src/Components/Components/test/Rendering/RenderTreeBuilderTest.cs @@ -2106,4 +2106,6 @@ protected override void HandleException(Exception exception) protected override Task UpdateDisplayAsync(in RenderBatch renderBatch) => throw new NotImplementedException(); } + + class TestRenderMode : IComponentRenderMode { } } diff --git a/src/Components/Components/test/RouteViewTest.cs b/src/Components/Components/test/RouteViewTest.cs index a3aaee7baa00..92abd7e97b47 100644 --- a/src/Components/Components/test/RouteViewTest.cs +++ b/src/Components/Components/test/RouteViewTest.cs @@ -22,8 +22,8 @@ public RouteViewTest() var services = serviceCollection.BuildServiceProvider(); _renderer = new TestRenderer(services); - var componentFactory = new ComponentFactory(new DefaultComponentActivator()); - _routeViewComponent = (RouteView)componentFactory.InstantiateComponent(services, typeof(RouteView)); + var componentFactory = new ComponentFactory(new DefaultComponentActivator(), _renderer); + _routeViewComponent = (RouteView)componentFactory.InstantiateComponent(services, typeof(RouteView), null); _routeViewComponentId = _renderer.AssignRootComponentId(_routeViewComponent); } diff --git a/src/Components/Endpoints/src/DependencyInjection/IComponentPrerenderer.cs b/src/Components/Endpoints/src/DependencyInjection/IComponentPrerenderer.cs index 9fc044c69781..40adf5a4a18a 100644 --- a/src/Components/Endpoints/src/DependencyInjection/IComponentPrerenderer.cs +++ b/src/Components/Endpoints/src/DependencyInjection/IComponentPrerenderer.cs @@ -22,7 +22,7 @@ public interface IComponentPrerenderer ValueTask PrerenderComponentAsync( HttpContext httpContext, Type componentType, - RenderMode renderMode, + IComponentRenderMode renderMode, ParameterView parameters); /// @@ -36,7 +36,7 @@ ValueTask PrerenderPersistedStateAsync( PersistedStateSerializationMode serializationMode); /// - /// Gets a that should be used for calls to . + /// Gets a that should be used for calls to . /// Dispatcher Dispatcher { get; } } diff --git a/src/Components/Endpoints/src/DependencyInjection/InvokedRenderModes.cs b/src/Components/Endpoints/src/DependencyInjection/InvokedRenderModes.cs index f3b6894da55e..8d0c6a2476bf 100644 --- a/src/Components/Endpoints/src/DependencyInjection/InvokedRenderModes.cs +++ b/src/Components/Endpoints/src/DependencyInjection/InvokedRenderModes.cs @@ -12,18 +12,11 @@ public InvokedRenderModes(Mode mode) public Mode Value { get; set; } - /// - /// Tracks for components. - /// internal enum Mode { None, Server, WebAssembly, - - /// - /// Tracks an app that has both components rendered both on the Server and WebAssembly. - /// ServerAndWebAssembly } } diff --git a/src/Components/Endpoints/src/DependencyInjection/RenderMode.cs b/src/Components/Endpoints/src/DependencyInjection/RenderMode.cs deleted file mode 100644 index e5496e28e14d..000000000000 --- a/src/Components/Endpoints/src/DependencyInjection/RenderMode.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace Microsoft.AspNetCore.Components; - -/// -/// Describes the render mode of the component. -/// -/// -/// The rendering mode determines how the component gets rendered on the page. It configures whether the component -/// is prerendered into the page or not and whether it simply renders static HTML on the page or if it includes the necessary -/// information to bootstrap a Blazor application from the user agent. -/// -public enum RenderMode -{ - /// - /// Renders the component into static HTML. - /// - Static = 1, - - /// - /// Renders a marker for a Blazor server-side application. This doesn't include any output from the component. - /// When the user-agent starts, it uses this marker to bootstrap a blazor application. - /// - Server = 2, - - /// - /// Renders the component into static HTML and includes a marker for a Blazor server-side application. - /// When the user-agent starts, it uses this marker to bootstrap a blazor application. - /// - ServerPrerendered = 3, - - /// - /// Renders a marker for a Blazor webassembly application. This doesn't include any output from the component. - /// When the user-agent starts, it uses this marker to bootstrap a blazor client-side application. - /// - WebAssembly = 4, - - /// - /// Renders the component into static HTML and includes a marker for a Blazor webassembly application. - /// When the user-agent starts, it uses this marker to bootstrap a blazor client-side application. - /// - WebAssemblyPrerendered = 5, -} diff --git a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt index 50ef3c68d401..0540e3b44129 100644 --- a/src/Components/Endpoints/src/PublicAPI.Unshipped.txt +++ b/src/Components/Endpoints/src/PublicAPI.Unshipped.txt @@ -10,7 +10,7 @@ Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata.ComponentTypeMet Microsoft.AspNetCore.Components.Endpoints.ComponentTypeMetadata.Type.get -> System.Type! Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! -Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.PrerenderComponentAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Type! componentType, Microsoft.AspNetCore.Components.RenderMode renderMode, Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.ValueTask +Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.PrerenderComponentAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, System.Type! componentType, Microsoft.AspNetCore.Components.IComponentRenderMode! renderMode, Microsoft.AspNetCore.Components.ParameterView parameters) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Endpoints.IComponentPrerenderer.PrerenderPersistedStateAsync(Microsoft.AspNetCore.Http.HttpContext! httpContext, Microsoft.AspNetCore.Components.PersistedStateSerializationMode serializationMode) -> System.Threading.Tasks.ValueTask Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder.Services.get -> Microsoft.Extensions.DependencyInjection.IServiceCollection! @@ -25,8 +25,6 @@ Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.PreventStreamingR Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Type! componentType) -> void Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Type! componentType, object? parameters) -> void Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RazorComponentResult(System.Type! componentType, System.Collections.Generic.IReadOnlyDictionary? parameters) -> void -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RenderMode.get -> Microsoft.AspNetCore.Components.RenderMode -Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.RenderMode.set -> void Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.StatusCode.get -> int? Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult.StatusCode.set -> void Microsoft.AspNetCore.Components.Endpoints.RazorComponentResult @@ -51,12 +49,6 @@ Microsoft.AspNetCore.Components.PersistedStateSerializationMode.Server = 2 -> Mi Microsoft.AspNetCore.Components.PersistedStateSerializationMode.WebAssembly = 3 -> Microsoft.AspNetCore.Components.PersistedStateSerializationMode Microsoft.AspNetCore.Components.RazorComponentApplication Microsoft.AspNetCore.Components.RazorComponentApplication.Pages.get -> System.Collections.Generic.IEnumerable! -Microsoft.AspNetCore.Components.RenderMode -Microsoft.AspNetCore.Components.RenderMode.Server = 2 -> Microsoft.AspNetCore.Components.RenderMode -Microsoft.AspNetCore.Components.RenderMode.ServerPrerendered = 3 -> Microsoft.AspNetCore.Components.RenderMode -Microsoft.AspNetCore.Components.RenderMode.Static = 1 -> Microsoft.AspNetCore.Components.RenderMode -Microsoft.AspNetCore.Components.RenderMode.WebAssembly = 4 -> Microsoft.AspNetCore.Components.RenderMode -Microsoft.AspNetCore.Components.RenderMode.WebAssemblyPrerendered = 5 -> Microsoft.AspNetCore.Components.RenderMode Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions static Microsoft.AspNetCore.Builder.RazorComponentsEndpointRouteBuilderExtensions.MapRazorComponents(this Microsoft.AspNetCore.Routing.IEndpointRouteBuilder! endpoints) -> Microsoft.AspNetCore.Builder.RazorComponentEndpointConventionBuilder! static Microsoft.Extensions.DependencyInjection.RazorComponentsServiceCollectionExtensions.AddRazorComponents(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services) -> Microsoft.AspNetCore.Components.Endpoints.IRazorComponentsBuilder! diff --git a/src/Components/Endpoints/src/RazorComponentEndpointHost.cs b/src/Components/Endpoints/src/RazorComponentEndpointHost.cs index 064941afc074..7eaddb877718 100644 --- a/src/Components/Endpoints/src/RazorComponentEndpointHost.cs +++ b/src/Components/Endpoints/src/RazorComponentEndpointHost.cs @@ -19,7 +19,7 @@ internal class RazorComponentEndpointHost : IComponent { private RenderHandle _renderHandle; - [Parameter] public RenderMode RenderMode { get; set; } + [Parameter] public IComponentRenderMode? RenderMode { get; set; } [Parameter] public Type ComponentType { get; set; } = default!; [Parameter] public IReadOnlyDictionary? ComponentParameters { get; set; } @@ -49,10 +49,10 @@ private void RenderPageWithParameters(RenderTreeBuilder builder) // go here. We need to switch into the rendermode given by RazorComponentResult.RenderMode for this // child component. That will cause the developer-supplied parameters to be serialized into a marker // but not attempt to serialize the RenderFragment that causes this to be hosted in its layout. - if (RenderMode != RenderMode.Static) + if (RenderMode is not null) { // Tracked by #46353 and #46354 - throw new NotSupportedException($"Currently, Razor Component endpoints only support the {RenderMode.Static} render mode."); + throw new NotSupportedException($"Currently, Razor Component endpoints don't support setting a render mode."); } builder.OpenComponent(0, ComponentType); diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs index 878fbeada893..7337adb65e1b 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Prerendering.cs @@ -5,7 +5,6 @@ using Microsoft.AspNetCore.Components.Web.HtmlRendering; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNetCore.Components.Endpoints; @@ -13,17 +12,53 @@ internal partial class EndpointHtmlRenderer { private static readonly object ComponentSequenceKey = new object(); + protected override IComponent ResolveComponentForRenderMode(Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode componentTypeRenderMode) + { + var closestRenderModeBoundary = parentComponentId.HasValue + ? GetClosestRenderModeBoundary(parentComponentId.Value) + : null; + + if (closestRenderModeBoundary is not null) + { + // We're already inside a subtree with a rendermode. Once it becomes interactive, the entire DOM subtree + // will get replaced anyway. So there is no point emitting further rendermode boundaries. + return componentActivator.CreateInstance(componentType); + } + else + { + // This component is the start of a subtree with a rendermode, so introduce a new rendermode boundary here + return new SSRRenderModeBoundary(componentType, componentTypeRenderMode); + } + } + + private SSRRenderModeBoundary? GetClosestRenderModeBoundary(int componentId) + { + var componentState = GetComponentState(componentId); + do + { + if (componentState.Component is SSRRenderModeBoundary boundary) + { + return boundary; + } + + componentState = componentState.ParentComponentState; + } + while (componentState is not null); + + return null; + } + public ValueTask PrerenderComponentAsync( HttpContext httpContext, Type componentType, - RenderMode prerenderMode, + IComponentRenderMode prerenderMode, ParameterView parameters) => PrerenderComponentAsync(httpContext, componentType, prerenderMode, parameters, waitForQuiescence: true); public async ValueTask PrerenderComponentAsync( HttpContext httpContext, Type componentType, - RenderMode prerenderMode, + IComponentRenderMode? prerenderMode, ParameterView parameters, bool waitForQuiescence) { @@ -45,15 +80,11 @@ public async ValueTask PrerenderComponentAsync( try { - var result = prerenderMode switch - { - RenderMode.Server => NonPrerenderedServerComponent(GetOrCreateInvocationId(httpContext), componentType, parameters), - RenderMode.ServerPrerendered => await PrerenderedServerComponentAsync(GetOrCreateInvocationId(httpContext), componentType, parameters), - RenderMode.Static => await StaticComponentAsync(componentType, parameters), - RenderMode.WebAssembly => NonPrerenderedWebAssemblyComponent(componentType, parameters), - RenderMode.WebAssemblyPrerendered => await PrerenderedWebAssemblyComponentAsync(componentType, parameters), - _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(prerenderMode), nameof(prerenderMode)), - }; + var rootComponent = prerenderMode is null + ? InstantiateComponent(componentType) + : new SSRRenderModeBoundary(componentType, prerenderMode); + var htmlRootComponent = await Dispatcher.InvokeAsync(() => BeginRenderingComponent(rootComponent, parameters)); + var result = new PrerenderedComponentHtmlContent(Dispatcher, htmlRootComponent); await WaitForResultReady(waitForQuiescence, result); @@ -76,7 +107,7 @@ internal async ValueTask RenderEndpointComponen try { var component = BeginRenderingComponent(rootComponentType, parameters); - var result = new PrerenderedComponentHtmlContent(Dispatcher, component, null, null); + var result = new PrerenderedComponentHtmlContent(Dispatcher, component); await WaitForResultReady(waitForQuiescence, result); @@ -123,7 +154,7 @@ private static ValueTask HandleNavigationExcept } } - private static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpContext httpContext) + internal static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpContext httpContext) { if (!httpContext.Items.TryGetValue(ComponentSequenceKey, out var result)) { @@ -134,87 +165,22 @@ private static ServerComponentInvocationSequence GetOrCreateInvocationId(HttpCon return (ServerComponentInvocationSequence)result!; } - private async Task StaticComponentAsync(Type type, ParameterView parametersCollection) - { - var htmlComponent = await Dispatcher.InvokeAsync(() => BeginRenderingComponent(type, parametersCollection)); - return new PrerenderedComponentHtmlContent(Dispatcher, htmlComponent, null, null); - } - - private async Task PrerenderedServerComponentAsync(ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) - { - if (!_httpContext.Response.HasStarted) - { - _httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0"; - } - - // Lazy because we don't actually want to require a whole chain of services including Data Protection - // to be required unless you actually use Server render mode. - var serverComponentSerializer = _services.GetRequiredService(); - - var marker = serverComponentSerializer.SerializeInvocation( - invocationId, - type, - parametersCollection, - prerendered: true); - - var htmlComponent = await Dispatcher.InvokeAsync(() => BeginRenderingComponent(type, parametersCollection)); - return new PrerenderedComponentHtmlContent(Dispatcher, htmlComponent, marker, null); - } - - private async ValueTask PrerenderedWebAssemblyComponentAsync(Type type, ParameterView parametersCollection) - { - var marker = WebAssemblyComponentSerializer.SerializeInvocation( - type, - parametersCollection, - prerendered: true); - - var htmlComponent = await Dispatcher.InvokeAsync(() => BeginRenderingComponent(type, parametersCollection)); - return new PrerenderedComponentHtmlContent(Dispatcher, htmlComponent, null, marker); - } - - private PrerenderedComponentHtmlContent NonPrerenderedServerComponent(ServerComponentInvocationSequence invocationId, Type type, ParameterView parametersCollection) - { - if (!_httpContext.Response.HasStarted) - { - _httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0"; - } - - // Lazy because we don't actually want to require a whole chain of services including Data Protection - // to be required unless you actually use Server render mode. - var serverComponentSerializer = _services.GetRequiredService(); - - var marker = serverComponentSerializer.SerializeInvocation(invocationId, type, parametersCollection, prerendered: false); - return new PrerenderedComponentHtmlContent(null, null, marker, null); - } - - private static PrerenderedComponentHtmlContent NonPrerenderedWebAssemblyComponent(Type type, ParameterView parametersCollection) - { - var marker = WebAssemblyComponentSerializer.SerializeInvocation(type, parametersCollection, prerendered: false); - return new PrerenderedComponentHtmlContent(null, null, null, marker); - } - // An implementation of IHtmlContent that holds a reference to a component until we're ready to emit it as HTML to the response. // We don't construct the actual HTML until we receive the call to WriteTo. public class PrerenderedComponentHtmlContent : IHtmlAsyncContent { private readonly Dispatcher? _dispatcher; private readonly HtmlRootComponent? _htmlToEmitOrNull; - private readonly ServerComponentMarker? _serverMarker; - private readonly WebAssemblyComponentMarker? _webAssemblyMarker; public static PrerenderedComponentHtmlContent Empty { get; } - = new PrerenderedComponentHtmlContent(null, default, null, null); + = new PrerenderedComponentHtmlContent(null, default); public PrerenderedComponentHtmlContent( Dispatcher? dispatcher, // If null, we're only emitting the markers - HtmlRootComponent? htmlToEmitOrNull, // If null, we're only emitting the markers - ServerComponentMarker? serverMarker, - WebAssemblyComponentMarker? webAssemblyMarker) + HtmlRootComponent? htmlToEmitOrNull) { _dispatcher = dispatcher; _htmlToEmitOrNull = htmlToEmitOrNull; - _serverMarker = serverMarker; - _webAssemblyMarker = webAssemblyMarker; } public async ValueTask WriteToAsync(TextWriter writer) @@ -231,27 +197,9 @@ public async ValueTask WriteToAsync(TextWriter writer) public void WriteTo(TextWriter writer, HtmlEncoder encoder) { - if (_serverMarker.HasValue) - { - ServerComponentSerializer.AppendPreamble(writer, _serverMarker.Value); - } - else if (_webAssemblyMarker.HasValue) - { - WebAssemblyComponentSerializer.AppendPreamble(writer, _webAssemblyMarker.Value); - } - if (_htmlToEmitOrNull is { } htmlToEmit) { htmlToEmit.WriteHtmlTo(writer); - - if (_serverMarker.HasValue) - { - ServerComponentSerializer.AppendEpilogue(writer, _serverMarker.Value); - } - else if (_webAssemblyMarker.HasValue) - { - WebAssemblyComponentSerializer.AppendEpilogue(writer, _webAssemblyMarker.Value); - } } } diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.PrerenderingState.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.PrerenderingState.cs index 5265acc49e67..baf91c157786 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.PrerenderingState.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.PrerenderingState.cs @@ -3,6 +3,7 @@ using System.Text.Encodings.Web; using Microsoft.AspNetCore.Components.Infrastructure; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -57,25 +58,25 @@ public async ValueTask PrerenderPersistedStateAsync(HttpContext ht } // Internal for test only - internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMode mode) + internal static void UpdateSaveStateRenderMode(HttpContext httpContext, IComponentRenderMode? mode) { // TODO: This will all have to change when we support multiple render modes in the same response - if (mode == RenderMode.ServerPrerendered || mode == RenderMode.WebAssemblyPrerendered) + if (ModeEnablesPrerendering(mode)) { - if (!httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result)) + var currentInvocation = mode switch { - result = new InvokedRenderModes(mode is RenderMode.ServerPrerendered ? - InvokedRenderModes.Mode.Server : - InvokedRenderModes.Mode.WebAssembly); + ServerRenderMode => InvokedRenderModes.Mode.Server, + WebAssemblyRenderMode => InvokedRenderModes.Mode.WebAssembly, + AutoRenderMode => throw new NotImplementedException("TODO: To be able to support AutoRenderMode, we have to serialize persisted state in both WebAssembly and Server formats, or unify the two formats."), + _ => throw new ArgumentException(Resources.FormatUnsupportedRenderMode(mode), nameof(mode)), + }; - httpContext.Items[InvokedRenderModesKey] = result; + if (!httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result)) + { + httpContext.Items[InvokedRenderModesKey] = new InvokedRenderModes(currentInvocation); } else { - var currentInvocation = mode is RenderMode.ServerPrerendered ? - InvokedRenderModes.Mode.Server : - InvokedRenderModes.Mode.WebAssembly; - var invokedMode = (InvokedRenderModes)result!; if (invokedMode.Value != currentInvocation) { @@ -85,6 +86,14 @@ internal static void UpdateSaveStateRenderMode(HttpContext httpContext, RenderMo } } + private static bool ModeEnablesPrerendering(IComponentRenderMode? mode) => mode switch + { + ServerRenderMode { Prerender: true } => true, + WebAssemblyRenderMode { Prerender: true } => true, + AutoRenderMode { Prerender: true } => true, + _ => false + }; + internal static InvokedRenderModes.Mode GetPersistStateRenderMode(HttpContext httpContext) { return httpContext.Items.TryGetValue(InvokedRenderModesKey, out var result) diff --git a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs index d1a90d2fc1f5..90fabc379eb0 100644 --- a/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs +++ b/src/Components/Endpoints/src/Rendering/EndpointHtmlRenderer.Streaming.cs @@ -162,8 +162,34 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo { _visitedComponentIdsInCurrentStreamingBatch?.Add(componentId); - var renderBoundaryMarkers = allowBoundaryMarkers - && ((EndpointComponentState)GetComponentState(componentId)).StreamRendering; + var componentState = (EndpointComponentState)GetComponentState(componentId); + var renderBoundaryMarkers = allowBoundaryMarkers && componentState.StreamRendering; + + // TODO: It's not clear that we actually want to emit the interactive component markers using this + // HTML-comment syntax that we've used historically, plus we likely want some way to coalesce both + // marker types into a single thing for auto mode (the code below emits both separately for auto). + // It may be better to use a custom element like [prerendered] + // so it's easier for the JS code to react automatically whenever this gets inserted or updated during + // streaming SSR or progressively-enhanced navigation. + + var (serverMarker, webAssemblyMarker) = componentState.Component is SSRRenderModeBoundary boundary + ? boundary.ToMarkers(_httpContext) + : default; + + if (serverMarker.HasValue) + { + if (!_httpContext.Response.HasStarted) + { + _httpContext.Response.Headers.CacheControl = "no-cache, no-store, max-age=0"; + } + + ServerComponentSerializer.AppendPreamble(output, serverMarker.Value); + } + + if (webAssemblyMarker.HasValue) + { + WebAssemblyComponentSerializer.AppendPreamble(output, webAssemblyMarker.Value); + } if (renderBoundaryMarkers) { @@ -180,6 +206,16 @@ private void WriteComponentHtml(int componentId, TextWriter output, bool allowBo output.Write(componentId); output.Write("-->"); } + + if (webAssemblyMarker.HasValue && webAssemblyMarker.Value.PrerenderId is not null) + { + WebAssemblyComponentSerializer.AppendEpilogue(output, webAssemblyMarker.Value); + } + + if (serverMarker.HasValue && serverMarker.Value.PrerenderId is not null) + { + ServerComponentSerializer.AppendEpilogue(output, serverMarker.Value); + } } private readonly record struct ComponentIdAndDepth(int ComponentId, int Depth); diff --git a/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs new file mode 100644 index 000000000000..0187815fb37d --- /dev/null +++ b/src/Components/Endpoints/src/Rendering/SSRRenderModeBoundary.cs @@ -0,0 +1,92 @@ +// 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.Web; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.AspNetCore.Components.Endpoints; + +/// +/// A component that describes a location in prerendered output where client-side code +/// should insert an interactive component. +/// +internal class SSRRenderModeBoundary : IComponent +{ + private readonly Type _componentType; + private readonly IComponentRenderMode _renderMode; + private readonly bool _prerender; + private RenderHandle _renderHandle; + private IReadOnlyDictionary? _latestParameters; + + public SSRRenderModeBoundary(Type componentType, IComponentRenderMode renderMode) + { + _componentType = componentType; + _renderMode = renderMode; + _prerender = renderMode switch + { + ServerRenderMode mode => mode.Prerender, + WebAssemblyRenderMode mode => mode.Prerender, + AutoRenderMode mode => mode.Prerender, + _ => throw new ArgumentException($"Server-side rendering does not support the render mode '{renderMode}'.", nameof(renderMode)) + }; + } + + public void Attach(RenderHandle renderHandle) + { + _renderHandle = renderHandle; + } + + public Task SetParametersAsync(ParameterView parameters) + { + // We have to snapshot the parameters because ParameterView is like a ref struct - it can't escape the + // call stack because the underlying buffer may get reused. This is enforced through a runtime check. + _latestParameters = parameters.ToDictionary(); + + if (_prerender) + { + _renderHandle.Render(Prerender); + } + + return Task.CompletedTask; + } + + private void Prerender(RenderTreeBuilder builder) + { + builder.OpenComponent(0, _componentType); + + foreach (var (name, value) in _latestParameters!) + { + builder.AddComponentParameter(1, name, value); + } + + builder.CloseComponent(); + } + + public (ServerComponentMarker?, WebAssemblyComponentMarker?) ToMarkers(HttpContext httpContext) + { + var parameters = _latestParameters is null + ? ParameterView.Empty + : ParameterView.FromDictionary((IDictionary)_latestParameters); + + ServerComponentMarker? serverMarker = null; + if (_renderMode is ServerRenderMode or AutoRenderMode) + { + // Lazy because we don't actually want to require a whole chain of services including Data Protection + // to be required unless you actually use Server render mode. + var serverComponentSerializer = httpContext.RequestServices.GetRequiredService(); + + var invocationId = EndpointHtmlRenderer.GetOrCreateInvocationId(httpContext); + serverMarker = serverComponentSerializer.SerializeInvocation(invocationId, _componentType, parameters, _prerender); + } + + WebAssemblyComponentMarker? webAssemblyMarker = null; + if (_renderMode is WebAssemblyRenderMode or AutoRenderMode) + { + webAssemblyMarker = WebAssemblyComponentSerializer.SerializeInvocation(_componentType, parameters, _prerender); + } + + return (serverMarker, webAssemblyMarker); + } +} diff --git a/src/Components/Endpoints/src/Results/RazorComponentResult.cs b/src/Components/Endpoints/src/Results/RazorComponentResult.cs index 47dc4a68e45e..5ef2725601db 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResult.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResult.cs @@ -74,11 +74,6 @@ public RazorComponentResult(Type componentType, IReadOnlyDictionary public IReadOnlyDictionary Parameters { get; } - /// - /// Gets or sets the rendering mode. - /// - public RenderMode RenderMode { get; set; } = RenderMode.Static; - /// /// Gets or sets a flag to indicate whether streaming rendering should be prevented. If true, the renderer will /// wait for the component hierarchy to complete asynchronous tasks such as loading before supplying the HTML response. diff --git a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs index a8918a526d19..9d61e92544a9 100644 --- a/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs +++ b/src/Components/Endpoints/src/Results/RazorComponentResultExecutor.cs @@ -37,7 +37,6 @@ public virtual Task ExecuteAsync(HttpContext httpContext, RazorComponentResult r return RenderComponentToResponse( httpContext, - result.RenderMode, result.ComponentType, result.Parameters, result.PreventStreamingRendering); @@ -45,7 +44,6 @@ public virtual Task ExecuteAsync(HttpContext httpContext, RazorComponentResult r internal static Task RenderComponentToResponse( HttpContext httpContext, - RenderMode renderMode, Type componentType, IReadOnlyDictionary? componentParameters, bool preventStreamingRendering) @@ -57,20 +55,19 @@ internal static Task RenderComponentToResponse( // backing buffers could come from a pool like they do during rendering. var hostParameters = ParameterView.FromDictionary(new Dictionary { - { nameof(RazorComponentEndpointHost.RenderMode), renderMode }, { nameof(RazorComponentEndpointHost.ComponentType), componentType }, { nameof(RazorComponentEndpointHost.ComponentParameters), componentParameters }, }); await using var writer = CreateResponseWriter(httpContext.Response.Body); - // Note that we always use Static rendering mode for the top-level output from a RazorComponentResult, + // Note that we don't set any interactive rendering mode for the top-level output from a RazorComponentResult, // because you never want to serialize the invocation of RazorComponentResultHost. Instead, that host // component takes care of switching into your desired render mode when it produces its own output. var htmlContent = (EndpointHtmlRenderer.PrerenderedComponentHtmlContent)(await endpointHtmlRenderer.PrerenderComponentAsync( httpContext, typeof(RazorComponentEndpointHost), - RenderMode.Static, + null, hostParameters, waitForQuiescence: preventStreamingRendering)); diff --git a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs index 97ebbeeb09f9..d3915dc35075 100644 --- a/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs +++ b/src/Components/Endpoints/test/EndpointHtmlRendererTest.cs @@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components.Infrastructure; using Microsoft.AspNetCore.Components.Rendering; using Microsoft.AspNetCore.Components.Test.Helpers; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -25,6 +26,7 @@ namespace Microsoft.AspNetCore.Components.Endpoints; public class EndpointHtmlRendererTest { + private const string MarkerPrefix = "(?.+?)$"; private const string ComponentPattern = "^$"; @@ -46,8 +48,8 @@ public async Task CanRender_ParameterlessComponent_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.WebAssembly, ParameterView.Empty); - result.WriteTo(writer, HtmlEncoder.Default); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new WebAssemblyRenderMode(prerender: false), ParameterView.Empty); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -69,7 +71,7 @@ public async Task CanPrerender_ParameterlessComponent_ClientMode() var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.WebAssemblyPrerendered, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.WebAssembly, ParameterView.Empty); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -108,12 +110,12 @@ public async Task CanRender_ComponentWithParameters_ClientMode() // Act var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), - RenderMode.WebAssembly, + new WebAssemblyRenderMode(prerender: false), ParameterView.FromDictionary(new Dictionary { { "Name", "Daniel" } })); - result.WriteTo(writer, HtmlEncoder.Default); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -145,12 +147,12 @@ public async Task CanRender_ComponentWithNullParameters_ClientMode() // Act var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), - RenderMode.WebAssembly, + new WebAssemblyRenderMode(prerender: false), ParameterView.FromDictionary(new Dictionary { { "Name", null } })); - result.WriteTo(writer, HtmlEncoder.Default); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); var match = Regex.Match(content, ComponentPattern); @@ -180,7 +182,7 @@ public async Task CanPrerender_ComponentWithParameters_ClientMode() // Act var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), - RenderMode.WebAssemblyPrerendered, + RenderMode.WebAssembly, ParameterView.FromDictionary(new Dictionary { { "Name", "Daniel" } @@ -229,7 +231,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ClientMode() // Act var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), - RenderMode.WebAssemblyPrerendered, + RenderMode.WebAssembly, ParameterView.FromDictionary(new Dictionary { { "Name", null } @@ -276,7 +278,7 @@ public async Task CanRender_ParameterlessComponent() var writer = new StringWriter(); // Act - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.Static, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), null, ParameterView.Empty); await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); var content = writer.ToString(); @@ -293,8 +295,8 @@ public async Task CanRender_ParameterlessComponent_ServerMode() .ToTimeLimitedDataProtector(); // Act - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.Server, ParameterView.Empty); - var content = HtmlContentToString(result); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new ServerRenderMode(false), ParameterView.Empty); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); var match = Regex.Match(content, ComponentPattern); // Assert @@ -325,7 +327,7 @@ public async Task CanPrerender_ParameterlessComponent_ServerMode() .ToTimeLimitedDataProtector(); // Act - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.ServerPrerendered, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.Server, ParameterView.Empty); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -369,8 +371,8 @@ public async Task Prerender_ServerAndClientComponentUpdatesInvokedPrerenderModes // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var server = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); - var client = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssemblyPrerendered, parameters); + var server = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); + var client = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.WebAssembly, parameters); // Assert var (_, mode) = Assert.Single(httpContext.Items, (kvp) => kvp.Value is InvokedRenderModes); @@ -386,11 +388,11 @@ public async Task CanRenderMultipleServerComponents() .ToTimeLimitedDataProtector(); // Act - var firstResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.ServerPrerendered, ParameterView.Empty); + var firstResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new ServerRenderMode(true), ParameterView.Empty); var firstComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(firstResult)); var firstMatch = Regex.Match(firstComponent, PrerenderedComponentPattern, RegexOptions.Multiline); - var secondResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), RenderMode.Server, ParameterView.Empty); + var secondResult = await renderer.PrerenderComponentAsync(httpContext, typeof(SimpleComponent), new ServerRenderMode(false), ParameterView.Empty); var secondComponent = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(secondResult)); var secondMatch = Regex.Match(secondComponent, ComponentPattern); @@ -427,7 +429,7 @@ public async Task CanRender_ComponentWithParametersObject() // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Static, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), null, parameters); // Assert var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); @@ -444,8 +446,8 @@ public async Task CanRender_ComponentWithParameters_ServerMode() // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); - var content = HtmlContentToString(result); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new ServerRenderMode(false), parameters); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); var match = Regex.Match(content, ComponentPattern); // Assert @@ -483,8 +485,8 @@ public async Task CanRender_ComponentWithNullParameters_ServerMode() // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", null } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); - var content = HtmlContentToString(result); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new ServerRenderMode(false), parameters); + var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); var match = Regex.Match(content, ComponentPattern); // Assert @@ -522,7 +524,7 @@ public async Task CanPrerender_ComponentWithParameters_ServerPrerenderedMode() // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -573,7 +575,7 @@ public async Task CanPrerender_ComponentWithNullParameters_ServerPrerenderedMode // Act var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", null } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.ServerPrerendered, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), RenderMode.Server, parameters); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Multiline); @@ -623,11 +625,13 @@ public async Task ComponentWithInvalidRenderMode_Throws() // Act & Assert var parameters = ParameterView.FromDictionary(new Dictionary { { "Name", "SomeName" } }); var ex = await ExceptionAssert.ThrowsArgumentAsync( - async () => await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), default, parameters), - "prerenderMode", - $"Unsupported RenderMode '{(RenderMode)default}'"); + async () => await renderer.PrerenderComponentAsync(httpContext, typeof(GreetingComponent), new NonexistentRenderMode(), parameters), + "renderMode", + $"Server-side rendering does not support the render mode '{typeof(NonexistentRenderMode)}'."); } + class NonexistentRenderMode : IComponentRenderMode { } + [Fact] public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() { @@ -637,7 +641,7 @@ public async Task RenderComponent_DoesNotInvokeOnAfterRenderInComponent() // Act var state = new OnAfterRenderState(); var parameters = ParameterView.FromDictionary(new Dictionary { { "state", state } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(OnAfterRenderComponent), RenderMode.Static, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(OnAfterRenderComponent), null, parameters); // Assert var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); @@ -667,7 +671,7 @@ public async Task DisposableComponents_GetDisposedAfterScopeCompletes() // Act var state = new AsyncDisposableState(); var parameters = ParameterView.FromDictionary(new Dictionary { { "state", state } }); - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(AsyncDisposableComponent), RenderMode.Static, parameters); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(AsyncDisposableComponent), null, parameters); // Assert var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); @@ -687,7 +691,7 @@ public async Task CanCatch_ComponentWithSynchronousException() var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( httpContext, typeof(ExceptionComponent), - RenderMode.Static, + null, ParameterView.FromDictionary(new Dictionary { { "IsAsync", false } @@ -707,7 +711,7 @@ public async Task CanCatch_ComponentWithAsynchronousException() var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( httpContext, typeof(ExceptionComponent), - RenderMode.Static, + null, ParameterView.FromDictionary(new Dictionary { { "IsAsync", true } @@ -727,7 +731,7 @@ public async Task Rendering_ComponentWithJsInteropThrows() var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( httpContext, typeof(ExceptionComponent), - RenderMode.Static, + null, ParameterView.FromDictionary(new Dictionary { { "JsInterop", true } @@ -759,7 +763,7 @@ public async Task UriHelperRedirect_ThrowsInvalidOperationException_WhenResponse var exception = await Assert.ThrowsAsync(async () => await renderer.PrerenderComponentAsync( httpContext, typeof(RedirectComponent), - RenderMode.Static, + null, ParameterView.FromDictionary(new Dictionary { { "RedirectUri", "http://localhost/redirect" } @@ -787,7 +791,7 @@ public async Task HtmlHelper_Redirects_WhenComponentNavigates() await renderer.PrerenderComponentAsync( httpContext, typeof(RedirectComponent), - RenderMode.Static, + null, ParameterView.FromDictionary(new Dictionary { { "RedirectUri", "http://localhost/redirect" } @@ -805,7 +809,7 @@ public async Task CanRender_AsyncComponent() var httpContext = GetHttpContext(); // Act - var result = await renderer.PrerenderComponentAsync(httpContext, typeof(AsyncComponent), RenderMode.Static, ParameterView.Empty); + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(AsyncComponent), null, ParameterView.Empty); var content = await renderer.Dispatcher.InvokeAsync(() => HtmlContentToString(result)); // Assert @@ -1007,6 +1011,139 @@ public void NamedEventHandlers_DifferentComponents_SameNamedHandlerInDifferentBa Assert.Equal(expectedError, exception.Message); } + [Fact] + public async Task RenderMode_CanRenderInteractiveComponents() + { + // Arrange + var httpContext = GetHttpContext(); + var writer = new StringWriter(); + var protector = _dataprotectorProvider.CreateProtector(ServerComponentSerializationSettings.DataProtectionProviderPurpose) + .ToTimeLimitedDataProtector(); + + // Act + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(ComponentWithInteractiveChildren), null, ParameterView.Empty); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); + var content = writer.ToString(); + + // Assert + var lines = content.Replace("\r\n", "\n").Split('\n'); + var serverMarkerMatch = Regex.Match(lines[0], PrerenderedComponentPattern); + var serverNonPrerenderedMarkerMatch = Regex.Match(lines[1], ComponentPattern); + var webAssemblyMarkerMatch = Regex.Match(lines[2], PrerenderedComponentPattern); + var webAssemblyNonPrerenderedMarkerMatch = Regex.Match(lines[3], ComponentPattern); + + // Server + { + var markerText = serverMarkerMatch.Groups[1].Value; + var innerHtml = serverMarkerMatch.Groups[2].Value; + + var marker = JsonSerializer.Deserialize(markerText, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(0, marker.Sequence); + Assert.NotNull(marker.PrerenderId); + Assert.NotNull(marker.Descriptor); + Assert.Equal("server", marker.Type); + + var unprotectedServerComponent = protector.Unprotect(marker.Descriptor); + var serverComponent = JsonSerializer.Deserialize(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(0, serverComponent.Sequence); + Assert.Equal(typeof(InteractiveGreetingServer).Assembly.GetName().Name, serverComponent.AssemblyName); + Assert.Equal(typeof(InteractiveGreetingServer).FullName, serverComponent.TypeName); + Assert.NotEqual(Guid.Empty, serverComponent.InvocationId); + + var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions); + Assert.Equal("Name", parameterDefinition.Name); + Assert.Equal("System.String", parameterDefinition.TypeName); + Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly); + + var value = Assert.Single(serverComponent.ParameterValues); + var rawValue = Assert.IsType(value); + Assert.Equal("ServerPrerendered", rawValue.GetString()); + + Assert.Equal("

Hello ServerPrerendered!

", innerHtml); + } + + // ServerNonPrerendered + { + var markerText = serverNonPrerenderedMarkerMatch.Groups[1].Value; + + var marker = JsonSerializer.Deserialize(markerText, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(1, marker.Sequence); + Assert.Null(marker.PrerenderId); + Assert.NotNull(marker.Descriptor); + Assert.Equal("server", marker.Type); + + var unprotectedServerComponent = protector.Unprotect(marker.Descriptor); + var serverComponent = JsonSerializer.Deserialize(unprotectedServerComponent, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(1, serverComponent.Sequence); + Assert.Equal(typeof(InteractiveGreetingServer).Assembly.GetName().Name, serverComponent.AssemblyName); + Assert.Equal(typeof(InteractiveGreetingServerNonPrerendered).FullName, serverComponent.TypeName); + Assert.NotEqual(Guid.Empty, serverComponent.InvocationId); + + var parameterDefinition = Assert.Single(serverComponent.ParameterDefinitions); + Assert.Equal("Name", parameterDefinition.Name); + Assert.Equal("System.String", parameterDefinition.TypeName); + Assert.Equal("System.Private.CoreLib", parameterDefinition.Assembly); + + var value = Assert.Single(serverComponent.ParameterValues); + var rawValue = Assert.IsType(value); + Assert.Equal("Server", rawValue.GetString()); + } + + // WebAssembly + { + var markerText = webAssemblyMarkerMatch.Groups[1].Value; + var innerHtml = webAssemblyMarkerMatch.Groups[2].Value; + + var marker = JsonSerializer.Deserialize(markerText, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(typeof(InteractiveGreetingWebAssembly).FullName, marker.TypeName); + + var parameterValues = JsonSerializer.Deserialize(Convert.FromBase64String(marker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal("WebAssemblyPrerendered", parameterValues.Single().ToString()); + + Assert.Equal("

Hello WebAssemblyPrerendered!

", innerHtml); + } + + // WebAssemblyNonPrerendered + { + var markerText = webAssemblyNonPrerenderedMarkerMatch.Groups[1].Value; + + var marker = JsonSerializer.Deserialize(markerText, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal(typeof(InteractiveGreetingWebAssemblyNonPrerendered).FullName, marker.TypeName); + + var parameterValues = JsonSerializer.Deserialize(Convert.FromBase64String(marker.ParameterValues), WebAssemblyComponentSerializationSettings.JsonSerializationOptions); + Assert.Equal("WebAssembly", parameterValues.Single().ToString()); + } + } + + [Fact] + public async Task DoesNotEmitNestedRenderModeBoundaries() + { + // Arrange + var httpContext = GetHttpContext(); + var writer = new StringWriter(); + + // Act + var result = await renderer.PrerenderComponentAsync(httpContext, typeof(InteractiveWithInteractiveChild), + null, + ParameterView.Empty); + await renderer.Dispatcher.InvokeAsync(() => result.WriteTo(writer, HtmlEncoder.Default)); + var content = writer.ToString(); + + // Assert + var numMarkers = Regex.Matches(content, MarkerPrefix).Count; + Assert.Equal(2, numMarkers); // A start and an end marker + + var match = Regex.Match(content, PrerenderedComponentPattern, RegexOptions.Singleline); + Assert.True(match.Success); + var preamble = match.Groups["preamble"].Value; + var preambleMarker = JsonSerializer.Deserialize(preamble, ServerComponentSerializationSettings.JsonSerializationOptions); + Assert.NotNull(preambleMarker.PrerenderId); + Assert.Equal("webassembly", preambleMarker.Type); + + var prerenderedContent = match.Groups["content"].Value; + Assert.Equal("

This is InteractiveWithInteractiveChild

\n\n

Hello from InteractiveGreetingServer!

", prerenderedContent.Replace("\r\n", "\n")); + } + private class NamedEventHandlerComponent : ComponentBase { [Parameter] diff --git a/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj b/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj index 2742b193db4c..5c6a32c5ac68 100644 --- a/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj +++ b/src/Components/Endpoints/test/Microsoft.AspNetCore.Components.Endpoints.Tests.csproj @@ -2,6 +2,7 @@ $(DefaultNetCoreTargetFramework) + 8.0 diff --git a/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs b/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs index d5874cf25ab6..8a62f594a33b 100644 --- a/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs +++ b/src/Components/Endpoints/test/RazorComponentResultExecutorTest.cs @@ -31,7 +31,6 @@ public async Task CanRenderComponentStatically() // Act await RazorComponentResultExecutor.RenderComponentToResponse( httpContext, - RenderMode.Static, typeof(SimpleComponent), componentParameters: null, preventStreamingRendering: false); @@ -52,7 +51,6 @@ public async Task PerformsStreamingRendering() // Act/Assert 1: Emits the initial pre-quiescent output to the response var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( httpContext, - RenderMode.Static, typeof(StreamingAsyncLoadingComponent), PropertyHelper.ObjectToDictionary(new { LoadingTask = tcs.Task }).AsReadOnly(), preventStreamingRendering: false); @@ -85,7 +83,6 @@ public async Task EmitsEachComponentOnlyOncePerStreamingUpdate_WhenAComponentRen // Act/Assert 1: Emits the initial pre-quiescent output to the response var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( httpContext, - RenderMode.Static, typeof(DoubleRenderingStreamingAsyncComponent), PropertyHelper.ObjectToDictionary(new { WaitFor = tcs.Task }).AsReadOnly(), preventStreamingRendering: false); @@ -118,7 +115,6 @@ public async Task EmitsEachComponentOnlyOncePerStreamingUpdate_WhenAnAncestorAls // Act/Assert 1: Emits the initial pre-quiescent output to the response var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( httpContext, - RenderMode.Static, typeof(StreamingComponentWithChild), PropertyHelper.ObjectToDictionary(new { LoadingTask = tcs.Task }).AsReadOnly(), preventStreamingRendering: false); @@ -148,7 +144,6 @@ public async Task WaitsForQuiescenceIfPreventStreamingRenderingIsTrue() // Act/Assert: Doesn't complete until loading finishes var completionTask = RazorComponentResultExecutor.RenderComponentToResponse( httpContext, - RenderMode.Static, typeof(StreamingAsyncLoadingComponent), PropertyHelper.ObjectToDictionary(new { LoadingTask = tcs.Task }).AsReadOnly(), preventStreamingRendering: true); @@ -173,7 +168,7 @@ public async Task SupportsLayouts() // Act await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(ComponentWithLayout), + httpContext, typeof(ComponentWithLayout), null, false); // Assert @@ -188,7 +183,7 @@ public async Task OnNavigationBeforeResponseStarted_Redirects() // Act await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(ComponentThatRedirectsSynchronously), + httpContext, typeof(ComponentThatRedirectsSynchronously), null, false); // Assert @@ -207,7 +202,7 @@ public async Task OnNavigationAfterResponseStarted_WithStreamingOff_Throws() // Act var ex = await Assert.ThrowsAsync( () => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(StreamingComponentThatRedirectsAsynchronously), + httpContext, typeof(StreamingComponentThatRedirectsAsynchronously), null, preventStreamingRendering: true)); // Assert @@ -224,7 +219,7 @@ public async Task OnNavigationAfterResponseStarted_WithStreamingOn_EmitsCommand( // Act await RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(StreamingComponentThatRedirectsAsynchronously), + httpContext, typeof(StreamingComponentThatRedirectsAsynchronously), null, preventStreamingRendering: false); // Assert @@ -241,7 +236,7 @@ public async Task OnUnhandledExceptionBeforeResponseStarted_Throws() // Act var ex = await Assert.ThrowsAsync(() => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(ComponentThatThrowsSynchronously), + httpContext, typeof(ComponentThatThrowsSynchronously), null, false)); // Assert @@ -256,7 +251,7 @@ public async Task OnUnhandledExceptionAfterResponseStarted_WithStreamingOff_Thro // Act var ex = await Assert.ThrowsAsync(() => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(StreamingComponentThatThrowsAsynchronously), + httpContext, typeof(StreamingComponentThatThrowsAsynchronously), null, preventStreamingRendering: true)); // Assert @@ -279,7 +274,7 @@ public async Task OnUnhandledExceptionAfterResponseStarted_WithStreamingOn_Emits // Act var ex = await Assert.ThrowsAsync(() => RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(StreamingComponentThatThrowsAsynchronously), + httpContext, typeof(StreamingComponentThatThrowsAsynchronously), null, preventStreamingRendering: false)); // Assert @@ -383,7 +378,7 @@ private VaryStreamingScenariosContext PrepareVaryStreamingScenariosTests() }; var quiescence = RazorComponentResultExecutor.RenderComponentToResponse( - httpContext, RenderMode.Static, typeof(VaryStreamingScenarios), + httpContext, typeof(VaryStreamingScenarios), parameters, preventStreamingRendering: false); return new(renderer, quiescence, responseBody, topLevelComponentTask, withinStreamingRegionTask, withinNestedNonstreamingRegionTask); diff --git a/src/Components/Endpoints/test/TestComponents/ComponentWithInteractiveChildren.razor b/src/Components/Endpoints/test/TestComponents/ComponentWithInteractiveChildren.razor new file mode 100644 index 000000000000..67076e7ed2b3 --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/ComponentWithInteractiveChildren.razor @@ -0,0 +1,4 @@ + + + + diff --git a/src/Components/Endpoints/test/TestComponents/InteractiveGreetingServer.razor b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingServer.razor new file mode 100644 index 000000000000..513134b3fae9 --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingServer.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Web +@attribute [RenderModeServer] + +

Hello @(Name ?? "(null)")!

+ +@code { + [Parameter] public string Name { get; set; } +} diff --git a/src/Components/Endpoints/test/TestComponents/InteractiveGreetingServerNonPrerendered.razor b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingServerNonPrerendered.razor new file mode 100644 index 000000000000..76aaea3b855f --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingServerNonPrerendered.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Web +@attribute [RenderModeServer(false)] + +

Hello @(Name ?? "(null)")!

+ +@code { + [Parameter] public string Name { get; set; } +} diff --git a/src/Components/Endpoints/test/TestComponents/InteractiveGreetingWebAssembly.razor b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingWebAssembly.razor new file mode 100644 index 000000000000..dc1573360f3b --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingWebAssembly.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Web +@attribute [RenderModeWebAssembly] + +

Hello @(Name ?? "(null)")!

+ +@code { + [Parameter] public string Name { get; set; } +} diff --git a/src/Components/Endpoints/test/TestComponents/InteractiveGreetingWebAssemblyNonPrerendered.razor b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingWebAssemblyNonPrerendered.razor new file mode 100644 index 000000000000..56293c9e76eb --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/InteractiveGreetingWebAssemblyNonPrerendered.razor @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Components.Web +@attribute [RenderModeWebAssembly(false)] + +

Hello @(Name ?? "(null)")!

+ +@code { + [Parameter] public string Name { get; set; } +} diff --git a/src/Components/Endpoints/test/TestComponents/InteractiveWithInteractiveChild.razor b/src/Components/Endpoints/test/TestComponents/InteractiveWithInteractiveChild.razor new file mode 100644 index 000000000000..f16bb8ded8b0 --- /dev/null +++ b/src/Components/Endpoints/test/TestComponents/InteractiveWithInteractiveChild.razor @@ -0,0 +1,6 @@ +@using Microsoft.AspNetCore.Components.Web +@attribute [RenderModeWebAssembly] + +

This is @nameof(InteractiveWithInteractiveChild)

+ + diff --git a/src/Components/Server/src/Circuits/RemoteRenderer.cs b/src/Components/Server/src/Circuits/RemoteRenderer.cs index 3799ebff3def..40f5f5cd164b 100644 --- a/src/Components/Server/src/Circuits/RemoteRenderer.cs +++ b/src/Components/Server/src/Circuits/RemoteRenderer.cs @@ -2,11 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Concurrent; +using System.Diagnostics.CodeAnalysis; using System.Linq; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Internal; using Microsoft.Extensions.Logging; +using static Microsoft.AspNetCore.Internal.LinkerFlags; namespace Microsoft.AspNetCore.Components.Server.Circuits; @@ -285,6 +288,13 @@ public Task OnRenderCompletedAsync(long incomingBatchId, string? errorMessageOrN } } + protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode componentTypeRenderMode) + => componentTypeRenderMode switch + { + ServerRenderMode or AutoRenderMode => componentActivator.CreateInstance(componentType), + _ => throw new NotSupportedException($"Cannot create a component of type '{componentType}' because its render mode '{componentTypeRenderMode}' is not supported by interactive server-side rendering."), + }; + private void ProcessPendingBatch(string? errorMessageOrNull, UnacknowledgedRenderBatch entry) { var elapsedTime = entry.ValueStopwatch.GetElapsedTime(); diff --git a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs index fa6757ecc6a8..2a403ac3e28e 100644 --- a/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs +++ b/src/Components/Web/src/HtmlRendering/StaticHtmlRenderer.cs @@ -32,12 +32,30 @@ public StaticHtmlRenderer(IServiceProvider serviceProvider, ILoggerFactory logge /// public override Dispatcher Dispatcher { get; } = Dispatcher.CreateDefault(); - /// + /// + /// Adds a root component of the specified type and begins rendering it. + /// + /// The component type. This must implement . + /// Parameters for the component. + /// An that can be used to obtain the rendered HTML. public HtmlRootComponent BeginRenderingComponent( [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] Type componentType, ParameterView initialParameters) { var component = InstantiateComponent(componentType); + return BeginRenderingComponent(component, initialParameters); + } + + /// + /// Adds a root component and begins rendering it. + /// + /// The root component instance to be added and rendered. This must not already be associated with any renderer. + /// Parameters for the component. + /// An that can be used to obtain the rendered HTML. + public HtmlRootComponent BeginRenderingComponent( + IComponent component, + ParameterView initialParameters) + { var componentId = AssignRootComponentId(component); var quiescenceTask = RenderRootComponentAsync(componentId, initialParameters); diff --git a/src/Components/Web/src/PublicAPI.Unshipped.txt b/src/Components/Web/src/PublicAPI.Unshipped.txt index 623c1fc6be48..ba70003e59d3 100644 --- a/src/Components/Web/src/PublicAPI.Unshipped.txt +++ b/src/Components/Web/src/PublicAPI.Unshipped.txt @@ -13,8 +13,13 @@ Microsoft.AspNetCore.Components.Forms.EditForm.BindingContext.set -> void Microsoft.AspNetCore.Components.Forms.EditForm.FormHandlerName.get -> string? Microsoft.AspNetCore.Components.Forms.EditForm.FormHandlerName.set -> void Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer +Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(Microsoft.AspNetCore.Components.IComponent! component, Microsoft.AspNetCore.Components.ParameterView initialParameters) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.BeginRenderingComponent(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView initialParameters) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.StaticHtmlRenderer(System.IServiceProvider! serviceProvider, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> void +Microsoft.AspNetCore.Components.Web.AutoRenderMode +Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode() -> void +Microsoft.AspNetCore.Components.Web.AutoRenderMode.AutoRenderMode(bool prerender) -> void +Microsoft.AspNetCore.Components.Web.AutoRenderMode.Prerender.get -> bool Microsoft.AspNetCore.Components.Web.HtmlRenderer Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(System.Type! componentType) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent Microsoft.AspNetCore.Components.Web.HtmlRenderer.BeginRenderingComponent(System.Type! componentType, Microsoft.AspNetCore.Components.ParameterView parameters) -> Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent @@ -34,7 +39,31 @@ Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent.HtmlRootComp Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent.QuiescenceTask.get -> System.Threading.Tasks.Task! Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent.ToHtmlString() -> string! Microsoft.AspNetCore.Components.Web.HtmlRendering.HtmlRootComponent.WriteHtmlTo(System.IO.TextWriter! output) -> void +Microsoft.AspNetCore.Components.Web.RenderMode +Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute +Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.RenderModeAutoAttribute() -> void +Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.RenderModeAutoAttribute(bool prerender) -> void +Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute +Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.RenderModeServerAttribute() -> void +Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.RenderModeServerAttribute(bool prerender) -> void +Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute +Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.RenderModeWebAssemblyAttribute() -> void +Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.RenderModeWebAssemblyAttribute(bool prerender) -> void +Microsoft.AspNetCore.Components.Web.ServerRenderMode +Microsoft.AspNetCore.Components.Web.ServerRenderMode.Prerender.get -> bool +Microsoft.AspNetCore.Components.Web.ServerRenderMode.ServerRenderMode() -> void +Microsoft.AspNetCore.Components.Web.ServerRenderMode.ServerRenderMode(bool prerender) -> void +Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode +Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode.Prerender.get -> bool +Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode.WebAssemblyRenderMode() -> void +Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode.WebAssemblyRenderMode(bool prerender) -> void override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.Dispatcher.get -> Microsoft.AspNetCore.Components.Dispatcher! override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.HandleException(System.Exception! exception) -> void override Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.UpdateDisplayAsync(in Microsoft.AspNetCore.Components.RenderTree.RenderBatch renderBatch) -> System.Threading.Tasks.Task! +override Microsoft.AspNetCore.Components.Web.RenderModeAutoAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! +override Microsoft.AspNetCore.Components.Web.RenderModeServerAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! +override Microsoft.AspNetCore.Components.Web.RenderModeWebAssemblyAttribute.Mode.get -> Microsoft.AspNetCore.Components.IComponentRenderMode! +static Microsoft.AspNetCore.Components.Web.RenderMode.Auto.get -> Microsoft.AspNetCore.Components.Web.AutoRenderMode! +static Microsoft.AspNetCore.Components.Web.RenderMode.Server.get -> Microsoft.AspNetCore.Components.Web.ServerRenderMode! +static Microsoft.AspNetCore.Components.Web.RenderMode.WebAssembly.get -> Microsoft.AspNetCore.Components.Web.WebAssemblyRenderMode! virtual Microsoft.AspNetCore.Components.HtmlRendering.Infrastructure.StaticHtmlRenderer.WriteComponentHtml(int componentId, System.IO.TextWriter! output) -> void diff --git a/src/Components/Web/src/RenderMode/AutoRenderMode.cs b/src/Components/Web/src/RenderMode/AutoRenderMode.cs new file mode 100644 index 000000000000..5ff58281cf66 --- /dev/null +++ b/src/Components/Web/src/RenderMode/AutoRenderMode.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// A indicating that the component's render mode should be determined automatically based on a policy. +/// +public class AutoRenderMode : IComponentRenderMode +{ + /// + /// Constructs an instance of . + /// + public AutoRenderMode() : this(true) + { + } + + /// + /// Constructs an instance of + /// + /// A flag indicating whether the component should first prerender on the server. The default value is true. + public AutoRenderMode(bool prerender) + { + Prerender = prerender; + } + + /// + /// A flag indicating whether the component should first prerender on the server. The default value is true. + /// + public bool Prerender { get; } +} diff --git a/src/Components/Web/src/RenderMode/RenderMode.cs b/src/Components/Web/src/RenderMode/RenderMode.cs new file mode 100644 index 000000000000..dcb53491d565 --- /dev/null +++ b/src/Components/Web/src/RenderMode/RenderMode.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// Provides pre-constructed instances that may be used during rendering. +/// +public static class RenderMode +{ + /// + /// Gets an that represents rendering interactively on the server via Blazor Server hosting + /// with server-side prerendering. + /// + public static ServerRenderMode Server { get; } = new(); + + /// + /// Gets an that represents rendering interactively on the client via Blazor WebAssembly hosting + /// with server-side prerendering. + /// + public static WebAssemblyRenderMode WebAssembly { get; } = new(); + + /// + /// Gets an that means the render mode will be determined automatically based on a policy. + /// + public static AutoRenderMode Auto { get; } = new(); +} diff --git a/src/Components/Web/src/RenderMode/ServerRenderMode.cs b/src/Components/Web/src/RenderMode/ServerRenderMode.cs new file mode 100644 index 000000000000..2928be842e26 --- /dev/null +++ b/src/Components/Web/src/RenderMode/ServerRenderMode.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// A indicating that the component should be rendered interactively on the server using Blazor Server hosting. +/// +public class ServerRenderMode : IComponentRenderMode +{ + /// + /// Constructs an instance of . + /// + public ServerRenderMode() : this(true) + { + } + + /// + /// Constructs an instance of + /// + /// A flag indicating whether the component should first prerender on the server. The default value is true. + public ServerRenderMode(bool prerender) + { + Prerender = prerender; + } + + /// + /// A flag indicating whether the component should first prerender on the server. The default value is true. + /// + public bool Prerender { get; } +} diff --git a/src/Components/Web/src/RenderMode/TemporaryRenderModeAttributes.cs b/src/Components/Web/src/RenderMode/TemporaryRenderModeAttributes.cs new file mode 100644 index 000000000000..2f96d74a6bbb --- /dev/null +++ b/src/Components/Web/src/RenderMode/TemporaryRenderModeAttributes.cs @@ -0,0 +1,83 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// Temporary attribute for indicating that a component should render interactively on the server. +/// This will later be replaced by a @rendermode directive. +/// +public class RenderModeServerAttribute : RenderModeAttribute +{ + /// + /// Constructs an instance of . + /// + public RenderModeServerAttribute() : this(true) + { + } + + /// + /// Constructs an instance of . + /// + /// A flag indicating whether to prerender the component on the server. The default value is true. + public RenderModeServerAttribute(bool prerender) + { + Mode = new ServerRenderMode(prerender); + } + + /// + public override IComponentRenderMode Mode { get; } +} + +/// +/// Temporary attribute for indicating that a component should render interactively using WebAssembly. +/// This will later be replaced by a @rendermode directive. +/// +public class RenderModeWebAssemblyAttribute : RenderModeAttribute +{ + /// + /// Constructs an instance of . + /// + public RenderModeWebAssemblyAttribute() : this(true) + { + } + + /// + /// Constructs an instance of . + /// + /// A flag indicating whether to prerender the component on the server. The default value is true. + public RenderModeWebAssemblyAttribute(bool prerender) + { + Mode = new WebAssemblyRenderMode(prerender); + } + + /// + public override IComponentRenderMode Mode { get; } +} + +/// +/// Temporary attribute for indicating that a component should render interactively, with +/// a mode automatically determined. +/// This will later be replaced by a @rendermode directive. +/// +public class RenderModeAutoAttribute : RenderModeAttribute +{ + /// + /// Constructs an instance of . + /// + public RenderModeAutoAttribute() : this(true) + { + } + + /// + /// Constructs an instance of . + /// + /// A flag indicating whether to prerender the component on the server. The default value is true. + public RenderModeAutoAttribute(bool prerender) + { + Mode = new AutoRenderMode(prerender); + } + + /// + public override IComponentRenderMode Mode { get; } +} diff --git a/src/Components/Web/src/RenderMode/WebAssemblyRenderMode.cs b/src/Components/Web/src/RenderMode/WebAssemblyRenderMode.cs new file mode 100644 index 000000000000..d270a39e061b --- /dev/null +++ b/src/Components/Web/src/RenderMode/WebAssemblyRenderMode.cs @@ -0,0 +1,31 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.AspNetCore.Components.Web; + +/// +/// A indicating that the component should be rendered on the client using WebAssembly. +/// +public class WebAssemblyRenderMode : IComponentRenderMode +{ + /// + /// Constructs an instance of . + /// + public WebAssemblyRenderMode() : this(true) + { + } + + /// + /// Constructs an instance of + /// + /// A flag indicating whether the component should first prerender on the server. The default value is true. + public WebAssemblyRenderMode(bool prerender) + { + Prerender = prerender; + } + + /// + /// A flag indicating whether the component should first prerender on the server. The default value is true. + /// + public bool Prerender { get; } +} diff --git a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs index 229c42407015..dacb372c7166 100644 --- a/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs +++ b/src/Components/WebAssembly/WebAssembly/src/Rendering/WebAssemblyRenderer.cs @@ -5,6 +5,7 @@ using System.Runtime.CompilerServices; using System.Runtime.InteropServices.JavaScript; using Microsoft.AspNetCore.Components.RenderTree; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web.Infrastructure; using Microsoft.AspNetCore.Components.WebAssembly.Hosting; using Microsoft.AspNetCore.Components.WebAssembly.Services; @@ -132,6 +133,13 @@ protected override void HandleException(Exception exception) } } + protected override IComponent ResolveComponentForRenderMode([DynamicallyAccessedMembers(Component)] Type componentType, int? parentComponentId, IComponentActivator componentActivator, IComponentRenderMode componentTypeRenderMode) + => componentTypeRenderMode switch + { + WebAssemblyRenderMode or AutoRenderMode => componentActivator.CreateInstance(componentType), + _ => throw new NotSupportedException($"Cannot create a component of type '{componentType}' because its render mode '{componentTypeRenderMode}' is not supported by WebAssembly rendering."), + }; + private static partial class Log { [LoggerMessage(100, LogLevel.Critical, "Unhandled exception rendering component: {Message}", EventName = "ExceptionRenderingComponent")] diff --git a/src/Components/test/E2ETest/ServerExecutionTests/MultipleRootComponentsTest.cs b/src/Components/test/E2ETest/ServerExecutionTests/MultipleRootComponentsTest.cs index 39a730e5fbb7..084276e592c9 100644 --- a/src/Components/test/E2ETest/ServerExecutionTests/MultipleRootComponentsTest.cs +++ b/src/Components/test/E2ETest/ServerExecutionTests/MultipleRootComponentsTest.cs @@ -76,19 +76,26 @@ public void CanRenderMultipleRootComponents() var expectedComponentSequence = new bool[] { // true means it was a prerendered component. - true, - false, - false, - false, - true, - false, - true, - false, - true, - false, - true, - false, - true, + + // Layout + false, // Server + true, // ServerPrerendered + + // Body + true, // ServerPrerendered + false, // Server + false, // Server + + false, // Server + true, // ServerPrerendered + false, // Server + true, // ServerPrerendered + false, // Server + true, // ServerPrerendered + + // Layout + false, // Server + true, // ServerPrerendered }; Assert.Equal(expectedComponentSequence, componentSequence); diff --git a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs index ad4655cba08f..9c8e36f1d1a9 100644 --- a/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs +++ b/src/Mvc/Mvc.TagHelpers/test/PersistComponentStateTagHelperTest.cs @@ -81,7 +81,7 @@ public async Task ExecuteAsync_RendersWebAssemblyStateImplicitlyWhenAWebAssembly ViewContext = GetViewContext() }; - EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.RenderMode.WebAssemblyPrerendered); + EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.WebAssembly); var context = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -129,7 +129,7 @@ public async Task ExecuteAsync_RendersServerStateImplicitlyWhenAServerComponentW ViewContext = GetViewContext() }; - EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.RenderMode.ServerPrerendered); + EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.Server); var context = GetTagHelperContext(); var output = GetTagHelperOutput(); @@ -154,8 +154,8 @@ public async Task ExecuteAsync_ThrowsIfItCantInferThePersistMode() ViewContext = GetViewContext() }; - EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.RenderMode.ServerPrerendered); - EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.RenderMode.WebAssemblyPrerendered); + EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.Server); + EndpointHtmlRenderer.UpdateSaveStateRenderMode(tagHelper.ViewContext.HttpContext, Components.Web.RenderMode.WebAssembly); var context = GetTagHelperContext(); var output = GetTagHelperOutput(); diff --git a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs index fb7a4007d28d..adf2ae18e9ed 100644 --- a/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs +++ b/src/Mvc/Mvc.ViewFeatures/src/Rendering/HtmlHelperComponentExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Endpoints; +using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.Extensions.DependencyInjection; @@ -63,17 +64,16 @@ public static async Task RenderComponentAsync( return await componentRenderer.PrerenderComponentAsync(httpContext, componentType, MapRenderMode(renderMode), parameterView); } - // This is unfortunate, and we might want to find a better solution. There's an existing public - // type Microsoft.AspNetCore.Mvc.Rendering.RenderMode which we now need to use in a lower layer, - // M.A.C.Endpoints. Even type-forwarding is not a good solution because we really want to change - // the namespace. So this code maps the old enum to the newer one. - internal static Components.RenderMode MapRenderMode(RenderMode renderMode) => renderMode switch + // The tag helper uses a simple enum to represent render mode, whereas Blazor internally has a richer + // object-based way to represent render modes. This converts from tag helper enum values to the + // object representation. + internal static IComponentRenderMode MapRenderMode(RenderMode renderMode) => renderMode switch { - RenderMode.Static => Components.RenderMode.Static, - RenderMode.Server => Components.RenderMode.Server, - RenderMode.ServerPrerendered => Components.RenderMode.ServerPrerendered, - RenderMode.WebAssembly => Components.RenderMode.WebAssembly, - RenderMode.WebAssemblyPrerendered => Components.RenderMode.WebAssemblyPrerendered, + RenderMode.Static => null, + RenderMode.Server => new ServerRenderMode(prerender: false), + RenderMode.ServerPrerendered => Components.Web.RenderMode.Server, + RenderMode.WebAssembly => new WebAssemblyRenderMode(prerender: false), + RenderMode.WebAssemblyPrerendered => Components.Web.RenderMode.WebAssembly, _ => throw new ArgumentException($"Unsupported render mode {renderMode}", nameof(renderMode)), }; }