File: Rendering\EndpointComponentState.cs
Web Access
Project: src\aspnetcore\src\Components\Endpoints\src\Microsoft.AspNetCore.Components.Endpoints.csproj (Microsoft.AspNetCore.Components.Endpoints)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Collections.Concurrent;
using System.Globalization;
using System.Reflection;
using System.Reflection.Metadata;
using Microsoft.AspNetCore.Components.Endpoints;
using Microsoft.AspNetCore.Components.HotReload;
using Microsoft.AspNetCore.Components.Rendering;
using Microsoft.AspNetCore.Components.RenderTree;
 
[assembly: MetadataUpdateHandler(typeof(EndpointComponentState))]
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
internal sealed class EndpointComponentState : ComponentState
{
    private static readonly ConcurrentDictionary<Type, StreamRenderingAttribute?> _streamRenderingAttributeByComponentType = new();
 
    private static readonly string _cacheViewTypeName = typeof(CacheView).FullName!;
 
    static EndpointComponentState()
    {
        if (HotReloadManager.IsSupported)
        {
            HotReloadManager.Default.OnDeltaApplied += _streamRenderingAttributeByComponentType.Clear;
        }
    }
 
    private readonly EndpointHtmlRenderer _renderer;
 
    public EndpointComponentState(Renderer renderer, int componentId, IComponent component, ComponentState? parentComponentState)
        : base(renderer, componentId, component, parentComponentState)
    {
        _renderer = (EndpointHtmlRenderer)renderer;
 
        var streamRenderingAttribute = _streamRenderingAttributeByComponentType.GetOrAdd(component.GetType(),
            type => type.GetCustomAttribute<StreamRenderingAttribute>());
 
        if (streamRenderingAttribute is not null)
        {
            StreamRendering = streamRenderingAttribute.Enabled;
        }
        else
        {
            var parentEndpointComponentState = (EndpointComponentState?)LogicalParentComponentState;
            StreamRendering = parentEndpointComponentState?.StreamRendering ?? false;
        }
 
        if (component is CacheView cacheView && parentComponentState is not null)
        {
            // Output caching inside a streaming render context is not yet supported.
            cacheView.IsInStreamingContext = StreamRendering;
 
            var ancestorTypeName = parentComponentState.Component?.GetType().FullName ?? "";
            cacheView.TreePositionKeyFactory = () =>
            {
                var sequence = FindSequenceInParent(parentComponentState, cacheView);
                var componentKey = GetComponentKey();
                var keyString = ComponentKeyHelper.FormatSerializableKey(componentKey);
                return ComputeTreePositionKey(ancestorTypeName, sequence, keyString);
            };
        }
    }
 
    public bool StreamRendering { get; }
 
    protected override object? GetComponentKey()
    {
        if (ParentComponentState != null && ParentComponentState.Component is SSRRenderModeBoundary boundary)
        {
            var (sequence, key) = _renderer.GetSequenceAndKey(ParentComponentState);
            var marker = boundary.GetComponentMarkerKey(sequence, key);
            if (!marker.Equals(default))
            {
                return marker.Serialized();
            }
        }
 
        // Fall back to the default implementation
        return base.GetComponentKey();
    }
 
    /// <summary>
    /// MetadataUpdateHandler event. This is invoked by the hot reload host via reflection.
    /// </summary>
    public static void UpdateApplication(Type[]? _) => _streamRenderingAttributeByComponentType.Clear();
 
    private static string ComputeTreePositionKey(string ancestorTypeName, int sequence, string? keyString)
    {
        return string.Concat(
            ancestorTypeName, ".",
            _cacheViewTypeName, "#",
            sequence.ToString(CultureInfo.InvariantCulture),
            keyString is not null ? "." : "",
            keyString);
    }
 
    // We need this calculation because otherwise multiple CacheView components under the same parent would have
    // the same key and would point to the same cache entry, which is incorrect.
    private int FindSequenceInParent(ComponentState parentState, CacheView target)
    {
        var frames = _renderer.GetRenderTreeFrames(parentState.ComponentId);
        for (var i = 0; i < frames.Count; i++)
        {
            ref var frame = ref frames.Array[i];
            if (frame.FrameType == RenderTreeFrameType.Component && ReferenceEquals(frame.Component, target))
            {
                return frame.Sequence;
            }
        }
 
        throw new InvalidOperationException(
            $"Could not locate the CacheView in the render tree of its parent component '{parentState.Component?.GetType().FullName}' while computing its cache key.");
    }
}