API review: Components SSR streaming and render modes #50057
Closed
Description
Background and Motivation
Server-side rendering for components in .NET 8. This is a pretty immense set of changes accounting for much of the work we've done for the release.
The team decided early on not to attempt to do API review for each piece as it was implemented, because there was too much churn as the designs evolved. As such it's now a huge review-bomb, but I think most of it should be quite understandable with a bit of explanation.
I'm trying to split up the API reviews into thematic areas and this one is meant to focus on rendermodes. However some other smaller, tangential bits have snuck in here too and that's probably fine.
Proposed API
namespace Microsoft.AspNetCore.Components
{
+ /// <summary>
+ /// Represents a render mode for a component.
+ /// </summary>
+ public interface IComponentRenderMode {} // Just a marker. Each renderer assigns its own meanings to these types.
+ /// <summary>
+ /// 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.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class)]
+ public abstract class RenderModeAttribute : Attribute
+ {
+ /// <summary>
+ /// Gets the fixed rendering mode for a component type.
+ /// </summary>
+ public abstract IComponentRenderMode Mode { get; }
+ }
+ /// <summary>
+ /// Supplies a cascading value that can be received by components using
+ /// <see cref="CascadingParameterAttribute"/>.
+ /// </summary>
+ public class CascadingValueSource<TValue> : ICascadingValueSupplier
+ {
+ public CascadingValueSource(TValue value, bool isFixed) {}
+ public CascadingValueSource(string name, TValue value, bool isFixed) {}
+ public CascadingValueSource(Func<TValue> valueFactory, bool isFixed) {}
+
+ /// <summary>
+ /// Notifies subscribers that the value has changed (for example, if it has been mutated).
+ /// </summary>
+ /// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns>
+ public Task NotifyChangedAsync();
+
+ /// <summary>
+ /// Notifies subscribers that the value has changed, supplying a new value.
+ /// </summary>
+ /// <param name="newValue"></param>
+ /// <returns>A <see cref="Task"/> that completes when the notifications have been issued.</returns>
+ public Task NotifyChangedAsync(TValue newValue)
+ }
public readonly struct ParameterView
{
- public IReadOnlyDictionary<string, object!> ToDictionary() {}
+ public IReadOnlyDictionary<string, object?> ToDictionary() {}
}
+ /// <summary>
+ /// An attribute to indicate whether to stream the rendering of a component and its descendants.
+ ///
+ /// This attribute only takes effect within renderers that support streaming rendering (for example,
+ /// server-side HTML rendering from a Razor Component endpoint). In other hosting models it has no effect.
+ ///
+ /// If a component type does not declare this attribute, then instances of that component type will share
+ /// the same streaming rendering mode as their parent component.
+ /// </summary>
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
+ public class StreamRenderingAttribute : Attribute
+ {
+ /// <summary>
+ /// Constructs an instance of <see cref="StreamRenderingAttribute"/>
+ /// </summary>
+ /// <param name="enabled">A flag to indicate whether this component and its descendants should stream their rendering.</param>
+ public StreamRenderingAttribute(bool enabled) {}
+
+ /// <summary>
+ /// Gets a flag indicating whether this component and its descendants should stream their rendering.
+ /// </summary>
+ public bool Enabled { get; }
+ }
// Note: Our own hosting models call this as needed. Applications don't need to call it manually, and if they do, it's a no-op because it uses TryAddEnumerable.
+ /// <summary>
+ /// Enables component parameters to be supplied from the query string with <see cref="SupplyParameterFromQueryAttribute"/>.
+ /// </summary>
+ public static class SupplyParameterFromQueryProviderServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Enables component parameters to be supplied from the query string with <see cref="SupplyParameterFromQueryAttribute"/>.
+ /// </summary>
+ /// <param name="services">The <see cref="IServiceCollection"/>.</param>
+ /// <returns>The <see cref="IServiceCollection"/>.</returns>
+ public static IServiceCollection AddSupplyValueFromQueryProvider(this IServiceCollection services) {}
+ }
+ /// <summary>
+ /// Extension methods for configuring cascading values on an <see cref="IServiceCollection"/>.
+ /// </summary>
+ public static class CascadingValueServiceCollectionExtensions
+ {
+ /// <summary>
+ /// Adds a cascading value to the <paramref name="serviceCollection"/>. This is equivalent to having
+ /// a fixed <see cref="CascadingValue{TValue}"/> at the root of the component hierarchy.
+ /// </summary>
+ /// <typeparam name="TValue">The value type.</typeparam>
+ /// <param name="serviceCollection">The <see cref="IServiceCollection"/>.</param>
+ /// <param name="valueFactory">A callback that supplies a fixed value within each service provider scope.</param>
+ /// <param name="sourceFactory">A callback that supplies a <see cref="CascadingValueSource{TValue}"/> within each service provider scope.</param>
+ /// <returns>The <see cref="IServiceCollection"/>.</returns>
+ public static IServiceCollection AddCascadingValue<TValue>(
+ this IServiceCollection serviceCollection, Func<IServiceProvider, TValue> valueFactory) {}
+
+ public static IServiceCollection AddCascadingValue<TValue>(
+ this IServiceCollection serviceCollection, string name, Func<IServiceProvider, TValue> valueFactory) {}
+
+ public static IServiceCollection AddCascadingValue<TValue>(
+ this IServiceCollection serviceCollection, Func<IServiceProvider, CascadingValueSource<TValue>> sourceFactory) {}
+ }
}
namespace Microsoft.AspNetCore.Components.Infrastructure
{
public class ComponentStatePersistenceManager
{
+ Task PersistStateAsync(IPersistentComponentStateStore store, Dispatcher dispatcher) {}
}
}
namespace Microsoft.AspNetCore.Components.Rendering
{
public class Renderer
{
+ /// <summary>
+ /// Gets the <see cref="ComponentState"/> associated with the specified component.
+ /// </summary>
+ /// <param name="componentId">The component ID</param>
+ /// <returns>The corresponding <see cref="ComponentState"/>.</returns>
+ protected ComponentState GetComponentState(int componentId) {}
+ /// <summary>
+ /// Notifies the renderer that there is a pending task associated with a component. The
+ /// renderer is regarded as quiescent when all such tasks have completed.
+ /// </summary>
+ /// <param name="componentState">The <see cref="ComponentState"/> for the component associated with this pending task, if any.</param>
+ /// <param name="task">The <see cref="Task"/>.</param>
+ protected virtual void AddPendingTask(ComponentState? componentState, Task task) {}
+ /// <summary>
+ /// Creates a <see cref="ComponentState"/> instance to track state associated with a newly-instantiated component.
+ /// This is called before the component is initialized and tracked within the <see cref="Renderer"/>. Subclasses
+ /// may override this method to use their own subclasses of <see cref="ComponentState"/>.
+ /// </summary>
+ /// <param name="componentId">The ID of the newly-created component.</param>
+ /// <param name="component">The component instance.</param>
+ /// <param name="parentComponentState">The <see cref="ComponentState"/> associated with the parent component, or null if this is a root component.</param>
+ /// <returns>A <see cref="ComponentState"/> for the new component.</returns>
+ protected virtual ComponentState CreateComponentState(int componentId, IComponent component, ComponentState? parentComponentState) {}
// Note: This is a new overload for the existing DispatchEventAsync, adding the waitForQuiescence flag. It's needed for SSR event dispatch.
+ public virtual Task DispatchEventAsync(ulong eventHandlerId, EventFieldInfo? fieldInfo, EventArgs eventArgs, bool waitForQuiescence)
+ /// <summary>
+ /// Determines how to handle an <see cref="IComponentRenderMode"/> when obtaining a component instance.
+ /// This is only called when a render mode is specified either at the call site or on the component type.
+ ///
+ /// 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.
+ /// </summary>
+ /// <param name="componentType">The type of component that was requested.</param>
+ /// <param name="parentComponentId">The parent component ID, or null if it is a root component.</param>
+ /// <param name="componentActivator">An <see cref="IComponentActivator"/> that should be used when instantiating component objects.</param>
+ /// <param name="renderMode">The <see cref="IComponentRenderMode"/> declared on <paramref name="componentType"/> or at the call site (for example, by the parent component).</param>
+ /// <returns>An <see cref="IComponent"/> instance.</returns>
+ protected internal virtual IComponent ResolveComponentForRenderMode(
+ [DynamicallyAccessedMembers(Component)] Type componentType,
+ int? parentComponentId,
+ IComponentActivator componentActivator,
+ IComponentRenderMode renderMode) {}
}
// Note: This type already existed before but was internal. The change is that we now made the type public and some of its properties.
// This is to support renderers that have to be able to inspect the component hierarchy more, e.g., for SSR.
+ public class ComponentState : IAsyncDisposable
+ {
+ /// <summary>
+ /// Constructs an instance of <see cref="ComponentState"/>.
+ /// </summary>
+ public ComponentState(Renderer renderer, int componentId, IComponent component, ComponentState? parentComponentState) {}
+
+ /// <summary>
+ /// Gets the component ID.
+ /// </summary>
+ public int ComponentId { get; }
+
+ /// <summary>
+ /// Gets the component instance.
+ /// </summary>
+ public IComponent Component { get; }
+
+ /// <summary>
+ /// Gets the <see cref="ComponentState"/> of the parent component, or null if this is a root component.
+ /// </summary>
+ public ComponentState? ParentComponentState { get; }
+
+ /// <summary>
+ /// Gets the <see cref="ComponentState"/> of the logical parent component, or null if this is a root component.
+ /// </summary>
+ public ComponentState? LogicalParentComponentState { get; }
+
+ /// <summary>
+ /// Disposes this instance and its associated component.
+ /// </summary>
+ public virtual ValueTask DisposeAsync() {}
+ }
public sealed class RenderTreeBuilder : IDisposable
{
+ /// <summary>
+ /// Adds a frame indicating the render mode on the enclosing component frame.
+ /// </summary>
+ /// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
+ /// <param name="renderMode">The <see cref="IComponentRenderMode"/>.</param>
+ public void AddComponentRenderMode(int sequence, IComponentRenderMode renderMode) {}
+ /// <summary>
+ /// Assigns a name to an event in the enclosing element.
+ /// </summary>
+ /// <param name="sequence">An integer that represents the position of the instruction in the source code.</param>
+ /// <param name="eventType">The event type, e.g., 'onsubmit'.</param>
+ /// <param name="assignedName">The application-assigned name.</param>
+ public void AddNamedEvent(int sequence, string eventType, string assignedName) {}
}
}
// NOTE: The following namespace is basically pubternal (and has been pubternal since .NET Core 3.0), so I'm not pasting in
// all the many copies of the following XML doc block we put on each of its types.
// The reason for the pubternalness is that we have multiple hosting models (Server, WebAssembly, MAUI) that need to
// work with the renderer's data formats but we don't wish to have a supported public API layer for custom hosting models.
/// <summary>
/// Types in the Microsoft.AspNetCore.Components.RenderTree namespace are not recommended for use outside
/// of the Blazor framework. These types will change in future release.
/// </summary>
namespace Microsoft.AspNetCore.Components.RenderTree
{
+ [Flags]
+ public enum ComponentFrameFlags : byte
+ {
+ HasCallerSpecifiedRenderMode = 1,
+ }
+ public readonly struct NamedEventChange(NamedEventChangeType changeType, int componentId, int frameIndex, string eventType, string assignedName)
+ {
+ public readonly NamedEventChangeType ChangeType { get; }
+ public readonly int ComponentId { get; }
+ public readonly int FrameIndex { get; }
+ public readonly string EventType { get; }
+ public readonly string AssignedName { get; }
+ }
+ public enum NamedEventChangeType : int
+ {
+ Added,
+ Removed,
+ }
public readonly struct RenderBatch
{
+ public ArrayRange<NamedEventChange>? NamedEventChanges { get; }
}
public struct RenderTreeFrame
{
+ public ComponentFrameFlags ComponentFrameFlags { get; }
+ public IComponentRenderMode ComponentRenderMode { get; }
+ public string NamedEventAssignedName { get; }
+ public string NamedEventType { get; }
}
public enum RenderTreeFrameType : short
{
+ ComponentRenderMode = 9,
+ NamedEvent = 10,
}
}