diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ConsolidatedMvcViewDocumentClassifierPass.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ConsolidatedMvcViewDocumentClassifierPass.cs new file mode 100644 index 000000000000..715ae3496e89 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/ConsolidatedMvcViewDocumentClassifierPass.cs @@ -0,0 +1,90 @@ +// 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 Microsoft.AspNetCore.Razor.Language; +using Microsoft.AspNetCore.Razor.Language.Intermediate; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + public sealed class ConsolidatedMvcViewDocumentClassifierPass : DocumentClassifierPassBase + { + public static readonly string MvcViewDocumentKind = "mvc.1.0.view"; + + protected override string DocumentKind => MvcViewDocumentKind; + + protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) => true; + + protected override void OnDocumentStructureCreated( + RazorCodeDocument codeDocument, + NamespaceDeclarationIntermediateNode @namespace, + ClassDeclarationIntermediateNode @class, + MethodDeclarationIntermediateNode method) + { + base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method); + + if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: false, out var namespaceName)) + { + @namespace.Content = "AspNetCoreGeneratedDocument"; + } + else + { + @namespace.Content = namespaceName; + } + + if (!TryComputeClassName(codeDocument, out var className)) + { + // It's possible for a Razor document to not have a file path. + // Eg. When we try to generate code for an in memory document like default imports. + var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum()); + @class.ClassName = $"AspNetCore_{checksum}"; + } + else + { + @class.ClassName = className; + } + + @class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage"; + @class.Modifiers.Clear(); + @class.Modifiers.Add("internal"); + @class.Modifiers.Add("sealed"); + + method.MethodName = "ExecuteAsync"; + method.Modifiers.Clear(); + method.Modifiers.Add("public"); + method.Modifiers.Add("async"); + method.Modifiers.Add("override"); + method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}"; + } + + private bool TryComputeClassName(RazorCodeDocument codeDocument, out string className) + { + var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath; + if (string.IsNullOrEmpty(filePath)) + { + className = null; + return false; + } + + className = GetClassNameFromPath(filePath); + return true; + } + + private static string GetClassNameFromPath(string path) + { + const string cshtmlExtension = ".cshtml"; + + if (string.IsNullOrEmpty(path)) + { + return path; + } + + if (path.EndsWith(cshtmlExtension, StringComparison.OrdinalIgnoreCase)) + { + path = path.Substring(0, path.Length - cshtmlExtension.Length); + } + + return CSharpIdentifier.SanitizeIdentifier(path); + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/PublicAPI.Unshipped.txt b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/PublicAPI.Unshipped.txt index 7dc5c58110bf..e4a5fa72ae36 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/PublicAPI.Unshipped.txt +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/PublicAPI.Unshipped.txt @@ -1 +1,4 @@ #nullable enable +Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass +Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass.ConsolidatedMvcViewDocumentClassifierPass() -> void +~static readonly Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass.MvcViewDocumentKind -> string \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/RazorExtensions.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/RazorExtensions.cs index dfaebba00740..0c7b298aa738 100644 --- a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/RazorExtensions.cs +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/RazorExtensions.cs @@ -37,7 +37,15 @@ public static void Register(RazorProjectEngineBuilder builder) builder.Features.Add(new PagesPropertyInjectionPass()); builder.Features.Add(new ViewComponentTagHelperPass()); builder.Features.Add(new RazorPageDocumentClassifierPass()); - builder.Features.Add(new MvcViewDocumentClassifierPass()); + + if (builder.Configuration.UseConsolidatedMvcViews) + { + builder.Features.Add(new ConsolidatedMvcViewDocumentClassifierPass()); + } + else + { + builder.Features.Add(new MvcViewDocumentClassifierPass()); + } builder.Features.Add(new MvcImportProjectFeature()); diff --git a/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ConsolidatedMvcViewDocumentClassifierPassTest.cs b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ConsolidatedMvcViewDocumentClassifierPassTest.cs new file mode 100644 index 000000000000..b4c6ed145ed7 --- /dev/null +++ b/src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/test/ConsolidatedMvcViewDocumentClassifierPassTest.cs @@ -0,0 +1,178 @@ +// 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 Xunit; + +namespace Microsoft.AspNetCore.Mvc.Razor.Extensions +{ + public class ConsolidatedMvcViewDocumentClassifierPassTest : RazorProjectEngineTestBase + { + protected override RazorLanguageVersion Version => RazorLanguageVersion.Latest; + + [Fact] + public void ConsolidatedMvcViewDocumentClassifierPass_SetsDifferentNamespace() + { + // Arrange + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "Test.cshtml")); + + var projectEngine = CreateProjectEngine(); + var irDocument = CreateIRDocument(projectEngine, codeDocument); + var pass = new ConsolidatedMvcViewDocumentClassifierPass + { + Engine = projectEngine.Engine + }; + + // Act + pass.Execute(codeDocument, irDocument); + var visitor = new Visitor(); + visitor.Visit(irDocument); + + // Assert + Assert.Equal("AspNetCoreGeneratedDocument", visitor.Namespace.Content); + } + + [Fact] + public void ConsolidatedMvcViewDocumentClassifierPass_SetsClass() + { + // Arrange + var properties = new RazorSourceDocumentProperties(filePath: "ignored", relativePath: "Test.cshtml"); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties)); + + var projectEngine = CreateProjectEngine(); + var irDocument = CreateIRDocument(projectEngine, codeDocument); + var pass = new ConsolidatedMvcViewDocumentClassifierPass + { + Engine = projectEngine.Engine + }; + + // Act + pass.Execute(codeDocument, irDocument); + var visitor = new Visitor(); + visitor.Visit(irDocument); + + // Assert + Assert.Equal("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage", visitor.Class.BaseType); + Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers); + Assert.Equal("Test", visitor.Class.ClassName); + } + + [Fact] + public void MvcViewDocumentClassifierPass_NullFilePath_SetsClass() + { + // Arrange + var properties = new RazorSourceDocumentProperties(filePath: null, relativePath: null); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties)); + + var projectEngine = CreateProjectEngine(); + var irDocument = CreateIRDocument(projectEngine, codeDocument); + var pass = new ConsolidatedMvcViewDocumentClassifierPass + { + Engine = projectEngine.Engine + }; + + // Act + pass.Execute(codeDocument, irDocument); + var visitor = new Visitor(); + visitor.Visit(irDocument); + + // Assert + Assert.Equal("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage", visitor.Class.BaseType); + Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers); + Assert.Equal("AspNetCore_d9f877a857a7e9928eac04d09a59f25967624155", visitor.Class.ClassName); + } + + [Theory] + [InlineData("/Views/Home/Index.cshtml", "_Views_Home_Index")] + [InlineData("/Areas/MyArea/Views/Home/About.cshtml", "_Areas_MyArea_Views_Home_About")] + public void MvcViewDocumentClassifierPass_UsesRelativePathToGenerateTypeName(string relativePath, string expected) + { + // Arrange + var properties = new RazorSourceDocumentProperties(filePath: "ignored", relativePath: relativePath); + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties)); + + var projectEngine = CreateProjectEngine(); + var irDocument = CreateIRDocument(projectEngine, codeDocument); + var pass = new ConsolidatedMvcViewDocumentClassifierPass + { + Engine = projectEngine.Engine + }; + + // Act + pass.Execute(codeDocument, irDocument); + var visitor = new Visitor(); + visitor.Visit(irDocument); + + // Assert + Assert.Equal(expected, visitor.Class.ClassName); + Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers); + } + + [Fact] + public void ConsolidatedMvcViewDocumentClassifierPass_SetsUpExecuteAsyncMethod() + { + // Arrange + var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "Test.cshtml")); + + var projectEngine = CreateProjectEngine(); + var irDocument = CreateIRDocument(projectEngine, codeDocument); + var pass = new ConsolidatedMvcViewDocumentClassifierPass + { + Engine = projectEngine.Engine + }; + + // Act + pass.Execute(codeDocument, irDocument); + var visitor = new Visitor(); + visitor.Visit(irDocument); + + // Assert + Assert.Equal("ExecuteAsync", visitor.Method.MethodName); + Assert.Equal("global::System.Threading.Tasks.Task", visitor.Method.ReturnType); + Assert.Equal(new[] { "public", "async", "override" }, visitor.Method.Modifiers); + } + + private static DocumentIntermediateNode CreateIRDocument(RazorProjectEngine projectEngine, RazorCodeDocument codeDocument) + { + for (var i = 0; i < projectEngine.Phases.Count; i++) + { + var phase = projectEngine.Phases[i]; + phase.Execute(codeDocument); + + if (phase is IRazorIntermediateNodeLoweringPhase) + { + break; + } + } + + return codeDocument.GetDocumentIntermediateNode(); + } + + private class Visitor : IntermediateNodeWalker + { + public NamespaceDeclarationIntermediateNode Namespace { get; private set; } + + public ClassDeclarationIntermediateNode Class { get; private set; } + + public MethodDeclarationIntermediateNode Method { get; private set; } + + public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode node) + { + Method = node; + } + + public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node) + { + Namespace = node; + base.VisitNamespaceDeclaration(node); + } + + public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node) + { + Class = node; + base.VisitClassDeclaration(node); + } + } + } +} \ No newline at end of file diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/PublicAPI.Unshipped.txt b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/PublicAPI.Unshipped.txt index 15ad64837f8a..8a9e35f076b7 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/PublicAPI.Unshipped.txt +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/PublicAPI.Unshipped.txt @@ -7,3 +7,6 @@ Microsoft.AspNetCore.Razor.Language.Intermediate.CascadingGenericTypeParameter.C ~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentIntermediateNode.ProvidesCascadingGenericTypes.set -> void ~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentTypeInferenceMethodIntermediateNode.ReceivesCascadingGenericTypes.get -> System.Collections.Generic.List ~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentTypeInferenceMethodIntermediateNode.ReceivesCascadingGenericTypes.set -> void +abstract Microsoft.AspNetCore.Razor.Language.RazorConfiguration.UseConsolidatedMvcViews.get -> bool +*REMOVED*~static Microsoft.AspNetCore.Razor.Language.RazorConfiguration.Create(Microsoft.AspNetCore.Razor.Language.RazorLanguageVersion languageVersion, string configurationName, System.Collections.Generic.IEnumerable extensions) -> Microsoft.AspNetCore.Razor.Language.RazorConfiguration +~static Microsoft.AspNetCore.Razor.Language.RazorConfiguration.Create(Microsoft.AspNetCore.Razor.Language.RazorLanguageVersion languageVersion, string configurationName, System.Collections.Generic.IEnumerable extensions, bool useConsolidatedMvcViews = false) -> Microsoft.AspNetCore.Razor.Language.RazorConfiguration diff --git a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorConfiguration.cs b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorConfiguration.cs index 1094b0a43238..e70676b2963d 100644 --- a/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorConfiguration.cs +++ b/src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorConfiguration.cs @@ -13,12 +13,14 @@ public abstract class RazorConfiguration : IEquatable public static readonly RazorConfiguration Default = new DefaultRazorConfiguration( RazorLanguageVersion.Latest, "unnamed", - Array.Empty()); + Array.Empty(), + false); public static RazorConfiguration Create( RazorLanguageVersion languageVersion, string configurationName, - IEnumerable extensions) + IEnumerable extensions, + bool useConsolidatedMvcViews = false) { if (languageVersion == null) { @@ -35,7 +37,7 @@ public static RazorConfiguration Create( throw new ArgumentNullException(nameof(extensions)); } - return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray()); + return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray(), useConsolidatedMvcViews); } public abstract string ConfigurationName { get; } @@ -44,6 +46,8 @@ public static RazorConfiguration Create( public abstract RazorLanguageVersion LanguageVersion { get; } + public abstract bool UseConsolidatedMvcViews { get; } + public override bool Equals(object obj) { return base.Equals(obj as RazorConfiguration); @@ -71,6 +75,11 @@ public virtual bool Equals(RazorConfiguration other) return false; } + if (UseConsolidatedMvcViews != other.UseConsolidatedMvcViews) + { + return false; + } + for (var i = 0; i < Extensions.Count; i++) { if (Extensions[i].ExtensionName != other.Extensions[i].ExtensionName) @@ -101,11 +110,13 @@ private class DefaultRazorConfiguration : RazorConfiguration public DefaultRazorConfiguration( RazorLanguageVersion languageVersion, string configurationName, - RazorExtension[] extensions) + RazorExtension[] extensions, + bool useConsolidatedMvcViews = false) { LanguageVersion = languageVersion; ConfigurationName = configurationName; Extensions = extensions; + UseConsolidatedMvcViews = useConsolidatedMvcViews; } public override string ConfigurationName { get; } @@ -113,6 +124,8 @@ public DefaultRazorConfiguration( public override IReadOnlyList Extensions { get; } public override RazorLanguageVersion LanguageVersion { get; } + + public override bool UseConsolidatedMvcViews { get; } } } }