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.
This commit is contained in:
Ryan Nowak 2017-02-13 15:00:00 -08:00
parent 4b2245eeb9
commit 7a1a6dd1d6
29 changed files with 765 additions and 73 deletions

View File

@ -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 */;
}
}

View File

@ -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<RazorIRNode> RenderChildren { get; set; }
}
}

View File

@ -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<IRuntimeTargetExtension> 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<TExtension>()
{
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<TExtension>()
{
throw new NotImplementedException();
for (var i = 0; i < Extensions.Length; i++)
{
var match = Extensions[i] as TExtension;
if (match != null)
{
return true;
}
}
return false;
}
}
}

View File

@ -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<IRuntimeTargetExtension>();
}
public RazorCodeDocument CodeDocument { get; }
public RazorParserOptions Options { get; }
public ICollection<IRuntimeTargetExtension> TargetExtensions { get; }
public RuntimeTarget Build()
{
return new DefaultRuntimeTarget(Options);
return new DefaultRuntimeTarget(Options, TargetExtensions);
}
}
}

View File

@ -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;

View File

@ -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<IRuntimeTargetExtension> TargetExtensions { get; }
RuntimeTarget Build();
}
}

View File

@ -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);
}
}

View File

@ -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)

View File

@ -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;

View File

@ -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);
}
}
}

View File

@ -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();

View File

@ -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<IRuntimeTargetExtension> TargetExtensions { get; } = new List<IRuntimeTargetExtension>();
}
}

View File

@ -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<IRazorTargetExtensionFeature>();
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.
}

View File

@ -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<IRuntimeTargetExtension> TargetExtensions { get; }
}
}

View File

@ -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>(TNode node, RazorIRNodeVisitor visitor)
where TNode : ExtensionIRNode
{
var typedVisitor = visitor as IExtensionIRNodeVisitor<TNode>;
if (typedVisitor == null)
{
visitor.VisitExtension(node);
}
else
{
typedVisitor.VisitExtension(node);
}
}
protected static TResult AcceptExtensionNode<TNode, TResult>(TNode node, RazorIRNodeVisitor<TResult> visitor)
where TNode : ExtensionIRNode
{
var typedVisitor = visitor as IExtensionIRNodeVisitor<TNode, TResult>;
if (typedVisitor == null)
{
return visitor.VisitExtension(node);
}
else
{
return typedVisitor.VisitExtension(node);
}
}
}
}

View File

@ -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<TNode> where TNode : ExtensionIRNode
{
void VisitExtension(TNode node);
}
}

View File

@ -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<TNode, TResult> where TNode : ExtensionIRNode
{
TResult VisitExtension(TNode node);
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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<RazorIRNode> Children { get; } = new List<RazorIRNode>();
@ -21,7 +22,7 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate
throw new ArgumentNullException(nameof(visitor));
}
visitor.VisitTemplate(this);
AcceptExtensionNode<TemplateIRNode>(this, visitor);
}
public override TResult Accept<TResult>(RazorIRNodeVisitor<TResult> visitor)
@ -31,7 +32,13 @@ namespace Microsoft.AspNetCore.Razor.Evolution.Intermediate
throw new ArgumentNullException(nameof(visitor));
}
return visitor.VisitTemplate(this);
return AcceptExtensionNode<TemplateIRNode, TResult>(this, visitor);
}
internal override void WriteNode(RuntimeTarget target, CSharpRenderingContext context)
{
var extension = target.GetExtension<ITemplateTargetExtension>();
extension.WriteTemplate(context, this);
}
}
}

View File

@ -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)

View File

@ -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<IRazorDirectiveFeature>().FirstOrDefault();
@ -37,5 +56,17 @@ namespace Microsoft.AspNetCore.Razor.Evolution
return directiveFeature;
}
private static IRazorTargetExtensionFeature GetTargetExtensionFeature(IRazorEngineBuilder builder)
{
var targetExtensionFeature = builder.Features.OfType<IRazorTargetExtensionFeature>().FirstOrDefault();
if (targetExtensionFeature == null)
{
targetExtensionFeature = new DefaultRazorTargetExtensionFeature();
builder.Features.Add(targetExtensionFeature);
}
return targetExtensionFeature;
}
}
}

View File

@ -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<DefaultRuntimeTarget>(target);
var target = Assert.IsType<DefaultRuntimeTarget>(result);
Assert.Equal(extensions, target.Extensions);
}
private class MyExtension1 : IRuntimeTargetExtension
{
}
private class MyExtension2 : IRuntimeTargetExtension
{
}
}
}

View File

@ -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<IRuntimeTargetExtension>());
// 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<IRuntimeTargetExtension>());
// Act
var renderer = target.CreateRenderer(new CSharpRenderingContext());
@ -38,5 +58,121 @@ namespace Microsoft.AspNetCore.Razor.Evolution.CodeGeneration
// Assert
Assert.IsType<RuntimeCSharpRenderer>(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<MyExtension1>();
// 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<MyExtension1>();
// 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<MyExtension1>();
// 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<MyExtension1>();
// 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<MyExtension1>();
// Assert
Assert.Null(result);
}
private class MyExtension1 : IRuntimeTargetExtension
{
}
private class MyExtension2 : IRuntimeTargetExtension
{
}
}
}

View File

@ -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<CSharpRedirectRenderingConventions>(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);
}
}
}

View File

@ -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<IRuntimeTargetBuilder> 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
{
}
}
}

View File

@ -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<bool>();
// 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<bool>();
// Act
node.Accept(visitor);
// Assert
Assert.False(visitor.WasStandardMethodCalled);
Assert.True(visitor.WasSpecificMethodCalled);
}
private class TestExtensionIRNode : ExtensionIRNode
{
public override IList<RazorIRNode> 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<TestExtensionIRNode>(this, visitor);
}
public override TResult Accept<TResult>(RazorIRNodeVisitor<TResult> visitor)
{
// This is the standard visitor boilerplate for an extension node.
return AcceptExtensionNode<TestExtensionIRNode, TResult>(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<T> : RazorIRNodeVisitor<T>
{
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<TestExtensionIRNode>
{
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<T> : RazorIRNodeVisitor<T>, IExtensionIRNodeVisitor<TestExtensionIRNode, T>
{
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);
}
}
}
}

View File

@ -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<IRazorTargetExtensionFeature>());
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<IRazorTargetExtensionFeature>());
Assert.IsType<DefaultRazorTargetExtensionFeature>(actual);
Assert.Same(extension, Assert.Single(actual.TargetExtensions));
}
private class MyTargetExtension : IRuntimeTargetExtension
{
}
}
}

View File

@ -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<IRazorTargetExtensionFeature>().FirstOrDefault();
Assert.NotNull(feature);
Assert.Collection(
feature.TargetExtensions,
f => Assert.IsType<TemplateTargetExtension>(f));
}
private static void AssertDefaultRuntimeFeatures(IEnumerable<IRazorEngineFeature> features)
{
Assert.Collection(
features,
feature => Assert.IsType<DefaultRazorDirectiveFeature>(feature),
feature => Assert.IsType<DefaultRazorTargetExtensionFeature>(feature),
feature => Assert.IsType<DefaultDirectiveSyntaxTreePass>(feature),
feature => Assert.IsType<HtmlNodeOptimizationPass>(feature),
feature => Assert.IsType<TagHelperBinderSyntaxTreePass>(feature),
@ -162,6 +175,8 @@ namespace Microsoft.AspNetCore.Razor.Evolution
{
Assert.Collection(
features,
feature => Assert.IsType<DefaultRazorDirectiveFeature>(feature),
feature => Assert.IsType<DefaultRazorTargetExtensionFeature>(feature),
feature => Assert.IsType<DefaultDirectiveSyntaxTreePass>(feature),
feature => Assert.IsType<HtmlNodeOptimizationPass>(feature),
feature => Assert.IsType<TagHelperBinderSyntaxTreePass>(feature),