Skip to content

Commit

Permalink
Add FlushAsync() method
Browse files Browse the repository at this point in the history
  • Loading branch information
ltrzesniewski committed Apr 3, 2024
1 parent fe6125b commit eb44883
Show file tree
Hide file tree
Showing 2 changed files with 128 additions and 37 deletions.
6 changes: 3 additions & 3 deletions src/RazorBlade.Library/HtmlLayout.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,20 @@ async Task<IRazorExecutionResult> IRazorLayout.ExecuteLayoutAsync(IRazorExecutio
try
{
_layoutInput = input;
return await ExecuteAsyncCore(input.CancellationToken);
return await ExecuteAsyncCore(null, input.CancellationToken);
}
finally
{
_layoutInput = null;
}
}

private protected override Task<IRazorExecutionResult> ExecuteAsyncCore(CancellationToken cancellationToken)
private protected override Task<IRazorExecutionResult> ExecuteAsyncCore(TextWriter? targetOutput, CancellationToken cancellationToken)
{
if (_layoutInput is null)
throw new InvalidOperationException(_contentsRequiredErrorMessage);

return base.ExecuteAsyncCore(cancellationToken);
return base.ExecuteAsyncCore(targetOutput, cancellationToken);
}

/// <summary>
Expand Down
159 changes: 125 additions & 34 deletions src/RazorBlade.Library/RazorTemplate.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ namespace RazorBlade;
/// </summary>
public abstract class RazorTemplate : IEncodedContent
{
private ExecutionScope? _executionScope;
private IRazorLayout? _layout;
private Dictionary<string, Func<Task>>? _sections;

private Dictionary<string, Func<Task>> Sections => _sections ??= new(StringComparer.OrdinalIgnoreCase);

/// <summary>
/// The <see cref="TextWriter"/> which receives the output.
/// </summary>
protected internal TextWriter Output { get; internal set; } = new StreamWriter(Stream.Null);
protected internal TextWriter Output { get; internal set; } = TextWriter.Null;

/// <summary>
/// The cancellation token.
Expand All @@ -32,7 +34,18 @@ public abstract class RazorTemplate : IEncodedContent
/// <summary>
/// The layout to use.
/// </summary>
private protected IRazorLayout? Layout { get; set; }
private protected IRazorLayout? Layout
{
get => _layout;
set
{
if (ReferenceEquals(value, _layout))
return;

_executionScope?.EnsureCanChangeLayout();
_layout = value;
}
}

/// <summary>
/// Renders the template synchronously and returns the result as a string.
Expand All @@ -46,11 +59,23 @@ public string Render(CancellationToken cancellationToken = default)
{
cancellationToken.ThrowIfCancellationRequested();

var renderTask = RenderAsyncCore(cancellationToken);
var textWriter = new StringWriter();
var renderTask = RenderAsyncCore(textWriter, cancellationToken);

if (renderTask.IsCompleted)
return renderTask.GetAwaiter().GetResult().ToString();
{
renderTask.GetAwaiter().GetResult();
return textWriter.ToString();
}

return Task.Run(async () => await renderTask.ConfigureAwait(false), CancellationToken.None).GetAwaiter().GetResult().ToString();
return Task.Run(
async () =>
{
await renderTask.ConfigureAwait(false);
return textWriter.ToString();
},
CancellationToken.None
).GetAwaiter().GetResult();
}

/// <summary>
Expand All @@ -66,7 +91,8 @@ public void Render(TextWriter textWriter, CancellationToken cancellationToken =
{
cancellationToken.ThrowIfCancellationRequested();

var renderTask = RenderAsync(textWriter, cancellationToken);
var renderTask = RenderAsyncCore(textWriter, cancellationToken);

if (renderTask.IsCompleted)
{
renderTask.GetAwaiter().GetResult();
Expand All @@ -87,8 +113,9 @@ public async Task<string> RenderAsync(CancellationToken cancellationToken = defa
{
cancellationToken.ThrowIfCancellationRequested();

var stringBuilder = await RenderAsyncCore(cancellationToken).ConfigureAwait(false);
return stringBuilder.ToString();
var textWriter = new StringWriter();
await RenderAsyncCore(textWriter, cancellationToken).ConfigureAwait(false);
return textWriter.ToString();
}

/// <summary>
Expand All @@ -103,51 +130,57 @@ public async Task RenderAsync(TextWriter textWriter, CancellationToken cancellat
{
cancellationToken.ThrowIfCancellationRequested();

var stringBuilder = await RenderAsyncCore(cancellationToken).ConfigureAwait(false);

#if NET6_0_OR_GREATER
await textWriter.WriteAsync(stringBuilder, cancellationToken).ConfigureAwait(false);
#else
await textWriter.WriteAsync(stringBuilder.ToString()).ConfigureAwait(false);
#endif
await RenderAsyncCore(textWriter, cancellationToken).ConfigureAwait(false);
}

/// <summary>
/// Renders the template asynchronously including its layout and returns the result as a <see cref="StringBuilder"/>.
/// </summary>
private async Task<StringBuilder> RenderAsyncCore(CancellationToken cancellationToken)
private async Task RenderAsyncCore(TextWriter targetOutput, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

var executionResult = await ExecuteAsyncCore(cancellationToken);
var executionResult = await ExecuteAsyncCore(targetOutput, cancellationToken);

while (executionResult.Layout is { } layout)
{
cancellationToken.ThrowIfCancellationRequested();
executionResult = await layout.ExecuteLayoutAsync(executionResult).ConfigureAwait(false);
}

if (executionResult.Body is EncodedContent { Output: var outputStringBuilder })
return outputStringBuilder;
switch (executionResult.Body)
{
case EncodedContent { Output: var bufferedOutput }:
await WriteStringBuilderToOutputAndFlushAsync(bufferedOutput, targetOutput, cancellationToken).ConfigureAwait(false);
break;

// Fallback case, shouldn't happen
var outputStringWriter = new StringWriter();
executionResult.Body.WriteTo(outputStringWriter);
return outputStringWriter.GetStringBuilder();
case { } body: // Fallback case, shouldn't happen
body.WriteTo(targetOutput);
break;
}
}

/// <summary>
/// Calls the <see cref="ExecuteAsync"/> method in a new <see cref="ExecutionScope"/>.
/// </summary>
private protected virtual async Task<IRazorExecutionResult> ExecuteAsyncCore(CancellationToken cancellationToken)
private protected virtual async Task<IRazorExecutionResult> ExecuteAsyncCore(TextWriter? targetOutput, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();

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

/// <summary>
/// Writes the buffered output to the target output then flushes the output stream.
/// </summary>
/// <remarks>
/// This feature is not compatible with layouts.
/// </remarks>
protected Task FlushAsync()
=> _executionScope?.FlushAsync() ?? Task.CompletedTask;

/// <summary>
/// Executes the template and appends the result to <see cref="Output"/>.
/// </summary>
Expand Down Expand Up @@ -230,6 +263,27 @@ protected internal void DefineSection(string name, Func<Task> action)
#endif
}

/// <summary>
/// Writes the contents of a <see cref="StringBuilder"/> to a <see cref="TextWriter"/> asynchronuously.
/// </summary>
private static async Task WriteStringBuilderToOutputAndFlushAsync(StringBuilder input, TextWriter output, CancellationToken cancellationToken)
{
if (input.Length == 0)
return;

#if NET6_0_OR_GREATER
await output.WriteAsync(input, cancellationToken).ConfigureAwait(false);
#else
await output.WriteAsync(input.ToString()).ConfigureAwait(false);
#endif

#if NET8_0_OR_GREATER
await output.FlushAsync(cancellationToken).ConfigureAwait(false);
#else
await output.FlushAsync().ConfigureAwait(false);
#endif
}

void IEncodedContent.WriteTo(TextWriter textWriter)
=> Render(textWriter, CancellationToken.None);

Expand All @@ -238,35 +292,69 @@ void IEncodedContent.WriteTo(TextWriter textWriter)
/// </summary>
private class ExecutionScope : IDisposable
{
private readonly ExecutionScope? _previousExecutionScope;
private readonly Dictionary<string, Func<Task>>? _previousSections;
private readonly TextWriter _previousOutput;
private readonly CancellationToken _previousCancellationToken;
private readonly IRazorLayout? _previousLayout;

private readonly TextWriter? _targetOutput;
private bool _layoutFrozen;

public RazorTemplate Page { get; }
public StringBuilder Output { get; } = new();
public StringBuilder BufferedOutput { get; } = new();

public ExecutionScope(RazorTemplate page, CancellationToken cancellationToken)
private ExecutionScope(RazorTemplate page, TextWriter? targetOutput, CancellationToken cancellationToken)
{
Page = page;
_targetOutput = targetOutput;

_previousExecutionScope = page._executionScope;
_previousSections = page._sections;
_previousOutput = page.Output;
_previousCancellationToken = page.CancellationToken;
_previousLayout = page.Layout;

page._executionScope = this;
page._layout = null;
page._sections = null;
page.Output = new StringWriter(Output);
page.Output = new StringWriter(BufferedOutput);
page.CancellationToken = cancellationToken;
page.Layout = null;
}

public static ExecutionScope Start(RazorTemplate page, TextWriter? targetOutput, CancellationToken cancellationToken)
=> new(page, targetOutput, cancellationToken);

public void Dispose()
{
Page._executionScope = _previousExecutionScope;
Page._layout = _previousLayout;
Page._sections = _previousSections;
Page.Output = _previousOutput;
Page.CancellationToken = _previousCancellationToken;
Page.Layout = _previousLayout;
}

public void FreezeLayout()
=> _layoutFrozen = true;

public void EnsureCanChangeLayout()
{
if (_layoutFrozen)
throw new InvalidOperationException("The layout can no longer be changed.");
}

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

FreezeLayout();

if (_targetOutput is not null)
{
await WriteStringBuilderToOutputAndFlushAsync(BufferedOutput, _targetOutput, Page.CancellationToken).ConfigureAwait(false);
BufferedOutput.Clear();
}
}
}

Expand All @@ -286,7 +374,7 @@ public ExecutionResult(ExecutionScope executionScope)
{
_page = executionScope.Page;
_sections = _page._sections;
Body = new EncodedContent(executionScope.Output);
Body = new EncodedContent(executionScope.BufferedOutput);
Layout = _page.Layout;
CancellationToken = _page.CancellationToken;
}
Expand All @@ -299,10 +387,13 @@ public bool IsSectionDefined(string name)
if (_sections is null || !_sections.TryGetValue(name, out var sectionAction))
return null;

using var executionScope = new ExecutionScope(_page, CancellationToken);
_page.Layout = Layout; // The section might reference this instance.
using var executionScope = ExecutionScope.Start(_page, null, CancellationToken);

_page._layout = Layout; // The section might reference this instance.
executionScope.FreezeLayout();

await sectionAction().ConfigureAwait(false);
return new EncodedContent(executionScope.Output);
return new EncodedContent(executionScope.BufferedOutput);
}
}

Expand Down

0 comments on commit eb44883

Please sign in to comment.