diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs
index b93a95b7dd..cc7842a081 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs
@@ -23,6 +23,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
{
FunctionsDirective.Register(configure);
TemporaryLayoutPass.Register(configure);
+ TemporaryImplementsPass.Register(configure);
configure.SetBaseType(BlazorComponent.FullTypeName);
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs
new file mode 100644
index 0000000000..36ad28d7db
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryImplementsPass.cs
@@ -0,0 +1,107 @@
+// 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.Intermediate;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.AspNetCore.Blazor.Razor
+{
+ ///
+ /// This code is temporary. It finds top-level expressions of the form
+ /// @Implements()
+ /// ... and converts them into interface declarations on the class.
+ /// Once we're able to add Blazor-specific directives and have them show up in tooling,
+ /// we'll replace this with a simpler and cleaner "@implements SomeInterfaceType" directive.
+ ///
+ internal class TemporaryImplementsPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
+ {
+ // Example: "Implements>()"
+ // Captures: MyApp.Namespace.ISomeType
+ private static readonly Regex ImplementsSourceRegex
+ = new Regex(@"^\s*Implements\s*<(.+)\>\s*\(\s*\)\s*$");
+
+ public static void Register(IRazorEngineBuilder configuration)
+ {
+ configuration.Features.Add(new TemporaryImplementsPass());
+ }
+
+ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
+ {
+ var visitor = new Visitor();
+ visitor.Visit(documentNode);
+
+ foreach (var implementsNode in visitor.ImplementsNodes)
+ {
+ visitor.MethodNode.Children.Remove(implementsNode);
+ }
+
+ if (visitor.ClassNode.Interfaces == null)
+ {
+ visitor.ClassNode.Interfaces = new List();
+ }
+
+ foreach (var implementsType in visitor.ImplementsTypes)
+ {
+ visitor.ClassNode.Interfaces.Add(implementsType);
+ }
+ }
+
+ private class Visitor : IntermediateNodeWalker
+ {
+ public ClassDeclarationIntermediateNode ClassNode { get; private set; }
+ public MethodDeclarationIntermediateNode MethodNode { get; private set; }
+ public List ImplementsNodes { get; private set; }
+ = new List();
+ public List ImplementsTypes { get; private set; }
+ = new List();
+
+ public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
+ {
+ ClassNode = node;
+ base.VisitClassDeclaration(node);
+ }
+
+ public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode methodNode)
+ {
+ MethodNode = methodNode;
+
+ var topLevelExpressions = methodNode.Children.OfType();
+ foreach (var csharpExpression in topLevelExpressions)
+ {
+ if (csharpExpression.Children.Count == 1)
+ {
+ var child = csharpExpression.Children[0];
+ if (child is IntermediateToken intermediateToken)
+ {
+ if (TryGetImplementsType(intermediateToken.Content, out string implementsType))
+ {
+ ImplementsNodes.Add(csharpExpression);
+ ImplementsTypes.Add(implementsType);
+ }
+ }
+ }
+ }
+
+ base.VisitMethodDeclaration(methodNode);
+ }
+
+ private bool TryGetImplementsType(string sourceCode, out string implementsType)
+ {
+ var match = ImplementsSourceRegex.Match(sourceCode);
+ if (match.Success)
+ {
+ implementsType = match.Groups[1].Value;
+ return true;
+ }
+ else
+ {
+ implementsType = null;
+ return false;
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs b/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs
index 9f69c73d6e..53ea414131 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs
@@ -30,6 +30,10 @@ namespace Microsoft.AspNetCore.Mvc.Razor
// It will be removed when we can add Blazor-specific directives.
public object Layout() where TLayout : IComponent
=> throw new NotImplementedException();
+
+ // Similar temporary mechanism as above
+ public object Implements()
+ => throw new NotImplementedException();
}
namespace Internal
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
index 59469f1159..9ec5eb25d7 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
@@ -389,6 +389,22 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
frame => AssertFrame.Text(frame, "Hello"));
}
+ [Fact]
+ public void SupportsImplementsDeclarationsViaTemporarySyntax()
+ {
+ // Arrange/Act
+ var testInterfaceTypeName = typeof(ITestInterface).FullName.Replace('+', '.');
+ var component = CompileToComponent(
+ $"@(Implements<{testInterfaceTypeName}>())" +
+ $"Hello");
+ var frames = GetRenderTree(component);
+
+ // Assert
+ Assert.IsAssignableFrom(component);
+ Assert.Collection(frames,
+ frame => AssertFrame.Text(frame, "Hello"));
+ }
+
private static RenderTreeFrame[] GetRenderTree(IComponent component)
{
var renderer = new TestRenderer();
@@ -523,5 +539,7 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
{
}
}
+
+ public interface ITestInterface { }
}
}