Create a pre compilation module and apis to allow meta programming

to precompile razor pages.

This is limited to sites where the .cshtml are still deployed. It's
current purpose is to speed up startup. Deploying without the razor
files is a separate feature.
This commit is contained in:
YishaiGalatzer 2014-09-19 11:25:48 -07:00
parent 43c7ddb9b7
commit 6600e68fc0
24 changed files with 853 additions and 75 deletions

View File

@ -9,5 +9,15 @@ namespace Microsoft.AspNet.Mvc.Razor
public interface IMvcRazorHost
{
GeneratorResults GenerateCode(string rootRelativePath, Stream inputStream);
/// <summary>
/// Represent the prefix off the main entry class in the view.
/// </summary>
string MainClassNamePrefix { get; }
/// <summary>
/// Represent the namespace the main entry class in the view.
/// </summary>
string DefaultNamespace { get; }
}
}

View File

@ -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"; }
}
/// <inheritdoc />
public string MainClassNamePrefix
{
get { return "ASPV_"; }
}
/// <summary>
/// Gets the list of chunks that are injected by default by this host.
/// </summary>
@ -121,7 +126,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <inheritdoc />
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);

View File

@ -120,7 +120,7 @@ namespace Microsoft.AspNet.Mvc.Razor
}
}
}
catch (IOException)
catch (Exception)
{
// Don't throw if reading the file fails.
return string.Empty;

View File

@ -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<string, Type> _cache;
private readonly ConcurrentDictionary<string, CompilerCacheEntry> _cache;
private static readonly Type[] EmptyType = new Type[0];
public CompilerCache()
public CompilerCache([NotNull] IEnumerable<Assembly> assemblies)
: this(GetFileInfos(assemblies))
{
_cache = new ConcurrentDictionary<string, Type>(StringComparer.OrdinalIgnoreCase);
}
public CompilationResult GetOrAdd(IFileInfo file, Func<CompilationResult> compile)
internal CompilerCache(IEnumerable<RazorFileInfoCollection> 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<string, CompilerCacheEntry>(StringComparer.OrdinalIgnoreCase);
}
internal static IEnumerable<RazorFileInfoCollection>
GetFileInfos(IEnumerable<Assembly> 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<CompilationResult> 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<CompilationResult> compile)
{
var result = compile();
var cacheEntry = new CompilerCacheEntry(file, result.CompiledType);
_cache.AddOrUpdate(file.RelativePath, cacheEntry, (a, b) => cacheEntry);
return result;
}
}
}

View File

@ -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; }
/// <summary>
/// The file hash, should only be available for pre compiled files.
/// </summary>
public string Hash { get; set; }
public bool IsPreCompiled { get { return Hash != null; } }
}
}

View File

@ -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<List<MetadataReference>> _applicationReferences;
private readonly string _classPrefix;
/// <summary>
/// Initalizes a new instance of the <see cref="RoslynCompilationService"/> class.
/// </summary>
/// <param name="environment">The environment for the executing application.</param>
/// <param name="loaderEngine">The loader used to load compiled assemblies.</param>
/// <param name="libraryManager">The library manager that provides export and reference information.</param>
/// <param name="host">The <see cref="IMvcRazorHost"/> that was used to generate the code.</param>
public RoslynCompilationService(IApplicationEnvironment environment,
IAssemblyLoaderEngine loaderEngine,
ILibraryManager libraryManager)
ILibraryManager libraryManager,
IMvcRazorHost host)
{
_environment = environment;
_loader = loaderEngine;
_libraryManager = libraryManager;
_applicationReferences = new Lazy<List<MetadataReference>>(GetApplicationReferences);
_classPrefix = host.MainClassNamePrefix;
}
/// <inheritdoc />
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);
}
}
}

View File

@ -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);
}
}
}

View File

@ -14,6 +14,8 @@ namespace Microsoft.AspNet.Mvc.Razor
{
}
public string RazorFileContent { get; private set; }
/// <summary>
/// Creates a <see cref="UncachedCompilationResult"/> that represents a success in compilation.
/// </summary>

View File

@ -11,8 +11,8 @@ namespace Microsoft.AspNet.Mvc.Razor
/// <summary>
/// Creates a <see cref="IRazorPage"/> for the specified path.
/// </summary>
/// <param name="path">The path to locate the page.</param>
/// <param name="relativePath">The path to locate the page.</param>
/// <returns>The IRazorPage instance if it exists, null otherwise.</returns>
IRazorPage CreateInstance(string path);
IRazorPage CreateInstance(string relativePath);
}
}

View File

@ -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);
}
}

View File

@ -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<ClassDeclarationSyntax>();
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;
}
}
}

View File

@ -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
{
/// <summary>
/// Type name including namespace.
/// </summary>
public string FullTypeName { get; set; }
/// <summary>
/// Last modified at compilation time.
/// </summary>
public DateTime LastModified { get; set; }
/// <summary>
/// The length of the file in bytes.
/// </summary>
public long Length { get; set; }
/// <summary>
/// Path to to the file relative to the application base.
/// </summary>
public string RelativePath { get; set; }
/// <summary>
/// A hash of the file content.
/// </summary>
public string Hash { get; set; }
}
}

View File

@ -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<RazorFileInfo> FileInfos { get; protected set; }
}
}

View File

@ -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<RazorFileInfo> FileInfos { get; private set; }
protected CSharpParseOptions Options { get; private set; }
public RazorFileInfoCollectionGenerator([NotNull] IReadOnlyList<RazorFileInfo> 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;
}
}
}
}

View File

@ -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<IMvcRazorHost>())
{
}
public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider,
[NotNull] IMvcRazorHost host)
{
_serviceProvider = designTimeServiceProvider;
_host = host;
var appEnv = _serviceProvider.GetService<IApplicationEnvironment>();
_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<RazorFileInfo> CreateCompilationDescriptors(
[NotNull] IBeforeCompileContext context)
{
var options = SyntaxTreeGenerator.GetParseOptions(context.CSharpCompilation);
var list = new List<RazorFileInfo>();
foreach (var info in GetFileInfosRecursive(string.Empty))
{
var descriptor = ParseView(info,
context,
options);
if (descriptor != null)
{
list.Add(descriptor);
}
}
return list;
}
private IEnumerable<RelativeFileInfo> GetFileInfosRecursive(string currentPath)
{
IEnumerable<IFileInfo> 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<ResourceDescription> Resources { get; }
IList<Diagnostic> Diagnostics { get; }
}
}

View File

@ -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; }
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}
}
}

View File

@ -30,15 +30,22 @@ namespace Microsoft.AspNet.Mvc.Razor
}
/// <inheritdoc />
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;
}

View File

@ -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",

View File

@ -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<Diagnostic> Diagnostics { get; }
}
}

View File

@ -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;

View File

@ -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<IFileInfo>();
var fileInfo = new Mock<IFileInfo>();
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<RazorFileInfo>();
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<IFileInfo>();
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);

View File

@ -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<IApplicationEnvironment>();
env.SetupGet(e => e.ApplicationName).Returns("MyTestApplication");
env.SetupGet(e => e.ApplicationBasePath).Returns(appPath);
var host = new Mock<IMvcRazorHost>();
host.Setup(h => h.GenerateCode(@"views\index\home.cshtml", It.IsAny<Stream>()))
.Returns(new GeneratorResults(new Block(new BlockBuilder { Type = BlockType.Comment }), new RazorError[0], new CodeBuilderResult("", new LineMapping[0])))
.Verifiable();
var ap = new Mock<IControllerAssemblyProvider>();
ap.SetupGet(e => e.CandidateAssemblies)
.Returns(Enumerable.Empty<Assembly>())
.Verifiable();
var fileInfo = new Mock<IFileInfo>();
fileInfo.Setup(f => f.PhysicalPath).Returns(viewPath);
fileInfo.Setup(f => f.CreateReadStream()).Returns(Stream.Null);
var compiler = new Mock<ICompilationService>();
compiler.Setup(c => c.Compile(fileInfo.Object, It.IsAny<string>()))
.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();