Skip to content

Commit

Permalink
Refactor ExecutionScope
Browse files Browse the repository at this point in the history
  • Loading branch information
ltrzesniewski committed Dec 5, 2024
1 parent 1516301 commit e37f872
Showing 1 changed file with 147 additions and 123 deletions.
270 changes: 147 additions & 123 deletions src/RazorBlade.Library/RazorTemplate.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Threading;
Expand Down Expand Up @@ -151,9 +150,9 @@ private protected virtual async Task<IRazorExecutionResult> ExecuteAsyncCore(Tex
{
cancellationToken.ThrowIfCancellationRequested();

using var executionScope = ExecutionScope.StartBody(this, targetOutput, cancellationToken);
using var bodyScope = ExecutionScope.StartBody(this, targetOutput, cancellationToken);
await ExecuteAsync().ConfigureAwait(false);
return new ExecutionResult(executionScope);
return new ExecutionResult(bodyScope);
}

/// <summary>
Expand Down Expand Up @@ -246,8 +245,8 @@ protected internal virtual void Write(IEncodedContent? content)
[EditorBrowsable(EditorBrowsableState.Never)]
protected internal void DefineSection(string name, Func<Task> action)
{
if (_executionScope is not { } executionScope)
throw new InvalidOperationException("Sections can only be defined while the template is executing.");
if (_executionScope is not ExecutionScope.BodyScope executionScope)
throw new InvalidOperationException("Sections can only be defined in a template body white it is executing.");

executionScope.DefineSection(name, action);
}
Expand Down Expand Up @@ -300,195 +299,220 @@ void IEncodedContent.WriteTo(TextWriter textWriter)
/// <summary>
/// Stores the state of a template execution.
/// </summary>
private class ExecutionScope : IDisposable
private abstract class ExecutionScope : IDisposable
{
private readonly RazorTemplate _page;
private readonly ScopeKind _kind;
private readonly TextWriter? _targetOutput;
private readonly StringWriter? _bufferedOutput;
private readonly ExecutionScope? _previousExecutionScope;

private IRazorLayout? _layout;
private bool _layoutFrozen;
private Dictionary<string, Func<Task>>? _sections;

public TextWriter Output { get; }
#if NET5_0_OR_GREATER
public TextWriter Output { get; private init; } = TextWriter.Null;
#else
public TextWriter Output { get; private set; } = TextWriter.Null;
#endif

public IRazorLayout? Layout => _layout;
public IRazorLayout? Layout { get; private set; }
public CancellationToken CancellationToken { get; }

public static ExecutionScope StartBody(RazorTemplate page, TextWriter? targetOutput, CancellationToken cancellationToken)
=> Start(new ExecutionScope(page, ScopeKind.Body, targetOutput, cancellationToken));
public static BodyScope StartBody(RazorTemplate page, TextWriter? targetOutput, CancellationToken cancellationToken)
=> Start(new BodyScope(page, targetOutput, cancellationToken));

private static ExecutionScope StartSection(ExecutionScope parent)
=> Start(new ExecutionScope(parent._page, ScopeKind.Section, null, parent.CancellationToken)
{
_layout = parent._layout, // The section might reference the layout instance.
_layoutFrozen = true
});
private static SectionScope StartSection(ExecutionScope parent)
=> Start(new SectionScope(parent));

private static void StartWriter(ExecutionScope parent, TextWriter writer)
=> Start(new ExecutionScope(parent._page, ScopeKind.Writer, writer, parent.CancellationToken));
=> Start(new WriterScope(parent, writer));

private static ExecutionScope Start(ExecutionScope executionScope)
private static TScope Start<TScope>(TScope executionScope)
where TScope : ExecutionScope
{
executionScope._page._executionScope = executionScope;
return executionScope;
}

private ExecutionScope(RazorTemplate page, ScopeKind kind, TextWriter? writer, CancellationToken cancellationToken)
private ExecutionScope(RazorTemplate page, CancellationToken cancellationToken)
{
_page = page;
_kind = kind;
CancellationToken = cancellationToken;

switch (kind)
{
case ScopeKind.Body:
_targetOutput = writer;
_bufferedOutput = new StringWriter();
Output = _bufferedOutput;
break;

case ScopeKind.Section:
Debug.Assert(writer is null);
_targetOutput = null;
_bufferedOutput = new StringWriter();
Output = _bufferedOutput;
break;

case ScopeKind.Writer:
_targetOutput = writer;
_bufferedOutput = null;
Output = _targetOutput ?? throw new ArgumentNullException(nameof(writer));
break;

default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, null);
}

_previousExecutionScope = page._executionScope;
}

private ExecutionScope(ExecutionScope parent)
: this(parent._page, parent.CancellationToken)
{
}

public void Dispose()
{
if (ReferenceEquals(_page._executionScope, this))
_page._executionScope = _previousExecutionScope;
}

public void SetLayout(IRazorLayout? layout)
{
if (ReferenceEquals(layout, _layout))
return;
public abstract void SetLayout(IRazorLayout? layout);

if (_layoutFrozen)
throw new InvalidOperationException("The layout can no longer be changed.");
public abstract Task FlushAsync();

_layout = layout;
}
public void PushWriter(TextWriter writer)
=> StartWriter(this, writer);

public virtual TextWriter PopWriter()
=> throw new InvalidOperationException("The writer stack is empty.");

public async Task FlushAsync()
#if NET8_0_OR_GREATER
private Task FlushWriterAsync(TextWriter? writer)
=> writer?.FlushAsync(CancellationToken) ?? Task.CompletedTask;
#else
private static Task FlushWriterAsync(TextWriter? writer)
=> writer?.FlushAsync() ?? Task.CompletedTask;
#endif

public sealed class BodyScope : ExecutionScope
{
if (_kind == ScopeKind.Writer)
private readonly TextWriter? _targetOutput;
private readonly StringWriter _bufferedOutput = new();

private Dictionary<string, Func<Task>>? _sections;
private bool _layoutIsFrozen;

public BodyScope(RazorTemplate page, TextWriter? targetOutput, CancellationToken cancellationToken)
: base(page, cancellationToken)
{
await FlushTargetOutputWriter().ConfigureAwait(false);
return;
_targetOutput = targetOutput;
Output = _bufferedOutput;
}

if (_layout is not null)
throw new InvalidOperationException("The output cannot be flushed when a layout is used.");
public BufferedContent ToBufferedContent()
=> new(_bufferedOutput.GetStringBuilder());

// A part of the output will be written to the target output and discarded,
// so disallow setting a layout later on, as that would lead to inconsistent results.
_layoutFrozen = true;
public override void SetLayout(IRazorLayout? layout)
{
if (ReferenceEquals(layout, Layout))
return;

if (_targetOutput is null)
return;
if (_layoutIsFrozen)
throw new InvalidOperationException("The layout can no longer be changed.");

if (_bufferedOutput?.GetStringBuilder() is { } bufferedOutput)
{
await WriteStringBuilderToOutputAsync(bufferedOutput, _targetOutput, CancellationToken).ConfigureAwait(false);
bufferedOutput.Clear();
Layout = layout;
}

await FlushTargetOutputWriter().ConfigureAwait(false);
public bool IsSectionDefined(string name)
=> _sections is { } sections && sections.ContainsKey(name);

Task FlushTargetOutputWriter()
public void DefineSection(string name, Func<Task> action)
{
if (_targetOutput is null)
return Task.CompletedTask;
var sections = _sections ??= new(StringComparer.OrdinalIgnoreCase);

#if NET8_0_OR_GREATER
return _targetOutput.FlushAsync(CancellationToken);
#if NET6_0_OR_GREATER
if (!sections.TryAdd(name, action))
throw new InvalidOperationException($"Section '{name}' is already defined.");
#else
return _targetOutput.FlushAsync();
if (sections.ContainsKey(name))
throw new InvalidOperationException($"Section '{name}' is already defined.");

sections[name] = action;
#endif
}
}

public BufferedContent ToBufferedContent()
=> new(_bufferedOutput?.GetStringBuilder() ?? new());

public bool IsSectionDefined(string name)
=> _sections is { } sections && sections.ContainsKey(name);
public async Task<IEncodedContent?> RenderSectionAsync(string name)
{
if (_sections is not { } sections || !sections.TryGetValue(name, out var sectionAction))
return null;

public void DefineSection(string name, Func<Task> action)
{
var sections = _sections ??= new(StringComparer.OrdinalIgnoreCase);
using var sectionScope = StartSection(this);
await sectionAction().ConfigureAwait(false);
return sectionScope.ToBufferedContent();
}

#if NET6_0_OR_GREATER
if (!sections.TryAdd(name, action))
throw new InvalidOperationException($"Section '{name}' is already defined.");
#else
if (sections.ContainsKey(name))
throw new InvalidOperationException($"Section '{name}' is already defined.");
public override async Task FlushAsync()
{
if (Layout is not null)
throw new InvalidOperationException("The output cannot be flushed when a layout is used.");

sections[name] = action;
#endif
}
// A part of the output will be written to the target output and discarded,
// so disallow setting a layout later on, as that would lead to inconsistent results.
_layoutIsFrozen = true;

public void PushWriter(TextWriter writer)
=> StartWriter(this, writer);
if (_targetOutput is null)
return;

public TextWriter PopWriter()
{
if (_kind != ScopeKind.Writer)
throw new InvalidOperationException("The writer stack is empty.");
var bufferedOutput = _bufferedOutput.GetStringBuilder();
await WriteStringBuilderToOutputAsync(bufferedOutput, _targetOutput, CancellationToken).ConfigureAwait(false);
bufferedOutput.Clear();

Dispose();
return Output;
await FlushWriterAsync(_targetOutput).ConfigureAwait(false);
}
}

public async Task<IEncodedContent?> RenderSectionAsync(string name)
private sealed class SectionScope : ExecutionScope
{
if (_sections is not { } sections || !sections.TryGetValue(name, out var sectionAction))
return null;
private readonly StringWriter _bufferedOutput = new();

public SectionScope(ExecutionScope parent)
: base(parent)
{
Output = _bufferedOutput;
Layout = parent.Layout; // The section might reference the layout instance.
}

public BufferedContent ToBufferedContent()
=> new(_bufferedOutput.GetStringBuilder());

using var sectionScope = StartSection(this);
await sectionAction().ConfigureAwait(false);
return sectionScope.ToBufferedContent();
public override void SetLayout(IRazorLayout? layout)
{
if (ReferenceEquals(layout, Layout))
return;

throw new InvalidOperationException("The layout can not be changed from a section.");
}

public override Task FlushAsync()
{
if (Layout is not null)
throw new InvalidOperationException("The output cannot be flushed when a layout is used.");

return Task.CompletedTask;
}
}

private enum ScopeKind
private sealed class WriterScope : ExecutionScope
{
Body,
Section,
Writer
public WriterScope(ExecutionScope parent, TextWriter writer)
: base(parent)
{
Output = writer ?? throw new ArgumentNullException(nameof(writer));
}

public override void SetLayout(IRazorLayout? layout)
{
if (ReferenceEquals(layout, Layout))
return;

throw new InvalidOperationException("The layout can not be changed from a helper.");
}

public override Task FlushAsync()
=> FlushWriterAsync(Output);

public override TextWriter PopWriter()
{
Dispose();
return Output;
}
}
}

/// <summary>
/// Stores the result of a template execution.
/// </summary>
private class ExecutionResult : IRazorExecutionResult
private sealed class ExecutionResult : IRazorExecutionResult
{
private readonly ExecutionScope _executionScope;
private readonly ExecutionScope.BodyScope _executionScope;

public IEncodedContent Body { get; }
public IRazorLayout? Layout => _executionScope.Layout;
public CancellationToken CancellationToken => _executionScope.CancellationToken;

public ExecutionResult(ExecutionScope executionScope)
public ExecutionResult(ExecutionScope.BodyScope executionScope)
{
_executionScope = executionScope;
Body = executionScope.ToBufferedContent();
Expand All @@ -508,7 +532,7 @@ public bool IsSectionDefined(string name)
/// StringBuilders can be combined more efficiently than strings, which is useful for layouts.
/// <see cref="TextWriter"/> has a dedicated <c>Write</c> overload for <see cref="StringBuilder"/> in some frameworks.
/// </remarks>
private class BufferedContent : IEncodedContent
private sealed class BufferedContent : IEncodedContent
{
public StringBuilder Output { get; }

Expand All @@ -526,7 +550,7 @@ public override string ToString()
/// Represents a deferred write operation.
/// </summary>
[PublicAPI]
protected internal class HelperResult : IEncodedContent
protected internal sealed class HelperResult : IEncodedContent
{
private readonly Func<TextWriter, Task> _action;

Expand Down

0 comments on commit e37f872

Please sign in to comment.