-
Notifications
You must be signed in to change notification settings - Fork 38
Expand file tree
/
Copy pathRoslynRazorViewEngine.cs
More file actions
273 lines (238 loc) · 12.8 KB
/
RoslynRazorViewEngine.cs
File metadata and controls
273 lines (238 loc) · 12.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Text;
using System;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading;
using System.Web;
using System.Web.Compilation;
using System.Web.Hosting;
using System.Web.Mvc;
using System.Web.Razor;
using System.Web.Razor.Generator;
using System.Web.Razor.Parser.SyntaxTree;
using System.Web.WebPages;
using System.Web.WebPages.Razor;
namespace StackExchange.Precompilation
{
/// <summary>
/// A replacement for the <see cref="RazorViewEngine"/> that uses roslyn (<see cref="Microsoft.CodeAnalysis"/>) instead of <see cref="System.CodeDom"/> to compile views.
/// </summary>
public class RoslynRazorViewEngine : ProfiledVirtualPathProviderViewEngine
{
/// <summary>
/// Creates a new <see cref="RoslynRazorViewEngine"/> instance.
/// </summary>
public RoslynRazorViewEngine()
{
AreaViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"
};
AreaMasterLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"
};
AreaPartialViewLocationFormats = new[] {
"~/Areas/{2}/Views/{1}/{0}.cshtml",
"~/Areas/{2}/Views/Shared/{0}.cshtml"
};
ViewLocationFormats = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
MasterLocationFormats = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
PartialViewLocationFormats = new[] {
"~/Views/{1}/{0}.cshtml",
"~/Views/Shared/{0}.cshtml"
};
FileExtensions = new[] {
"cshtml"
};
}
/// <summary>When set to <c>true</c>, configured <see cref="ICompileModule" />s are used when the views are compiled</summary>
public bool UseCompilationModules { get; set; }
private readonly ICompileModule[] _noModule = new ICompileModule[0];
private readonly PrecompilationModuleLoader _moduleLoader = new PrecompilationModuleLoader(PrecompilerSection.Current);
/// <inheritdoc />
protected override IVirtualPathFactory CreateVirtualPathFactory() => new PrecompilationVirtualPathFactory(
runtime: this,
precompiled: ViewEngines.Engines.OfType<PrecompiledViewEngine>().FirstOrDefault());
/// <inheritdoc />
protected override IView CreatePartialView(ControllerContext controllerContext, string partialPath) =>
new PrecompilationView(partialPath, null, GetTypeFromVirtualPath(partialPath), false, this);
/// <inheritdoc />
protected override IView CreateView(ControllerContext controllerContext, string viewPath, string masterPath) =>
new PrecompilationView(viewPath, masterPath, GetTypeFromVirtualPath(viewPath), true, this);
internal bool FileExists(string virtualPath) =>
HostingEnvironment.VirtualPathProvider.FileExists(virtualPath);
internal Type GetTypeFromVirtualPath(string virtualPath)
{
virtualPath = VirtualPathUtility.ToAbsolute(virtualPath);
var cacheKey = "RoslynRazor_" + virtualPath;
var type = HttpRuntime.Cache[cacheKey] as Type;
if (type == null)
{
type = GetTypeFromVirtualPathNoCache(virtualPath);
// Cache it, and make it dependent on the razor file
var cacheDependency = HostingEnvironment.VirtualPathProvider.GetCacheDependency(virtualPath, new string[] { virtualPath }, DateTime.UtcNow);
HttpRuntime.Cache.Insert(cacheKey, type, cacheDependency);
}
return type;
}
private Type GetTypeFromVirtualPathNoCache(string virtualPath)
{
using (this.DoProfileStep($"{nameof(RoslynRazorViewEngine)}: Compiling {virtualPath}"))
{
OnCodeGenerationStarted();
var args = new CompilingPathEventArgs(virtualPath, WebRazorHostFactory.CreateHostFromConfig(virtualPath));
OnBeforeCompilePath(args);
var host = args.Host;
var razorResult = RunRazorGenerator(virtualPath, host);
var syntaxTree = GetSyntaxTree(host, razorResult);
var assembly = CompileToAssembly(host, syntaxTree, UseCompilationModules ? _moduleLoader.LoadedModules : _noModule);
return assembly.GetType($"{host.DefaultNamespace}.{host.DefaultClassName}");
}
}
private GeneratorResults RunRazorGenerator(string virtualPath, WebPageRazorHost host)
{
var file = HostingEnvironment.VirtualPathProvider.GetFile(virtualPath);
var engine = new RazorTemplateEngine(host);
using (var viewStream = file.Open())
using (var viewReader = new StreamReader(viewStream))
{
var razorResult = engine.GenerateCode(viewReader, className: null, rootNamespace: null, sourceFileName: host.PhysicalPath);
if (!razorResult.Success)
{
var sourceCode = (string)null;
if (viewStream.CanSeek)
{
viewStream.Seek(0, SeekOrigin.Begin);
sourceCode = viewReader.ReadToEnd();
}
throw CreateExceptionFromParserError(razorResult.ParserErrors.Last(), virtualPath, sourceCode);
}
OnCodeGenerationCompleted(razorResult.GeneratedCode, host);
return razorResult;
}
}
private static SyntaxTree GetSyntaxTree(WebPageRazorHost host, GeneratorResults razorResult)
{
// Use CodeDom to generate source code from the CodeCompileUnit
// Use roslyn to parse it back
using (var codeDomProvider = CodeDomProvider.CreateProvider(host.CodeLanguage.LanguageName))
using (var viewCodeStream = new MemoryStream())
using (var viewCodeWriter = new StreamWriter(viewCodeStream))
{
codeDomProvider.GenerateCodeFromCompileUnit(razorResult.GeneratedCode, viewCodeWriter, new CodeGeneratorOptions());
viewCodeWriter.Flush();
viewCodeStream.Position = 0;
var sourceText = SourceText.From(viewCodeStream);
// We need a different file path for the generated file, otherwise breakpoints won't get
// hit due to #line directives pointing at the original .cshtml. If we'd want breakpoint
// in the generated .cs code, we'd have to dump them somewhere on disk, and specify the path here.
var sourcePath = string.IsNullOrEmpty(host.PhysicalPath)
? host.VirtualPath // yay virtual paths, won't point at the original file
: Path.ChangeExtension(host.PhysicalPath, ".roslynviewengine");
return SyntaxFactory.ParseSyntaxTree(sourceText, path: sourcePath);
}
}
// we were getting OutOfMemory exceptions caused by MetadataReference.CreateFrom* creating
// System.Reflection.PortableExecutable.PEReader instances for the same assembly for each view being compiled
private static readonly ConcurrentDictionary<string, Lazy<MetadataReference>> ReferenceCache = new ConcurrentDictionary<string, Lazy<MetadataReference>>();
internal static MetadataReference ResolveReference(Assembly assembly)
{
var key = assembly.Location;
Uri uri;
if (Uri.TryCreate(assembly.CodeBase, UriKind.Absolute, out uri) && uri.IsFile)
{
key = uri.LocalPath;
}
return ReferenceCache.GetOrAdd(
key,
loc => new Lazy<MetadataReference>(
() => MetadataReference.CreateFromFile(loc),
LazyThreadSafetyMode.ExecutionAndPublication)).Value;
}
private static Assembly CompileToAssembly(WebPageRazorHost host, SyntaxTree syntaxTree, ICollection<ICompileModule> compilationModules)
{
var strArgs = new List<string>();
strArgs.Add("/target:library");
strArgs.Add(host.DefaultDebugCompilation ? "/o-" : "/o+");
strArgs.Add(host.DefaultDebugCompilation ? "/debug+" : "/debug-");
var cscArgs = CSharpCommandLineParser.Default.Parse(strArgs, null, null);
var compilation = CSharpCompilation.Create(
"RoslynRazor", // Note: using a fixed assembly name, which doesn't matter as long as we don't expect cross references of generated assemblies
new[] { syntaxTree },
BuildManager.GetReferencedAssemblies().OfType<Assembly>().Select(ResolveReference),
cscArgs.CompilationOptions.WithAssemblyIdentityComparer(DesktopAssemblyIdentityComparer.Default));
compilation = Hacks.MakeValueTuplesWorkWhenRunningOn47RuntimeAndTargetingNet45Plus(compilation);
var diagnostics = new List<Diagnostic>();
var context = new CompileContext(compilationModules);
context.Before(new BeforeCompileContext
{
Arguments = cscArgs,
Compilation = compilation,
Diagnostics = diagnostics,
});
compilation = context.BeforeCompileContext.Compilation;
using (var dllStream = new MemoryStream())
using (var pdbStream = new MemoryStream())
{
EmitResult emitResult = compilation.Emit(dllStream, pdbStream);
diagnostics.AddRange(emitResult.Diagnostics);
if (!emitResult.Success)
{
Diagnostic diagnostic = diagnostics.First(x => x.Severity == DiagnosticSeverity.Error);
string message = diagnostic.ToString();
LinePosition linePosition = diagnostic.Location.GetMappedLineSpan().StartLinePosition;
throw new HttpParseException(message, null, host.VirtualPath, null, linePosition.Line + 1);
}
context.After(new AfterCompileContext
{
Arguments = context.BeforeCompileContext.Arguments,
AssemblyStream = dllStream,
Compilation = compilation,
Diagnostics = diagnostics,
SymbolStream = pdbStream,
XmlDocStream = null,
});
return Assembly.Load(dllStream.GetBuffer(), pdbStream.GetBuffer());
}
}
private static HttpParseException CreateExceptionFromParserError(RazorError error, string virtualPath, string sourceCode) =>
new HttpParseException(error.Message + Environment.NewLine, null, virtualPath, sourceCode, error.Location.LineIndex + 1);
/// <summary>
/// This is the equivalent of the <see cref="RazorBuildProvider.CompilingPath"/> event, since <see cref="PrecompiledViewEngine"/> bypasses <see cref="RazorBuildProvider"/> completely.
/// </summary>
public static event EventHandler<CompilingPathEventArgs> CompilingPath;
/// <summary>
/// Raises the <see cref="CompilingPath"/> event.
/// </summary>
/// <param name="args"></param>
protected virtual void OnBeforeCompilePath(CompilingPathEventArgs args) =>
CompilingPath?.Invoke(this, args);
/// <summary>
/// This is the equivalent of the <see cref="RazorBuildProvider.CodeGenerationStarted"/> event, since <see cref="PrecompiledViewEngine"/> bypasses <see cref="RazorBuildProvider"/> completely.
/// </summary>
public static event EventHandler CodeGenerationStarted;
private void OnCodeGenerationStarted() =>
CodeGenerationStarted?.Invoke(this, EventArgs.Empty);
/// <summary>
/// This is the equivalent of the <see cref="RazorBuildProvider.CodeGenerationCompleted"/> event, since <see cref="PrecompiledViewEngine"/> bypasses <see cref="RazorBuildProvider"/> completely.
/// </summary>
public static event EventHandler<CodeGenerationCompleteEventArgs> CodeGenerationCompleted;
private void OnCodeGenerationCompleted(CodeCompileUnit codeCompileUnit, WebPageRazorHost host) =>
CodeGenerationCompleted?.Invoke(this, new CodeGenerationCompleteEventArgs(host.VirtualPath, host.PhysicalPath, codeCompileUnit));
}
}