From 7a1a6dd1d6726aa9a7b531bfa405d6c829eead1e Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Mon, 13 Feb 2017 15:00:00 -0800 Subject: [PATCH] Part 2 of RuntimeTarget Introducing ExtensionIRNode and an implementation of templates based on the new feature set. Now TemplateIRNode is-a ExtensionIRNode. It's implemented using just extensibility and isn't part of the standard razor codegen. I'm adding it to the RazorEngine so that it's still there by default. I've also included a pattern for visitors to special case ExtensionIRNode-derived classes that they know about. This requires a little bit of boilerplate but makes it easy to traverse just the nodes you care about while keeping the set of nodes open. For now the general codegen feature still hasn't had a refactor, but this opens things up for us to start finishing things like MVC's @inject directive. --- .../CSharpRedirectRenderingConventions.cs | 16 +- .../CodeGeneration/CSharpRenderingContext.cs | 3 + .../CodeGeneration/DefaultRuntimeTarget.cs | 34 +++- .../DefaultRuntimeTargetBuilder.cs | 7 +- .../DesignTimeCSharpRenderer.cs | 24 +-- .../CodeGeneration/IRuntimeTargetBuilder.cs | 4 + .../ITemplateTargetExtension.cs | 12 ++ .../PageStructureCSharpRenderer.cs | 9 +- .../CodeGeneration/RuntimeCSharpRenderer.cs | 24 +-- .../CodeGeneration/TemplateTargetExtension.cs | 34 ++++ .../DefaultRazorCSharpLoweringPhase.cs | 2 + .../DefaultRazorTargetExtensionFeature.cs | 15 ++ .../DocumentClassifierPassBase.cs | 26 ++- .../IRazorTargetExtensionFeature.cs | 13 ++ .../Intermediate/ExtensionIRNode.cs | 40 +++++ .../Intermediate/IExtensionIRNodeVisitor`1.cs | 10 ++ .../Intermediate/IExtensionIRNodeVisitor`2.cs | 10 ++ .../Intermediate/RazorIRNodeVisitor.cs | 2 +- .../Intermediate/RazorIRNodeVisitorOfT.cs | 2 +- .../Intermediate/TemplateIRNode.cs | 13 +- .../RazorEngine.cs | 8 + .../RazorEngineBuilderExtensions.cs | 33 +++- .../DefaultRuntimeTargetBuilderTest.cs | 24 ++- .../DefaultRuntimeTargetTest.cs | 140 ++++++++++++++- .../TemplateTargetExtensionTest.cs | 51 ++++++ .../DocumentClassifierPassBaseTest.cs | 52 ++++++ .../Intermediate/ExtensionIRNodeTest.cs | 168 ++++++++++++++++++ .../RazorEngineBuilderExtensionsTest.cs | 47 +++++ .../RazorEngineTest.cs | 15 ++ 29 files changed, 765 insertions(+), 73 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/ITemplateTargetExtension.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorTargetExtensionFeature.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/IRazorTargetExtensionFeature.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ExtensionIRNode.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`1.cs create mode 100644 src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`2.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs create mode 100644 test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/ExtensionIRNodeTest.cs diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRedirectRenderingConventions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRedirectRenderingConventions.cs index 85c1d8b998..138216fb4b 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRedirectRenderingConventions.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRedirectRenderingConventions.cs @@ -7,22 +7,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { internal class CSharpRedirectRenderingConventions : CSharpRenderingConventions { - private readonly string _redirectWriter; - public CSharpRedirectRenderingConventions(string redirectWriter, CSharpCodeWriter writer) : base(writer) { - _redirectWriter = redirectWriter; + RedirectWriter = redirectWriter; } - public override string StartWriteMethod => "WriteTo(" + _redirectWriter + ", " /* ORIGINAL: WriteToMethodName */; + public string RedirectWriter { get; } - public override string StartWriteLiteralMethod => "WriteLiteralTo(" + _redirectWriter + ", " /* ORIGINAL: WriteLiteralToMethodName */; + public override string StartWriteMethod => "WriteTo(" + RedirectWriter + ", " /* ORIGINAL: WriteToMethodName */; - public override string StartBeginWriteAttributeMethod => "BeginWriteAttributeTo(" + _redirectWriter + ", " /* ORIGINAL: BeginWriteAttributeToMethodName */; + public override string StartWriteLiteralMethod => "WriteLiteralTo(" + RedirectWriter + ", " /* ORIGINAL: WriteLiteralToMethodName */; - public override string StartWriteAttributeValueMethod => "WriteAttributeValueTo(" + _redirectWriter + ", " /* ORIGINAL: WriteAttributeValueToMethodName */; + public override string StartBeginWriteAttributeMethod => "BeginWriteAttributeTo(" + RedirectWriter + ", " /* ORIGINAL: BeginWriteAttributeToMethodName */; - public override string StartEndWriteAttributeMethod => "EndWriteAttributeTo(" + _redirectWriter /* ORIGINAL: EndWriteAttributeToMethodName */; + public override string StartWriteAttributeValueMethod => "WriteAttributeValueTo(" + RedirectWriter + ", " /* ORIGINAL: WriteAttributeValueToMethodName */; + + public override string StartEndWriteAttributeMethod => "EndWriteAttributeTo(" + RedirectWriter /* ORIGINAL: EndWriteAttributeToMethodName */; } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs index 7cdff0e5bd..ea0ef88004 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/CSharpRenderingContext.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { @@ -43,5 +44,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration public RazorParserOptions Options { get; set; } public TagHelperRenderingContext TagHelperRenderingContext { get; set; } + + public Action RenderChildren { get; set; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTarget.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTarget.cs index 48726f3df2..f529ebb7a9 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTarget.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTarget.cs @@ -1,7 +1,8 @@ // 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 System.Linq; namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { @@ -9,31 +10,52 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { private readonly RazorParserOptions _options; - public DefaultRuntimeTarget(RazorParserOptions options) + public DefaultRuntimeTarget(RazorParserOptions options, IEnumerable extensions) { _options = options; + Extensions = extensions.ToArray(); } + public IRuntimeTargetExtension[] Extensions { get; } + internal override PageStructureCSharpRenderer CreateRenderer(CSharpRenderingContext context) { if (_options.DesignTimeMode) { - return new DesignTimeCSharpRenderer(context); + return new DesignTimeCSharpRenderer(this, context); } else { - return new RuntimeCSharpRenderer(context); + return new RuntimeCSharpRenderer(this, context); } } public override TExtension GetExtension() { - throw new NotImplementedException(); + for (var i = 0; i < Extensions.Length; i++) + { + var match = Extensions[i] as TExtension; + if (match != null) + { + return match; + } + } + + return null; } public override bool HasExtension() { - throw new NotImplementedException(); + for (var i = 0; i < Extensions.Length; i++) + { + var match = Extensions[i] as TExtension; + if (match != null) + { + return true; + } + } + + return false; } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTargetBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTargetBuilder.cs index 01e92da500..eda7a9998e 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTargetBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DefaultRuntimeTargetBuilder.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections.Generic; namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { @@ -11,15 +12,19 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { CodeDocument = codeDocument; Options = options; + + TargetExtensions = new List(); } public RazorCodeDocument CodeDocument { get; } public RazorParserOptions Options { get; } + public ICollection TargetExtensions { get; } + public RuntimeTarget Build() { - return new DefaultRuntimeTarget(Options); + return new DefaultRuntimeTarget(Options, TargetExtensions); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs index cbb1a742eb..46ee7b2d06 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/DesignTimeCSharpRenderer.cs @@ -9,7 +9,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { internal class DesignTimeCSharpRenderer : PageStructureCSharpRenderer { - public DesignTimeCSharpRenderer(CSharpRenderingContext context) : base(context) + public DesignTimeCSharpRenderer(RuntimeTarget target, CSharpRenderingContext context) + : base(target, context) { } @@ -150,27 +151,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration Context.Writer.WriteLine("))();"); } - public override void VisitTemplate(TemplateIRNode node) - { - const string ItemParameterName = "item"; - const string TemplateWriterName = "__razor_template_writer"; - - Context.Writer - .Write(ItemParameterName).Write(" => ") - .WriteStartNewObject("Microsoft.AspNetCore.Mvc.Razor.HelperResult" /* ORIGINAL: TemplateTypeName */); - - var initialRenderingConventions = Context.RenderingConventions; - var redirectConventions = new CSharpRedirectRenderingConventions(TemplateWriterName, Context.Writer); - Context.RenderingConventions = redirectConventions; - using (Context.Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) - { - VisitDefault(node); - } - Context.RenderingConventions = initialRenderingConventions; - - Context.Writer.WriteEndMethodInvocation(endLine: false); - } - public override void VisitTagHelper(TagHelperIRNode node) { var initialTagHelperRenderingContext = Context.TagHelperRenderingContext; diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/IRuntimeTargetBuilder.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/IRuntimeTargetBuilder.cs index 65cc959e15..d67b2eeb48 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/IRuntimeTargetBuilder.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/IRuntimeTargetBuilder.cs @@ -1,6 +1,8 @@ // 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.Collections.Generic; + namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { public interface IRuntimeTargetBuilder @@ -9,6 +11,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration RazorParserOptions Options { get; } + ICollection TargetExtensions { get; } + RuntimeTarget Build(); } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/ITemplateTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/ITemplateTargetExtension.cs new file mode 100644 index 0000000000..c8f7cfd254 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/ITemplateTargetExtension.cs @@ -0,0 +1,12 @@ +// 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 interface ITemplateTargetExtension : IRuntimeTargetExtension + { + void WriteTemplate(CSharpRenderingContext context, TemplateIRNode node); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/PageStructureCSharpRenderer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/PageStructureCSharpRenderer.cs index ade3a7a95b..dde8a540d7 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/PageStructureCSharpRenderer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/PageStructureCSharpRenderer.cs @@ -9,10 +9,12 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration internal class PageStructureCSharpRenderer : RazorIRNodeWalker { protected readonly CSharpRenderingContext Context; + protected readonly RuntimeTarget Target; - public PageStructureCSharpRenderer(CSharpRenderingContext context) + public PageStructureCSharpRenderer(RuntimeTarget target, CSharpRenderingContext context) { Context = context; + Target = target; } public override void VisitNamespace(NamespaceDeclarationIRNode node) @@ -107,6 +109,11 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration } } + public override void VisitExtension(ExtensionIRNode node) + { + node.WriteNode(Target, Context); + } + protected static void RenderExpressionInline(RazorIRNode node, CSharpRenderingContext context) { if (node is CSharpTokenIRNode) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs index bf01f6a366..71d48c265d 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/RuntimeCSharpRenderer.cs @@ -12,8 +12,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { internal class RuntimeCSharpRenderer : PageStructureCSharpRenderer { - public RuntimeCSharpRenderer(CSharpRenderingContext context) - : base(context) + public RuntimeCSharpRenderer(RuntimeTarget target, CSharpRenderingContext context) + : base(target, context) { } @@ -213,26 +213,6 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration } } - public override void VisitTemplate(TemplateIRNode node) - { - const string ItemParameterName = "item"; - const string TemplateWriterName = "__razor_template_writer"; - - Context.Writer - .Write(ItemParameterName).Write(" => ") - .WriteStartNewObject("Microsoft.AspNetCore.Mvc.Razor.HelperResult" /* ORIGINAL: TemplateTypeName */); - - var initialRenderingConventions = Context.RenderingConventions; - Context.RenderingConventions = new CSharpRedirectRenderingConventions(TemplateWriterName, Context.Writer); - using (Context.Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) - { - VisitDefault(node); - } - Context.RenderingConventions = initialRenderingConventions; - - Context.Writer.WriteEndMethodInvocation(endLine: false); - } - public override void VisitTagHelper(TagHelperIRNode node) { var initialTagHelperRenderingContext = Context.TagHelperRenderingContext; diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs new file mode 100644 index 0000000000..bfea04169f --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/CodeGeneration/TemplateTargetExtension.cs @@ -0,0 +1,34 @@ +// 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 TemplateTargetExtension : ITemplateTargetExtension + { + public static readonly string DefaultTemplateTypeName = "Microsoft.AspNetCore.Mvc.Razor.HelperResult"; + + public string TemplateTypeName { get; set; } = DefaultTemplateTypeName; + + public void WriteTemplate(CSharpRenderingContext context, TemplateIRNode node) + { + const string ItemParameterName = "item"; + const string TemplateWriterName = "__razor_template_writer"; + + context.Writer + .Write(ItemParameterName).Write(" => ") + .WriteStartNewObject(TemplateTypeName); + + var initialRenderingConventions = context.RenderingConventions; + context.RenderingConventions = new CSharpRedirectRenderingConventions(TemplateWriterName, context.Writer); + using (context.Writer.BuildAsyncLambda(endLine: false, parameterNames: TemplateWriterName)) + { + context.RenderChildren(node); + } + context.RenderingConventions = initialRenderingConventions; + + context.Writer.WriteEndMethodInvocation(endLine: false); + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs index cffe510cf4..d03a2d852b 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorCSharpLoweringPhase.cs @@ -56,6 +56,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution } var renderer = target.CreateRenderer(renderingContext); + renderingContext.RenderChildren = renderer.VisitDefault; + renderer.VisitDocument(irDocument); var combinedErrors = syntaxTree.Diagnostics.Concat(renderingContext.ErrorSink.Errors).ToList(); diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorTargetExtensionFeature.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorTargetExtensionFeature.cs new file mode 100644 index 0000000000..2a8d8ff797 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DefaultRazorTargetExtensionFeature.cs @@ -0,0 +1,15 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + internal class DefaultRazorTargetExtensionFeature : IRazorTargetExtensionFeature + { + public RazorEngine Engine { get; set; } + + public ICollection TargetExtensions { get; } = new List(); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs b/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs index 166bdaecee..64066420d8 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/DocumentClassifierPassBase.cs @@ -2,15 +2,27 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Collections.Generic; -using Microsoft.AspNetCore.Razor.Evolution.Intermediate; +using System.Linq; using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; +using Microsoft.AspNetCore.Razor.Evolution.Intermediate; namespace Microsoft.AspNetCore.Razor.Evolution { public abstract class DocumentClassifierPassBase : RazorIRPassBase, IRazorDocumentClassifierPass { + private static readonly IRuntimeTargetExtension[] EmptyExtensionArray = new IRuntimeTargetExtension[0]; + protected abstract string DocumentKind { get; } + protected IRuntimeTargetExtension[] TargetExtensions { get; private set; } + + protected override void OnIntialized() + { + var feature = Engine.Features.OfType(); + + TargetExtensions = feature.FirstOrDefault()?.TargetExtensions.ToArray() ?? EmptyExtensionArray; + } + public sealed override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIRNode irDocument) { if (irDocument.DocumentKind != null) @@ -82,10 +94,18 @@ namespace Microsoft.AspNetCore.Razor.Evolution private RuntimeTarget CreateTarget(RazorCodeDocument codeDocument, RazorParserOptions options) { - return RuntimeTarget.CreateDefault(codeDocument, options, (builder) => ConfigureTarget(codeDocument, builder)); + return RuntimeTarget.CreateDefault(codeDocument, options, (builder) => + { + for (var i = 0; i < TargetExtensions.Length; i++) + { + builder.TargetExtensions.Add(TargetExtensions[i]); + } + + ConfigureTarget(builder); + }); } - protected virtual void ConfigureTarget(RazorCodeDocument codeDocument, IRuntimeTargetBuilder builder) + protected virtual void ConfigureTarget(IRuntimeTargetBuilder builder) { // Intentionally empty. } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/IRazorTargetExtensionFeature.cs b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorTargetExtensionFeature.cs new file mode 100644 index 0000000000..515c7fbe75 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/IRazorTargetExtensionFeature.cs @@ -0,0 +1,13 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; + +namespace Microsoft.AspNetCore.Razor.Evolution +{ + public interface IRazorTargetExtensionFeature : IRazorEngineFeature + { + ICollection TargetExtensions { get; } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ExtensionIRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ExtensionIRNode.cs new file mode 100644 index 0000000000..11644fc968 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/ExtensionIRNode.cs @@ -0,0 +1,40 @@ +// 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.CodeGeneration; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public abstract class ExtensionIRNode : RazorIRNode + { + internal abstract void WriteNode(RuntimeTarget target, CSharpRenderingContext context); + + protected static void AcceptExtensionNode(TNode node, RazorIRNodeVisitor visitor) + where TNode : ExtensionIRNode + { + var typedVisitor = visitor as IExtensionIRNodeVisitor; + if (typedVisitor == null) + { + visitor.VisitExtension(node); + } + else + { + typedVisitor.VisitExtension(node); + } + } + + protected static TResult AcceptExtensionNode(TNode node, RazorIRNodeVisitor visitor) + where TNode : ExtensionIRNode + { + var typedVisitor = visitor as IExtensionIRNodeVisitor; + if (typedVisitor == null) + { + return visitor.VisitExtension(node); + } + else + { + return typedVisitor.VisitExtension(node); + } + } + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`1.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`1.cs new file mode 100644 index 0000000000..1ef2d3f420 --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`1.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public interface IExtensionIRNodeVisitor where TNode : ExtensionIRNode + { + void VisitExtension(TNode node); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`2.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`2.cs new file mode 100644 index 0000000000..bebd0ace8e --- /dev/null +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/IExtensionIRNodeVisitor`2.cs @@ -0,0 +1,10 @@ +// 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. + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + public interface IExtensionIRNodeVisitor where TNode : ExtensionIRNode + { + TResult VisitExtension(TNode node); + } +} diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs index 456140b6b8..263f4341bd 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitor.cs @@ -29,7 +29,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate VisitDefault(node); } - public virtual void VisitTemplate(TemplateIRNode node) + public virtual void VisitExtension(ExtensionIRNode node) { VisitDefault(node); } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs index 400f02e9c1..b8f9a6d33b 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/RazorIRNodeVisitorOfT.cs @@ -30,7 +30,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate return VisitDefault(node); } - public virtual TResult VisitTemplate(TemplateIRNode node) + public virtual TResult VisitExtension(ExtensionIRNode node) { return VisitDefault(node); } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/TemplateIRNode.cs b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/TemplateIRNode.cs index 9d2a653cc3..baebcfe841 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/TemplateIRNode.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/Intermediate/TemplateIRNode.cs @@ -3,10 +3,11 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate { - public class TemplateIRNode : RazorIRNode + public class TemplateIRNode : ExtensionIRNode { public override IList Children { get; } = new List(); @@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate throw new ArgumentNullException(nameof(visitor)); } - visitor.VisitTemplate(this); + AcceptExtensionNode(this, visitor); } public override TResult Accept(RazorIRNodeVisitor visitor) @@ -31,7 +32,13 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate throw new ArgumentNullException(nameof(visitor)); } - return visitor.VisitTemplate(this); + return AcceptExtensionNode(this, visitor); + } + + internal override void WriteNode(RuntimeTarget target, CSharpRenderingContext context) + { + var extension = target.GetExtension(); + extension.WriteTemplate(context, this); } } } diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs index 56c91966be..c20352a147 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngine.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; namespace Microsoft.AspNetCore.Razor.Evolution { @@ -56,6 +57,10 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Phases.Add(new DefaultRazorIROptimizationPhase()); builder.Phases.Add(new DefaultRazorCSharpLoweringPhase()); + // General extensibility + builder.Features.Add(new DefaultRazorDirectiveFeature()); + builder.Features.Add(new DefaultRazorTargetExtensionFeature()); + // Syntax Tree passes builder.Features.Add(new DefaultDirectiveSyntaxTreePass()); builder.Features.Add(new HtmlNodeOptimizationPass()); @@ -65,6 +70,9 @@ namespace Microsoft.AspNetCore.Razor.Evolution builder.Features.Add(new DefaultDocumentClassifierPass()); builder.Features.Add(new DefaultDirectiveIRPass()); builder.Features.Add(new DirectiveRemovalIROptimizationPass()); + + // Default Runtime Targets + builder.AddTargetExtension(new TemplateTargetExtension()); } internal static void AddRuntimeDefaults(IRazorEngineBuilder builder) diff --git a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs index f0f6fe9751..6effde8441 100644 --- a/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs +++ b/src/Microsoft.AspNetCore.Razor.Evolution/RazorEngineBuilderExtensions.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; namespace Microsoft.AspNetCore.Razor.Evolution { @@ -12,7 +13,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution { if (builder == null) { - throw new ArgumentNullException(nameof(directive)); + throw new ArgumentNullException(nameof(builder)); } if (directive == null) @@ -26,6 +27,24 @@ namespace Microsoft.AspNetCore.Razor.Evolution return builder; } + public static IRazorEngineBuilder AddTargetExtension(this IRazorEngineBuilder builder, IRuntimeTargetExtension extension) + { + if (builder == null) + { + throw new ArgumentNullException(nameof(builder)); + } + + if (extension == null) + { + throw new ArgumentNullException(nameof(extension)); + } + + var targetExtensionFeature = GetTargetExtensionFeature(builder); + targetExtensionFeature.TargetExtensions.Add(extension); + + return builder; + } + private static IRazorDirectiveFeature GetDirectiveFeature(IRazorEngineBuilder builder) { var directiveFeature = builder.Features.OfType().FirstOrDefault(); @@ -37,5 +56,17 @@ namespace Microsoft.AspNetCore.Razor.Evolution return directiveFeature; } + + private static IRazorTargetExtensionFeature GetTargetExtensionFeature(IRazorEngineBuilder builder) + { + var targetExtensionFeature = builder.Features.OfType().FirstOrDefault(); + if (targetExtensionFeature == null) + { + targetExtensionFeature = new DefaultRazorTargetExtensionFeature(); + builder.Features.Add(targetExtensionFeature); + } + + return targetExtensionFeature; + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetBuilderTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetBuilderTest.cs index 58f993855b..eae4b36ec9 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetBuilderTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetBuilderTest.cs @@ -16,11 +16,31 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration var builder = new DefaultRuntimeTargetBuilder(codeDocument, options); + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension1(), + new MyExtension2(), + }; + + for (var i = 0; i < extensions.Length; i++) + { + builder.TargetExtensions.Add(extensions[i]); + } + // Act - var target = builder.Build(); + var result = builder.Build(); // Assert - Assert.IsType(target); + var target = Assert.IsType(result); + Assert.Equal(extensions, target.Extensions); + } + + private class MyExtension1 : IRuntimeTargetExtension + { + } + + private class MyExtension2 : IRuntimeTargetExtension + { } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetTest.cs index 724c4a8e94..0d0cd35073 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/DefaultRuntimeTargetTest.cs @@ -1,12 +1,32 @@ // 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.Linq; using Xunit; namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration { public class DefaultRuntimeTargetTest { + [Fact] + public void Constructor_CreatesDefensiveCopy() + { + // Arrange + var options = RazorParserOptions.CreateDefaultOptions(); + + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension2(), + new MyExtension1(), + }; + + // Act + var target = new DefaultRuntimeTarget(options, extensions); + + // Assert + Assert.NotSame(extensions, target); + } + [Fact] public void CreateRenderer_DesignTime_CreatesDesignTimeRenderer() { @@ -14,7 +34,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration var options = RazorParserOptions.CreateDefaultOptions(); options.DesignTimeMode = true; - var target = new DefaultRuntimeTarget(options); + var target = new DefaultRuntimeTarget(options, Enumerable.Empty()); // Act var renderer = target.CreateRenderer(new CSharpRenderingContext()); @@ -30,7 +50,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration var options = RazorParserOptions.CreateDefaultOptions(); options.DesignTimeMode = false; - var target = new DefaultRuntimeTarget(options); + var target = new DefaultRuntimeTarget(options, Enumerable.Empty()); // Act var renderer = target.CreateRenderer(new CSharpRenderingContext()); @@ -38,5 +58,121 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration // Assert Assert.IsType(renderer); } + + [Fact] + public void HasExtension_ReturnsTrue_WhenExtensionFound() + { + // Arrange + var options = RazorParserOptions.CreateDefaultOptions(); + + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension2(), + new MyExtension1(), + }; + + var target = new DefaultRuntimeTarget(options, extensions); + + // Act + var result = target.HasExtension(); + + // Assert + Assert.True(result); + } + + [Fact] + public void HasExtension_ReturnsFalse_WhenExtensionNotFound() + { + // Arrange + var options = RazorParserOptions.CreateDefaultOptions(); + + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension2(), + new MyExtension2(), + }; + + var target = new DefaultRuntimeTarget(options, extensions); + + // Act + var result = target.HasExtension(); + + // Assert + Assert.False(result); + } + + [Fact] + public void GetExtension_ReturnsExtension_WhenExtensionFound() + { + // Arrange + var options = RazorParserOptions.CreateDefaultOptions(); + + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension2(), + new MyExtension1(), + }; + + var target = new DefaultRuntimeTarget(options, extensions); + + // Act + var result = target.GetExtension(); + + // Assert + Assert.Same(extensions[1], result); + } + + [Fact] + public void GetExtension_ReturnsFirstMatch_WhenExtensionFound() + { + // Arrange + var options = RazorParserOptions.CreateDefaultOptions(); + + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension2(), + new MyExtension1(), + new MyExtension2(), + new MyExtension1(), + }; + + var target = new DefaultRuntimeTarget(options, extensions); + + // Act + var result = target.GetExtension(); + + // Assert + Assert.Same(extensions[1], result); + } + + + [Fact] + public void GetExtension_ReturnsNull_WhenExtensionNotFound() + { + // Arrange + var options = RazorParserOptions.CreateDefaultOptions(); + + var extensions = new IRuntimeTargetExtension[] + { + new MyExtension2(), + new MyExtension2(), + }; + + var target = new DefaultRuntimeTarget(options, extensions); + + // Act + var result = target.GetExtension(); + + // Assert + Assert.Null(result); + } + + private class MyExtension1 : IRuntimeTargetExtension + { + } + + private class MyExtension2 : IRuntimeTargetExtension + { + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs new file mode 100644 index 0000000000..a22e14916e --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/CodeGeneration/TemplateTargetExtensionTest.cs @@ -0,0 +1,51 @@ +// 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; +using Microsoft.AspNetCore.Razor.Evolution.Legacy; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration +{ + public class TemplateTargetExtensionTest + { + [Fact] + public void WriteTemplate_WritesTemplateCode() + { + // Arrange + var node = new TemplateIRNode(); + + var extension = new TemplateTargetExtension() + { + TemplateTypeName = "global::TestTemplate", + }; + + var context = new CSharpRenderingContext() + { + Writer = new CSharpCodeWriter(), + }; + + context.RenderChildren = (n) => + { + Assert.Same(node, n); + + var conventions = Assert.IsType(context.RenderingConventions); + Assert.Equal("__razor_template_writer", conventions.RedirectWriter); + + context.Writer.Write(" var s = \"Inside\""); + }; + + // Act + extension.WriteTemplate(context, node); + + // Assert + var expected = @"item => new global::TestTemplate(async(__razor_template_writer) => { + var s = ""Inside"" +} +)"; + + var output = context.Writer.Builder.ToString(); + Assert.Equal(expected, output); + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs index 3fcd234496..d957159f93 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/DocumentClassifierPassBaseTest.cs @@ -3,6 +3,8 @@ using System; +using System.Linq; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; using Microsoft.AspNetCore.Razor.Evolution.Intermediate; using Xunit; using static Microsoft.AspNetCore.Razor.Evolution.Intermediate.RazorIRAssert; @@ -55,6 +57,41 @@ namespace Microsoft.AspNetCore.Razor.Evolution NoChildren(irDocument); } + [Fact] + public void Execute_Match_AddsGlobalTargetExtensions() + { + // Arrange + var irDocument = new DocumentIRNode() + { + Options = RazorParserOptions.CreateDefaultOptions(), + }; + + var expected = new IRuntimeTargetExtension[] + { + new MyExtension1(), + new MyExtension2(), + }; + + var pass = new TestDocumentClassifierPass(); + pass.Engine = RazorEngine.CreateEmpty(b => + { + for (var i = 0; i < expected.Length; i++) + { + b.AddTargetExtension(expected[i]); + } + }); + + IRuntimeTargetExtension[] extensions = null; + + pass.RuntimeTargetCallback = (builder) => extensions = builder.TargetExtensions.ToArray(); + + // Act + pass.Execute(TestRazorCodeDocument.CreateEmpty(), irDocument); + + // Assert + Assert.Equal(expected, extensions); + } + [Fact] public void Execute_Match_SetsDocumentType_AndCreatesStructure() { @@ -228,6 +265,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution public bool ShouldMatch { get; set; } = true; + public Action RuntimeTargetCallback { get; set; } + public string Namespace { get; set; } public string Class { get; set; } @@ -251,6 +290,19 @@ namespace Microsoft.AspNetCore.Razor.Evolution @class.Name = Class; @method.Name = Method; } + + protected override void ConfigureTarget(IRuntimeTargetBuilder builder) + { + RuntimeTargetCallback?.Invoke(builder); + } + } + + private class MyExtension1 : IRuntimeTargetExtension + { + } + + private class MyExtension2 : IRuntimeTargetExtension + { } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/ExtensionIRNodeTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/ExtensionIRNodeTest.cs new file mode 100644 index 0000000000..197621214a --- /dev/null +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/Intermediate/ExtensionIRNodeTest.cs @@ -0,0 +1,168 @@ +// 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.CodeGeneration; +using Xunit; + +namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate +{ + // These tests cover the methods on ExtensionIRNode that are used to implement visitors + // that special case an extension node. + public class ExtensionIRNodeTest + { + [Fact] + public void Accept_CallsStandardVisitExtension_ForStandardVisitor() + { + // Arrange + var node = new TestExtensionIRNode(); + var visitor = new StandardVisitor(); + + // Act + node.Accept(visitor); + + // Assert + Assert.True(visitor.WasStandardMethodCalled); + Assert.False(visitor.WasSpecificMethodCalled); + } + + [Fact] + public void Accept_CallsSpecialVisitExtension_ForSpecialVisitor() + { + // Arrange + var node = new TestExtensionIRNode(); + var visitor = new SpecialVisitor(); + + // Act + node.Accept(visitor); + + // Assert + Assert.False(visitor.WasStandardMethodCalled); + Assert.True(visitor.WasSpecificMethodCalled); + } + + [Fact] + public void Accept_TResult_CallsStandardVisitExtension_ForStandardVisitor() + { + // Arrange + var node = new TestExtensionIRNode(); + var visitor = new StandardVisitor(); + + // Act + node.Accept(visitor); + + // Assert + Assert.True(visitor.WasStandardMethodCalled); + Assert.False(visitor.WasSpecificMethodCalled); + } + + [Fact] + public void Accept_TResult_CallsSpecialVisitExtension_ForSpecialVisitor() + { + // Arrange + var node = new TestExtensionIRNode(); + var visitor = new SpecialVisitor(); + + // Act + node.Accept(visitor); + + // Assert + Assert.False(visitor.WasStandardMethodCalled); + Assert.True(visitor.WasSpecificMethodCalled); + } + + private class TestExtensionIRNode : ExtensionIRNode + { + public override IList Children => throw new NotImplementedException(); + + public override RazorIRNode Parent { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + public override SourceSpan? Source { get => throw new NotImplementedException(); set => throw new NotImplementedException(); } + + public override void Accept(RazorIRNodeVisitor visitor) + { + // This is the standard visitor boilerplate for an extension node. + AcceptExtensionNode(this, visitor); + } + + public override TResult Accept(RazorIRNodeVisitor visitor) + { + // This is the standard visitor boilerplate for an extension node. + return AcceptExtensionNode(this, visitor); + } + + internal override void WriteNode(RuntimeTarget target, CSharpRenderingContext context) + { + throw new NotImplementedException(); + } + } + + private class StandardVisitor : RazorIRNodeVisitor + { + public bool WasStandardMethodCalled { get; private set; } + public bool WasSpecificMethodCalled { get; private set; } + + public override void VisitExtension(ExtensionIRNode node) + { + WasStandardMethodCalled = true; + } + + public void VisitExtension(TestExtensionIRNode node) + { + WasSpecificMethodCalled = true; + } + } + + private class StandardVisitor : RazorIRNodeVisitor + { + public bool WasStandardMethodCalled { get; private set; } + public bool WasSpecificMethodCalled { get; private set; } + + public override T VisitExtension(ExtensionIRNode node) + { + WasStandardMethodCalled = true; + return default(T); + } + + public T VisitExtension(TestExtensionIRNode node) + { + WasSpecificMethodCalled = true; + return default(T); + } + } + + private class SpecialVisitor : RazorIRNodeVisitor, IExtensionIRNodeVisitor + { + public bool WasStandardMethodCalled { get; private set; } + public bool WasSpecificMethodCalled { get; private set; } + + public override void VisitExtension(ExtensionIRNode node) + { + WasStandardMethodCalled = true; + } + + public void VisitExtension(TestExtensionIRNode node) + { + WasSpecificMethodCalled = true; + } + } + + private class SpecialVisitor : RazorIRNodeVisitor, IExtensionIRNodeVisitor + { + public bool WasStandardMethodCalled { get; private set; } + public bool WasSpecificMethodCalled { get; private set; } + + public override T VisitExtension(ExtensionIRNode node) + { + WasStandardMethodCalled = true; + return default(T); + } + + public T VisitExtension(TestExtensionIRNode node) + { + WasSpecificMethodCalled = true; + return default(T); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs index 1078789e74..a48c0f87c1 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineBuilderExtensionsTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Linq; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; using Xunit; namespace Microsoft.AspNetCore.Razor.Evolution @@ -29,6 +30,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution Assert.Equal("test_directive", directive.Name); } + [Fact] public void AddDirective_NoFeature_CreatesFeature() { // Arrange @@ -45,5 +47,50 @@ namespace Microsoft.AspNetCore.Razor.Evolution var directive = Assert.Single(actual.Directives); Assert.Equal("test_directive", directive.Name); } + + [Fact] + public void AddTargetExtensions_ExistingFeature_UsesFeature() + { + // Arrange + var extension = new MyTargetExtension(); + + var expected = new DefaultRazorTargetExtensionFeature(); + var engine = RazorEngine.CreateEmpty(b => + { + b.Features.Add(expected); + + // Act + b.AddTargetExtension(extension); + }); + + // Assert + var actual = Assert.Single(engine.Features.OfType()); + Assert.Same(expected, actual); + + Assert.Same(extension, Assert.Single(actual.TargetExtensions)); + } + + [Fact] + public void AddTargetExtensions_NoFeature_CreatesFeature() + { + // Arrange + var extension = new MyTargetExtension(); + + var engine = RazorEngine.CreateEmpty(b => + { + // Act + b.AddTargetExtension(extension); + }); + + // Assert + var actual = Assert.Single(engine.Features.OfType()); + Assert.IsType(actual); + + Assert.Same(extension, Assert.Single(actual.TargetExtensions)); + } + + private class MyTargetExtension : IRuntimeTargetExtension + { + } } } diff --git a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs index 433b502755..eac1ac9c75 100644 --- a/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs +++ b/test/Microsoft.AspNetCore.Razor.Evolution.Test/RazorEngineTest.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using Moq; using Xunit; +using Microsoft.AspNetCore.Razor.Evolution.CodeGeneration; namespace Microsoft.AspNetCore.Razor.Evolution { @@ -132,10 +133,22 @@ namespace Microsoft.AspNetCore.Razor.Evolution p => Assert.Same(phases[1], p)); } + private static void AssertDefaultTargetExtensions(RazorEngine engine) + { + var feature = engine.Features.OfType().FirstOrDefault(); + Assert.NotNull(feature); + + Assert.Collection( + feature.TargetExtensions, + f => Assert.IsType(f)); + } + private static void AssertDefaultRuntimeFeatures(IEnumerable features) { Assert.Collection( features, + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), @@ -162,6 +175,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution { Assert.Collection( features, + feature => Assert.IsType(feature), + feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature), feature => Assert.IsType(feature),