Skip to content

Commit 54d04c5

Browse files
authored
Add Consolidated view classifier to make view types internal (#30976)
* Add Consolidated view classifier to make view types internal * Address feedback from peer review * Address feedback and fix tests
1 parent 0a7a620 commit 54d04c5

File tree

6 files changed

+300
-5
lines changed

6 files changed

+300
-5
lines changed
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using Microsoft.AspNetCore.Razor.Language;
6+
using Microsoft.AspNetCore.Razor.Language.Intermediate;
7+
8+
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
9+
{
10+
public sealed class ConsolidatedMvcViewDocumentClassifierPass : DocumentClassifierPassBase
11+
{
12+
public static readonly string MvcViewDocumentKind = "mvc.1.0.view";
13+
14+
protected override string DocumentKind => MvcViewDocumentKind;
15+
16+
protected override bool IsMatch(RazorCodeDocument codeDocument, DocumentIntermediateNode documentNode) => true;
17+
18+
protected override void OnDocumentStructureCreated(
19+
RazorCodeDocument codeDocument,
20+
NamespaceDeclarationIntermediateNode @namespace,
21+
ClassDeclarationIntermediateNode @class,
22+
MethodDeclarationIntermediateNode method)
23+
{
24+
base.OnDocumentStructureCreated(codeDocument, @namespace, @class, method);
25+
26+
if (!codeDocument.TryComputeNamespace(fallbackToRootNamespace: false, out var namespaceName))
27+
{
28+
@namespace.Content = "AspNetCoreGeneratedDocument";
29+
}
30+
else
31+
{
32+
@namespace.Content = namespaceName;
33+
}
34+
35+
if (!TryComputeClassName(codeDocument, out var className))
36+
{
37+
// It's possible for a Razor document to not have a file path.
38+
// Eg. When we try to generate code for an in memory document like default imports.
39+
var checksum = Checksum.BytesToString(codeDocument.Source.GetChecksum());
40+
@class.ClassName = $"AspNetCore_{checksum}";
41+
}
42+
else
43+
{
44+
@class.ClassName = className;
45+
}
46+
47+
@class.BaseType = "global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>";
48+
@class.Modifiers.Clear();
49+
@class.Modifiers.Add("internal");
50+
@class.Modifiers.Add("sealed");
51+
52+
method.MethodName = "ExecuteAsync";
53+
method.Modifiers.Clear();
54+
method.Modifiers.Add("public");
55+
method.Modifiers.Add("async");
56+
method.Modifiers.Add("override");
57+
method.ReturnType = $"global::{typeof(System.Threading.Tasks.Task).FullName}";
58+
}
59+
60+
private bool TryComputeClassName(RazorCodeDocument codeDocument, out string className)
61+
{
62+
var filePath = codeDocument.Source.RelativePath ?? codeDocument.Source.FilePath;
63+
if (string.IsNullOrEmpty(filePath))
64+
{
65+
className = null;
66+
return false;
67+
}
68+
69+
className = GetClassNameFromPath(filePath);
70+
return true;
71+
}
72+
73+
private static string GetClassNameFromPath(string path)
74+
{
75+
const string cshtmlExtension = ".cshtml";
76+
77+
if (string.IsNullOrEmpty(path))
78+
{
79+
return path;
80+
}
81+
82+
if (path.EndsWith(cshtmlExtension, StringComparison.OrdinalIgnoreCase))
83+
{
84+
path = path.Substring(0, path.Length - cshtmlExtension.Length);
85+
}
86+
87+
return CSharpIdentifier.SanitizeIdentifier(path);
88+
}
89+
}
90+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
#nullable enable
2+
Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass
3+
Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass.ConsolidatedMvcViewDocumentClassifierPass() -> void
4+
~static readonly Microsoft.AspNetCore.Mvc.Razor.Extensions.ConsolidatedMvcViewDocumentClassifierPass.MvcViewDocumentKind -> string

src/Razor/Microsoft.AspNetCore.Mvc.Razor.Extensions/src/RazorExtensions.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,15 @@ public static void Register(RazorProjectEngineBuilder builder)
3737
builder.Features.Add(new PagesPropertyInjectionPass());
3838
builder.Features.Add(new ViewComponentTagHelperPass());
3939
builder.Features.Add(new RazorPageDocumentClassifierPass());
40-
builder.Features.Add(new MvcViewDocumentClassifierPass());
40+
41+
if (builder.Configuration.UseConsolidatedMvcViews)
42+
{
43+
builder.Features.Add(new ConsolidatedMvcViewDocumentClassifierPass());
44+
}
45+
else
46+
{
47+
builder.Features.Add(new MvcViewDocumentClassifierPass());
48+
}
4149

4250
builder.Features.Add(new MvcImportProjectFeature());
4351

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.AspNetCore.Razor.Language;
5+
using Microsoft.AspNetCore.Razor.Language.Intermediate;
6+
using Xunit;
7+
8+
namespace Microsoft.AspNetCore.Mvc.Razor.Extensions
9+
{
10+
public class ConsolidatedMvcViewDocumentClassifierPassTest : RazorProjectEngineTestBase
11+
{
12+
protected override RazorLanguageVersion Version => RazorLanguageVersion.Latest;
13+
14+
[Fact]
15+
public void ConsolidatedMvcViewDocumentClassifierPass_SetsDifferentNamespace()
16+
{
17+
// Arrange
18+
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "Test.cshtml"));
19+
20+
var projectEngine = CreateProjectEngine();
21+
var irDocument = CreateIRDocument(projectEngine, codeDocument);
22+
var pass = new ConsolidatedMvcViewDocumentClassifierPass
23+
{
24+
Engine = projectEngine.Engine
25+
};
26+
27+
// Act
28+
pass.Execute(codeDocument, irDocument);
29+
var visitor = new Visitor();
30+
visitor.Visit(irDocument);
31+
32+
// Assert
33+
Assert.Equal("AspNetCoreGeneratedDocument", visitor.Namespace.Content);
34+
}
35+
36+
[Fact]
37+
public void ConsolidatedMvcViewDocumentClassifierPass_SetsClass()
38+
{
39+
// Arrange
40+
var properties = new RazorSourceDocumentProperties(filePath: "ignored", relativePath: "Test.cshtml");
41+
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties));
42+
43+
var projectEngine = CreateProjectEngine();
44+
var irDocument = CreateIRDocument(projectEngine, codeDocument);
45+
var pass = new ConsolidatedMvcViewDocumentClassifierPass
46+
{
47+
Engine = projectEngine.Engine
48+
};
49+
50+
// Act
51+
pass.Execute(codeDocument, irDocument);
52+
var visitor = new Visitor();
53+
visitor.Visit(irDocument);
54+
55+
// Assert
56+
Assert.Equal("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>", visitor.Class.BaseType);
57+
Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers);
58+
Assert.Equal("Test", visitor.Class.ClassName);
59+
}
60+
61+
[Fact]
62+
public void MvcViewDocumentClassifierPass_NullFilePath_SetsClass()
63+
{
64+
// Arrange
65+
var properties = new RazorSourceDocumentProperties(filePath: null, relativePath: null);
66+
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties));
67+
68+
var projectEngine = CreateProjectEngine();
69+
var irDocument = CreateIRDocument(projectEngine, codeDocument);
70+
var pass = new ConsolidatedMvcViewDocumentClassifierPass
71+
{
72+
Engine = projectEngine.Engine
73+
};
74+
75+
// Act
76+
pass.Execute(codeDocument, irDocument);
77+
var visitor = new Visitor();
78+
visitor.Visit(irDocument);
79+
80+
// Assert
81+
Assert.Equal("global::Microsoft.AspNetCore.Mvc.Razor.RazorPage<TModel>", visitor.Class.BaseType);
82+
Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers);
83+
Assert.Equal("AspNetCore_d9f877a857a7e9928eac04d09a59f25967624155", visitor.Class.ClassName);
84+
}
85+
86+
[Theory]
87+
[InlineData("/Views/Home/Index.cshtml", "_Views_Home_Index")]
88+
[InlineData("/Areas/MyArea/Views/Home/About.cshtml", "_Areas_MyArea_Views_Home_About")]
89+
public void MvcViewDocumentClassifierPass_UsesRelativePathToGenerateTypeName(string relativePath, string expected)
90+
{
91+
// Arrange
92+
var properties = new RazorSourceDocumentProperties(filePath: "ignored", relativePath: relativePath);
93+
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", properties));
94+
95+
var projectEngine = CreateProjectEngine();
96+
var irDocument = CreateIRDocument(projectEngine, codeDocument);
97+
var pass = new ConsolidatedMvcViewDocumentClassifierPass
98+
{
99+
Engine = projectEngine.Engine
100+
};
101+
102+
// Act
103+
pass.Execute(codeDocument, irDocument);
104+
var visitor = new Visitor();
105+
visitor.Visit(irDocument);
106+
107+
// Assert
108+
Assert.Equal(expected, visitor.Class.ClassName);
109+
Assert.Equal(new[] { "internal", "sealed" }, visitor.Class.Modifiers);
110+
}
111+
112+
[Fact]
113+
public void ConsolidatedMvcViewDocumentClassifierPass_SetsUpExecuteAsyncMethod()
114+
{
115+
// Arrange
116+
var codeDocument = RazorCodeDocument.Create(RazorSourceDocument.Create("some-content", "Test.cshtml"));
117+
118+
var projectEngine = CreateProjectEngine();
119+
var irDocument = CreateIRDocument(projectEngine, codeDocument);
120+
var pass = new ConsolidatedMvcViewDocumentClassifierPass
121+
{
122+
Engine = projectEngine.Engine
123+
};
124+
125+
// Act
126+
pass.Execute(codeDocument, irDocument);
127+
var visitor = new Visitor();
128+
visitor.Visit(irDocument);
129+
130+
// Assert
131+
Assert.Equal("ExecuteAsync", visitor.Method.MethodName);
132+
Assert.Equal("global::System.Threading.Tasks.Task", visitor.Method.ReturnType);
133+
Assert.Equal(new[] { "public", "async", "override" }, visitor.Method.Modifiers);
134+
}
135+
136+
private static DocumentIntermediateNode CreateIRDocument(RazorProjectEngine projectEngine, RazorCodeDocument codeDocument)
137+
{
138+
for (var i = 0; i < projectEngine.Phases.Count; i++)
139+
{
140+
var phase = projectEngine.Phases[i];
141+
phase.Execute(codeDocument);
142+
143+
if (phase is IRazorIntermediateNodeLoweringPhase)
144+
{
145+
break;
146+
}
147+
}
148+
149+
return codeDocument.GetDocumentIntermediateNode();
150+
}
151+
152+
private class Visitor : IntermediateNodeWalker
153+
{
154+
public NamespaceDeclarationIntermediateNode Namespace { get; private set; }
155+
156+
public ClassDeclarationIntermediateNode Class { get; private set; }
157+
158+
public MethodDeclarationIntermediateNode Method { get; private set; }
159+
160+
public override void VisitMethodDeclaration(MethodDeclarationIntermediateNode node)
161+
{
162+
Method = node;
163+
}
164+
165+
public override void VisitNamespaceDeclaration(NamespaceDeclarationIntermediateNode node)
166+
{
167+
Namespace = node;
168+
base.VisitNamespaceDeclaration(node);
169+
}
170+
171+
public override void VisitClassDeclaration(ClassDeclarationIntermediateNode node)
172+
{
173+
Class = node;
174+
base.VisitClassDeclaration(node);
175+
}
176+
}
177+
}
178+
}

src/Razor/Microsoft.AspNetCore.Razor.Language/src/PublicAPI.Unshipped.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@ Microsoft.AspNetCore.Razor.Language.Intermediate.CascadingGenericTypeParameter.C
77
~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentIntermediateNode.ProvidesCascadingGenericTypes.set -> void
88
~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentTypeInferenceMethodIntermediateNode.ReceivesCascadingGenericTypes.get -> System.Collections.Generic.List<Microsoft.AspNetCore.Razor.Language.Intermediate.CascadingGenericTypeParameter>
99
~Microsoft.AspNetCore.Razor.Language.Intermediate.ComponentTypeInferenceMethodIntermediateNode.ReceivesCascadingGenericTypes.set -> void
10+
abstract Microsoft.AspNetCore.Razor.Language.RazorConfiguration.UseConsolidatedMvcViews.get -> bool
11+
*REMOVED*~static Microsoft.AspNetCore.Razor.Language.RazorConfiguration.Create(Microsoft.AspNetCore.Razor.Language.RazorLanguageVersion languageVersion, string configurationName, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Razor.Language.RazorExtension> extensions) -> Microsoft.AspNetCore.Razor.Language.RazorConfiguration
12+
~static Microsoft.AspNetCore.Razor.Language.RazorConfiguration.Create(Microsoft.AspNetCore.Razor.Language.RazorLanguageVersion languageVersion, string configurationName, System.Collections.Generic.IEnumerable<Microsoft.AspNetCore.Razor.Language.RazorExtension> extensions, bool useConsolidatedMvcViews = false) -> Microsoft.AspNetCore.Razor.Language.RazorConfiguration

src/Razor/Microsoft.AspNetCore.Razor.Language/src/RazorConfiguration.cs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ public abstract class RazorConfiguration : IEquatable<RazorConfiguration>
1313
public static readonly RazorConfiguration Default = new DefaultRazorConfiguration(
1414
RazorLanguageVersion.Latest,
1515
"unnamed",
16-
Array.Empty<RazorExtension>());
16+
Array.Empty<RazorExtension>(),
17+
false);
1718

1819
public static RazorConfiguration Create(
1920
RazorLanguageVersion languageVersion,
2021
string configurationName,
21-
IEnumerable<RazorExtension> extensions)
22+
IEnumerable<RazorExtension> extensions,
23+
bool useConsolidatedMvcViews = false)
2224
{
2325
if (languageVersion == null)
2426
{
@@ -35,7 +37,7 @@ public static RazorConfiguration Create(
3537
throw new ArgumentNullException(nameof(extensions));
3638
}
3739

38-
return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray());
40+
return new DefaultRazorConfiguration(languageVersion, configurationName, extensions.ToArray(), useConsolidatedMvcViews);
3941
}
4042

4143
public abstract string ConfigurationName { get; }
@@ -44,6 +46,8 @@ public static RazorConfiguration Create(
4446

4547
public abstract RazorLanguageVersion LanguageVersion { get; }
4648

49+
public abstract bool UseConsolidatedMvcViews { get; }
50+
4751
public override bool Equals(object obj)
4852
{
4953
return base.Equals(obj as RazorConfiguration);
@@ -71,6 +75,11 @@ public virtual bool Equals(RazorConfiguration other)
7175
return false;
7276
}
7377

78+
if (UseConsolidatedMvcViews != other.UseConsolidatedMvcViews)
79+
{
80+
return false;
81+
}
82+
7483
for (var i = 0; i < Extensions.Count; i++)
7584
{
7685
if (Extensions[i].ExtensionName != other.Extensions[i].ExtensionName)
@@ -101,18 +110,22 @@ private class DefaultRazorConfiguration : RazorConfiguration
101110
public DefaultRazorConfiguration(
102111
RazorLanguageVersion languageVersion,
103112
string configurationName,
104-
RazorExtension[] extensions)
113+
RazorExtension[] extensions,
114+
bool useConsolidatedMvcViews = false)
105115
{
106116
LanguageVersion = languageVersion;
107117
ConfigurationName = configurationName;
108118
Extensions = extensions;
119+
UseConsolidatedMvcViews = useConsolidatedMvcViews;
109120
}
110121

111122
public override string ConfigurationName { get; }
112123

113124
public override IReadOnlyList<RazorExtension> Extensions { get; }
114125

115126
public override RazorLanguageVersion LanguageVersion { get; }
127+
128+
public override bool UseConsolidatedMvcViews { get; }
116129
}
117130
}
118131
}

0 commit comments

Comments
 (0)