File: Rendering\CacheViewTextWriter.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.Text;
using Microsoft.AspNetCore.Components.RenderTree;
using Microsoft.Extensions.Logging;
 
namespace Microsoft.AspNetCore.Components.Endpoints;
 
internal sealed class CacheViewTextWriter : TextWriter
{
    // Approximate per-live-cached-component footprint used only for cache size accounting; live cached components
    // are component nodes rather than strings, so they have no captured length to measure directly.
    private const int LiveCachedComponentSizeEstimate = 256;
 
    private readonly TextWriter _innerWriter;
    private readonly StringBuilder _buffer = new();
    private readonly List<CacheCaptureEntry> _entries = [];
    private bool _capturing;
    private bool _validateOnly;
 
    public CacheViewTextWriter(TextWriter inner, CacheVaryBy varyBy)
    {
        _innerWriter = inner;
        VaryBy = varyBy;
    }
 
    public CacheVaryBy VaryBy { get; set; }
 
    public bool IsCapturing => _capturing;
 
    public bool IsValidationOnly => _validateOnly;
 
    public override Encoding Encoding => _innerWriter.Encoding;
 
    public override void Write(char value)
    {
        _innerWriter.Write(value);
        if (_capturing && !_validateOnly)
        {
            _buffer.Append(value);
        }
    }
 
    public override void Write(string? value)
    {
        _innerWriter.Write(value);
        if (_capturing && !_validateOnly)
        {
            _buffer.Append(value);
        }
    }
 
    public override void Write(char[] buffer, int index, int count)
    {
        _innerWriter.Write(buffer, index, count);
        if (_capturing && !_validateOnly)
        {
            _buffer.Append(buffer, index, count);
        }
    }
 
    public override void Write(ReadOnlySpan<char> buffer)
    {
        _innerWriter.Write(buffer);
        if (_capturing && !_validateOnly)
        {
            _buffer.Append(buffer);
        }
    }
 
    public void PauseCapture()
    {
        FlushBuffer();
        _capturing = false;
    }
 
    public void StartCapture()
    {
        _capturing = true;
    }
 
    public void StartValidation()
    {
        _capturing = true;
        _validateOnly = true;
    }
 
    public void CreateLiveCachedComponent(Type componentType, IComponentRenderMode? renderMode, RenderFragmentCapture capture, ILogger renderFragmentSerializationLogger)
    {
        ThrowIfLiveCachedComponentHasRenderFragmentParameter(componentType, capture);
 
        RenderTreeNode? liveCachedComponentNode = null;
        foreach (var node in RenderFragmentSerializer.SerializeFrames(capture, renderFragmentSerializationLogger))
        {
            if (node.Type is "component")
            {
                liveCachedComponentNode = node;
                break;
            }
        }
 
        if (liveCachedComponentNode is null)
        {
            throw new InvalidOperationException(
                $"CacheView could not serialize the live cached component '{componentType.FullName}' from its parent's render tree.");
        }
 
        // The serializer fills RenderModeName from an inline @rendermode frame. For components that
        // declare their render mode via [RenderModeAttribute] instead, the capture has no render-mode
        // frame, so patch it from the boundary's runtime render mode.
        var renderModeName = RenderFragmentSerializer.GetRenderModeName(renderMode);
        if (renderModeName is not null && liveCachedComponentNode.RenderModeName is null)
        {
            liveCachedComponentNode.RenderModeName = renderModeName;
            liveCachedComponentNode.Prerender = RenderFragmentSerializer.GetRenderModePrerender(renderMode);
        }
 
        _entries.Add(CacheCaptureEntry.LiveCachedComponent(liveCachedComponentNode));
    }
 
    public void StopCapture()
    {
        _capturing = false;
        FlushBuffer();
    }
 
    // Assembles the cache payload by walking the recorded entries in render order: markup segments become
    // markup nodes and live cached components contribute the component node serialized at CreateLiveCachedComponent time.
    public SerializedRenderFragment GetSerializedRenderFragment()
    {
        var nodes = new List<RenderTreeNode>(_entries.Count);
        long contentSize = 0;
 
        foreach (var entry in _entries)
        {
            if (entry.LiveCachedComponentNode is { } liveCachedComponentNode)
            {
                // Live components are component nodes, not strings; charge a flat overhead so
                // live-cached-component-heavy boundaries still count toward the cache size limit.
                contentSize += LiveCachedComponentSizeEstimate;
                nodes.Add(liveCachedComponentNode);
            }
            else
            {
                contentSize += entry.Markup?.Length ?? 0;
                nodes.Add(new RenderTreeNode { Type = "markup", Content = entry.Markup });
            }
        }
 
        return new SerializedRenderFragment { Nodes = nodes, ContentSize = contentSize };
    }
 
    // A live cached component is serialized from its parent's frames, which carry no nested RenderFragment captures, so a
    // RenderFragment parameter could not be replayed correctly. Surface an actionable error before the
    // generic serializer error fires.
    private static void ThrowIfLiveCachedComponentHasRenderFragmentParameter(Type liveCachedComponentType, RenderFragmentCapture capture)
    {
        foreach (ref readonly var frame in capture.GetCapturedFrames().AsSpan())
        {
            if (frame.FrameType is RenderTreeFrameType.Attribute && IsRenderFragmentParameter(frame.AttributeValue))
            {
                throw new InvalidOperationException(
                    $"The live cached component '{liveCachedComponentType.FullName}' cannot be used inside a CacheView because its RenderFragment parameter '{frame.AttributeName}' would be frozen to the first render's content (a live cached component's parameters are captured once and replayed). " +
                    "To fix this, remove the RenderFragment parameter or move the component outside the CacheView.");
            }
        }
    }
 
    private static bool IsRenderFragmentParameter(object? value)
        => value is RenderFragment || (value is Delegate d && d.GetType().IsGenericType && d.GetType().GetGenericTypeDefinition() == typeof(RenderFragment<>));
 
    private void FlushBuffer()
    {
        if (_buffer.Length > 0)
        {
            _entries.Add(CacheCaptureEntry.MarkupEntry(_buffer.ToString()));
            _buffer.Clear();
        }
    }
}
 
internal readonly struct CacheCaptureEntry
{
    public string? Markup { get; }
    public RenderTreeNode? LiveCachedComponentNode { get; }
 
    private CacheCaptureEntry(string? markup, RenderTreeNode? liveCachedComponentNode)
    {
        Markup = markup;
        LiveCachedComponentNode = liveCachedComponentNode;
    }
 
    public static CacheCaptureEntry MarkupEntry(string markup)
        => new(markup, liveCachedComponentNode: null);
 
    public static CacheCaptureEntry LiveCachedComponent(RenderTreeNode liveCachedComponentNode)
        => new(markup: null, liveCachedComponentNode);
}