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();