From a648e903a83a0adca0501dd036c64d5c5179f40d Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Wed, 11 Dec 2024 18:04:04 -0800 Subject: [PATCH 1/5] Initial support for templated razor delegates (buggy!) --- samples/RazorSlices.Samples.WebApp/Program.cs | 1 + .../Slices/Templated.cshtml | 58 ++++++++++++++ .../Slices/_FooterLinks.cshtml | 1 + .../Slices/_TemplatedPartial.cshtml | 8 ++ src/RazorSlices/IUsesLayout.cs | 4 +- src/RazorSlices/RazorSlice.Partials.cs | 2 +- src/RazorSlices/RazorSlice.Write.cs | 80 +++++++++++++++++-- src/RazorSlices/RazorSlice.cs | 39 +++++++++ 8 files changed, 185 insertions(+), 8 deletions(-) create mode 100644 samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml create mode 100644 samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml diff --git a/samples/RazorSlices.Samples.WebApp/Program.cs b/samples/RazorSlices.Samples.WebApp/Program.cs index 0d1b923..f3e80d6 100644 --- a/samples/RazorSlices.Samples.WebApp/Program.cs +++ b/samples/RazorSlices.Samples.WebApp/Program.cs @@ -41,6 +41,7 @@ }); app.MapGet("/encoding", () => Results.Extensions.RazorSlice()); app.MapGet("/unicode", () => Results.Extensions.RazorSlice()); +app.MapGet("/templated", () => Results.Extensions.RazorSlice()); app.MapGet("/library", () => Results.Extensions.RazorSlice()); app.MapGet("/render-to-string", async () => { diff --git a/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml b/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml new file mode 100644 index 0000000..94a2016 --- /dev/null +++ b/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml @@ -0,0 +1,58 @@ +@using Microsoft.AspNetCore.Html +@using Microsoft.AspNetCore.Mvc.Razor + +@inherits RazorSliceHttpResult +@implements IUsesLayout<_Layout, LayoutModel> + +@{ + Func tmpl = @

+ Hello from a templated Razor delegate! The following value was passed in: @item +

; + Func asyncTmpl = @

+ @{await Task.Delay(16);} + Hello from an async templated Razor delegate! The following value was passed in: @item +

; +} + +

@Title()

+ +
+

This is from the page. What follows is from rendering the templated delegate declared on this page:

+
+ @tmpl(DateTime.Now) +
+
+ +
+

This is from the page. What follows is from rendering the async templated delegate declared on this page:

+
+ @asyncTmpl(DateTime.Now) +
+
+ +@await RenderPartialAsync(_TemplatedPartial.Create(tmpl)) + +@await RenderPartialAsync(_TemplatedPartial.Create(asyncTmpl)) + +@functions { + public LayoutModel LayoutModel => new() { Title = Title() }; + + static string Title() => "Templated Razor Delegates"; + + private IHtmlContent Content() + { +
+ +
+ return HtmlString.Empty; + } + + private async Task AsyncContent() + { + await Task.Delay(16); +
+ +
+ return HtmlString.Empty; + } +} diff --git a/samples/RazorSlices.Samples.WebApp/Slices/_FooterLinks.cshtml b/samples/RazorSlices.Samples.WebApp/Slices/_FooterLinks.cshtml index b3691f2..407cfc9 100644 --- a/samples/RazorSlices.Samples.WebApp/Slices/_FooterLinks.cshtml +++ b/samples/RazorSlices.Samples.WebApp/Slices/_FooterLinks.cshtml @@ -1,6 +1,7 @@  Home | Lorem | +Templated | Encoding | Unicode | Render to string | diff --git a/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml b/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml new file mode 100644 index 0000000..49f1719 --- /dev/null +++ b/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml @@ -0,0 +1,8 @@ +@using Microsoft.AspNetCore.Mvc.Razor +@inherits RazorSlice> +
+

This is from a partial with a templated model. What follows is from the templated delegate passed in as the model:

+
+ @Model(DateTime.Now) +
+
diff --git a/src/RazorSlices/IUsesLayout.cs b/src/RazorSlices/IUsesLayout.cs index 80bb9bc..47635f0 100644 --- a/src/RazorSlices/IUsesLayout.cs +++ b/src/RazorSlices/IUsesLayout.cs @@ -24,7 +24,7 @@ public interface IUsesLayout /// Use this interface to specify the layout type for a Razor Slice, e.g: /// /// -/// @inherits IUseLayout<_Layout[]> +/// @implements IUseLayout<_Layout[]> /// /// /// @@ -47,7 +47,7 @@ public interface IUsesLayout : IUsesLayout /// Use this interface to specify the layout type for a Razor Slice, e.g: /// /// -/// @inherits IUseLayout<_Layout[], LayoutModel> +/// @implements IUseLayout<_Layout[], LayoutModel> /// /// /// diff --git a/src/RazorSlices/RazorSlice.Partials.cs b/src/RazorSlices/RazorSlice.Partials.cs index 15bcea9..31088ea 100644 --- a/src/RazorSlices/RazorSlice.Partials.cs +++ b/src/RazorSlices/RazorSlice.Partials.cs @@ -72,7 +72,7 @@ protected internal ValueTask RenderPartialAsync(TMod internal ValueTask RenderChildSliceAsync(RazorSlice child) { - Debug.WriteLine($"Rendering child slice of type '{child.GetType().Name}' from layout slice of type '{GetType().Name}'"); + Debug.WriteLine($"Rendering child slice of type '{child.GetType().Name}' from parent slice of type '{GetType().Name}'"); CopySliceState(this, child); diff --git a/src/RazorSlices/RazorSlice.Write.cs b/src/RazorSlices/RazorSlice.Write.cs index 15f44aa..636b577 100644 --- a/src/RazorSlices/RazorSlice.Write.cs +++ b/src/RazorSlices/RazorSlice.Write.cs @@ -1,8 +1,12 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Internal; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Razor.TagHelpers; using System.Buffers; +using System.Diagnostics; using System.Globalization; +using System.Runtime.CompilerServices; namespace RazorSlices; @@ -260,14 +264,80 @@ protected HtmlString WriteHtml(T htmlContent) { if (htmlContent is not null) { - if (_pipeWriter is not null) + if (htmlContent is HelperResult helperResult) { - _utf8BufferTextWriter ??= Utf8PipeTextWriter.Get(_pipeWriter); - htmlContent.WriteTo(_utf8BufferTextWriter, _htmlEncoder); + // HelperResult captures the generated async templated delegate and blocks synchronously when calling it! + // This is not ideal for performance, but it's the best we can do without changing the Razor compiler (writes are synchronous). + // However we can access the captured delegate, invoke it to get the Task and detect the case where it hasn't + // completed (i.e. has gone async) and in that case log a warning. + var textWriter = _pipeWriter is not null ? _utf8BufferTextWriter ??= Utf8PipeTextWriter.Get(_pipeWriter) : _textWriter!; + var actionResult = helperResult.WriteAction(textWriter); + if (!actionResult.IsCompleted) + { + if (Debugger.IsAttached && Debugger.IsLogging()) + { + Debugger.Log(0, "RazorSlices", """ + + ---------------------------- + !!! RazorSlices Warning !!! + ---------------------------- + The WriteAction of a HelperResult instance has gone async but will be synchonously waited on (sync-over-async). + This can cause performance and scale issues. + Consider using async templated methods instead of async templated Razor delegates, i.e.: + + Do this: + + ``` + @await TemplatedMethod(DateTime.Now); + + @functions { + private async Task TemplatedMethod(T data) + { + await SomeAsyncThing(); +

+ The following data was passed: @data +

+ } + } + ``` + + Instead of doing this: + + ``` + @{ + Func templatedRazorDelegate = @

+ @{await SomeAsyncThing()} + Hello! The following value was passed in: @item +

; + } + + @templatedRazorDelegate(DateTime.Now) + ``` + + ---------------------------- + + + """); + } + } + actionResult.GetAwaiter().GetResult(); + if (_utf8BufferTextWriter is not null) + { + _utf8BufferTextWriter.Flush(); + } } - if (_textWriter is not null) + else { - htmlContent.WriteTo(_textWriter, _htmlEncoder); + if (_pipeWriter is not null) + { + _utf8BufferTextWriter ??= Utf8PipeTextWriter.Get(_pipeWriter); + htmlContent.WriteTo(_utf8BufferTextWriter, _htmlEncoder); + _utf8BufferTextWriter.Flush(); + } + if (_textWriter is not null) + { + htmlContent.WriteTo(_textWriter, _htmlEncoder); + } } } diff --git a/src/RazorSlices/RazorSlice.cs b/src/RazorSlices/RazorSlice.cs index ad3c804..16f39bc 100644 --- a/src/RazorSlices/RazorSlice.cs +++ b/src/RazorSlices/RazorSlice.cs @@ -20,6 +20,9 @@ public abstract partial class RazorSlice : IDisposable private static readonly FlushResult _noFlushResult = new(false, false); + // Used to support nested writers emitted by the Razor compiler when using templated razor delegates + private Stack<(PipeWriter?, TextWriter?)>? _writerStack; + private IServiceProvider? _serviceProvider; private HtmlEncoder _htmlEncoder = HtmlEncoder.Default; private PipeWriter? _pipeWriter; @@ -79,6 +82,42 @@ internal Task ExecuteAsyncImpl() return ExecuteAsync(); } + /// + /// Puts a text writer on the stack. + /// + /// + /// This method should not be interacted with directly. It's used by the Razor Slices infrastructure. + /// + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected void PushWriter(TextWriter writer) + { + Debug.WriteLine($"Pushing current writers to stack. Now rendering to passed {writer.GetType().Name}."); + + _writerStack ??= new(); + _writerStack.Push((_pipeWriter, _textWriter)); + + _textWriter = writer; + _pipeWriter = null; + } + + /// + /// Retrieves a text writer from the stack. + /// + /// + /// This method should not be interacted with directly. It's used by the Razor Slices infrastructure. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + protected void PopWriter() + { + Debug.Assert(_writerStack is not null, "No writer to pop from the stack. PushWriter should have been called first."); + Debug.Assert(_textWriter is not null, $"{nameof(_textWriter)} should not be null!"); + Debug.Assert(_pipeWriter is null, $"{nameof(_pipeWriter)} should be null!"); + + (_pipeWriter, _textWriter) = _writerStack.Pop(); + Debug.WriteLine($"Popping previous writers off stack. Now rendering to {(_pipeWriter is not null ? "PipeWriter" : "TextWriter")}."); + } + /// /// Renders the template to the specified . /// From 413f0ac277fbab5031c3ee1c05ecd01fd7bae64e Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 12 Dec 2024 14:26:41 -0800 Subject: [PATCH 2/5] Fixed templated delegates - Throws for async templated delegates --- README.md | 23 ++- samples/RazorSlices.Samples.WebApp/Program.cs | 2 +- .../Slices/Templated.cshtml | 86 +++++++---- .../Slices/_TemplatedPartial.cshtml | 11 +- src/RazorSlices/RazorSlice.Write.cs | 142 ++++++++++-------- src/RazorSlices/RazorSlice.cs | 13 -- .../WebAppTests.cs | 1 + 7 files changed, 167 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index ea21941..d6f363a 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ The library is still new and features are being actively added. - [Looping](https://learn.microsoft.com/aspnet/core/mvc/views/razor#looping-for-foreach-while-and-do-while), e.g. `@for`, `@foreach`, `@while`, `@do` - [Code blocks](https://learn.microsoft.com/aspnet/core/mvc/views/razor#razor-code-blocks), e.g. `@{ var someThing = someOtherThing; }` - [Conditional attribute rendering](https://learn.microsoft.com/aspnet/core/mvc/views/razor#conditional-attribute-rendering) - - Functions, e.g. + - [Functions](https://learn.microsoft.com/aspnet/core/mvc/views/razor#functions): ```cshtml @functions { @@ -109,8 +109,8 @@ The library is still new and features are being actively added. private int DoAThing() => 123; } ``` - - - [Templated Razor delegates](https://learn.microsoft.com/aspnet/core/mvc/views/razor#templated-razor-delegates), e.g. + + - [Templated Razor methods](https://learn.microsoft.com/aspnet/core/mvc/views/razor#code-try-48), e.g. ```cshtml @inherits RazorSlice @@ -126,6 +126,23 @@ The library is still new and features are being actively added. } ``` + - [Templated Razor delegates](https://learn.microsoft.com/aspnet/core/mvc/views/razor#templated-razor-delegates), e.g. + + ```cshtml + @inherits RazorSlice + + @{ + var tmpl = @
+ This is a templated Razor delegate. The following value was passed in: @item +
; + } + + @tmpl(DateTime.Now) + ``` + + > [!IMPORTANT] + > Async templated Razor delegates are **NOT** supported and will throw an exception at runtime. + - DI-activated properties via `@inject` - Rendering slices from slices (aka partials) via `@(await RenderPartialAsync())` - Using slices as layouts for other slices, including layouts with strongly-typed models: diff --git a/samples/RazorSlices.Samples.WebApp/Program.cs b/samples/RazorSlices.Samples.WebApp/Program.cs index f3e80d6..983ec23 100644 --- a/samples/RazorSlices.Samples.WebApp/Program.cs +++ b/samples/RazorSlices.Samples.WebApp/Program.cs @@ -41,7 +41,7 @@ }); app.MapGet("/encoding", () => Results.Extensions.RazorSlice()); app.MapGet("/unicode", () => Results.Extensions.RazorSlice()); -app.MapGet("/templated", () => Results.Extensions.RazorSlice()); +app.MapGet("/templated", (bool async = false) => Results.Extensions.RazorSlice(async)); app.MapGet("/library", () => Results.Extensions.RazorSlice()); app.MapGet("/render-to-string", async () => { diff --git a/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml b/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml index 94a2016..0a3843e 100644 --- a/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml +++ b/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml @@ -1,58 +1,90 @@ @using Microsoft.AspNetCore.Html @using Microsoft.AspNetCore.Mvc.Razor -@inherits RazorSliceHttpResult +@inherits RazorSliceHttpResult @implements IUsesLayout<_Layout, LayoutModel> @{ - Func tmpl = @

- Hello from a templated Razor delegate! The following value was passed in: @item -

; - Func asyncTmpl = @

- @{await Task.Delay(16);} - Hello from an async templated Razor delegate! The following value was passed in: @item -

; + var async = Model; + Func tmpl = !async ? + @

+ Hello from a templated Razor delegate! The following value was passed in: @item +

+ : + @

+ @{await Task.Delay(16);} + Hello from an async templated Razor delegate! The following value was passed in: @item +

; }

@Title()

-
-

This is from the page. What follows is from rendering the templated delegate declared on this page:

-
- @tmpl(DateTime.Now) +
+
+

This is from the page. What follows is from rendering the templated delegate declared on this slice:

+
+
+ @tmpl(DateTime.Now) +
+ +
-
- -
-

This is from the page. What follows is from rendering the async templated delegate declared on this page:

-
- @asyncTmpl(DateTime.Now) +
+

This is from the page. What follows is from rendering a templated method declared in the @@functions block on this slice:

+
+
+ @if (!async) + { + @Content(DateTime.Now) + } + else + { + @await AsyncContent(DateTime.Now) + } +
+ +
+
+
+

This is from the page. What follows is from rendering a partial that has a templated delegated passed in as the model:

+
+
+ @await RenderPartialAsync(_TemplatedPartial.Create(tmpl)) +
+ +
-@await RenderPartialAsync(_TemplatedPartial.Create(tmpl)) - -@await RenderPartialAsync(_TemplatedPartial.Create(asyncTmpl)) - @functions { public LayoutModel LayoutModel => new() { Title = Title() }; - static string Title() => "Templated Razor Delegates"; + static string Title() => "Templated Razor Delegates/Methods"; - private IHtmlContent Content() + private IHtmlContent Content(T item) {
- + Hello from a templated method! The following value was passed in: @item
+ + // Returning HtmlContent.Empty makes it possible to call this using a Razor expression instead of a block return HtmlString.Empty; } - private async Task AsyncContent() + private async Task AsyncContent(T item) { await Task.Delay(16);
- + Hello from a templated async method! The following value was passed in: @item
+ + // Returning HtmlContent.Empty makes it possible to call this using a Razor expression instead of a block return HtmlString.Empty; } } diff --git a/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml b/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml index 49f1719..716716b 100644 --- a/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml +++ b/samples/RazorSlices.Samples.WebApp/Slices/_TemplatedPartial.cshtml @@ -2,7 +2,12 @@ @inherits RazorSlice>

This is from a partial with a templated model. What follows is from the templated delegate passed in as the model:

-
- @Model(DateTime.Now) -
+
+
+ @Model(DateTime.Now) +
+ +
diff --git a/src/RazorSlices/RazorSlice.Write.cs b/src/RazorSlices/RazorSlice.Write.cs index 636b577..defaae0 100644 --- a/src/RazorSlices/RazorSlice.Write.cs +++ b/src/RazorSlices/RazorSlice.Write.cs @@ -1,12 +1,9 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Internal; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using Microsoft.AspNetCore.Razor.TagHelpers; using System.Buffers; -using System.Diagnostics; using System.Globalization; -using System.Runtime.CompilerServices; namespace RazorSlices; @@ -262,82 +259,99 @@ protected HtmlString WriteUtf8SpanFormattable(T? formattable, ReadOnlySpan(T htmlContent) where T : IHtmlContent { - if (htmlContent is not null) +#pragma warning disable CA2000 // Dispose objects before losing scope: Utf8PipeTextWriter is returned to the pool in the finally block + TextWriter textWriter = _textWriter ?? Utf8PipeTextWriter.Get(_pipeWriter!); +#pragma warning restore CA2000 + var faulted = false; + + try { if (htmlContent is HelperResult helperResult) { + // A templated Razor delegate is being rendered: https://learn.microsoft.com/aspnet/core/mvc/views/razor#templated-razor-delegates // HelperResult captures the generated async templated delegate and blocks synchronously when calling it! - // This is not ideal for performance, but it's the best we can do without changing the Razor compiler (writes are synchronous). + // This is not ideal for performance and in our case breaks the optimization used by Utf8PipeTextWriter which + // is cached in a thread static, but it can't be helped without changing the Razor compiler (writes are synchronous). // However we can access the captured delegate, invoke it to get the Task and detect the case where it hasn't - // completed (i.e. has gone async) and in that case log a warning. - var textWriter = _pipeWriter is not null ? _utf8BufferTextWriter ??= Utf8PipeTextWriter.Get(_pipeWriter) : _textWriter!; + // completed (i.e. has gone async) and in that case throw an exception. var actionResult = helperResult.WriteAction(textWriter); + if (!actionResult.IsCompleted) { - if (Debugger.IsAttached && Debugger.IsLogging()) - { - Debugger.Log(0, "RazorSlices", """ - - ---------------------------- - !!! RazorSlices Warning !!! - ---------------------------- - The WriteAction of a HelperResult instance has gone async but will be synchonously waited on (sync-over-async). - This can cause performance and scale issues. - Consider using async templated methods instead of async templated Razor delegates, i.e.: - - Do this: - - ``` - @await TemplatedMethod(DateTime.Now); - - @functions { - private async Task TemplatedMethod(T data) - { - await SomeAsyncThing(); -

- The following data was passed: @data -

- } - } - ``` - - Instead of doing this: - - ``` - @{ - Func templatedRazorDelegate = @

- @{await SomeAsyncThing()} - Hello! The following value was passed in: @item -

; - } - - @templatedRazorDelegate(DateTime.Now) - ``` - - ---------------------------- - - - """); - } + // NOTE: There's still a chance here that the Task run asynchronously but is completed by the time we check it (albeit it's a small window) + // and in that case it's very likely the Utf8PipeTextWriter will fault as it can't handle cross-thread writes (pooled via thread static). + // I don't think this causes any issues as the exception will be thrown and the request will fail, but it's worth noting. + + throw new InvalidOperationException(""" + ---------------------------- + !!! Razor Slices Error !!! + ---------------------------- + The WriteAction of a HelperResult instance has gone async but will be synchronously waited on (sync-over-async). + This causes performance and scale issues and is not supported in Razor Slices. + This happens when a templated Razor delegate does async work (i.e. has `@await SomethingAsync()` in it). + Use async templated methods instead of async templated Razor delegates. They have the advantage of supporting + regular method features too like generics and multiple parameters! + + Do this: + + ``` +
+ @await TemplatedMethod(DateTime.Now); +
+ + @functions { + private async Task TemplatedMethod(T data, IHtmlContent? htmlPrefix = null) + { + @htmlPrefix +

+ @await SomeAsyncThing(); + The following data was passed: @data +

+ + // Returning HtmlContent.Empty makes it possible to call this using a Razor expression instead of a block + return HtmlContent.Empty; + } + } + ``` + + Instead of doing this: + + ``` + @{ + Func templatedRazorDelegate = @

+ @{ await SomeAsyncThing(); } + Hello! The following value was passed: @item +

; + } + +
+ @templatedRazorDelegate(DateTime.Now) +
+ ``` + """); } + actionResult.GetAwaiter().GetResult(); - if (_utf8BufferTextWriter is not null) - { - _utf8BufferTextWriter.Flush(); - } } else { - if (_pipeWriter is not null) - { - _utf8BufferTextWriter ??= Utf8PipeTextWriter.Get(_pipeWriter); - htmlContent.WriteTo(_utf8BufferTextWriter, _htmlEncoder); - _utf8BufferTextWriter.Flush(); - } - if (_textWriter is not null) + htmlContent?.WriteTo(textWriter, _htmlEncoder); + } + } + catch + { + faulted = true; + throw; + } + finally + { + if (textWriter is Utf8PipeTextWriter utf8PipeTextWriter) + { + if (!faulted) { - htmlContent.WriteTo(_textWriter, _htmlEncoder); + utf8PipeTextWriter.Flush(); } + Utf8PipeTextWriter.Return(utf8PipeTextWriter); } } diff --git a/src/RazorSlices/RazorSlice.cs b/src/RazorSlices/RazorSlice.cs index 16f39bc..3de4c0f 100644 --- a/src/RazorSlices/RazorSlice.cs +++ b/src/RazorSlices/RazorSlice.cs @@ -2,7 +2,6 @@ using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO.Pipelines; -using System.Runtime.CompilerServices; using System.Text.Encodings.Web; using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Http; @@ -27,7 +26,6 @@ public abstract partial class RazorSlice : IDisposable private HtmlEncoder _htmlEncoder = HtmlEncoder.Default; private PipeWriter? _pipeWriter; private TextWriter? _textWriter; - private Utf8PipeTextWriter? _utf8BufferTextWriter; private bool _disposed; /// @@ -342,15 +340,6 @@ private static async ValueTask AwaitTextWriterFlushAsyncTask(Task fl return HtmlString.Empty; } - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void ReturnPooledObjects() - { - if (_utf8BufferTextWriter is not null) - { - Utf8PipeTextWriter.Return(_utf8BufferTextWriter); - } - } - /// /// Disposes the instance. Overriding implementations should ensure they call base.Dispose() after performing their /// custom dispose logic, e.g.: @@ -382,8 +371,6 @@ private void DisposeInternal() Debug.WriteLine($"Disposing content slice of type '{contentSlice.GetType().Name}'"); contentSlice.Dispose(); } - - ReturnPooledObjects(); _disposed = true; diff --git a/tests/RazorSlices.Samples.WebApp.Tests/WebAppTests.cs b/tests/RazorSlices.Samples.WebApp.Tests/WebAppTests.cs index 03a0a80..90abac4 100644 --- a/tests/RazorSlices.Samples.WebApp.Tests/WebAppTests.cs +++ b/tests/RazorSlices.Samples.WebApp.Tests/WebAppTests.cs @@ -27,6 +27,7 @@ public async Task WafHosted_EndpointsRenderOK(string path, string shouldContain, ["/1", "Wash the dishes", MediaTypeNames.Text.Html], ["/encoding", "{'antiForgery'", MediaTypeNames.Text.Html], ["/unicode", "🐻", MediaTypeNames.Text.Html], + ["/templated", "This is from a partial with a templated model", MediaTypeNames.Text.Html], ["/library", "This slice was loaded from a referenced Razor Class Library!", MediaTypeNames.Text.Html], ["/render-to-string", "htmlString", MediaTypeNames.Application.Json], ["/render-to-stringbuilder", "htmlString", MediaTypeNames.Application.Json], From 984fb3082759a9e34b9567b1b1619f5f87032005 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 12 Dec 2024 16:40:03 -0800 Subject: [PATCH 3/5] Tweak detection of async templated Razor delegates --- .../Slices/Templated.cshtml | 2 +- src/RazorSlices/RazorSlice.Write.cs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml b/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml index 0a3843e..204dc43 100644 --- a/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml +++ b/samples/RazorSlices.Samples.WebApp/Slices/Templated.cshtml @@ -12,7 +12,7 @@

: @

- @{await Task.Delay(16);} + @{await Task.Yield();} Hello from an async templated Razor delegate! The following value was passed in: @item

; } diff --git a/src/RazorSlices/RazorSlice.Write.cs b/src/RazorSlices/RazorSlice.Write.cs index defaae0..43c75b6 100644 --- a/src/RazorSlices/RazorSlice.Write.cs +++ b/src/RazorSlices/RazorSlice.Write.cs @@ -251,6 +251,11 @@ protected HtmlString WriteUtf8SpanFormattable(T? formattable, ReadOnlySpan /// Writes the specified value to the output. ///
@@ -276,7 +281,13 @@ protected HtmlString WriteHtml(T htmlContent) // completed (i.e. has gone async) and in that case throw an exception. var actionResult = helperResult.WriteAction(textWriter); - if (!actionResult.IsCompleted) +#if DEBUG + // Force a small delay when debugging to make it easier to create scenario where method is async but has already completed + Thread.Sleep(20); +#endif + + // If the Task is not completed or it's from a generated async method (i.e. one with an 'await' in it) throw an exception + if (!actionResult.IsCompleted || IsTaskFromAsyncMethod(actionResult)) { // NOTE: There's still a chance here that the Task run asynchronously but is completed by the time we check it (albeit it's a small window) // and in that case it's very likely the Utf8PipeTextWriter will fault as it can't handle cross-thread writes (pooled via thread static). @@ -286,7 +297,7 @@ protected HtmlString WriteHtml(T htmlContent) ---------------------------- !!! Razor Slices Error !!! ---------------------------- - The WriteAction of a HelperResult instance has gone async but will be synchronously waited on (sync-over-async). + The WriteAction of a HelperResult instance returned a Task from an async templated Razor delegate. This causes performance and scale issues and is not supported in Razor Slices. This happens when a templated Razor delegate does async work (i.e. has `@await SomethingAsync()` in it). Use async templated methods instead of async templated Razor delegates. They have the advantage of supporting From 60eec01d79bd7234136b51f9cfc90d7496df6d1c Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 12 Dec 2024 17:31:36 -0800 Subject: [PATCH 4/5] Bump to version 0.9.0 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 4badbbf..a02e304 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 0.8.2 + 0.9.0 dev From 6c512a3f27ef3a8ebd764deeaa212a02e9761254 Mon Sep 17 00:00:00 2001 From: Damian Edwards Date: Thu, 12 Dec 2024 18:00:38 -0800 Subject: [PATCH 5/5] Update README.md --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d6f363a..fe27672 100644 --- a/README.md +++ b/README.md @@ -140,8 +140,7 @@ The library is still new and features are being actively added. @tmpl(DateTime.Now) ``` - > [!IMPORTANT] - > Async templated Razor delegates are **NOT** supported and will throw an exception at runtime. + **NOTE: Async templated Razor delegates are *NOT* supported and will throw an exception at runtime** - DI-activated properties via `@inject` - Rendering slices from slices (aka partials) via `@(await RenderPartialAsync())` @@ -196,7 +195,7 @@ The library is still new and features are being actively added. } ``` - **Note: The `@section` directive is not supported as it's incompatible with the rendering approach of Razor Slices** + **NOTE: The `@section` directive is not supported as it's incompatible with the rendering approach of Razor Slices** - Asynchronous rendering, i.e. the template can contain `await` statements, e.g. `@await WriteTheThing()` - Writing UTF8 `byte[]` values directly to the output