diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs
index 023479e13c..1011b242dc 100644
--- a/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor.Host/IMvcRazorHost.cs
@@ -9,5 +9,15 @@ namespace Microsoft.AspNet.Mvc.Razor
public interface IMvcRazorHost
{
GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream);
+
+ ///
+ /// Represent the prefix off the main entry class in the view.
+ ///
+ string MainClassNamePrefix { get; }
+
+ ///
+ /// Represent the namespace the main entry class in the view.
+ ///
+ string DefaultNamespace { get; }
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs
index b1a4f7476a..4f7cc4ec85 100644
--- a/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor.Host/MvcRazorHost.cs
@@ -58,7 +58,6 @@ namespace Microsoft.AspNet.Mvc.Razor
public MvcRazorHost(string root) :
this(new PhysicalFileSystem(root))
{
-
}
#endif
@@ -101,6 +100,12 @@ namespace Microsoft.AspNet.Mvc.Razor
get { return "dynamic"; }
}
+ ///
+ public string MainClassNamePrefix
+ {
+ get { return "ASPV_"; }
+ }
+
///
/// Gets the list of chunks that are injected by default by this host.
///
@@ -121,7 +126,8 @@ namespace Microsoft.AspNet.Mvc.Razor
///
public GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream)
{
- var className = ParserHelpers.SanitizeClassName(rootRelativePath);
+ // Adding a prefix so that the main view class can be easily identified.
+ var className = MainClassNamePrefix + ParserHelpers.SanitizeClassName(rootRelativePath);
using (var reader = new StreamReader(inputStream))
{
var engine = new RazorTemplateEngine(this);
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs
index 04e628161f..6c6daba5e8 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilationResult.cs
@@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
}
- catch (IOException)
+ catch (Exception)
{
// Don't throw if reading the file fails.
return string.Empty;
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs
index 619b815235..70bc97e407 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs
@@ -3,34 +3,110 @@
using System;
using System.Collections.Concurrent;
-using Microsoft.AspNet.FileSystems;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
namespace Microsoft.AspNet.Mvc.Razor
{
public class CompilerCache
{
- private readonly ConcurrentDictionary _cache;
+ private readonly ConcurrentDictionary _cache;
+ private static readonly Type[] EmptyType = new Type[0];
- public CompilerCache()
+ public CompilerCache([NotNull] IEnumerable assemblies)
+ : this(GetFileInfos(assemblies))
{
- _cache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
}
- public CompilationResult GetOrAdd(IFileInfo file, Func compile)
+ internal CompilerCache(IEnumerable viewCollections) : this()
{
- // Generate a content id
- var contentId = file.PhysicalPath + '|' + file.LastModified.Ticks;
-
- Type compiledType;
- if (!_cache.TryGetValue(contentId, out compiledType))
+ foreach (var viewCollection in viewCollections)
{
- var result = compile();
- _cache.TryAdd(contentId, result.CompiledType);
+ foreach (var fileInfo in viewCollection.FileInfos)
+ {
+ var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly;
+ var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
+ var cacheEntry = new CompilerCacheEntry(fileInfo, viewType);
- return result;
+ // There shouldn't be any duplicates and if there are any the first will win.
+ // If the result doesn't match the one on disk its going to recompile anyways.
+ _cache.TryAdd(fileInfo.RelativePath, cacheEntry);
+ }
+ }
+ }
+
+ internal CompilerCache()
+ {
+ _cache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ internal static IEnumerable
+ GetFileInfos(IEnumerable assemblies)
+ {
+ return assemblies.SelectMany(a => a.ExportedTypes)
+ .Where(Match)
+ .Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c));
+ }
+
+ private static bool Match(Type t)
+ {
+ var inAssemblyType = typeof(RazorFileInfoCollection);
+ if (inAssemblyType.IsAssignableFrom(t))
+ {
+ var hasParameterlessConstructor = t.GetConstructor(EmptyType) != null;
+
+ return hasParameterlessConstructor
+ && !t.GetTypeInfo().IsAbstract
+ && !t.GetTypeInfo().ContainsGenericParameters;
}
- return CompilationResult.Successful(compiledType);
+ return false;
+ }
+
+ public CompilationResult GetOrAdd(RelativeFileInfo fileInfo, Func compile)
+ {
+ if (!_cache.TryGetValue(fileInfo.RelativePath, out var cacheEntry))
+ {
+ return OnCacheMiss(fileInfo, compile);
+ }
+ else
+ {
+ if (cacheEntry.Length != fileInfo.FileInfo.Length)
+ {
+ // it's not a match, recompile
+ return OnCacheMiss(fileInfo, compile);
+ }
+
+ if (cacheEntry.LastModified == fileInfo.FileInfo.LastModified)
+ {
+ // Match, not update needed
+ return CompilationResult.Successful(cacheEntry.ViewType);
+ }
+
+ var hash = RazorFileHash.GetHash(fileInfo.FileInfo);
+
+ // Timestamp doesn't match but it might be because of deployment, compare the hash.
+ if (cacheEntry.IsPreCompiled &&
+ string.Equals(cacheEntry.Hash, hash, StringComparison.Ordinal))
+ {
+ // Cache hit, but we need to update the entry
+ return OnCacheMiss(fileInfo, () => CompilationResult.Successful(cacheEntry.ViewType));
+ }
+
+ // it's not a match, recompile
+ return OnCacheMiss(fileInfo, compile);
+ }
+ }
+
+ private CompilationResult OnCacheMiss(RelativeFileInfo file, Func compile)
+ {
+ var result = compile();
+
+ var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
+ _cache.AddOrUpdate(file.RelativePath, cacheEntry, (a, b) => cacheEntry);
+
+ return result;
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs
new file mode 100644
index 0000000000..107ddf757b
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCacheEntry.cs
@@ -0,0 +1,39 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public class CompilerCacheEntry
+ {
+ public CompilerCacheEntry([NotNull] RazorFileInfo info, [NotNull] Type viewType)
+ {
+ ViewType = viewType;
+ RelativePath = info.RelativePath;
+ Length = info.Length;
+ LastModified = info.LastModified;
+ Hash = info.Hash;
+ }
+
+ public CompilerCacheEntry([NotNull] RelativeFileInfo info, [NotNull] Type viewType)
+ {
+ ViewType = viewType;
+ RelativePath = info.RelativePath;
+ Length = info.FileInfo.Length;
+ LastModified = info.FileInfo.LastModified;
+ }
+
+ public Type ViewType { get; set; }
+ public string RelativePath { get; set; }
+ public long Length { get; set; }
+ public DateTime LastModified { get; set; }
+
+ ///
+ /// The file hash, should only be available for pre compiled files.
+ ///
+ public string Hash { get; set; }
+
+ public bool IsPreCompiled { get { return Hash != null; } }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs
index 0d1d1b8493..6f9abe6170 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs
@@ -7,12 +7,10 @@ using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
-using System.Text;
using Microsoft.AspNet.FileSystems;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit;
-using Microsoft.CodeAnalysis.Text;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor.Compilation
@@ -31,27 +29,31 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
private readonly Lazy> _applicationReferences;
+ private readonly string _classPrefix;
+
///
/// Initalizes a new instance of the class.
///
/// The environment for the executing application.
/// The loader used to load compiled assemblies.
/// The library manager that provides export and reference information.
+ /// The that was used to generate the code.
public RoslynCompilationService(IApplicationEnvironment environment,
IAssemblyLoaderEngine loaderEngine,
- ILibraryManager libraryManager)
+ ILibraryManager libraryManager,
+ IMvcRazorHost host)
{
_environment = environment;
_loader = loaderEngine;
_libraryManager = libraryManager;
_applicationReferences = new Lazy>(GetApplicationReferences);
+ _classPrefix = host.MainClassNamePrefix;
}
///
public CompilationResult Compile(IFileInfo fileInfo, string compilationContent)
{
- var sourceText = SourceText.From(compilationContent, Encoding.UTF8);
- var syntaxTrees = new[] { CSharpSyntaxTree.ParseText(sourceText, path: fileInfo.PhysicalPath) };
+ var syntaxTrees = new[] { SyntaxTreeGenerator.Generate(compilationContent, fileInfo.PhysicalPath) };
var references = _applicationReferences.Value;
@@ -103,9 +105,10 @@ namespace Microsoft.AspNet.Mvc.Razor.Compilation
}
var type = assembly.GetExportedTypes()
- .First();
+ .First(t => t.Name.
+ StartsWith(_classPrefix, StringComparison.Ordinal));
- return UncachedCompilationResult.Successful(type, compilationContent);
+ return UncachedCompilationResult.Successful(type);
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/SyntaxTreeGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/SyntaxTreeGenerator.cs
new file mode 100644
index 0000000000..0598a1a90e
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/SyntaxTreeGenerator.cs
@@ -0,0 +1,52 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.CodeAnalysis.Text;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public static class SyntaxTreeGenerator
+ {
+ private static CSharpParseOptions DefaultOptions
+ {
+ get
+ {
+ return CSharpParseOptions.Default
+ .WithLanguageVersion(LanguageVersion.CSharp6);
+ }
+ }
+
+ public static SyntaxTree Generate([NotNull] string text, [NotNull] string path)
+ {
+ return GenerateCore(text, path, DefaultOptions);
+ }
+
+ public static SyntaxTree Generate([NotNull] string text,
+ [NotNull] string path,
+ [NotNull] CSharpParseOptions options)
+ {
+ return GenerateCore(text, path, options);
+ }
+
+ public static SyntaxTree GenerateCore([NotNull] string text,
+ [NotNull] string path,
+ [NotNull] CSharpParseOptions options)
+ {
+ var sourceText = SourceText.From(text, Encoding.UTF8);
+ var syntaxTree = CSharpSyntaxTree.ParseText(sourceText,
+ path: path,
+ options: options);
+
+ return syntaxTree;
+ }
+
+ public static CSharpParseOptions GetParseOptions(CSharpCompilation compilation)
+ {
+ return CSharpParseOptions.Default
+ .WithLanguageVersion(compilation.LanguageVersion);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs
index 7c4799b7ed..493643dca0 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/UncachedCompilationResult.cs
@@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
}
+ public string RazorFileContent { get; private set; }
+
///
/// Creates a that represents a success in compilation.
///
diff --git a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs
index af2276d16e..daaf28222e 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/IRazorPageFactory.cs
@@ -11,8 +11,8 @@ namespace Microsoft.AspNet.Mvc.Razor
///
/// Creates a for the specified path.
///
- /// The path to locate the page.
+ /// The path to locate the page.
/// The IRazorPage instance if it exists, null otherwise.
- IRazorPage CreateInstance(string path);
+ IRazorPage CreateInstance(string relativePath);
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs
index 6c6a94f88f..6cc16d396e 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/IRazorCompilationService.cs
@@ -1,12 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using Microsoft.AspNet.FileSystems;
-
namespace Microsoft.AspNet.Mvc.Razor
{
public interface IRazorCompilationService
{
- CompilationResult Compile(IFileInfo fileInfo);
+ CompilationResult Compile(RelativeFileInfo fileInfo);
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/GeneratorResultExtensions.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/GeneratorResultExtensions.cs
new file mode 100644
index 0000000000..b083c32417
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/GeneratorResultExtensions.cs
@@ -0,0 +1,35 @@
+using System;
+using System.Linq;
+using Microsoft.AspNet.Razor;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public static class GeneratorResultExtensions
+ {
+ public static string GetMainClassName([NotNull] this GeneratorResults results,
+ [NotNull] IMvcRazorHost host,
+ [NotNull] SyntaxTree syntaxTree)
+ {
+ // The mainClass name should return directly from the generator results.
+ var classes = syntaxTree.GetRoot().DescendantNodes().OfType();
+ var mainClass = classes.FirstOrDefault(c =>
+ c.Identifier.ValueText.StartsWith(host.MainClassNamePrefix, StringComparison.Ordinal));
+
+ if (mainClass != null)
+ {
+ var typeName = mainClass.Identifier.ValueText;
+
+ if (!string.IsNullOrEmpty(host.DefaultNamespace))
+ {
+ typeName = host.DefaultNamespace + "." + typeName;
+ }
+
+ return typeName;
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfo.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfo.cs
new file mode 100644
index 0000000000..211970f045
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfo.cs
@@ -0,0 +1,35 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public class RazorFileInfo
+ {
+ ///
+ /// Type name including namespace.
+ ///
+ public string FullTypeName { get; set; }
+
+ ///
+ /// Last modified at compilation time.
+ ///
+ public DateTime LastModified { get; set; }
+
+ ///
+ /// The length of the file in bytes.
+ ///
+ public long Length { get; set; }
+
+ ///
+ /// Path to to the file relative to the application base.
+ ///
+ public string RelativePath { get; set; }
+
+ ///
+ /// A hash of the file content.
+ ///
+ public string Hash { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollection.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollection.cs
new file mode 100644
index 0000000000..412f628d71
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollection.cs
@@ -0,0 +1,12 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public abstract class RazorFileInfoCollection
+ {
+ public IReadOnlyList FileInfos { get; protected set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs
new file mode 100644
index 0000000000..f8adf5689f
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs
@@ -0,0 +1,117 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public class RazorFileInfoCollectionGenerator
+ {
+ private string _fileFormat;
+
+ protected IReadOnlyList FileInfos { get; private set; }
+ protected CSharpParseOptions Options { get; private set; }
+
+ public RazorFileInfoCollectionGenerator([NotNull] IReadOnlyList fileInfos,
+ [NotNull] CSharpParseOptions options)
+ {
+ FileInfos = fileInfos;
+ Options = options;
+ }
+
+ public virtual SyntaxTree GenerateCollection()
+ {
+ var builder = new StringBuilder();
+ builder.Append(Top);
+
+ foreach (var fileInfo in FileInfos)
+ {
+ var perFileEntry = GenerateFile(fileInfo);
+ builder.Append(perFileEntry);
+ }
+
+ builder.Append(Bottom);
+
+ // TODO: consider saving the file for debuggability
+ var sourceCode = builder.ToString();
+ var syntaxTree = SyntaxTreeGenerator.Generate(sourceCode,
+ "__AUTO__GeneratedViewsCollection.cs",
+ Options);
+
+ return syntaxTree;
+ }
+
+
+ protected virtual string GenerateFile([NotNull] RazorFileInfo fileInfo)
+ {
+ return string.Format(FileFormat,
+ fileInfo.LastModified.ToFileTimeUtc(),
+ fileInfo.Length,
+ fileInfo.RelativePath,
+ fileInfo.FullTypeName,
+ fileInfo.Hash);
+ }
+
+ protected virtual string Top
+ {
+ get
+ {
+ return
+@"using System;
+using System.Collections.Generic;
+using Microsoft.AspNet.Mvc.Razor;
+
+namespace __ASP_ASSEMBLY
+{
+ public class __PreGeneratedViewCollection : " + nameof(RazorFileInfoCollection) + @"
+ {
+ public __PreGeneratedViewCollection()
+ {
+ var fileInfos = new List<" + nameof(RazorFileInfo) + @">();
+ " + nameof(RazorFileInfoCollection.FileInfos) + @" = fileInfos;
+ " + nameof(RazorFileInfo) + @" info;
+
+";
+ }
+ }
+
+ protected virtual string Bottom
+ {
+ get
+ {
+ return
+ @" }
+ }
+}
+";
+ }
+ }
+
+ protected virtual string FileFormat
+ {
+ get
+ {
+ if (_fileFormat == null)
+ {
+ _fileFormat =
+ " info = new "
+ + nameof(RazorFileInfo) + @"
+ {{
+ " + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}),
+ " + nameof(RazorFileInfo.Length) + @" = {1:D},
+ " + nameof(RazorFileInfo.RelativePath) + @" = @""{2}"",
+ " + nameof(RazorFileInfo.FullTypeName) + @" = @""{3}"",
+ " + nameof(RazorFileInfo.Hash) + @" = @""{4}"",
+ }};
+ fileInfos.Add(info);
+";
+ }
+
+ return _fileFormat;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs
new file mode 100644
index 0000000000..1f890996c7
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs
@@ -0,0 +1,158 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.IO;
+using Microsoft.AspNet.FileSystems;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.Runtime;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public class RazorPreCompiler
+ {
+ private readonly IServiceProvider _serviceProvider;
+ private readonly IFileSystem _fileSystem;
+ private readonly IMvcRazorHost _host;
+
+ protected virtual string FileExtension
+ {
+ get
+ {
+ return ".cshtml";
+ }
+ }
+
+ public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider) :
+ this(designTimeServiceProvider, designTimeServiceProvider.GetService())
+ {
+ }
+
+ public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider,
+ [NotNull] IMvcRazorHost host)
+ {
+ _serviceProvider = designTimeServiceProvider;
+ _host = host;
+
+ var appEnv = _serviceProvider.GetService();
+ _fileSystem = new PhysicalFileSystem(appEnv.ApplicationBasePath);
+ }
+
+ public virtual void CompileViews([NotNull] IBeforeCompileContext context)
+ {
+ var descriptors = CreateCompilationDescriptors(context);
+ var collectionGenerator = new RazorFileInfoCollectionGenerator(
+ descriptors,
+ SyntaxTreeGenerator.GetParseOptions(context.CSharpCompilation));
+
+ var tree = collectionGenerator.GenerateCollection();
+ context.CSharpCompilation = context.CSharpCompilation.AddSyntaxTrees(tree);
+ }
+
+ protected virtual IReadOnlyList CreateCompilationDescriptors(
+ [NotNull] IBeforeCompileContext context)
+ {
+ var options = SyntaxTreeGenerator.GetParseOptions(context.CSharpCompilation);
+ var list = new List();
+
+ foreach (var info in GetFileInfosRecursive(string.Empty))
+ {
+ var descriptor = ParseView(info,
+ context,
+ options);
+
+ if (descriptor != null)
+ {
+ list.Add(descriptor);
+ }
+ }
+
+ return list;
+ }
+
+ private IEnumerable GetFileInfosRecursive(string currentPath)
+ {
+ IEnumerable fileInfos;
+ string path = currentPath;
+
+ if (!_fileSystem.TryGetDirectoryContents(path, out fileInfos))
+ {
+ yield break;
+ }
+
+ foreach (var fileInfo in fileInfos)
+ {
+ if (fileInfo.IsDirectory)
+ {
+ var subPath = Path.Combine(path, fileInfo.Name);
+
+ foreach (var info in GetFileInfosRecursive(subPath))
+ {
+ yield return info;
+ }
+ }
+ else if (Path.GetExtension(fileInfo.Name)
+ .Equals(FileExtension, StringComparison.OrdinalIgnoreCase))
+ {
+ var info = new RelativeFileInfo()
+ {
+ FileInfo = fileInfo,
+ RelativePath = Path.Combine(currentPath, fileInfo.Name),
+ };
+
+ yield return info;
+ }
+ }
+ }
+
+ protected virtual RazorFileInfo ParseView([NotNull] RelativeFileInfo fileInfo,
+ [NotNull] IBeforeCompileContext context,
+ [NotNull] CSharpParseOptions options)
+ {
+ using (var stream = fileInfo.FileInfo.CreateReadStream())
+ {
+ var results = _host.GenerateCode(fileInfo.RelativePath, stream);
+ if (results.Success)
+ {
+ var syntaxTree = SyntaxTreeGenerator.Generate(results.GeneratedCode, fileInfo.FileInfo.PhysicalPath, options);
+ var fullTypeName = results.GetMainClassName(_host, syntaxTree);
+
+ if (fullTypeName != null)
+ {
+ context.CSharpCompilation = context.CSharpCompilation.AddSyntaxTrees(syntaxTree);
+
+ var hash = RazorFileHash.GetHash(fileInfo.FileInfo);
+
+ return new RazorFileInfo()
+ {
+ FullTypeName = fullTypeName,
+ RelativePath = fileInfo.RelativePath,
+ LastModified = fileInfo.FileInfo.LastModified,
+ Length = fileInfo.FileInfo.Length,
+ Hash = hash,
+ };
+ }
+ }
+ }
+
+ // TODO: Add diagnostics when view parsing/code generation failed.
+ return null;
+ }
+ }
+}
+
+namespace Microsoft.Framework.Runtime
+{
+ [AssemblyNeutral]
+ public interface IBeforeCompileContext
+ {
+ CSharpCompilation CSharpCompilation { get; set; }
+
+ IList Resources { get; }
+
+ IList Diagnostics { get; }
+ }
+}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RelativeFileInfo.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RelativeFileInfo.cs
new file mode 100644
index 0000000000..c561fdcba9
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RelativeFileInfo.cs
@@ -0,0 +1,13 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using Microsoft.AspNet.FileSystems;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public class RelativeFileInfo
+ {
+ public IFileInfo FileInfo { get; set; }
+ public string RelativePath { get; set; }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs
index 2b06869907..af77886ad6 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorCompilationService.cs
@@ -1,66 +1,48 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
-using System;
-using System.Diagnostics.Contracts;
-using System.IO;
using System.Linq;
-using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
-using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor
{
public class RazorCompilationService : IRazorCompilationService
{
// This class must be registered as a singleton service for the caching to work.
- private readonly CompilerCache _cache = new CompilerCache();
- private readonly IApplicationEnvironment _environment;
+ private readonly CompilerCache _cache;
private readonly ICompilationService _baseCompilationService;
private readonly IMvcRazorHost _razorHost;
- private readonly string _appRoot;
- public RazorCompilationService(IApplicationEnvironment environment,
- ICompilationService compilationService,
+ public RazorCompilationService(ICompilationService compilationService,
+ IControllerAssemblyProvider _controllerAssemblyProvider,
IMvcRazorHost razorHost)
{
- _environment = environment;
_baseCompilationService = compilationService;
_razorHost = razorHost;
- _appRoot = EnsureTrailingSlash(environment.ApplicationBasePath);
+ _cache = new CompilerCache(_controllerAssemblyProvider.CandidateAssemblies);
}
- public CompilationResult Compile([NotNull] IFileInfo file)
+ public CompilationResult Compile([NotNull] RelativeFileInfo file)
{
return _cache.GetOrAdd(file, () => CompileCore(file));
}
- internal CompilationResult CompileCore(IFileInfo file)
+ internal CompilationResult CompileCore(RelativeFileInfo file)
{
GeneratorResults results;
- using (var inputStream = file.CreateReadStream())
+ using (var inputStream = file.FileInfo.CreateReadStream())
{
- Contract.Assert(file.PhysicalPath.StartsWith(_appRoot, StringComparison.OrdinalIgnoreCase));
- var rootRelativePath = file.PhysicalPath.Substring(_appRoot.Length);
- results = _razorHost.GenerateCode(rootRelativePath, inputStream);
+ results = _razorHost.GenerateCode(
+ file.RelativePath, inputStream);
}
if (!results.Success)
{
var messages = results.ParserErrors.Select(e => new CompilationMessage(e.Message));
- return CompilationResult.Failed(file, results.GeneratedCode, messages);
+ return CompilationResult.Failed(file.FileInfo, results.GeneratedCode, messages);
}
- return _baseCompilationService.Compile(file, results.GeneratedCode);
- }
-
- private static string EnsureTrailingSlash([NotNull]string path)
- {
- if (!path.EndsWith(Path.DirectorySeparatorChar.ToString()))
- {
- path += Path.DirectorySeparatorChar;
- }
- return path;
+ return _baseCompilationService.Compile(file.FileInfo, results.GeneratedCode);
}
}
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorFileHash.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorFileHash.cs
new file mode 100644
index 0000000000..bb752cbb97
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/RazorFileHash.cs
@@ -0,0 +1,37 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.IO;
+using System.Security.Cryptography;
+using Microsoft.AspNet.FileSystems;
+
+namespace Microsoft.AspNet.Mvc.Razor
+{
+ public static class RazorFileHash
+ {
+ public static string GetHash([NotNull] IFileInfo file)
+ {
+ try
+ {
+ using (var stream = file.CreateReadStream())
+ {
+ return GetHash(stream);
+ }
+ }
+ catch (Exception)
+ {
+ // Don't throw if reading the file fails.
+ return string.Empty;
+ }
+ }
+
+ internal static string GetHash(Stream stream)
+ {
+ using (var md5 = MD5.Create())
+ {
+ return BitConverter.ToString(md5.ComputeHash(stream));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs
index b55ede7a85..304a0b9509 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs
+++ b/src/Microsoft.AspNet.Mvc.Razor/VirtualPathRazorPageFactory.cs
@@ -30,15 +30,22 @@ namespace Microsoft.AspNet.Mvc.Razor
}
///
- public IRazorPage CreateInstance([NotNull] string path)
+ public IRazorPage CreateInstance([NotNull] string relativePath)
{
- var fileInfo = _fileInfoCache.GetFileInfo(path);
+ var fileInfo = _fileInfoCache.GetFileInfo(relativePath);
if (fileInfo != null)
{
- var result = _compilationService.Compile(fileInfo);
+ var relativeFileInfo = new RelativeFileInfo()
+ {
+ FileInfo = fileInfo,
+ RelativePath = relativePath,
+ };
+
+ var result = _compilationService.Compile(relativeFileInfo);
var page = (IRazorPage)_activator.CreateInstance(_serviceProvider, result.CompiledType);
- page.Path = path;
+ page.Path = relativePath;
+
return page;
}
diff --git a/src/Microsoft.AspNet.Mvc.Razor/project.json b/src/Microsoft.AspNet.Mvc.Razor/project.json
index c6c45fcc33..7cf17263a0 100644
--- a/src/Microsoft.AspNet.Mvc.Razor/project.json
+++ b/src/Microsoft.AspNet.Mvc.Razor/project.json
@@ -30,6 +30,7 @@
"System.Collections": "4.0.10.0",
"System.Collections.Concurrent": "4.0.0.0",
"System.ComponentModel": "4.0.0.0",
+ "System.Security.Cryptography.Hashing.Algorithms": "4.0.0.0",
"System.Diagnostics.Contracts": "4.0.0.0",
"System.Diagnostics.Debug": "4.0.10.0",
"System.Diagnostics.Tools": "4.0.0.0",
diff --git a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs
new file mode 100644
index 0000000000..560a157143
--- /dev/null
+++ b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs
@@ -0,0 +1,57 @@
+// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using Microsoft.AspNet.Mvc.Razor;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp;
+using Microsoft.Framework.Runtime;
+using Microsoft.Framework.DependencyInjection;
+using Microsoft.Framework.DependencyInjection.Fallback;
+
+namespace Microsoft.AspNet.Mvc
+{
+ public abstract class RazorPreCompileModule : ICompileModule
+ {
+ private readonly IServiceProvider _appServices;
+
+ public RazorPreCompileModule(IServiceProvider services)
+ {
+ _appServices = services;
+ }
+
+ public virtual void BeforeCompile(IBeforeCompileContext context)
+ {
+ var sc = new ServiceCollection();
+ sc.Add(MvcServices.GetDefaultServices());
+ var sp = sc.BuildServiceProvider(_appServices);
+
+ var viewCompiler = new RazorPreCompiler(sp);
+ viewCompiler.CompileViews(context);
+ }
+
+ public void AfterCompile(IAfterCompileContext context)
+ {
+ }
+ }
+}
+
+namespace Microsoft.Framework.Runtime
+{
+ [AssemblyNeutral]
+ public interface ICompileModule
+ {
+ void BeforeCompile(IBeforeCompileContext context);
+
+ void AfterCompile(IAfterCompileContext context);
+ }
+
+ [AssemblyNeutral]
+ public interface IAfterCompileContext
+ {
+ CSharpCompilation CSharpCompilation { get; set; }
+
+ IList Diagnostics { get; }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs
index e5c3c6ef12..7da7fe847e 100644
--- a/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Core.Test/Filters/DefaultFilterProviderTest.cs
@@ -28,8 +28,6 @@ namespace Microsoft.AspNet.Mvc.Filters
var provider = CreateProvider();
- //System.Diagnostics.Debugger.Launch();
- //System.Diagnostics.Debugger.Break();
// Act
provider.Invoke(context, () => { });
var results = context.Results;
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs
index 13a02efabe..3230f626fc 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs
@@ -2,6 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
using Microsoft.AspNet.FileSystems;
using Moq;
using Xunit;
@@ -15,12 +18,23 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
{
// Arrange
var cache = new CompilerCache();
- var fileInfo = Mock.Of();
+ var fileInfo = new Mock();
+
+ fileInfo
+ .SetupGet(i => i.LastModified)
+ .Returns(DateTime.FromFileTimeUtc(10000));
+
var type = GetType();
var expected = UncachedCompilationResult.Successful(type, "hello world");
+ var runtimeFileInfo = new RelativeFileInfo()
+ {
+ FileInfo = fileInfo.Object,
+ RelativePath = "ab",
+ };
+
// Act
- var actual = cache.GetOrAdd(fileInfo, () => expected);
+ var actual = cache.GetOrAdd(runtimeFileInfo, () => expected);
// Assert
Assert.Same(expected, actual);
@@ -28,6 +42,117 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
Assert.Same(type, actual.CompiledType);
}
+ private abstract class View
+ {
+ public abstract string Content { get; }
+ }
+
+ private class PreCompile : View
+ {
+ public override string Content { get { return "Hello World it's @DateTime.Now"; } }
+ }
+
+ private class RuntimeCompileIdentical : View
+ {
+ public override string Content { get { return new PreCompile().Content; } }
+ }
+
+ private class RuntimeCompileDifferent : View
+ {
+ public override string Content { get { return new PreCompile().Content.Substring(1) + " "; } }
+ }
+
+ private class RuntimeCompileDifferentLength : View
+ {
+ public override string Content
+ {
+ get
+ {
+ return new PreCompile().Content + " longer because it was modified at runtime";
+ }
+ }
+ }
+
+ private class ViewCollection : RazorFileInfoCollection
+ {
+ public ViewCollection()
+ {
+ var fileInfos = new List();
+ FileInfos = fileInfos;
+
+ var content = new PreCompile().Content;
+ var length = Encoding.UTF8.GetByteCount(content);
+
+ fileInfos.Add(new RazorFileInfo()
+ {
+ FullTypeName = typeof(PreCompile).FullName,
+ Hash = RazorFileHash.GetHash(GetMemoryStream(content)),
+ LastModified = DateTime.FromFileTimeUtc(10000),
+ Length = length,
+ RelativePath = "ab",
+ });
+ }
+ }
+
+ private static Stream GetMemoryStream(string content)
+ {
+ var bytes = Encoding.UTF8.GetBytes(content);
+
+ return new MemoryStream(bytes);
+ }
+
+ [Theory]
+ [InlineData(typeof(RuntimeCompileIdentical), 10000, false)]
+ [InlineData(typeof(RuntimeCompileIdentical), 11000, false)]
+ [InlineData(typeof(RuntimeCompileDifferent), 10000, false)] // expected failure: same time and length
+ [InlineData(typeof(RuntimeCompileDifferent), 11000, true)]
+ [InlineData(typeof(RuntimeCompileDifferentLength), 10000, true)]
+ [InlineData(typeof(RuntimeCompileDifferentLength), 10000, true)]
+ public void FileWithTheSameLengthAndDifferentTime_DoesNot_OverridesPrecompilation(
+ Type resultViewType,
+ long fileTimeUTC,
+ bool swapsPreCompile)
+ {
+ // Arrange
+ var instance = (View)Activator.CreateInstance(resultViewType);
+ var length = Encoding.UTF8.GetByteCount(instance.Content);
+
+ var collection = new ViewCollection();
+ var cache = new CompilerCache(new[] { new ViewCollection() });
+
+ var fileInfo = new Mock();
+ fileInfo
+ .SetupGet(i => i.Length)
+ .Returns(length);
+ fileInfo
+ .SetupGet(i => i.LastModified)
+ .Returns(DateTime.FromFileTimeUtc(fileTimeUTC));
+ fileInfo.Setup(i => i.CreateReadStream())
+ .Returns(GetMemoryStream(instance.Content));
+
+ var preCompileType = typeof(PreCompile);
+
+ var runtimeFileInfo = new RelativeFileInfo()
+ {
+ FileInfo = fileInfo.Object,
+ RelativePath = "ab",
+ };
+
+ // Act
+ var actual = cache.GetOrAdd(runtimeFileInfo,
+ () => CompilationResult.Successful(resultViewType));
+
+ // Assert
+ if (swapsPreCompile)
+ {
+ Assert.Equal(actual.CompiledType, resultViewType);
+ }
+ else
+ {
+ Assert.Equal(actual.CompiledType, typeof(PreCompile));
+ }
+ }
+
[Fact]
public void GetOrAdd_DoesNotCacheCompiledContent_OnCallsAfterInitial()
{
@@ -42,10 +167,16 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
var type = GetType();
var uncachedResult = UncachedCompilationResult.Successful(type, "hello world");
+ var runtimeFileInfo = new RelativeFileInfo()
+ {
+ FileInfo = fileInfo.Object,
+ RelativePath = "test",
+ };
+
// Act
- cache.GetOrAdd(fileInfo.Object, () => uncachedResult);
- var actual1 = cache.GetOrAdd(fileInfo.Object, () => uncachedResult);
- var actual2 = cache.GetOrAdd(fileInfo.Object, () => uncachedResult);
+ cache.GetOrAdd(runtimeFileInfo, () => uncachedResult);
+ var actual1 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult);
+ var actual2 = cache.GetOrAdd(runtimeFileInfo, () => uncachedResult);
// Assert
Assert.NotSame(uncachedResult, actual1);
diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs
index fc43e1e07f..93c2157e53 100644
--- a/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs
+++ b/test/Microsoft.AspNet.Mvc.Razor.Test/RazorCompilationServiceTest.cs
@@ -2,11 +2,12 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.IO;
+using System.Linq;
+using System.Reflection;
using Microsoft.AspNet.FileSystems;
using Microsoft.AspNet.Razor;
using Microsoft.AspNet.Razor.Generator.Compiler;
using Microsoft.AspNet.Razor.Parser.SyntaxTree;
-using Microsoft.Framework.Runtime;
using Moq;
using Xunit;
@@ -20,24 +21,32 @@ namespace Microsoft.AspNet.Mvc.Razor.Test
public void CompileCoreCalculatesRootRelativePath(string appPath, string viewPath)
{
// Arrange
- var env = new Mock();
- env.SetupGet(e => e.ApplicationName).Returns("MyTestApplication");
- env.SetupGet(e => e.ApplicationBasePath).Returns(appPath);
var host = new Mock();
host.Setup(h => h.GenerateCode(@"views\index\home.cshtml", It.IsAny()))
.Returns(new GeneratorResults(new Block(new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], new CodeBuilderResult("", new LineMapping[0])))
.Verifiable();
+ var ap = new Mock();
+ ap.SetupGet(e => e.CandidateAssemblies)
+ .Returns(Enumerable.Empty())
+ .Verifiable();
+
var fileInfo = new Mock();
fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath);
fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null);
var compiler = new Mock();
compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny()))
.Returns(CompilationResult.Successful(typeof(RazorCompilationServiceTest)));
- var razorService = new RazorCompilationService(env.Object, compiler.Object, host.Object);
+ var razorService = new RazorCompilationService(compiler.Object, ap.Object, host.Object);
+
+ var relativeFileInfo = new RelativeFileInfo()
+ {
+ FileInfo = fileInfo.Object,
+ RelativePath = @"views\index\home.cshtml",
+ };
// Act
- razorService.CompileCore(fileInfo.Object);
+ razorService.CompileCore(relativeFileInfo);
// Assert
host.Verify();