diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs index a19542e547..3d4bf22794 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs @@ -96,7 +96,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration throw new ArgumentNullException(nameof(writer)); } - var scope = new TagHelperWriterScope(this, BasicWriter); + var scope = new TagHelperWriterScope(this, TagHelperWriter); TagHelperWriter = writer; return scope; } @@ -131,9 +131,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public struct TagHelperWriterScope : IDisposable { private readonly CSharpRenderingContext _context; - private readonly BasicWriter _writer; + private readonly TagHelperWriter _writer; - public TagHelperWriterScope(CSharpRenderingContext context, BasicWriter writer) + public TagHelperWriterScope(CSharpRenderingContext context, TagHelperWriter writer) { if (context == null) { @@ -151,7 +151,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public void Dispose() { - _context.BasicWriter = _writer; + _context.TagHelperWriter = _writer; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultDocumentWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultDocumentWriter.cs index 1f3c4934bd..9c45ba163c 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultDocumentWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultDocumentWriter.cs @@ -211,6 +211,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration Context.TagHelperWriter.WriteCreateTagHelper(Context, node); } + public override void VisitExecuteTagHelpers(ExecuteTagHelpersIRNode node) + { + Context.TagHelperWriter.WriteExecuteTagHelpers(Context, node); + } + public override void VisitDefault(RazorIRNode node) { // This is a temporary bridge to the renderer, which allows us to move functionality piecemeal diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeTagHelperWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeTagHelperWriter.cs index ec42ea0b06..cdba168045 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeTagHelperWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeTagHelperWriter.cs @@ -43,7 +43,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public override void WriteExecuteTagHelpers(CSharpRenderingContext context, ExecuteTagHelpersIRNode node) { - throw new NotImplementedException(); + // Do nothing } public override void WriteInitializeTagHelperStructure(CSharpRenderingContext context, InitializeTagHelperStructureIRNode node) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RedirectedTagHelperWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RedirectedTagHelperWriter.cs new file mode 100644 index 0000000000..4502b221fb --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RedirectedTagHelperWriter.cs @@ -0,0 +1,108 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; + +namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration +{ + internal class RedirectedTagHelperWriter : TagHelperWriter + { + private readonly TagHelperWriter _previous; + private readonly string _textWriter; + + public RedirectedTagHelperWriter(TagHelperWriter previous, string textWriter) + { + _previous = previous; + _textWriter = textWriter; + } + + public string ExecutionContextVariableName { get; set; } = "__tagHelperExecutionContext"; + + public string ExecutionContextOutputPropertyName { get; set; } = "Output"; + + public string ExecutionContextSetOutputContentAsyncMethodName { get; set; } = "SetOutputContentAsync"; + + public string RunnerVariableName { get; set; } = "__tagHelperRunner"; + + public string RunnerRunAsyncMethodName { get; set; } = "RunAsync"; + + public string ScopeManagerVariableName { get; set; } = "__tagHelperScopeManager"; + + public string ScopeManagerEndMethodName { get; set; } = "End"; + + public string TagHelperOutputIsContentModifiedPropertyName { get; set; } = "IsContentModified"; + + public string WriteTagHelperOutputMethod { get; set; } = "WriteTo"; + + public override void WriteAddTagHelperHtmlAttribute(CSharpRenderingContext context, AddTagHelperHtmlAttributeIRNode node) + { + _previous.WriteAddTagHelperHtmlAttribute(context, node); + } + + public override void WriteCreateTagHelper(CSharpRenderingContext context, CreateTagHelperIRNode node) + { + _previous.WriteCreateTagHelper(context, node); + } + + public override void WriteDeclareTagHelperFields(CSharpRenderingContext context, DeclareTagHelperFieldsIRNode node) + { + _previous.WriteDeclareTagHelperFields(context, node); + } + + public override void WriteExecuteTagHelpers(CSharpRenderingContext context, ExecuteTagHelpersIRNode node) + { + if (context.Options.DesignTimeMode) + { + _previous.WriteExecuteTagHelpers(context, node); + return; + } + + context.Writer + .Write("await ") + .WriteStartInstanceMethodInvocation( + RunnerVariableName, + RunnerRunAsyncMethodName) + .Write(ExecutionContextVariableName) + .WriteEndMethodInvocation(); + + var tagHelperOutputAccessor = $"{ExecutionContextVariableName}.{ExecutionContextOutputPropertyName}"; + + context.Writer + .Write("if (!") + .Write(tagHelperOutputAccessor) + .Write(".") + .Write(TagHelperOutputIsContentModifiedPropertyName) + .WriteLine(")"); + + using (context.Writer.BuildScope()) + { + context.Writer + .Write("await ") + .WriteInstanceMethodInvocation( + ExecutionContextVariableName, + ExecutionContextSetOutputContentAsyncMethodName); + } + + context.Writer + .WriteStartMethodInvocation(WriteTagHelperOutputMethod) + .Write(_textWriter) + .WriteParameterSeparator() + .Write(tagHelperOutputAccessor) + .WriteEndMethodInvocation() + .WriteStartAssignment(ExecutionContextVariableName) + .WriteInstanceMethodInvocation( + ScopeManagerVariableName, + ScopeManagerEndMethodName); + } + + public override void WriteInitializeTagHelperStructure(CSharpRenderingContext context, InitializeTagHelperStructureIRNode node) + { + _previous.WriteInitializeTagHelperStructure(context, node); + } + + public override void WriteSetTagHelperProperty(CSharpRenderingContext context, SetTagHelperPropertyIRNode node) + { + _previous.WriteSetTagHelperProperty(context, node); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs index c6010eb486..4c388e6585 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs @@ -456,46 +456,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration .WriteEndMethodInvocation(); } - public override void VisitExecuteTagHelpers(ExecuteTagHelpersIRNode node) - { - Context.Writer - .Write("await ") - .WriteStartInstanceMethodInvocation( - "__tagHelperRunner" /* ORIGINAL: RunnerVariableName */, - "RunAsync" /* ORIGINAL: RunnerRunAsyncMethodName */) - .Write("__tagHelperExecutionContext" /* ORIGINAL: ExecutionContextVariableName */) - .WriteEndMethodInvocation(); - - var executionContextVariableName = "__tagHelperExecutionContext" /* ORIGINAL: ExecutionContextVariableName */; - var executionContextOutputPropertyName = "Output" /* ORIGINAL: ExecutionContextOutputPropertyName */; - var tagHelperOutputAccessor = $"{executionContextVariableName}.{executionContextOutputPropertyName}"; - - Context.Writer - .Write("if (!") - .Write(tagHelperOutputAccessor) - .Write(".") - .Write("IsContentModified" /* ORIGINAL: TagHelperOutputIsContentModifiedPropertyName */) - .WriteLine(")"); - - using (Context.Writer.BuildScope()) - { - Context.Writer - .Write("await ") - .WriteInstanceMethodInvocation( - executionContextVariableName, - "SetOutputContentAsync" /* ORIGINAL: ExecutionContextSetOutputContentAsyncMethodName */); - } - - Context.Writer - .Write(Context.RenderingConventions.StartWriteMethod) - .Write(tagHelperOutputAccessor) - .WriteEndMethodInvocation() - .WriteStartAssignment(executionContextVariableName) - .WriteInstanceMethodInvocation( - "__tagHelperScopeManager" /* ORIGINAL: ScopeManagerVariableName */, - "End" /* ORIGINAL: ScopeManagerEndMethodName */); - } - public override void VisitDeclarePreallocatedTagHelperHtmlAttribute(DeclarePreallocatedTagHelperHtmlAttributeIRNode node) { Context.Writer diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeTagHelperWriter.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeTagHelperWriter.cs index 395425fcb4..ae6f0d1115 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeTagHelperWriter.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeTagHelperWriter.cs @@ -16,16 +16,24 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public string ExecutionContextAddMethodName { get; set; } = "Add"; + public string ExecutionContextOutputPropertyName { get; set; } = "Output"; + + public string ExecutionContextSetOutputContentAsyncMethodName { get; set; } = "SetOutputContentAsync"; + public string RunnerTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperRunner"; public string RunnerVariableName { get; set; } = "__tagHelperRunner"; + public string RunnerRunAsyncMethodName { get; set; } = "RunAsync"; + public string ScopeManagerTypeName { get; set; } = "global::Microsoft.AspNetCore.Razor.Runtime.TagHelpers.TagHelperScopeManager"; public string ScopeManagerVariableName { get; set; } = "__tagHelperScopeManager"; public string ScopeManagerBeginMethodName { get; set; } = "Begin"; + public string ScopeManagerEndMethodName { get; set; } = "End"; + public string StartTagHelperWritingScopeMethodName { get; set; } = "StartTagHelperWritingScope"; public string EndTagHelperWritingScopeMethodName { get; set; } = "EndTagHelperWritingScope"; @@ -34,6 +42,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public string CreateTagHelperMethodName { get; set; } = "CreateTagHelper"; + public string TagHelperOutputIsContentModifiedPropertyName { get; set; } = "IsContentModified"; + + public string WriteTagHelperOutputMethod { get; set; } = "Write"; + public override void WriteDeclareTagHelperFields(CSharpRenderingContext context, DeclareTagHelperFieldsIRNode node) { context.Writer.WriteLineHiddenDirective(); @@ -137,7 +149,40 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public override void WriteExecuteTagHelpers(CSharpRenderingContext context, ExecuteTagHelpersIRNode node) { - throw new NotImplementedException(); + context.Writer + .Write("await ") + .WriteStartInstanceMethodInvocation( + RunnerVariableName, + RunnerRunAsyncMethodName) + .Write(ExecutionContextVariableName) + .WriteEndMethodInvocation(); + + var tagHelperOutputAccessor = $"{ExecutionContextVariableName}.{ExecutionContextOutputPropertyName}"; + + context.Writer + .Write("if (!") + .Write(tagHelperOutputAccessor) + .Write(".") + .Write(TagHelperOutputIsContentModifiedPropertyName) + .WriteLine(")"); + + using (context.Writer.BuildScope()) + { + context.Writer + .Write("await ") + .WriteInstanceMethodInvocation( + ExecutionContextVariableName, + ExecutionContextSetOutputContentAsyncMethodName); + } + + context.Writer + .WriteStartMethodInvocation(WriteTagHelperOutputMethod) + .Write(tagHelperOutputAccessor) + .WriteEndMethodInvocation() + .WriteStartAssignment(ExecutionContextVariableName) + .WriteInstanceMethodInvocation( + ScopeManagerVariableName, + ScopeManagerEndMethodName); } public override void WriteInitializeTagHelperStructure(CSharpRenderingContext context, InitializeTagHelperStructureIRNode node) @@ -167,6 +212,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration context.RenderingConventions = new CSharpRenderingConventions(context.Writer); using (context.Push(new RuntimeBasicWriter())) + using (context.Push(new RuntimeTagHelperWriter())) { using (context.Writer.BuildAsyncLambda(endLine: false)) { diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs index e1fabf667f..1d7ef2b60a 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs @@ -24,6 +24,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration context.RenderingConventions = new CSharpRedirectRenderingConventions(TemplateWriterName, context.Writer); using (context.Push(new RedirectedBasicWriter(context.BasicWriter, TemplateWriterName))) + using (context.Push(new RedirectedTagHelperWriter(context.TagHelperWriter, TemplateWriterName))) { using (context.Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) { diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RedirectedTagHelperWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RedirectedTagHelperWriterTest.cs new file mode 100644 index 0000000000..7836c87550 --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RedirectedTagHelperWriterTest.cs @@ -0,0 +1,76 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration +{ + public class RedirectedTagHelperWriterTest + { + // In design time this will not include the 'text writer' parameter. + [Fact] + public void WriteExecuteTagHelpers_DesignTime_DoesNormalWrite() + { + // Arrange + var writer = new RedirectedTagHelperWriter(new DesignTimeTagHelperWriter(), "test_writer") + { + WriteTagHelperOutputMethod = "Test", + }; + + var context = new CSharpRenderingContext() + { + Options = RazorParserOptions.CreateDefaultOptions(), + Writer = new Legacy.CSharpCodeWriter(), + }; + + context.Options.DesignTimeMode = true; + + var node = new ExecuteTagHelpersIRNode(); + + // Act + writer.WriteExecuteTagHelpers(context, node); + + // Assert + var csharp = context.Writer.Builder.ToString(); + Assert.Empty(csharp); + } + + [Fact] + public void WriteExecuteTagHelpers_Runtime_RendersWithRedirectWriter() + { + // Arrange + var writer = new RedirectedTagHelperWriter(new RuntimeTagHelperWriter(), "test_writer") + { + WriteTagHelperOutputMethod = "Test", + }; + + var context = new CSharpRenderingContext() + { + Options = RazorParserOptions.CreateDefaultOptions(), + Writer = new Legacy.CSharpCodeWriter(), + }; + + var node = new ExecuteTagHelpersIRNode(); + + // Act + writer.WriteExecuteTagHelpers(context, node); + + // Assert + var csharp = context.Writer.Builder.ToString(); + Assert.Equal( +@"await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); +if (!__tagHelperExecutionContext.Output.IsContentModified) +{ + await __tagHelperExecutionContext.SetOutputContentAsync(); +} +Test(test_writer, __tagHelperExecutionContext.Output); +__tagHelperExecutionContext = __tagHelperScopeManager.End(); +", + csharp, + ignoreLineEndingDifferences: true); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs index 6f2808c4dd..f196b78dca 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/RuntimeTagHelperWriterTest.cs @@ -101,6 +101,7 @@ private global::MyTagHelper __MyTagHelper = null; { Writer = new Legacy.CSharpCodeWriter(), BasicWriter = new RuntimeBasicWriter(), + TagHelperWriter = new RuntimeTagHelperWriter(), IdGenerator = () => "test", RenderChildren = n => { } }; @@ -146,6 +147,35 @@ private global::MyTagHelper __MyTagHelper = null; Assert.Equal( @"__TestNamespace_MyTagHelper = CreateTagHelper(); __tagHelperExecutionContext.Add(__TestNamespace_MyTagHelper); +", + csharp, + ignoreLineEndingDifferences: true); + } + + [Fact] + public void WriteExecuteTagHelpers_RendersCorrectly() + { + // Arrange + var writer = new RuntimeTagHelperWriter(); + var context = new CSharpRenderingContext() + { + Writer = new Legacy.CSharpCodeWriter(), + }; + var node = new ExecuteTagHelpersIRNode(); + + // Act + writer.WriteExecuteTagHelpers(context, node); + + // Assert + var csharp = context.Writer.Builder.ToString(); + Assert.Equal( +@"await __tagHelperRunner.RunAsync(__tagHelperExecutionContext); +if (!__tagHelperExecutionContext.Output.IsContentModified) +{ + await __tagHelperExecutionContext.SetOutputContentAsync(); +} +Write(__tagHelperExecutionContext.Output); +__tagHelperExecutionContext = __tagHelperScopeManager.End(); ", csharp, ignoreLineEndingDifferences: true); diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs index 2116bf195f..c2999234c1 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs @@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration var context = new CSharpRenderingContext() { BasicWriter = new RuntimeBasicWriter(), + TagHelperWriter = new RuntimeTagHelperWriter(), Writer = new CSharpCodeWriter(), };