File: Buffers\ViewBuffer.cs
Web Access
Project: src\src\Mvc\Mvc.ViewFeatures\src\Microsoft.AspNetCore.Mvc.ViewFeatures.csproj (Microsoft.AspNetCore.Mvc.ViewFeatures)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Text.Encodings.Web;
using Microsoft.AspNetCore.Html;
 
namespace Microsoft.AspNetCore.Mvc.ViewFeatures.Buffers;
 
/// <summary>
/// An <see cref="IHtmlContentBuilder"/> that is backed by a buffer provided by <see cref="IViewBufferScope"/>.
/// </summary>
[DebuggerDisplay("{DebuggerToString()}")]
internal sealed class ViewBuffer : IHtmlContentBuilder
{
    public const int PartialViewPageSize = 32;
    public const int TagHelperPageSize = 32;
    public const int ViewComponentPageSize = 32;
    public const int ViewPageSize = 256;
 
    private readonly IViewBufferScope _bufferScope;
    private readonly string _name;
    private readonly int _pageSize;
    private ViewBufferPage _currentPage;         // Limits allocation if the ViewBuffer has only one page (frequent case).
    private List<ViewBufferPage> _multiplePages; // Allocated only if necessary
 
    /// <summary>
    /// Initializes a new instance of <see cref="ViewBuffer"/>.
    /// </summary>
    /// <param name="bufferScope">The <see cref="IViewBufferScope"/>.</param>
    /// <param name="name">A name to identify this instance.</param>
    /// <param name="pageSize">The size of buffer pages.</param>
    public ViewBuffer(IViewBufferScope bufferScope, string name, int pageSize)
    {
        ArgumentNullException.ThrowIfNull(bufferScope);
        ArgumentOutOfRangeException.ThrowIfNegativeOrZero(pageSize);
 
        _bufferScope = bufferScope;
        _name = name;
        _pageSize = pageSize;
    }
 
    /// <summary>
    /// Get the <see cref="ViewBufferPage"/> count.
    /// </summary>
    public int Count
    {
        get
        {
            if (_multiplePages != null)
            {
                return _multiplePages.Count;
            }
            if (_currentPage != null)
            {
                return 1;
            }
            return 0;
        }
    }
 
    /// <summary>
    /// Gets a <see cref="ViewBufferPage"/>.
    /// </summary>
    public ViewBufferPage this[int index]
    {
        get
        {
            if (_multiplePages != null)
            {
                return _multiplePages[index];
            }
            if (index == 0 && _currentPage != null)
            {
                return _currentPage;
            }
            throw new ArgumentOutOfRangeException(nameof(index));
        }
    }
 
    /// <inheritdoc />
    // Very common trivial method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public IHtmlContentBuilder Append(string unencoded)
    {
        if (unencoded != null)
        {
            // Text that needs encoding is the uncommon case in views, which is why it
            // creates a wrapper and pre-encoded text does not.
            AppendValue(new ViewBufferValue(new EncodingWrapper(unencoded)));
        }
 
        return this;
    }
 
    /// <inheritdoc />
    // Very common trivial method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public IHtmlContentBuilder AppendHtml(IHtmlContent content)
    {
        if (content != null)
        {
            AppendValue(new ViewBufferValue(content));
        }
 
        return this;
    }
 
    /// <inheritdoc />
    // Very common trivial method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public IHtmlContentBuilder AppendHtml(string encoded)
    {
        if (encoded != null)
        {
            AppendValue(new ViewBufferValue(encoded));
        }
 
        return this;
    }
 
    // Very common trivial method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private void AppendValue(ViewBufferValue value)
    {
        var page = GetCurrentPage();
        page.Append(value);
    }
 
    // Very common trivial method; nudge it to inline https://github.com/aspnet/Mvc/pull/8339
    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private ViewBufferPage GetCurrentPage()
    {
        var currentPage = _currentPage;
        if (currentPage == null || currentPage.IsFull)
        {
            // Uncommon slow-path
            return AppendNewPage();
        }
 
        return currentPage;
    }
 
    // Slow path for above, don't inline
    [MethodImpl(MethodImplOptions.NoInlining)]
    private ViewBufferPage AppendNewPage()
    {
        AddPage(new ViewBufferPage(_bufferScope.GetPage(_pageSize)));
        return _currentPage;
    }
 
    private void AddPage(ViewBufferPage page)
    {
        if (_multiplePages != null)
        {
            _multiplePages.Add(page);
        }
        else if (_currentPage != null)
        {
            _multiplePages = new List<ViewBufferPage>(2);
            _multiplePages.Add(_currentPage);
            _multiplePages.Add(page);
        }
 
        _currentPage = page;
    }
 
    /// <inheritdoc />
    public IHtmlContentBuilder Clear()
    {
        _multiplePages = null;
        _currentPage = null;
        return this;
    }
 
    /// <inheritdoc />
    public void WriteTo(TextWriter writer, HtmlEncoder encoder)
    {
        ArgumentNullException.ThrowIfNull(writer);
        ArgumentNullException.ThrowIfNull(encoder);
 
        for (var i = 0; i < Count; i++)
        {
            var page = this[i];
            for (var j = 0; j < page.Count; j++)
            {
                var value = page.Buffer[j];
 
                if (value.Value is string valueAsString)
                {
                    writer.Write(valueAsString);
                    continue;
                }
 
                if (value.Value is IHtmlContent valueAsHtmlContent)
                {
                    valueAsHtmlContent.WriteTo(writer, encoder);
                    continue;
                }
            }
        }
    }
 
    /// <summary>
    /// Writes the buffered content to <paramref name="writer"/>.
    /// </summary>
    /// <param name="writer">The <see cref="TextWriter"/>.</param>
    /// <param name="encoder">The <see cref="HtmlEncoder"/>.</param>
    /// <returns>A <see cref="Task"/> which will complete once content has been written.</returns>
    public async Task WriteToAsync(TextWriter writer, HtmlEncoder encoder)
    {
        ArgumentNullException.ThrowIfNull(writer);
        ArgumentNullException.ThrowIfNull(encoder);
 
        for (var i = 0; i < Count; i++)
        {
            var page = this[i];
            for (var j = 0; j < page.Count; j++)
            {
                var value = page.Buffer[j];
 
                if (value.Value is string valueAsString)
                {
                    await writer.WriteAsync(valueAsString);
                    continue;
                }
 
                if (value.Value is ViewBuffer valueAsViewBuffer)
                {
                    await valueAsViewBuffer.WriteToAsync(writer, encoder);
                    continue;
                }
 
                if (value.Value is IHtmlAsyncContent valueAsHtmlAsyncContent)
                {
                    await valueAsHtmlAsyncContent.WriteToAsync(writer);
                    await writer.FlushAsync();
                    continue;
                }
 
                if (value.Value is IHtmlContent valueAsHtmlContent)
                {
                    valueAsHtmlContent.WriteTo(writer, encoder);
                    await writer.FlushAsync();
                    continue;
                }
            }
        }
    }
 
    private string DebuggerToString() => _name;
 
    public void CopyTo(IHtmlContentBuilder destination)
    {
        ArgumentNullException.ThrowIfNull(destination);
 
        for (var i = 0; i < Count; i++)
        {
            var page = this[i];
            for (var j = 0; j < page.Count; j++)
            {
                var value = page.Buffer[j];
 
                string valueAsString;
                IHtmlContentContainer valueAsContainer;
                if ((valueAsString = value.Value as string) != null)
                {
                    destination.AppendHtml(valueAsString);
                }
                else if ((valueAsContainer = value.Value as IHtmlContentContainer) != null)
                {
                    valueAsContainer.CopyTo(destination);
                }
                else
                {
                    destination.AppendHtml((IHtmlContent)value.Value);
                }
            }
        }
    }
 
    public void MoveTo(IHtmlContentBuilder destination)
    {
        ArgumentNullException.ThrowIfNull(destination);
 
        // Perf: We have an efficient implementation when the destination is another view buffer,
        // we can just insert our pages as-is.
        if (destination is ViewBuffer other)
        {
            MoveTo(other);
            return;
        }
 
        for (var i = 0; i < Count; i++)
        {
            var page = this[i];
            for (var j = 0; j < page.Count; j++)
            {
                var value = page.Buffer[j];
 
                string valueAsString;
                IHtmlContentContainer valueAsContainer;
                if ((valueAsString = value.Value as string) != null)
                {
                    destination.AppendHtml(valueAsString);
                }
                else if ((valueAsContainer = value.Value as IHtmlContentContainer) != null)
                {
                    valueAsContainer.MoveTo(destination);
                }
                else
                {
                    destination.AppendHtml((IHtmlContent)value.Value);
                }
            }
        }
 
        for (var i = 0; i < Count; i++)
        {
            var page = this[i];
            Array.Clear(page.Buffer, 0, page.Count);
            _bufferScope.ReturnSegment(page.Buffer);
        }
 
        Clear();
    }
 
    private void MoveTo(ViewBuffer destination)
    {
        for (var i = 0; i < Count; i++)
        {
            var page = this[i];
 
            var destinationPage = destination.Count == 0 ? null : destination[destination.Count - 1];
 
            // If the source page is less or equal to than half full, let's copy it's content to the destination
            // page if possible.
            var isLessThanHalfFull = 2 * page.Count <= page.Capacity;
            if (isLessThanHalfFull &&
                destinationPage != null &&
                destinationPage.Capacity - destinationPage.Count >= page.Count)
            {
                // We have room, let's copy the items.
                Array.Copy(
                    sourceArray: page.Buffer,
                    sourceIndex: 0,
                    destinationArray: destinationPage.Buffer,
                    destinationIndex: destinationPage.Count,
                    length: page.Count);
 
                destinationPage.Count += page.Count;
 
                // Now we can return the source page, and it can be reused in the scope of this request.
                Array.Clear(page.Buffer, 0, page.Count);
                _bufferScope.ReturnSegment(page.Buffer);
            }
            else
            {
                // Otherwise, let's just add the source page to the other buffer.
                destination.AddPage(page);
            }
        }
 
        Clear();
    }
 
    private sealed class EncodingWrapper : IHtmlContent
    {
        private readonly string _unencoded;
 
        public EncodingWrapper(string unencoded)
        {
            _unencoded = unencoded;
        }
 
        public void WriteTo(TextWriter writer, HtmlEncoder encoder)
        {
            encoder.Encode(writer, _unencoded);
        }
    }
}