In RazorCompiler, support @inject directive

This commit is contained in:
Steve Sanderson 2018-02-22 22:51:12 +00:00
parent 3f9d358004
commit c408045e31
3 changed files with 178 additions and 8 deletions

View File

@ -25,6 +25,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
FunctionsDirective.Register(configure);
InheritsDirective.Register(configure);
InjectDirective.Register(configure);
TemporaryLayoutPass.Register(configure);
TemporaryImplementsPass.Register(configure);

View File

@ -0,0 +1,116 @@
// 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.Language;
using Microsoft.AspNetCore.Razor.Language.CodeGeneration;
using Microsoft.AspNetCore.Razor.Language.Intermediate;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Microsoft.AspNetCore.Blazor.Razor
{
// Much of the following is equivalent to Microsoft.AspNetCore.Mvc.Razor.Extensions's InjectDirective,
// but this one outputs properties annotated for Blazor's property injector, plus it doesn't need to
// support multiple CodeTargets.
internal class InjectDirective
{
const string InjectAttributeTypeName = "Microsoft.AspNetCore.Blazor.Components.InjectAttribute";
public static readonly DirectiveDescriptor Directive = DirectiveDescriptor.CreateDirective(
"inject",
DirectiveKind.SingleLine,
builder =>
{
builder.Tokens.Add(DirectiveTokenDescriptor.CreateToken(
DirectiveTokenKind.Type, optional: false));
builder.Tokens.Add(DirectiveTokenDescriptor.CreateToken(
DirectiveTokenKind.Member, optional: false));
builder.Usage = DirectiveUsage.FileScopedMultipleOccurring;
builder.Description = "Inject a service from the application's service container into a property.";
});
public static void Register(IRazorEngineBuilder builder)
{
builder.AddDirective(Directive);
builder.Features.Add(new Pass());
}
private class Pass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
{
protected override void ExecuteCore(
RazorCodeDocument codeDocument,
DocumentIntermediateNode documentNode)
{
var visitor = new Visitor();
visitor.Visit(documentNode);
var properties = new HashSet<string>(StringComparer.Ordinal);
var classNode = documentNode.FindPrimaryClass();
for (var i = visitor.Directives.Count - 1; i >= 0; i--)
{
var directive = visitor.Directives[i];
var tokens = directive.Tokens.ToArray();
if (tokens.Length < 2)
{
continue;
}
var typeName = tokens[0].Content;
var memberName = tokens[1].Content;
if (!properties.Add(memberName))
{
continue;
}
classNode.Children.Add(new InjectIntermediateNode(typeName, memberName));
}
}
private class Visitor : IntermediateNodeWalker
{
public IList<DirectiveIntermediateNode> Directives { get; }
= new List<DirectiveIntermediateNode>();
public override void VisitDirective(DirectiveIntermediateNode node)
{
if (node.Directive == Directive)
{
Directives.Add(node);
}
}
}
internal class InjectIntermediateNode : ExtensionIntermediateNode
{
private static readonly IList<string> _injectedPropertyModifiers = new[]
{
$"[global::{InjectAttributeTypeName}]",
"private" // Encapsulation is the default
};
public string TypeName { get; }
public string MemberName { get; }
public override IntermediateNodeCollection Children => IntermediateNodeCollection.ReadOnly;
public InjectIntermediateNode(string typeName, string memberName)
{
TypeName = typeName;
MemberName = memberName;
}
public override void Accept(IntermediateNodeVisitor visitor)
=> AcceptExtensionNode(this, visitor);
public override void WriteNode(CodeTarget target, CodeRenderingContext context)
=> context.CodeWriter.WriteAutoPropertyDeclaration(
_injectedPropertyModifiers,
TypeName,
MemberName);
}
}
}
}

View File

@ -366,7 +366,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void SupportsChildComponentsViaTemporarySyntax()
{
// Arrange/Act
var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent($"<c:{testComponentTypeName} />");
var frames = GetRenderTree(component);
@ -379,8 +379,8 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void CanPassParametersToComponents()
{
// Arrange/Act
var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
var testObjectTypeName = typeof(SomeType).FullName.Replace('+', '.');
var testComponentTypeName = FullTypeName<TestComponent>();
var testObjectTypeName = FullTypeName<SomeType>();
// TODO: Once we have the improved component tooling and can allow syntax
// like StringProperty="My string" or BoolProperty=true, update this
// test to use that syntax.
@ -444,7 +444,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void CanIncludeChildrenInComponents()
{
// Arrange/Act
var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent($"<c:{testComponentTypeName} MyAttr=@(\"abc\")>" +
$"Some text" +
$"<some-child a='1'>Nested text</some-child>" +
@ -470,7 +470,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void CanNestComponentChildContent()
{
// Arrange/Act
var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
var testComponentTypeName = FullTypeName<TestComponent>();
var component = CompileToComponent(
$"<c:{testComponentTypeName}>" +
$"<c:{testComponentTypeName}>" +
@ -514,7 +514,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void SupportsLayoutDeclarationsViaTemporarySyntax()
{
// Arrange/Act
var testComponentTypeName = typeof(TestLayout).FullName.Replace('+', '.');
var testComponentTypeName = FullTypeName<TestLayout>();
var component = CompileToComponent(
$"@(Layout<{testComponentTypeName}>())\n" +
$"Hello");
@ -532,7 +532,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void SupportsImplementsDeclarationsViaTemporarySyntax()
{
// Arrange/Act
var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.');
var testInterfaceTypeName = FullTypeName<ITestInterface>();
var component = CompileToComponent(
$"@(Implements<{testInterfaceTypeName}>())\n" +
$"Hello");
@ -548,7 +548,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public void SupportsInheritsDirective()
{
// Arrange/Act
var testBaseClassTypeName = typeof(TestBaseClass).FullName.Replace('+', '.');
var testBaseClassTypeName = FullTypeName<TestBaseClass>();
var component = CompileToComponent(
$"@inherits {testBaseClassTypeName}" + Environment.NewLine +
$"Hello");
@ -616,6 +616,51 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
});
}
[Fact]
public void SupportsInjectDirective()
{
// Arrange/Act 1: Compilation
var componentType = CompileToComponent(
$"@inject {FullTypeName<IMyService1>()} MyService1\n" +
$"@inject {FullTypeName<IMyService2>()} MyService2\n" +
$"Hello from @MyService1 and @MyService2").GetType();
// Assert 1: Compiled type has correct properties
var propertyFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.NonPublic;
var injectableProperties = componentType.GetProperties(propertyFlags)
.Where(p => p.GetCustomAttribute<InjectAttribute>() != null);
Assert.Collection(injectableProperties.OrderBy(p => p.Name),
property =>
{
Assert.Equal("MyService1", property.Name);
Assert.Equal(typeof(IMyService1), property.PropertyType);
Assert.False(property.GetMethod.IsPublic);
Assert.False(property.SetMethod.IsPublic);
},
property =>
{
Assert.Equal("MyService2", property.Name);
Assert.Equal(typeof(IMyService2), property.PropertyType);
Assert.False(property.GetMethod.IsPublic);
Assert.False(property.SetMethod.IsPublic);
});
// Arrange/Act 2: DI-supplied component has correct behavior
var serviceProvider = new TestServiceProvider();
serviceProvider.AddService<IMyService1>(new MyService1Impl());
serviceProvider.AddService<IMyService2>(new MyService2Impl());
var componentFactory = new ComponentFactory(serviceProvider);
var component = componentFactory.InstantiateComponent(componentType);
var frames = GetRenderTree(component);
// Assert 2: Rendered component behaves correctly
Assert.Collection(frames,
frame => AssertFrame.Text(frame, "Hello from "),
frame => AssertFrame.Text(frame, typeof(MyService1Impl).FullName),
frame => AssertFrame.Text(frame, " and "),
frame => AssertFrame.Text(frame, typeof(MyService2Impl).FullName));
}
private static RenderTreeFrame[] GetRenderTree(IComponent component)
{
var renderer = new TestRenderer();
@ -789,5 +834,13 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
public class TestBaseClass : BlazorComponent { }
public class SomeType { }
public interface IMyService1 { }
public interface IMyService2 { }
public class MyService1Impl : IMyService1 { }
public class MyService2Impl : IMyService2 { }
private static string FullTypeName<T>()
=> typeof(T).FullName.Replace('+', '.');
}
}