diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs
index 040b4ec7b2..b93a95b7dd 100644
--- a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/BlazorRazorEngine.cs
@@ -22,6 +22,7 @@ namespace Microsoft.AspNetCore.Blazor.Razor
_engine = RazorEngine.Create(configure =>
{
FunctionsDirective.Register(configure);
+ TemporaryLayoutPass.Register(configure);
configure.SetBaseType(BlazorComponent.FullTypeName);
diff --git a/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryLayoutPass.cs b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryLayoutPass.cs
new file mode 100644
index 0000000000..f18e7f6207
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor.Razor.Extensions/TemporaryLayoutPass.cs
@@ -0,0 +1,118 @@
+// 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;
+using System.Linq;
+using System.Text.RegularExpressions;
+
+namespace Microsoft.AspNetCore.Blazor.Razor
+{
+ ///
+ /// This code is temporary. It finds top-level expressions of the form
+ /// @Layout()
+ /// ... and converts them into [Layout(typeof(SomeType))] attributes 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 "@Layout SomeType" directive.
+ ///
+ internal class TemporaryLayoutPass : IntermediateNodePassBase, IRazorDirectiveClassifierPass
+ {
+ // Example: "Layout>()"
+ // Captures: MyApp.Namespace.SomeType
+ private static readonly Regex LayoutSourceRegex
+ = new Regex(@"^\s*Layout\s*<(.+)\>\s*\(\s*\)\s*$");
+ private const string LayoutAttributeTypeName
+ = "Microsoft.AspNetCore.Blazor.Layouts.LayoutAttribute";
+
+ public static void Register(IRazorEngineBuilder configuration)
+ {
+ configuration.Features.Add(new TemporaryLayoutPass());
+ }
+
+ protected override void ExecuteCore(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode)
+ {
+ var visitor = new Visitor();
+ visitor.Visit(documentNode);
+
+ if (visitor.DidFindLayoutDeclaration)
+ {
+ visitor.MethodNode.Children.Remove(visitor.LayoutNode);
+
+ var attributeNode = new CSharpCodeIntermediateNode();
+ attributeNode.Children.Add(new IntermediateToken()
+ {
+ Kind = TokenKind.CSharp,
+ Content = $"[{LayoutAttributeTypeName}(typeof ({visitor.LayoutType}))]" + Environment.NewLine,
+ });
+
+ var classNodeIndex = visitor
+ .NamespaceNode
+ .Children
+ .IndexOf(visitor.ClassNode);
+ visitor.NamespaceNode.Children.Insert(classNodeIndex, attributeNode);
+ }
+ }
+
+ private class Visitor : IntermediateNodeWalker
+ {
+ public bool DidFindLayoutDeclaration { get; private set; }
+ public NamespaceDeclarationIntermediateNode NamespaceNode { get; private set; }
+ public ClassDeclarationIntermediateNode ClassNode { get; private set; }
+ public MethodDeclarationIntermediateNode MethodNode { get; private set; }
+ public CSharpExpressionIntermediateNode LayoutNode { get; private set; }
+ public string LayoutType { get; private set; }
+
+ public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
+ {
+ NamespaceNode = node;
+ base.VisitNamespaceDeclaration(node);
+ }
+
+ public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
+ {
+ ClassNode = node;
+ base.VisitClassDeclaration(node);
+ }
+
+ public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode 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 (TryGetLayoutType(intermediateToken.Content, out string layoutType))
+ {
+ DidFindLayoutDeclaration = true;
+ MethodNode = methodNode;
+ LayoutNode = csharpExpression;
+ LayoutType = layoutType;
+ }
+ }
+ }
+ }
+
+ base.VisitMethodDeclaration(methodNode);
+ }
+
+ private bool TryGetLayoutType(string sourceCode, out string layoutType)
+ {
+ var match = LayoutSourceRegex.Match(sourceCode);
+ if (match.Success)
+ {
+ layoutType = match.Groups[1].Value;
+ return true;
+ }
+ else
+ {
+ layoutType = null;
+ return false;
+ }
+ }
+ }
+ }
+}
diff --git a/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs b/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs
index db7b860b2c..9f69c73d6e 100644
--- a/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs
+++ b/src/Microsoft.AspNetCore.Blazor/Components/RazorToolingWorkaround.cs
@@ -24,7 +24,13 @@ namespace Microsoft.AspNetCore.Mvc
namespace Microsoft.AspNetCore.Mvc.Razor
{
- public class RazorPage: BlazorComponent { }
+ public class RazorPage: BlazorComponent
+ {
+ // This is temporary and exists only to support TemporaryLayoutPass.
+ // It will be removed when we can add Blazor-specific directives.
+ public object Layout() where TLayout : IComponent
+ => throw new NotImplementedException();
+ }
namespace Internal
{
diff --git a/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs
new file mode 100644
index 0000000000..d05798a866
--- /dev/null
+++ b/src/Microsoft.AspNetCore.Blazor/Layouts/LayoutAttribute.cs
@@ -0,0 +1,35 @@
+// 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.Blazor.Components;
+using System;
+
+namespace Microsoft.AspNetCore.Blazor.Layouts
+{
+ ///
+ /// Indicates that the associated component type uses a specified layout.
+ ///
+ [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+ public class LayoutAttribute : Attribute
+ {
+ ///
+ /// The type of the layout. The type always implements .
+ ///
+ public Type LayoutType { get; private set; }
+
+ ///
+ /// Constructs an instance of .
+ ///
+ /// The type of the layout. This must implement .
+ public LayoutAttribute(Type layoutType)
+ {
+ LayoutType = layoutType ?? throw new ArgumentNullException(nameof(layoutType));
+
+ if (!typeof(IComponent).IsAssignableFrom(layoutType))
+ {
+ throw new ArgumentException($"Invalid layout type: {layoutType.FullName} " +
+ $"does not implement {typeof(IComponent).FullName}.");
+ }
+ }
+ }
+}
diff --git a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
index 2c6b911788..59469f1159 100644
--- a/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
+++ b/test/Microsoft.AspNetCore.Blazor.Build.Test/RazorCompilerTest.cs
@@ -3,6 +3,7 @@
using Microsoft.AspNetCore.Blazor.Build.Core.RazorCompilation;
using Microsoft.AspNetCore.Blazor.Components;
+using Microsoft.AspNetCore.Blazor.Layouts;
using Microsoft.AspNetCore.Blazor.Rendering;
using Microsoft.AspNetCore.Blazor.RenderTree;
using Microsoft.AspNetCore.Blazor.Test.Shared;
@@ -360,6 +361,34 @@ namespace Microsoft.AspNetCore.Blazor.Build.Test
frame => AssertFrame.Component(frame, 0));
}
+ [Fact]
+ public void ComponentsDoNotHaveLayoutAttributeByDefault()
+ {
+ // Arrange/Act
+ var component = CompileToComponent($"Hello");
+
+ // Assert
+ Assert.Null(component.GetType().GetCustomAttribute());
+ }
+
+ [Fact]
+ public void SupportsLayoutDeclarationsViaTemporarySyntax()
+ {
+ // Arrange/Act
+ var testComponentTypeName = typeof(TestComponent).FullName.Replace('+', '.');
+ var component = CompileToComponent(
+ $"@(Layout<{testComponentTypeName}>())" +
+ $"Hello");
+ var frames = GetRenderTree(component);
+
+ // Assert
+ var layoutAttribute = component.GetType().GetCustomAttribute();
+ Assert.NotNull(layoutAttribute);
+ Assert.Equal(typeof(TestComponent), layoutAttribute.LayoutType);
+ Assert.Collection(frames,
+ frame => AssertFrame.Text(frame, "Hello"));
+ }
+
private static RenderTreeFrame[] GetRenderTree(IComponent component)
{
var renderer = new TestRenderer();