From d499e761d359f2753a51d012a4c592bfacc041b6 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 23 Sep 2026 09:53:55 -0500 Subject: [PATCH 1/4] Fix RazorSlices TextWriter rendering of UTF-8 literals and ISpanFormattable values WriteUtf8(TextWriter, ReadOnlySpan) decoded UTF-8 bytes with Encoding.Unicode (UTF-16LE), so every static literal emitted via WriteLiteral(ReadOnlySpan) rendered as garbage when a slice was rendered to a TextWriter. Reachable since the .NET SDK 10.0.400 Razor compiler emits "..."u8 literals for that overload (dotnet/razor#12848). Also ports DamianEdwards/RazorSlices#136: HtmlEncodeAndWriteSpanFormattable wrote the whole rented encode buffer instead of the filled portion (leaking pooled memory into output), and the small fast path returned true without writing anything. Ports DamianEdwards/RazorSlices#127 and #136. Relates to DuendeSoftware/products-private#3682. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../RazorSlices/TextWriterHtmlExtensions.cs | 15 ++- .../TextWriterHtmlWritingTests.cs | 125 ++++++++++++++++++ 2 files changed, 133 insertions(+), 7 deletions(-) create mode 100644 razor-slices/test/RazorSlices.Tests/TextWriterHtmlWritingTests.cs diff --git a/razor-slices/src/RazorSlices/TextWriterHtmlExtensions.cs b/razor-slices/src/RazorSlices/TextWriterHtmlExtensions.cs index ecfe9f37..cedefcb4 100644 --- a/razor-slices/src/RazorSlices/TextWriterHtmlExtensions.cs +++ b/razor-slices/src/RazorSlices/TextWriterHtmlExtensions.cs @@ -67,7 +67,7 @@ public static void HtmlEncodeAndWriteSpanFormattable(this TextWriter textWrit if (waitingToWrite > 0) { - textWriter.Write(encodeBufferSpan); + textWriter.Write(encodeBuffer.AsSpan()[..waitingToWrite]); } ArrayPool.Shared.Return(encodeBuffer); @@ -85,8 +85,9 @@ private static bool TryHtmlEncodeAndWriteSpanFormattableSmall(TextWriter text if ((charsWritten * BufferSizes.HtmlEncodeAllowanceRatio) < BufferSizes.SmallFormattableWriteCharSize) { Span encodedBuffer = stackalloc char[BufferSizes.SmallFormattableWriteCharSize]; - if (htmlEncoder.Encode(formatBuffer, encodedBuffer, out var charsConsumed, out var charsEncoded) == OperationStatus.Done) + if (htmlEncoder.Encode(formatBuffer[..charsWritten], encodedBuffer, out var charsConsumed, out var charsEncoded) == OperationStatus.Done) { + textWriter.Write(encodedBuffer[..charsEncoded]); return true; } } @@ -147,9 +148,9 @@ public static void HtmlEncodeAndWriteUtf8(this TextWriter textWriter, ReadOnlySp { var charCount = Encoding.UTF8.GetCharCount(value); var buffer = ArrayPool.Shared.Rent(charCount); - var bytesDecoded = Encoding.UTF8.GetChars(value, buffer); + var charsWritten = Encoding.UTF8.GetChars(value, buffer); - Debug.Assert(bytesDecoded == value.Length, "Bad decoding when writing to TextWriter in HtmlEncodeAndWriteUtf8(ReadOnlySpan)"); + Debug.Assert(charsWritten == charCount, "Bad decoding when writing to TextWriter in HtmlEncodeAndWriteUtf8(ReadOnlySpan)"); htmlEncoder.Encode(textWriter, buffer, 0, charCount); ArrayPool.Shared.Return(buffer); @@ -157,11 +158,11 @@ public static void HtmlEncodeAndWriteUtf8(this TextWriter textWriter, ReadOnlySp public static void WriteUtf8(this TextWriter textWriter, ReadOnlySpan value) { - var charCount = Encoding.Unicode.GetCharCount(value); + var charCount = Encoding.UTF8.GetCharCount(value); var buffer = ArrayPool.Shared.Rent(charCount); - var bytesDecoded = Encoding.Unicode.GetChars(value, buffer); + var charsWritten = Encoding.UTF8.GetChars(value, buffer); - Debug.Assert(bytesDecoded == value.Length, "Bad decoding when writing to TextWriter in WriteUtf8(ReadOnlySpan)"); + Debug.Assert(charsWritten == charCount, "Bad decoding when writing to TextWriter in WriteUtf8(ReadOnlySpan)"); textWriter.Write(buffer, 0, charCount); ArrayPool.Shared.Return(buffer); diff --git a/razor-slices/test/RazorSlices.Tests/TextWriterHtmlWritingTests.cs b/razor-slices/test/RazorSlices.Tests/TextWriterHtmlWritingTests.cs new file mode 100644 index 00000000..7c20fb95 --- /dev/null +++ b/razor-slices/test/RazorSlices.Tests/TextWriterHtmlWritingTests.cs @@ -0,0 +1,125 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +using System.Text.Encodings.Web; + +namespace Duende.RazorSlices.Tests; + +public class TextWriterHtmlWritingTests +{ + [Fact] + public async Task RenderAsync_TextWriter_WritesUtf8LiteralsAsText() + { + // The Razor compiler in .NET SDK 10.0.400+ emits WriteLiteral(ReadOnlySpan) with "..."u8 literals, + // so static markup takes this path regardless of which SDK compiled the template. + var slice = new Utf8LiteralSlice(); + using var writer = new StringWriter(); + + await slice.RenderAsync(writer); + + writer.ToString().ShouldBe("

hello

héllo wörld ✓

"); + } + + [Fact] + public async Task RenderAsync_PipeWriter_WritesUtf8LiteralsAsText() + { + var slice = new Utf8LiteralSlice(); + using var stream = new MemoryStream(); + var pipeWriter = System.IO.Pipelines.PipeWriter.Create(stream); + + await slice.RenderAsync(pipeWriter); + await pipeWriter.FlushAsync(); + + System.Text.Encoding.UTF8.GetString(stream.ToArray()).ShouldBe("

hello

héllo wörld ✓

"); + } + + [Fact] + public void WriteUtf8_DecodesAsUtf8() + { + using var writer = new StringWriter(); + + writer.WriteUtf8("

héllo wörld ✓

"u8); + + writer.ToString().ShouldBe("

héllo wörld ✓

"); + } + + [Fact] + public void HtmlEncodeAndWriteUtf8_DecodesAsUtf8AndEncodesHtml() + { + using var writer = new StringWriter(); + + writer.HtmlEncodeAndWriteUtf8("

héllo

"u8, HtmlEncoder.Default); + + writer.ToString().ShouldBe("<p>héllo</p>"); + } + + [Fact] + public void HtmlEncodeAndWriteSpanFormattable_WritesOnlyTheFormattedChars() + { + using var writer = new StringWriter(); + + writer.HtmlEncodeAndWriteSpanFormattable(SampleStatus.Open, HtmlEncoder.Default); + + writer.ToString().ShouldBe("Open"); + } + + [Fact] + public void HtmlEncodeAndWriteSpanFormattable_DoesNotLeakPreviousRenterContent() + { + // Dirty the shared pool with a long value first, then write a short one. Without slicing to the + // encoded length the short write carries the tail of the long one into the output. + using var warmup = new StringWriter(); + warmup.HtmlEncodeAndWriteSpanFormattable(1234567890123456789L, HtmlEncoder.Default); + + using var writer = new StringWriter(); + writer.HtmlEncodeAndWriteSpanFormattable(SampleStatus.Posted, HtmlEncoder.Default); + + writer.ToString().ShouldBe("Posted"); + } + + [Fact] + public void HtmlEncodeAndWriteSpanFormattable_EncodesHtmlOnSmallPath() + { + using var writer = new StringWriter(); + + writer.HtmlEncodeAndWriteSpanFormattable(new HtmlishFormattable(), HtmlEncoder.Default); + + writer.ToString().ShouldBe("a<b>c"); + } + + private sealed class Utf8LiteralSlice : RazorSlice + { + public override Task ExecuteAsync() + { + WriteLiteral("

hello

"u8); + WriteLiteral("

héllo wörld ✓

"u8); + return Task.CompletedTask; + } + } + + private enum SampleStatus + { + Open = 0, + Posted = 1 + } + + private readonly struct HtmlishFormattable : ISpanFormattable + { + private const string Value = "ac"; + + public string ToString(string? format, IFormatProvider? formatProvider) => Value; + + public bool TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) + { + if (destination.Length < Value.Length) + { + charsWritten = 0; + return false; + } + + Value.AsSpan().CopyTo(destination); + charsWritten = Value.Length; + return true; + } + } +} From 4ac2121605d63b778d0ab29140522ef02a37c7e5 Mon Sep 17 00:00:00 2001 From: Joe DeCock Date: Wed, 23 Sep 2026 10:17:30 -0500 Subject: [PATCH 2/4] Sync RazorSlices to upstream c286ca0 Re-syncs the vendored Duende.RazorSlices component (library, source generator, tests, samples) from upstream 640458f to c286ca0 (DamianEdwards/RazorSlices main as of 2026-09-19). Brings in #108, #111, #113, #115, #117, #122, #127, #128, #130, #131, #134, #136, #139, #141, #146. Duende-specific transformations re-applied: Duende.RazorSlices namespace (including the source generator's ModelTypeResolver namespace check), license headers, LF line endings, dotnet format, and explicit 'using Duende.RazorSlices;' in samples. Build props/targets updated for RazorSliceProxiesAsRecords (#117). README records the upstream sync point. Adds RazorSliceUtf8LiteralTests as a slice-level regression test for the TextWriter UTF-8 literal bug (DuendeSoftware/products-private#3682). Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- razor-slices/README.md | 7 + .../PagesAndSlices/Pages/Index.cshtml.cs | 3 + .../PagesAndSlices/Pages/_ViewImports.cshtml | 2 +- .../PagesAndSlices/PagesAndSlices.csproj | 4 + .../samples/PagesAndSlices/Program.cs | 3 + .../PagesAndSlices/Slices/_Layout.cshtml | 2 +- .../PagesAndSlices/Slices/_ViewImports.cshtml | 2 +- .../samples/RazorClassLibrary/MarkerClass.cs | 3 + .../Slices/FromLibrary.cshtml | 2 +- .../Slices/FromLibrary.cshtml.cs | 3 + .../RazorClassLibrary/_ViewImports.cshtml | 2 +- .../WebApp/Alternate/Lorem/LoremExtra.cs | 9 + razor-slices/samples/WebApp/AppJsonContext.cs | 3 + .../samples/WebApp/HtmlContentParams.cs | 3 + razor-slices/samples/WebApp/LoremParams.cs | 3 + razor-slices/samples/WebApp/MapSlices.cs | 13 +- razor-slices/samples/WebApp/Models/Todo.cs | 10 +- .../samples/WebApp/Program.Testing.cs | 3 + razor-slices/samples/WebApp/Program.cs | 6 +- ...ringEndpointConventionBuilderExtensions.cs | 14 + .../WebApp/ResponseBufferingMetadata.cs | 13 + .../WebApp/ResponseBufferingMiddleware.cs | 12 +- razor-slices/samples/WebApp/ResultDtos.cs | 3 + .../samples/WebApp/Services/LoremService.cs | 3 + .../WebApp/Slices/AttributeRendering.cshtml | 17 + .../samples/WebApp/Slices/Encoding.cshtml | 2 +- .../samples/WebApp/Slices/LayoutModel.cs | 3 + .../Slices/Lorem/LoremHtmlContent.cshtml | 3 + .../Lorem/LoremInjectableProperties.cshtml | 3 +- .../WebApp/Slices/Lorem/PageContent.cs | 3 + .../WebApp/Slices/Lorem/_LoremMenu.cshtml | 2 +- .../WebApp/Slices/Lorem/_ViewImports.cshtml | 2 + .../samples/WebApp/Slices/Nested.cshtml | 5 + .../samples/WebApp/Slices/SliceBaseClasses.cs | 13 + .../samples/WebApp/Slices/Streaming.cshtml | 32 ++ .../samples/WebApp/Slices/Todo.cshtml | 2 +- .../samples/WebApp/Slices/Todos.cshtml | 2 +- .../samples/WebApp/Slices/UnicodeData.cs | 3 + .../samples/WebApp/Slices/_FooterLinks.cshtml | 2 + .../samples/WebApp/Slices/_ViewImports.cshtml | 2 +- .../src/RazorSlices/IResultExtensions.cs | 2 +- .../RazorSlices/RazorSlice.Formattables.cs | 3 + .../RazorSlices/RazorSlice.Formattables.tt | 5 +- .../RazorSlices/RazorSlice.HtmlAttributes.cs | 35 +- .../src/RazorSlices/RazorSlice.IResult.cs | 4 +- .../src/RazorSlices/RazorSlice.Write.cs | 20 +- razor-slices/src/RazorSlices/RazorSlice.cs | 31 +- .../src/RazorSlices/RazorSliceFactory.cs | 240 ++++------ .../src/RazorSlices/ReusableStringWriter.cs | 3 + .../src/RazorSlices/SliceDefinition.cs | 125 ----- .../src/RazorSlices/TaskExtensions.cs | 1 + .../build/Duende.RazorSlices.props | 2 +- .../build/Duende.RazorSlices.targets | 1 + .../src/SourceGenerator/ModelTypeResolver.cs | 276 ++++++++++- razor-slices/src/SourceGenerator/PathUtils.cs | 4 + .../SourceGenerator/RazorDirectiveParser.cs | 21 + .../RazorSliceProxyGenerator.cs | 92 ++-- .../SourceGenerator/ViewImportsResolver.cs | 30 +- .../RazorSliceFactoryTests.cs | 254 ++++++++++ .../RazorSliceHtmlAttributesTests.cs | 39 ++ .../RazorSliceIResultTests.cs | 51 ++ .../RazorSliceUtf8LiteralTests.cs | 48 ++ .../RazorSliceWriteHtmlTests.cs | 62 +++ .../RazorSlices.Tests/RazorSliceWriteTests.cs | 31 ++ .../TextWriterHtmlExtensionsTests.cs | 213 +++++++++ .../TextWriterHtmlWritingTests.cs | 125 ----- .../ModelTypeResolverTests.cs | 442 ++++++++++++++++++ .../RazorDirectiveParserTests.cs | 22 + .../RazorSliceProxyGeneratorTests.cs | 144 ++++++ 69 files changed, 2071 insertions(+), 479 deletions(-) create mode 100644 razor-slices/samples/WebApp/Alternate/Lorem/LoremExtra.cs create mode 100644 razor-slices/samples/WebApp/ResponseBufferingEndpointConventionBuilderExtensions.cs create mode 100644 razor-slices/samples/WebApp/ResponseBufferingMetadata.cs create mode 100644 razor-slices/samples/WebApp/Slices/AttributeRendering.cshtml create mode 100644 razor-slices/samples/WebApp/Slices/Nested.cshtml create mode 100644 razor-slices/samples/WebApp/Slices/SliceBaseClasses.cs create mode 100644 razor-slices/samples/WebApp/Slices/Streaming.cshtml delete mode 100644 razor-slices/src/RazorSlices/SliceDefinition.cs create mode 100644 razor-slices/test/RazorSlices.Tests/RazorSliceFactoryTests.cs create mode 100644 razor-slices/test/RazorSlices.Tests/RazorSliceHtmlAttributesTests.cs create mode 100644 razor-slices/test/RazorSlices.Tests/RazorSliceIResultTests.cs create mode 100644 razor-slices/test/RazorSlices.Tests/RazorSliceUtf8LiteralTests.cs create mode 100644 razor-slices/test/RazorSlices.Tests/RazorSliceWriteHtmlTests.cs create mode 100644 razor-slices/test/RazorSlices.Tests/RazorSliceWriteTests.cs create mode 100644 razor-slices/test/RazorSlices.Tests/TextWriterHtmlExtensionsTests.cs delete mode 100644 razor-slices/test/RazorSlices.Tests/TextWriterHtmlWritingTests.cs create mode 100644 razor-slices/test/SourceGenerator.Tests/ModelTypeResolverTests.cs create mode 100644 razor-slices/test/SourceGenerator.Tests/RazorSliceProxyGeneratorTests.cs diff --git a/razor-slices/README.md b/razor-slices/README.md index 894b1084..dfeefc78 100644 --- a/razor-slices/README.md +++ b/razor-slices/README.md @@ -4,9 +4,16 @@ This is a vendored fork of [DamianEdwards/RazorSlices](https://github.com/Damian The upstream project is copyright Damian Edwards and licensed under the [Apache 2.0 license](../LICENSE). See [ThirdPartyNotices.txt](ThirdPartyNotices.txt) for the full attribution. +## Upstream sync point + +Last synced to upstream commit [`c286ca0`](https://github.com/DamianEdwards/RazorSlices/commit/c286ca021b05345e74785364c4a14515b082c90b) (2026-09-19, `main` after DamianEdwards/RazorSlices#146). When re-syncing, copy upstream `src/`, `tests/`, and `samples/` source files, then apply the transformations listed below. + ## Differences from upstream - Package ID, assembly name, and root namespace changed from `RazorSlices` to `Duende.RazorSlices` - Source generator package renamed from `RazorSlices.SourceGenerator` to `Duende.RazorSlices.SourceGenerator` +- The source generator's model-type resolver matches the `Duende.RazorSlices` namespace when walking slice base types +- Samples keep the upstream `RazorSlices.Samples.*` namespace and add explicit `using Duende.RazorSlices;` directives where needed +- Duende license headers, LF line endings, and `dotnet format` applied - Target framework updated to `net10.0` - Strong-naming disabled (not required for vendored use) diff --git a/razor-slices/samples/PagesAndSlices/Pages/Index.cshtml.cs b/razor-slices/samples/PagesAndSlices/Pages/Index.cshtml.cs index 14ecc8da..e6291087 100644 --- a/razor-slices/samples/PagesAndSlices/Pages/Index.cshtml.cs +++ b/razor-slices/samples/PagesAndSlices/Pages/Index.cshtml.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; diff --git a/razor-slices/samples/PagesAndSlices/Pages/_ViewImports.cshtml b/razor-slices/samples/PagesAndSlices/Pages/_ViewImports.cshtml index 1ec7a2f7..1c8a7552 100644 --- a/razor-slices/samples/PagesAndSlices/Pages/_ViewImports.cshtml +++ b/razor-slices/samples/PagesAndSlices/Pages/_ViewImports.cshtml @@ -1 +1 @@ -@using RazorSlices.Samples.PagesAndSlices.Pages \ No newline at end of file +@using RazorSlices.Samples.PagesAndSlices.Pages diff --git a/razor-slices/samples/PagesAndSlices/PagesAndSlices.csproj b/razor-slices/samples/PagesAndSlices/PagesAndSlices.csproj index 5167849f..733c4a35 100644 --- a/razor-slices/samples/PagesAndSlices/PagesAndSlices.csproj +++ b/razor-slices/samples/PagesAndSlices/PagesAndSlices.csproj @@ -11,6 +11,10 @@ false + + true + + diff --git a/razor-slices/samples/PagesAndSlices/Program.cs b/razor-slices/samples/PagesAndSlices/Program.cs index 90a0d4d2..5faeb19f 100644 --- a/razor-slices/samples/PagesAndSlices/Program.cs +++ b/razor-slices/samples/PagesAndSlices/Program.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using RazorSlices.Samples.PagesAndSlices.Slices; var builder = WebApplication.CreateBuilder(args); diff --git a/razor-slices/samples/PagesAndSlices/Slices/_Layout.cshtml b/razor-slices/samples/PagesAndSlices/Slices/_Layout.cshtml index cf0dc2e4..bcd4cbc6 100644 --- a/razor-slices/samples/PagesAndSlices/Slices/_Layout.cshtml +++ b/razor-slices/samples/PagesAndSlices/Slices/_Layout.cshtml @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/razor-slices/samples/PagesAndSlices/Slices/_ViewImports.cshtml b/razor-slices/samples/PagesAndSlices/Slices/_ViewImports.cshtml index 01d1aa0b..8b56fc32 100644 --- a/razor-slices/samples/PagesAndSlices/Slices/_ViewImports.cshtml +++ b/razor-slices/samples/PagesAndSlices/Slices/_ViewImports.cshtml @@ -8,4 +8,4 @@ @using RazorSlices.Samples.PagesAndSlices.Slices @tagHelperPrefix __disable_tagHelpers__: -@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor \ No newline at end of file +@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor diff --git a/razor-slices/samples/RazorClassLibrary/MarkerClass.cs b/razor-slices/samples/RazorClassLibrary/MarkerClass.cs index 2fec60e4..9a1fc339 100644 --- a/razor-slices/samples/RazorClassLibrary/MarkerClass.cs +++ b/razor-slices/samples/RazorClassLibrary/MarkerClass.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.RazorClassLibrary; internal class MarkerClass diff --git a/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml b/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml index 1c677377..88811f4a 100644 --- a/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml +++ b/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml @@ -33,4 +33,4 @@ - \ No newline at end of file + diff --git a/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml.cs b/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml.cs index c8f80bde..626c24e0 100644 --- a/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml.cs +++ b/razor-slices/samples/RazorClassLibrary/Slices/FromLibrary.cshtml.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using Duende.RazorSlices; namespace RazorSlices.Samples.RazorClassLibrary.Slices; diff --git a/razor-slices/samples/RazorClassLibrary/_ViewImports.cshtml b/razor-slices/samples/RazorClassLibrary/_ViewImports.cshtml index 0d061ec1..fa903157 100644 --- a/razor-slices/samples/RazorClassLibrary/_ViewImports.cshtml +++ b/razor-slices/samples/RazorClassLibrary/_ViewImports.cshtml @@ -8,4 +8,4 @@ @using RazorSlices.Samples.RazorClassLibrary; @tagHelperPrefix __disable_tagHelpers__: -@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor \ No newline at end of file +@removeTagHelper *, Microsoft.AspNetCore.Mvc.Razor diff --git a/razor-slices/samples/WebApp/Alternate/Lorem/LoremExtra.cs b/razor-slices/samples/WebApp/Alternate/Lorem/LoremExtra.cs new file mode 100644 index 00000000..11a88bb1 --- /dev/null +++ b/razor-slices/samples/WebApp/Alternate/Lorem/LoremExtra.cs @@ -0,0 +1,9 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace RazorSlices.Samples.WebApp.Alternate.Lorem; + +public static class LoremExtra +{ + public const string AdditionalContent = "
Additional content
"; +} diff --git a/razor-slices/samples/WebApp/AppJsonContext.cs b/razor-slices/samples/WebApp/AppJsonContext.cs index 79d85b69..9532d003 100644 --- a/razor-slices/samples/WebApp/AppJsonContext.cs +++ b/razor-slices/samples/WebApp/AppJsonContext.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System.Text.Json.Serialization; namespace RazorSlices.Samples.WebApp; diff --git a/razor-slices/samples/WebApp/HtmlContentParams.cs b/razor-slices/samples/WebApp/HtmlContentParams.cs index dafe872b..65c6a7dc 100644 --- a/razor-slices/samples/WebApp/HtmlContentParams.cs +++ b/razor-slices/samples/WebApp/HtmlContentParams.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp; public struct HtmlContentParams diff --git a/razor-slices/samples/WebApp/LoremParams.cs b/razor-slices/samples/WebApp/LoremParams.cs index 9c3936c8..5e9f93d8 100644 --- a/razor-slices/samples/WebApp/LoremParams.cs +++ b/razor-slices/samples/WebApp/LoremParams.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp; public struct LoremParams { diff --git a/razor-slices/samples/WebApp/MapSlices.cs b/razor-slices/samples/WebApp/MapSlices.cs index efcc82a3..e0440cf7 100644 --- a/razor-slices/samples/WebApp/MapSlices.cs +++ b/razor-slices/samples/WebApp/MapSlices.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using Duende.RazorSlices; using LibrarySlices = RazorSlices.Samples.RazorClassLibrary.Slices; using Microsoft.AspNetCore.Http.HttpResults; @@ -31,6 +34,9 @@ public static IEndpointRouteBuilder MapSlices(this IEndpointRouteBuilder endpoin await slice.RenderAsync(httpResponse.Body); }); endpoints.MapGet("/encoding", () => Results.RazorSlice()); + endpoints.MapGet("/attribute-rendering", () => Results.RazorSlice()); + endpoints.MapGet("/streaming", () => Results.RazorSlice()) + .DisableResponseBuffering(); endpoints.MapGet("/unicode", () => Results.RazorSlice()); endpoints.MapGet("/templated", (bool async = false) => Results.RazorSlice(async)); endpoints.MapGet("/library", () => Results.RazorSlice()); @@ -49,6 +55,7 @@ public static IEndpointRouteBuilder MapSlices(this IEndpointRouteBuilder endpoin return Results.Ok(new ResultDto(stringBuilder.ToString())); }); + endpoints.MapGet("/nested", () => Results.RazorSlice(new Models.Todo.Nested { Extra = "Test" })); endpoints.MapGet("/", () => Results.RazorSlice(Models.Todos.AllTodos)); endpoints.MapGet("/{id:int}", Results (int id) => { @@ -58,7 +65,8 @@ public static IEndpointRouteBuilder MapSlices(this IEndpointRouteBuilder endpoin : TypedResults.NotFound(); }); #else - endpoints.MapGet("/lorem-static", () => Results.Extensions.RazorSlice()); + endpoints.MapGet("/nested", () => Results.Extensions.RazorSlice(new Models.Todo.Nested { Extra = "Test" })); + endpoints.MapGet("/lorem-static",() => Results.Extensions.RazorSlice()); endpoints.MapGet("/lorem-dynamic", (int? paraCount, int? paraLength) => Results.Extensions.RazorSlice(new LoremParams(paraCount, paraLength))); endpoints.MapGet("/lorem-formattable", (int? paraCount, int? paraLength) => @@ -76,6 +84,9 @@ public static IEndpointRouteBuilder MapSlices(this IEndpointRouteBuilder endpoin await slice.RenderAsync(httpResponse.Body); }); endpoints.MapGet("/encoding", () => Results.Extensions.RazorSlice()); + endpoints.MapGet("/attribute-rendering", () => Results.Extensions.RazorSlice()); + endpoints.MapGet("/streaming", () => Results.Extensions.RazorSlice()) + .DisableResponseBuffering(); endpoints.MapGet("/unicode", () => Results.Extensions.RazorSlice()); endpoints.MapGet("/templated", (bool async = false) => Results.Extensions.RazorSlice(async)); endpoints.MapGet("/library", () => Results.Extensions.RazorSlice()); diff --git a/razor-slices/samples/WebApp/Models/Todo.cs b/razor-slices/samples/WebApp/Models/Todo.cs index 952db4c8..fcaf96e3 100644 --- a/razor-slices/samples/WebApp/Models/Todo.cs +++ b/razor-slices/samples/WebApp/Models/Todo.cs @@ -1,9 +1,17 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System.Text; namespace RazorSlices.Samples.WebApp.Models; public class Todo { + public class Nested + { + public string? Extra { get; set; } + } + public int Id { get; set; } private byte[] _titleUtf8 = []; @@ -43,4 +51,4 @@ internal static class Todos new() { Id = 4, TitleUtf8 = "Walk the kangaroo."u8.ToArray(), DueBy = DateOnly.FromDateTime(DateTime.Now.AddDays(1)), IsComplete = false }, new() { Id = 5, TitleUtf8 = "Call Grandma."u8.ToArray(), DueBy = DateOnly.FromDateTime(DateTime.Now.AddDays(1)), IsComplete = false }, ]; -} \ No newline at end of file +} diff --git a/razor-slices/samples/WebApp/Program.Testing.cs b/razor-slices/samples/WebApp/Program.Testing.cs index 9f08e4f2..693b5783 100644 --- a/razor-slices/samples/WebApp/Program.Testing.cs +++ b/razor-slices/samples/WebApp/Program.Testing.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp; public partial class Program diff --git a/razor-slices/samples/WebApp/Program.cs b/razor-slices/samples/WebApp/Program.cs index 7646b73e..86d9f313 100644 --- a/razor-slices/samples/WebApp/Program.cs +++ b/razor-slices/samples/WebApp/Program.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using System.Runtime.CompilerServices; using RazorSlices.Samples.WebApp; using RazorSlices.Samples.WebApp.Services; @@ -22,11 +25,12 @@ app.UseStatusCodePages(); app.UseStaticFiles(); +app.UseRouting(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); - if (Environment.GetEnvironmentVariable("ENABLE_RESPONSE_BUFFERING") == "true") + if (builder.Configuration["ENABLE_RESPONSE_BUFFERING"] == "true") { // Enable response buffering middleware to allow for response interception during local development app.UseResponseBuffering(); diff --git a/razor-slices/samples/WebApp/ResponseBufferingEndpointConventionBuilderExtensions.cs b/razor-slices/samples/WebApp/ResponseBufferingEndpointConventionBuilderExtensions.cs new file mode 100644 index 00000000..88350d9e --- /dev/null +++ b/razor-slices/samples/WebApp/ResponseBufferingEndpointConventionBuilderExtensions.cs @@ -0,0 +1,14 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace RazorSlices.Samples.WebApp; + +public static class ResponseBufferingEndpointConventionBuilderExtensions +{ + public static TBuilder DisableResponseBuffering(this TBuilder builder) + where TBuilder : IEndpointConventionBuilder + { + ArgumentNullException.ThrowIfNull(builder); + return builder.WithMetadata(new DisableResponseBufferingAttribute()); + } +} diff --git a/razor-slices/samples/WebApp/ResponseBufferingMetadata.cs b/razor-slices/samples/WebApp/ResponseBufferingMetadata.cs new file mode 100644 index 00000000..507e7f75 --- /dev/null +++ b/razor-slices/samples/WebApp/ResponseBufferingMetadata.cs @@ -0,0 +1,13 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + +namespace RazorSlices.Samples.WebApp; + +public interface IDisableResponseBufferingMetadata +{ +} + +[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] +public sealed class DisableResponseBufferingAttribute : Attribute, IDisableResponseBufferingMetadata +{ +} diff --git a/razor-slices/samples/WebApp/ResponseBufferingMiddleware.cs b/razor-slices/samples/WebApp/ResponseBufferingMiddleware.cs index 018e9618..a7f6682c 100644 --- a/razor-slices/samples/WebApp/ResponseBufferingMiddleware.cs +++ b/razor-slices/samples/WebApp/ResponseBufferingMiddleware.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp; using System.IO; @@ -9,6 +12,12 @@ internal sealed class ResponseBufferingMiddleware(RequestDelegate next) { public async Task InvokeAsync(HttpContext context) { + if (context.GetEndpoint()?.Metadata.GetMetadata() is not null) + { + await next(context); + return; + } + // Save the original response body stream var originalBodyStream = context.Response.Body; @@ -42,11 +51,10 @@ public async Task InvokeAsync(HttpContext context) } } -internal static class ResponseBufferingMiddlewareExtensions +public static class ResponseBufferingMiddlewareExtensions { public static IApplicationBuilder UseResponseBuffering(this IApplicationBuilder app) { return app.UseMiddleware(); } } - diff --git a/razor-slices/samples/WebApp/ResultDtos.cs b/razor-slices/samples/WebApp/ResultDtos.cs index b8253150..2d1117c1 100644 --- a/razor-slices/samples/WebApp/ResultDtos.cs +++ b/razor-slices/samples/WebApp/ResultDtos.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp; record ResultDto(string HtmlString); diff --git a/razor-slices/samples/WebApp/Services/LoremService.cs b/razor-slices/samples/WebApp/Services/LoremService.cs index efee4cee..0df17898 100644 --- a/razor-slices/samples/WebApp/Services/LoremService.cs +++ b/razor-slices/samples/WebApp/Services/LoremService.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + using RazorSlices.Samples.WebApp.Slices.Lorem; using System.Text; diff --git a/razor-slices/samples/WebApp/Slices/AttributeRendering.cshtml b/razor-slices/samples/WebApp/Slices/AttributeRendering.cshtml new file mode 100644 index 00000000..3f2f293c --- /dev/null +++ b/razor-slices/samples/WebApp/Slices/AttributeRendering.cshtml @@ -0,0 +1,17 @@ +@inherits RazorSlice +@implements IUsesLayout<_Layout, LayoutModel> + +

Attribute rendering

+ +
False
+
Null
+
Empty
+
False String
+
String
+ + + + +@functions { + public LayoutModel LayoutModel => new() { Title = "Attribute rendering" }; +} diff --git a/razor-slices/samples/WebApp/Slices/Encoding.cshtml b/razor-slices/samples/WebApp/Slices/Encoding.cshtml index 0f926ee3..f9ae5799 100644 --- a/razor-slices/samples/WebApp/Slices/Encoding.cshtml +++ b/razor-slices/samples/WebApp/Slices/Encoding.cshtml @@ -9,4 +9,4 @@ Encoded:
@functions { public LayoutModel LayoutModel => new() { Title = "Encoding" }; -} \ No newline at end of file +} diff --git a/razor-slices/samples/WebApp/Slices/LayoutModel.cs b/razor-slices/samples/WebApp/Slices/LayoutModel.cs index 06f54711..79a59706 100644 --- a/razor-slices/samples/WebApp/Slices/LayoutModel.cs +++ b/razor-slices/samples/WebApp/Slices/LayoutModel.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp.Slices; public class LayoutModel diff --git a/razor-slices/samples/WebApp/Slices/Lorem/LoremHtmlContent.cshtml b/razor-slices/samples/WebApp/Slices/Lorem/LoremHtmlContent.cshtml index 03fcac57..352b1990 100644 --- a/razor-slices/samples/WebApp/Slices/Lorem/LoremHtmlContent.cshtml +++ b/razor-slices/samples/WebApp/Slices/Lorem/LoremHtmlContent.cshtml @@ -1,3 +1,4 @@ +@namespace RazorSlices.Samples.WebApp.Alternate.Lorem @using System.IO @using System.Text.Encodings.Web @@ -32,10 +33,12 @@ if (HtmlEncode) { writer.Write(encoder.Encode(PageContent.Html)); + writer.Write(encoder.Encode(LoremExtra.AdditionalContent)); } else { writer.Write(PageContent.Html); + writer.Write(LoremExtra.AdditionalContent); } } } diff --git a/razor-slices/samples/WebApp/Slices/Lorem/LoremInjectableProperties.cshtml b/razor-slices/samples/WebApp/Slices/Lorem/LoremInjectableProperties.cshtml index 14cc2099..002665dd 100644 --- a/razor-slices/samples/WebApp/Slices/Lorem/LoremInjectableProperties.cshtml +++ b/razor-slices/samples/WebApp/Slices/Lorem/LoremInjectableProperties.cshtml @@ -1,3 +1,4 @@ +@namespace RazorSlices.Samples.WebApp.Slices.Lorem @inherits RazorSlice @implements IUsesLayout<_LoremLayout, LayoutModel> @@ -22,4 +23,4 @@ return Task.CompletedTask; } -} \ No newline at end of file +} diff --git a/razor-slices/samples/WebApp/Slices/Lorem/PageContent.cs b/razor-slices/samples/WebApp/Slices/Lorem/PageContent.cs index 8bdc7fde..241e06f0 100644 --- a/razor-slices/samples/WebApp/Slices/Lorem/PageContent.cs +++ b/razor-slices/samples/WebApp/Slices/Lorem/PageContent.cs @@ -1,3 +1,6 @@ +// Copyright (c) Duende Software. All rights reserved. +// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. + namespace RazorSlices.Samples.WebApp.Slices.Lorem; public static class PageContent diff --git a/razor-slices/samples/WebApp/Slices/Lorem/_LoremMenu.cshtml b/razor-slices/samples/WebApp/Slices/Lorem/_LoremMenu.cshtml index f6aa1e27..0ca7b245 100644 --- a/razor-slices/samples/WebApp/Slices/Lorem/_LoremMenu.cshtml +++ b/razor-slices/samples/WebApp/Slices/Lorem/_LoremMenu.cshtml @@ -1,5 +1,5 @@ -@inherits RazorSlice +@inherits Duende.RazorSlices.RazorSlice