* Move precompiled assemblies into a resource in the primary assembly.

* Emit the primary assembly if a tag helper requires it.
* Make TagHelperSample.Web use precompilation.

Fixes #1693
This commit is contained in:
Pranav K 2015-02-02 07:57:23 -08:00
parent 6b3119e2c5
commit 57f5b19f25
25 changed files with 698 additions and 154 deletions

View File

@ -3,13 +3,19 @@
using System; using System;
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.Framework.Runtime;
namespace MvcSample.Web namespace MvcSample.Web
{ {
public class RazorPreCompilation : RazorPreCompileModule public class RazorPreCompilation : RazorPreCompileModule
{ {
public RazorPreCompilation(IServiceProvider provider) : base(provider) public RazorPreCompilation(IServiceProvider provider,
IApplicationEnvironment applicationEnvironment)
: base(provider)
{ {
GenerateSymbols = string.Equals(applicationEnvironment.Configuration,
"debug",
StringComparison.OrdinalIgnoreCase);
} }
} }
} }

View File

@ -0,0 +1,21 @@
// 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 Microsoft.AspNet.Mvc;
using Microsoft.Framework.Runtime;
namespace TagHelperSample.Web
{
public class TagHelperPrecompilation : RazorPreCompileModule
{
public TagHelperPrecompilation(IServiceProvider provider,
IApplicationEnvironment applicationEnvironment)
: base(provider)
{
GenerateSymbols = string.Equals(applicationEnvironment.Configuration,
"debug",
StringComparison.OrdinalIgnoreCase);
}
}
}

View File

@ -8,6 +8,7 @@ using System.Reflection;
using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.FileProviders;
using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
@ -16,6 +17,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public class CompilerCache : ICompilerCache public class CompilerCache : ICompilerCache
{ {
private static readonly TypeInfo RazorFileInfoCollectionType = typeof(RazorFileInfoCollection).GetTypeInfo();
private readonly IFileProvider _fileProvider; private readonly IFileProvider _fileProvider;
private readonly IMemoryCache _cache; private readonly IMemoryCache _cache;
@ -23,27 +25,30 @@ namespace Microsoft.AspNet.Mvc.Razor
/// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views /// Initializes a new instance of <see cref="CompilerCache"/> populated with precompiled views
/// discovered using <paramref name="provider"/>. /// discovered using <paramref name="provider"/>.
/// </summary> /// </summary>
/// <param name="provider"> /// <param name="assemblyProvider">The <see cref="IAssemblyProvider"/> that provides assemblies
/// An <see cref="IAssemblyProvider"/> representing the assemblies /// for precompiled view discovery.</param>
/// used to search for pre-compiled views. /// <param name="loaderContextAccessor">The <see cref="IAssemblyLoadContextAccessor"/>.</param>
/// </param>
/// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param> /// <param name="optionsAccessor">An accessor to the <see cref="RazorViewEngineOptions"/>.</param>
public CompilerCache(IAssemblyProvider provider, public CompilerCache(IAssemblyProvider assemblyProvider,
IAssemblyLoadContextAccessor loadContextAccessor,
IOptions<RazorViewEngineOptions> optionsAccessor) IOptions<RazorViewEngineOptions> optionsAccessor)
: this(GetFileInfos(provider.CandidateAssemblies), optionsAccessor.Options.FileProvider) : this(GetFileInfos(assemblyProvider.CandidateAssemblies),
loadContextAccessor.GetLoadContext(RazorFileInfoCollectionType.Assembly),
optionsAccessor.Options.FileProvider)
{ {
} }
// Internal for unit testing internal CompilerCache(IEnumerable<RazorFileInfoCollection> razorFileInfoCollections,
internal CompilerCache(IEnumerable<RazorFileInfoCollection> razorFileInfoCollection, IAssemblyLoadContext loadContext,
IFileProvider fileProvider) IFileProvider fileProvider)
{ {
_fileProvider = fileProvider; _fileProvider = fileProvider;
_cache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false }); _cache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false });
var cacheEntries = new List<CompilerCacheEntry>(); var cacheEntries = new List<CompilerCacheEntry>();
foreach (var viewCollection in razorFileInfoCollection) foreach (var viewCollection in razorFileInfoCollections)
{ {
var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly; var containingAssembly = viewCollection.LoadAssembly(loadContext);
foreach (var fileInfo in viewCollection.FileInfos) foreach (var fileInfo in viewCollection.FileInfos)
{ {
var viewType = containingAssembly.GetType(fileInfo.FullTypeName); var viewType = containingAssembly.GetType(fileInfo.FullTypeName);
@ -247,7 +252,7 @@ namespace Microsoft.AspNet.Mvc.Razor
.Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c)); .Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c));
} }
private static bool Match(Type t) internal static bool Match(Type t)
{ {
var inAssemblyType = typeof(RazorFileInfoCollection); var inAssemblyType = typeof(RazorFileInfoCollection);
if (inAssemblyType.IsAssignableFrom(t)) if (inAssemblyType.IsAssignableFrom(t))
@ -262,7 +267,6 @@ namespace Microsoft.AspNet.Mvc.Razor
return false; return false;
} }
private class GetOrAddResult private class GetOrAddResult
{ {
public CompilerCacheEntry CompilerCacheEntry { get; set; } public CompilerCacheEntry CompilerCacheEntry { get; set; }

View File

@ -9,8 +9,8 @@ using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Reflection;
using System.Reflection.PortableExecutable; using System.Reflection.PortableExecutable;
using System.Runtime.InteropServices;
using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Mvc.Razor.Internal;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.Emit; using Microsoft.CodeAnalysis.Emit;
@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.Razor
/// </summary> /// </summary>
public class RoslynCompilationService : ICompilationService public class RoslynCompilationService : ICompilationService
{ {
private readonly Lazy<bool> _supportsPdbGeneration = new Lazy<bool>(SupportsPdbGeneration); private readonly Lazy<bool> _supportsPdbGeneration = new Lazy<bool>(SymbolsUtility.SupportsSymbolsGeneration);
private readonly ConcurrentDictionary<string, AssemblyMetadata> _metadataFileCache = private readonly ConcurrentDictionary<string, AssemblyMetadata> _metadataFileCache =
new ConcurrentDictionary<string, AssemblyMetadata>(StringComparer.OrdinalIgnoreCase); new ConcurrentDictionary<string, AssemblyMetadata>(StringComparer.OrdinalIgnoreCase);
@ -229,32 +229,6 @@ namespace Microsoft.AspNet.Mvc.Razor
return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error; return diagnostic.IsWarningAsError || diagnostic.Severity == DiagnosticSeverity.Error;
} }
private static bool SupportsPdbGeneration()
{
try
{
if (PlatformHelper.IsMono)
{
return false;
}
// Check for the pdb writer component that roslyn uses to generate pdbs
const string SymWriterGuid = "0AE2DEB0-F901-478b-BB9F-881EE8066788";
var type = Marshal.GetTypeFromCLSID(new Guid(SymWriterGuid));
if (type != null)
{
// This line will throw if pdb generation is not supported.
Activator.CreateInstance(type);
return true;
}
}
catch
{
}
return false;
}
} }
} }

View File

@ -0,0 +1,45 @@
// 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.Runtime.InteropServices;
namespace Microsoft.AspNet.Mvc.Razor.Internal
{
/// <summary>
/// Utility type for determining if a platform supports symbol file generation.
/// </summary>
public class SymbolsUtility
{
private const string SymWriterGuid = "0AE2DEB0-F901-478b-BB9F-881EE8066788";
/// <summary>
/// Determines if the current platform supports symbols (pdb) generation.
/// </summary>
/// <returns><c>true</c> if pdb generation is supported; <c>false</c> otherwise.</returns>
public static bool SupportsSymbolsGeneration()
{
if (PlatformHelper.IsMono)
{
return false;
}
try
{
// Check for the pdb writer component that roslyn uses to generate pdbs
var type = Marshal.GetTypeFromCLSID(new Guid(SymWriterGuid));
if (type != null)
{
// This line will throw if pdb generation is not supported.
Activator.CreateInstance(type);
return true;
}
}
catch
{
}
return false;
}
}
}

View File

@ -378,6 +378,22 @@ namespace Microsoft.AspNet.Mvc.Razor
return GetString("RazorHash_UnsupportedHashAlgorithm"); return GetString("RazorHash_UnsupportedHashAlgorithm");
} }
/// <summary>
/// The resource '{0}' specified by '{1}' could not be found.
/// </summary>
internal static string RazorFileInfoCollection_ResourceCouldNotBeFound
{
get { return GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"); }
}
/// <summary>
/// The resource '{0}' specified by '{1}' could not be found.
/// </summary>
internal static string FormatRazorFileInfoCollection_ResourceCouldNotBeFound(object p0, object p1)
{
return string.Format(CultureInfo.CurrentCulture, GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"), p0, p1);
}
private static string GetString(string name, params string[] formatterNames) private static string GetString(string name, params string[] formatterNames)
{ {
var value = _resourceManager.GetString(name); var value = _resourceManager.GetString(name);

View File

@ -0,0 +1,80 @@
// 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 System.Linq;
using System.Reflection;
using System.Threading;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.Framework.Runtime;
using Microsoft.Framework.Runtime.Roslyn;
namespace Microsoft.AspNet.Mvc.Razor
{
/// <summary>
/// <see cref="TagHelperDescriptorResolver"/> used during Razor precompilation.
/// </summary>
public class PrecompilationTagHelperTypeResolver : TagHelperTypeResolver
{
private static readonly string TagHelperTypeName = typeof(ITagHelper).FullName;
private readonly IBeforeCompileContext _compileContext;
private readonly IAssemblyLoadContext _loadContext;
private object _compilationLock = new object();
private bool _assemblyEmited;
private TypeInfo[] _exportedTypeInfos;
/// <summary>
/// Initializes a new instance of <see cref="PrecompilationTagHelperTypeResolver"/>.
/// </summary>
/// <param name="compileContext">The <see cref="IBeforeCompileContext"/>.</param>
/// <param name="loadContext">The <see cref="IAssemblyLoadContext"/>.</param>
public PrecompilationTagHelperTypeResolver([NotNull] IBeforeCompileContext compileContext,
[NotNull] IAssemblyLoadContext loadContext)
{
_compileContext = compileContext;
_loadContext = loadContext;
}
/// <inheritdoc />
protected override IEnumerable<TypeInfo> GetExportedTypes([NotNull] AssemblyName assemblyName)
{
var compilingAssemblyName = _compileContext.Compilation.AssemblyName;
if (string.Equals(assemblyName.Name, compilingAssemblyName, StringComparison.Ordinal))
{
return LazyInitializer.EnsureInitialized(ref _exportedTypeInfos,
ref _assemblyEmited,
ref _compilationLock,
GetExportedTypesFromCompilation);
}
return base.GetExportedTypes(assemblyName);
}
private TypeInfo[] GetExportedTypesFromCompilation()
{
using (var stream = new MemoryStream())
{
var assemblyName = string.Join(".", _compileContext.Compilation.AssemblyName,
nameof(PrecompilationTagHelperTypeResolver),
Path.GetRandomFileName());
var emitResult = _compileContext.Compilation
.WithAssemblyName(assemblyName)
.Emit(stream);
if (!emitResult.Success)
{
// Return an empty sequence. Compilation will fail once precompilation completes.
return new TypeInfo[0];
}
stream.Position = 0;
var assembly = _loadContext.LoadStream(stream, assemblySymbols: null);
return assembly.ExportedTypes
.Select(type => type.GetTypeInfo())
.ToArray();
}
}
}
}

View File

@ -1,12 +1,63 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // 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.Collections.Generic;
using System.IO;
using System.Reflection;
using Microsoft.Framework.Runtime;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
{ {
/// <summary>
/// Specifies metadata about precompiled views.
/// </summary>
public abstract class RazorFileInfoCollection public abstract class RazorFileInfoCollection
{ {
/// <summary>
/// Gets or sets the name of the resource containing the precompiled binary.
/// </summary>
public string AssemblyResourceName { get; protected set; }
/// <summary>
/// Gets or sets the name of the resource that contains the symbols (pdb).
/// </summary>
public string SymbolsResourceName { get; protected set; }
/// <summary>
/// Gets the <see cref="IReadOnlyList{T}{T}"/> of <see cref="RazorFileInfo"/>s.
/// </summary>
public IReadOnlyList<RazorFileInfo> FileInfos { get; protected set; } public IReadOnlyList<RazorFileInfo> FileInfos { get; protected set; }
/// <summary>
/// Loads the assembly containing precompiled views.
/// </summary>
/// <param name="loadContext">The <see cref="IAssemblyLoadContext"/>.</param>
/// <returns>The <see cref="Assembly"/> containing precompiled views.</returns>
public virtual Assembly LoadAssembly(IAssemblyLoadContext loadContext)
{
var viewCollectionAssembly = GetType().GetTypeInfo().Assembly;
using (var assemblyStream = viewCollectionAssembly.GetManifestResourceStream(AssemblyResourceName))
{
if (assemblyStream == null)
{
var message = Resources.FormatRazorFileInfoCollection_ResourceCouldNotBeFound(AssemblyResourceName,
GetType().FullName);
throw new InvalidOperationException(message);
}
Stream symbolsStream = null;
if (!string.IsNullOrEmpty(SymbolsResourceName))
{
symbolsStream = viewCollectionAssembly.GetManifestResourceStream(SymbolsResourceName);
}
using (symbolsStream)
{
return loadContext.LoadStream(assemblyStream, symbolsStream);
}
}
}
} }
} }

View File

@ -1,9 +1,10 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. // 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. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Collections.Generic; using System.Globalization;
using System.Text; using System.Text;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.Framework.Runtime;
using Microsoft.Framework.Runtime.Roslyn; using Microsoft.Framework.Runtime.Roslyn;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
@ -12,23 +13,26 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
private string _fileFormat; private string _fileFormat;
public RazorFileInfoCollectionGenerator([NotNull] IEnumerable<RazorFileInfo> fileInfos, public RazorFileInfoCollectionGenerator([NotNull] RazorFileInfoCollection fileInfoCollection,
[NotNull] CompilationSettings compilationSettings) [NotNull] CompilationSettings compilationSettings)
{ {
FileInfos = fileInfos; RazorFileInfoCollection = fileInfoCollection;
CompilationSettings = compilationSettings; CompilationSettings = compilationSettings;
} }
protected IEnumerable<RazorFileInfo> FileInfos { get; } protected RazorFileInfoCollection RazorFileInfoCollection { get; }
protected CompilationSettings CompilationSettings { get; } protected CompilationSettings CompilationSettings { get; }
public virtual SyntaxTree GenerateCollection() public virtual SyntaxTree GenerateCollection()
{ {
var builder = new StringBuilder(); var builder = new StringBuilder();
builder.Append(Top); builder.AppendFormat(CultureInfo.InvariantCulture,
TopFormat,
RazorFileInfoCollection.AssemblyResourceName,
RazorFileInfoCollection.SymbolsResourceName);
foreach (var fileInfo in FileInfos) foreach (var fileInfo in RazorFileInfoCollection.FileInfos)
{ {
var perFileEntry = GenerateFile(fileInfo); var perFileEntry = GenerateFile(fileInfo);
builder.Append(perFileEntry); builder.Append(perFileEntry);
@ -55,25 +59,27 @@ namespace Microsoft.AspNet.Mvc.Razor
fileInfo.HashAlgorithmVersion); fileInfo.HashAlgorithmVersion);
} }
protected virtual string Top protected virtual string TopFormat
{ {
get get
{ {
return return
@"using System; $@"using System;
using System.Collections.Generic; using System.Collections.Generic;
using Microsoft.AspNet.Mvc.Razor; using System.Reflection;
using {typeof(RazorFileInfoCollection).Namespace};
namespace __ASP_ASSEMBLY namespace __ASP_ASSEMBLY
{ {{{{
public class __PreGeneratedViewCollection : " + nameof(RazorFileInfoCollection) + @" public class __PreGeneratedViewCollection : {nameof(RazorFileInfoCollection)}
{ {{{{
public __PreGeneratedViewCollection() public __PreGeneratedViewCollection()
{ {{{{
var fileInfos = new List<" + nameof(RazorFileInfo) + @">(); {nameof(RazorFileInfoCollection.AssemblyResourceName)} = ""{{0}}"";
" + nameof(RazorFileInfoCollection.FileInfos) + @" = fileInfos; {nameof(RazorFileInfoCollection.SymbolsResourceName)} = ""{{1}}"";
" + nameof(RazorFileInfo) + @" info; var fileInfos = new List<{nameof(RazorFileInfo)}>();
{nameof(RazorFileInfoCollection.FileInfos)} = fileInfos;
{nameof(RazorFileInfo)} info;
"; ";
} }
} }
@ -83,9 +89,20 @@ namespace __ASP_ASSEMBLY
get get
{ {
return return
@" } $@"
} }}
} private static Assembly _loadedAssembly;
public override Assembly LoadAssembly({typeof(IAssemblyLoadContext).FullName} loadContext)
{{
if (_loadedAssembly == null)
{{
_loadedAssembly = base.LoadAssembly(loadContext);
}}
return _loadedAssembly;
}}
}}
}}
"; ";
} }
} }
@ -97,16 +114,16 @@ namespace __ASP_ASSEMBLY
if (_fileFormat == null) if (_fileFormat == null)
{ {
_fileFormat = _fileFormat =
" info = new " $@"
+ nameof(RazorFileInfo) + @" info = new {nameof(RazorFileInfo)}
{{ {{{{
" + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}).ToLocalTime(), {nameof(RazorFileInfo.LastModified)} = DateTime.FromFileTimeUtc({{0:D}}).ToLocalTime(),
" + nameof(RazorFileInfo.Length) + @" = {1:D}, {nameof(RazorFileInfo.Length)} = {{1:D}},
" + nameof(RazorFileInfo.RelativePath) + @" = @""{2}"", {nameof(RazorFileInfo.RelativePath)} = @""{{2}}"",
" + nameof(RazorFileInfo.FullTypeName) + @" = @""{3}"", {nameof(RazorFileInfo.FullTypeName)} = @""{{3}}"",
" + nameof(RazorFileInfo.Hash) + @" = ""{4}"", {nameof(RazorFileInfo.Hash)} = ""{{4}}"",
" + nameof(RazorFileInfo.HashAlgorithmVersion) + @" = {5}, {nameof(RazorFileInfo.HashAlgorithmVersion)} = {{5}},
}}; }}}};
fileInfos.Add(info); fileInfos.Add(info);
"; ";
} }

View File

@ -5,12 +5,18 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.FileProviders; using Microsoft.AspNet.FileProviders;
using Microsoft.AspNet.Mvc.Razor.Directives;
using Microsoft.AspNet.Mvc.Razor.Internal;
using Microsoft.AspNet.Razor.Runtime.TagHelpers;
using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.Cache.Memory;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Runtime;
using Microsoft.Framework.Runtime.Roslyn; using Microsoft.Framework.Runtime.Roslyn;
namespace Microsoft.AspNet.Mvc.Razor namespace Microsoft.AspNet.Mvc.Razor
@ -21,9 +27,12 @@ namespace Microsoft.AspNet.Mvc.Razor
private readonly IFileProvider _fileProvider; private readonly IFileProvider _fileProvider;
public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider,
[NotNull] IBeforeCompileContext compileContext,
[NotNull] IMemoryCache precompilationCache, [NotNull] IMemoryCache precompilationCache,
[NotNull] CompilationSettings compilationSettings) : [NotNull] CompilationSettings compilationSettings) :
this(designTimeServiceProvider, this(designTimeServiceProvider,
compileContext,
designTimeServiceProvider.GetRequiredService<IAssemblyLoadContextAccessor>(),
designTimeServiceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(), designTimeServiceProvider.GetRequiredService<IOptions<RazorViewEngineOptions>>(),
precompilationCache, precompilationCache,
compilationSettings) compilationSettings)
@ -31,16 +40,30 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider,
[NotNull] IBeforeCompileContext compileContext,
[NotNull] IAssemblyLoadContextAccessor loadContextAccessor,
[NotNull] IOptions<RazorViewEngineOptions> optionsAccessor, [NotNull] IOptions<RazorViewEngineOptions> optionsAccessor,
[NotNull] IMemoryCache precompilationCache, [NotNull] IMemoryCache precompilationCache,
[NotNull] CompilationSettings compilationSettings) [NotNull] CompilationSettings compilationSettings)
{ {
_serviceProvider = designTimeServiceProvider; _serviceProvider = designTimeServiceProvider;
CompileContext = compileContext;
LoadContext = loadContextAccessor.GetLoadContext(GetType().GetTypeInfo().Assembly);
_fileProvider = optionsAccessor.Options.FileProvider; _fileProvider = optionsAccessor.Options.FileProvider;
CompilationSettings = compilationSettings; CompilationSettings = compilationSettings;
PreCompilationCache = precompilationCache; PreCompilationCache = precompilationCache;
TagHelperTypeResolver = new PrecompilationTagHelperTypeResolver(CompileContext, LoadContext);
} }
/// <summary>
/// Gets or sets a value that determines if symbols (.pdb) file for the precompiled views.
/// </summary>
public bool GenerateSymbols { get; set; }
protected IBeforeCompileContext CompileContext { get; }
protected IAssemblyLoadContext LoadContext { get; }
protected CompilationSettings CompilationSettings { get; } protected CompilationSettings CompilationSettings { get; }
protected IMemoryCache PreCompilationCache { get; } protected IMemoryCache PreCompilationCache { get; }
@ -49,32 +72,37 @@ namespace Microsoft.AspNet.Mvc.Razor
protected virtual int MaxDegreesOfParallelism { get; } = Environment.ProcessorCount; protected virtual int MaxDegreesOfParallelism { get; } = Environment.ProcessorCount;
protected virtual TagHelperTypeResolver TagHelperTypeResolver { get; }
public virtual void CompileViews([NotNull] IBeforeCompileContext context) public virtual void CompileViews()
{ {
var descriptors = CreateCompilationDescriptors(context); var result = CreateFileInfoCollection();
if (descriptors.Any()) if (result != null)
{ {
var collectionGenerator = new RazorFileInfoCollectionGenerator( var collectionGenerator = new RazorFileInfoCollectionGenerator(
descriptors, result,
CompilationSettings); CompilationSettings);
var tree = collectionGenerator.GenerateCollection(); var tree = collectionGenerator.GenerateCollection();
context.Compilation = context.Compilation.AddSyntaxTrees(tree); CompileContext.Compilation = CompileContext.Compilation.AddSyntaxTrees(tree);
} }
} }
protected virtual IEnumerable<RazorFileInfo> CreateCompilationDescriptors( protected virtual RazorFileInfoCollection CreateFileInfoCollection()
[NotNull] IBeforeCompileContext context)
{ {
var filesToProcess = new List<RelativeFileInfo>(); var filesToProcess = new List<RelativeFileInfo>();
GetFileInfosRecursive(root: string.Empty, razorFiles: filesToProcess); GetFileInfosRecursive(root: string.Empty, razorFiles: filesToProcess);
if (filesToProcess.Count == 0)
{
return null;
}
var razorFiles = new RazorFileInfo[filesToProcess.Count]; var razorFiles = new RazorFileInfo[filesToProcess.Count];
var syntaxTrees = new SyntaxTree[filesToProcess.Count]; var syntaxTrees = new SyntaxTree[filesToProcess.Count];
var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = MaxDegreesOfParallelism }; var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = MaxDegreesOfParallelism };
var diagnosticsLock = new object(); var diagnosticsLock = new object();
var hasErrors = false;
Parallel.For(0, filesToProcess.Count, parallelOptions, index => Parallel.For(0, filesToProcess.Count, parallelOptions, index =>
{ {
@ -91,25 +119,91 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
else else
{ {
hasErrors = true;
lock (diagnosticsLock) lock (diagnosticsLock)
{ {
foreach (var diagnostic in cacheEntry.Diagnostics) AddRange(CompileContext.Diagnostics, cacheEntry.Diagnostics);
{
context.Diagnostics.Add(diagnostic);
}
} }
} }
} }
}); });
context.Compilation = context.Compilation if (hasErrors)
.AddSyntaxTrees(syntaxTrees.Where(tree => tree != null)); {
return razorFiles.Where(file => file != null); // If any of the Razor files had syntax errors, don't emit the precompiled views assembly.
return null;
}
return GeneratePrecompiledAssembly(syntaxTrees.Where(tree => tree != null),
razorFiles.Where(file => file != null));
}
protected virtual RazorFileInfoCollection GeneratePrecompiledAssembly(
[NotNull] IEnumerable<SyntaxTree> syntaxTrees,
[NotNull] IEnumerable<RazorFileInfo> razorFileInfos)
{
var resourcePrefix = string.Join(".", CompileContext.Compilation.AssemblyName,
nameof(RazorPreCompiler),
Path.GetRandomFileName());
var assemblyResourceName = resourcePrefix + ".dll";
var applicationReference = CompileContext.Compilation.ToMetadataReference();
var references = CompileContext.Compilation.References
.Concat(new[] { applicationReference });
var preCompilationOptions = CompilationSettings.CompilationOptions
.WithOutputKind(OutputKind.DynamicallyLinkedLibrary);
var compilation = CSharpCompilation.Create(assemblyResourceName,
options: preCompilationOptions,
syntaxTrees: syntaxTrees,
references: references);
var generateSymbols = GenerateSymbols && SymbolsUtility.SupportsSymbolsGeneration();
// These streams are returned to the runtime and consequently cannot be disposed.
var assemblyStream = new MemoryStream();
var pdbStream = generateSymbols ? new MemoryStream() : null;
var emitResult = compilation.Emit(assemblyStream, pdbStream);
if (!emitResult.Success)
{
AddRange(CompileContext.Diagnostics, emitResult.Diagnostics);
return null;
}
else
{
assemblyStream.Position = 0;
var assemblyResource = new ResourceDescription(assemblyResourceName,
() => assemblyStream,
isPublic: true);
CompileContext.Resources.Add(assemblyResource);
string symbolsResourceName = null;
if (pdbStream != null)
{
symbolsResourceName = resourcePrefix + ".pdb";
pdbStream.Position = 0;
var pdbResource = new ResourceDescription(symbolsResourceName,
() => pdbStream,
isPublic: true);
CompileContext.Resources.Add(pdbResource);
}
return new PrecompileRazorFileInfoCollection(assemblyResourceName,
symbolsResourceName,
razorFileInfos.ToList());
}
} }
protected IMvcRazorHost GetRazorHost() protected IMvcRazorHost GetRazorHost()
{ {
return _serviceProvider.GetRequiredService<IMvcRazorHost>(); var descriptorResolver = new TagHelperDescriptorResolver(TagHelperTypeResolver);
return new MvcRazorHost(new DefaultCodeTreeCache(_fileProvider))
{
TagHelperDescriptorResolver = descriptorResolver
};
} }
private PrecompilationCacheEntry OnCacheMiss(ICacheSetContext cacheSetContext) private PrecompilationCacheEntry OnCacheMiss(ICacheSetContext cacheSetContext)
@ -193,5 +287,25 @@ namespace Microsoft.AspNet.Mvc.Razor
return null; return null;
} }
private static void AddRange<TVal>(IList<TVal> target, IEnumerable<TVal> source)
{
foreach (var diagnostic in source)
{
target.Add(diagnostic);
}
}
private class PrecompileRazorFileInfoCollection : RazorFileInfoCollection
{
public PrecompileRazorFileInfoCollection(string assemblyResourceName,
string symbolsResourceName,
IReadOnlyList<RazorFileInfo> fileInfos)
{
AssemblyResourceName = assemblyResourceName;
SymbolsResourceName = symbolsResourceName;
FileInfos = fileInfos;
}
}
} }
} }

View File

@ -186,4 +186,7 @@
<data name="RazorHash_UnsupportedHashAlgorithm" xml:space="preserve"> <data name="RazorHash_UnsupportedHashAlgorithm" xml:space="preserve">
<value>Unsupported hash algorithm.</value> <value>Unsupported hash algorithm.</value>
</data> </data>
<data name="RazorFileInfoCollection_ResourceCouldNotBeFound" xml:space="preserve">
<value>The resource '{0}' specified by '{1}' could not be found.</value>
</data>
</root> </root>

View File

@ -124,7 +124,7 @@ namespace Microsoft.AspNet.Mvc
// The host is designed to be discarded after consumption and is very inexpensive to initialize. // The host is designed to be discarded after consumption and is very inexpensive to initialize.
yield return describe.Transient<IMvcRazorHost, MvcRazorHost>(); yield return describe.Transient<IMvcRazorHost, MvcRazorHost>();
// Caches compilation artifacts across the lifetime of the application. // Caches compilation artifacts across the lifetime of the application.
yield return describe.Singleton<ICompilerCache, CompilerCache>(); yield return describe.Singleton<ICompilerCache, CompilerCache>();

View File

@ -31,6 +31,11 @@ namespace Microsoft.AspNet.Mvc
_memoryCache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false }); _memoryCache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false });
} }
/// <summary>
/// Gets or sets a value that determines if symbols (.pdb) file for the precompiled views.
/// </summary>
public bool GenerateSymbols { get; protected set; }
protected virtual string FileExtension { get; } = ".cshtml"; protected virtual string FileExtension { get; } = ".cshtml";
public virtual void BeforeCompile(IBeforeCompileContext context) public virtual void BeforeCompile(IBeforeCompileContext context)
@ -45,8 +50,11 @@ namespace Microsoft.AspNet.Mvc
sc.AddMvc(); sc.AddMvc();
var serviceProvider = BuildFallbackServiceProvider(sc, _appServices); var serviceProvider = BuildFallbackServiceProvider(sc, _appServices);
var viewCompiler = new RazorPreCompiler(serviceProvider, _memoryCache, compilationSettings); var viewCompiler = new RazorPreCompiler(serviceProvider, context, _memoryCache, compilationSettings)
viewCompiler.CompileViews(context); {
GenerateSymbols = GenerateSymbols
};
viewCompiler.CompileViews();
} }
public void AfterCompile(IAfterCompileContext context) public void AfterCompile(IAfterCompileContext context)

View File

@ -8,6 +8,7 @@ using System.Net;
using System.Reflection; using System.Reflection;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Builder; using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Mvc.Razor;
using Microsoft.AspNet.TestHost; using Microsoft.AspNet.TestHost;
using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.DependencyInjection;
using Microsoft.Framework.Runtime; using Microsoft.Framework.Runtime;
@ -39,7 +40,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// We will render a view that writes the fully qualified name of the Assembly containing the type of // We will render a view that writes the fully qualified name of the Assembly containing the type of
// the view. If the view is precompiled, this assembly will be PrecompilationWebsite. // the view. If the view is precompiled, this assembly will be PrecompilationWebsite.
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); var assemblyNamePrefix = GetAssemblyNamePrefix();
try try
{ {
@ -50,9 +51,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert - 1 // Assert - 1
Assert.Equal(HttpStatusCode.OK, response.StatusCode); Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var parsedResponse1 = new ParsedResponse(responseContent); var parsedResponse1 = new ParsedResponse(responseContent);
Assert.Equal(assemblyName, parsedResponse1.ViewStart); Assert.StartsWith(assemblyNamePrefix, parsedResponse1.ViewStart);
Assert.Equal(assemblyName, parsedResponse1.Layout); Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Layout);
Assert.Equal(assemblyName, parsedResponse1.Index); Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Index);
// Act - 2 // Act - 2
// Touch the Layout file and verify it is now dynamically compiled. // Touch the Layout file and verify it is now dynamically compiled.
@ -61,9 +62,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert - 2 // Assert - 2
var response2 = new ParsedResponse(responseContent); var response2 = new ParsedResponse(responseContent);
Assert.NotEqual(assemblyName, response2.Layout); Assert.StartsWith(assemblyNamePrefix, response2.ViewStart);
Assert.Equal(assemblyName, response2.ViewStart); Assert.StartsWith(assemblyNamePrefix, response2.Index);
Assert.Equal(assemblyName, response2.Index); Assert.DoesNotContain(assemblyNamePrefix, response2.Layout);
// Act - 3 // Act - 3
// Touch the _ViewStart file and verify it is is dynamically compiled. // Touch the _ViewStart file and verify it is is dynamically compiled.
@ -72,8 +73,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
// Assert - 3 // Assert - 3
var response3 = new ParsedResponse(responseContent); var response3 = new ParsedResponse(responseContent);
Assert.NotEqual(assemblyName, response3.ViewStart); Assert.NotEqual(assemblyNamePrefix, response3.ViewStart);
Assert.Equal(assemblyName, response3.Index); Assert.Equal(response2.Index, response3.Index);
Assert.Equal(response2.Layout, response3.Layout); Assert.Equal(response2.Layout, response3.Layout);
// Act - 4 // Act - 4
@ -159,13 +160,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
public async Task PrecompiledView_UsesCompilationOptionsFromApplication() public async Task PrecompiledView_UsesCompilationOptionsFromApplication()
{ {
// Arrange // Arrange
var assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); var assemblyNamePrefix = GetAssemblyNamePrefix();
#if ASPNET50 #if ASPNET50
var expected = var expected =
@"Value set inside ASPNET50 " + assemblyName; @"Value set inside ASPNET50 " + assemblyNamePrefix;
#elif ASPNETCORE50 #elif ASPNETCORE50
var expected = var expected =
@"Value set inside ASPNETCORE50 " + assemblyName; @"Value set inside ASPNETCORE50 " + assemblyNamePrefix;
#endif #endif
var server = TestServer.Create(_services, _app); var server = TestServer.Create(_services, _app);
@ -176,14 +177,14 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var responseContent = await response.Content.ReadAsStringAsync(); var responseContent = await response.Content.ReadAsStringAsync();
// Assert // Assert
Assert.Equal(expected, responseContent.Trim()); Assert.StartsWith(expected, responseContent.Trim());
} }
[Fact] [Fact]
public async Task DeletingPrecompiledGlobalFile_PriorToFirstRequestToAView_CausesViewToBeRecompiled() public async Task DeletingPrecompiledGlobalFile_PriorToFirstRequestToAView_CausesViewToBeRecompiled()
{ {
// Arrange // Arrange
var expected = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); var expected = GetAssemblyNamePrefix();
var server = TestServer.Create(_services, _app); var server = TestServer.Create(_services, _app);
var client = server.CreateClient(); var client = server.CreateClient();
@ -209,7 +210,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
var response2 = await client.GetStringAsync("http://localhost/Home/GlobalDeletedPriorToFirstRequest"); var response2 = await client.GetStringAsync("http://localhost/Home/GlobalDeletedPriorToFirstRequest");
// Assert - 2 // Assert - 2
Assert.NotEqual(expected, response2.Trim()); Assert.DoesNotContain(expected, response2.Trim());
} }
finally finally
{ {
@ -217,6 +218,49 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests
} }
} }
[Fact]
public async Task TagHelpersFromTheApplication_CanBeAdded()
{
// Arrange
var assemblyNamePrefix = GetAssemblyNamePrefix();
var expected = @"<root data-root=""true""><input class=""form-control"" type=""number"" data-val=""true""" +
@" data-val-range=""The field Age must be between 10 and 100."" data-val-range-max=""100"" "+
@"data-val-range-min=""10"" id=""Age"" name=""Age"" value="""" /><a href="""">Back to List</a></root>";
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TagHelpers/Add");
// Assert
var responseLines = response.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith(assemblyNamePrefix, responseLines[0]);
Assert.Equal(expected, responseLines[1]);
}
[Fact]
public async Task TagHelpersFromTheApplication_CanBeRemoved()
{
// Arrange
var assemblyNamePrefix = GetAssemblyNamePrefix();
var expected = @"<root>root-content</root>";
var server = TestServer.Create(_services, _app);
var client = server.CreateClient();
// Act
var response = await client.GetStringAsync("http://localhost/TagHelpers/Remove");
// Assert
var responseLines = response.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries);
Assert.StartsWith(assemblyNamePrefix, responseLines[0]);
Assert.Equal(expected, responseLines[1]);
}
private static string GetAssemblyNamePrefix()
{
return typeof(Startup).GetTypeInfo().Assembly.GetName().Name + "." + nameof(RazorPreCompiler) + ".";
}
private static async Task<string> TouchFile(string viewsDir, string file) private static async Task<string> TouchFile(string viewsDir, string file)
{ {
var path = Path.Combine(viewsDir, file); var path = Path.Combine(viewsDir, file);

View File

@ -6,7 +6,9 @@ using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Text; using System.Text;
using Microsoft.Framework.Runtime;
using Moq; using Moq;
using Xunit; using Xunit;
@ -15,13 +17,14 @@ namespace Microsoft.AspNet.Mvc.Razor
public class CompilerCacheTest public class CompilerCacheTest
{ {
private const string ViewPath = "view-path"; private const string ViewPath = "view-path";
private readonly IAssemblyLoadContext TestLoadContext = Mock.Of<IAssemblyLoadContext>();
[Fact] [Fact]
public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem() public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem()
{ {
// Arrange // Arrange
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = GetType(); var type = GetType();
// Act // Act
@ -38,7 +41,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = GetType(); var type = GetType();
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = UncachedCompilationResult.Successful(type, "hello world");
@ -60,7 +63,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = typeof(RuntimeCompileIdentical); var type = typeof(RuntimeCompileIdentical);
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = UncachedCompilationResult.Successful(type, "hello world");
@ -88,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = typeof(RuntimeCompileIdentical); var type = typeof(RuntimeCompileIdentical);
var expected1 = UncachedCompilationResult.Successful(type, "hello world"); var expected1 = UncachedCompilationResult.Successful(type, "hello world");
var expected2 = UncachedCompilationResult.Successful(type, "different content"); var expected2 = UncachedCompilationResult.Successful(type, "different content");
@ -116,7 +119,7 @@ namespace Microsoft.AspNet.Mvc.Razor
var mockFileProvider = new Mock<TestFileProvider> { CallBase = true }; var mockFileProvider = new Mock<TestFileProvider> { CallBase = true };
var fileProvider = mockFileProvider.Object; var fileProvider = mockFileProvider.Object;
fileProvider.AddFile(ViewPath, "some content"); fileProvider.AddFile(ViewPath, "some content");
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var type = typeof(RuntimeCompileIdentical); var type = typeof(RuntimeCompileIdentical);
var expected = UncachedCompilationResult.Successful(type, "hello world"); var expected = UncachedCompilationResult.Successful(type, "hello world");
@ -168,34 +171,6 @@ namespace Microsoft.AspNet.Mvc.Razor
} }
} }
private class ViewCollection : RazorFileInfoCollection
{
private readonly List<RazorFileInfo> _fileInfos = new List<RazorFileInfo>();
public ViewCollection()
{
FileInfos = _fileInfos;
var content = new PreCompile().Content;
var length = Encoding.UTF8.GetByteCount(content);
Add(new RazorFileInfo()
{
FullTypeName = typeof(PreCompile).FullName,
Hash = Crc32.Calculate(GetMemoryStream(content)).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000),
Length = length,
RelativePath = ViewPath,
});
}
public void Add(RazorFileInfo fileInfo)
{
_fileInfos.Add(fileInfo);
}
}
private static Stream GetMemoryStream(string content) private static Stream GetMemoryStream(string content)
{ {
var bytes = Encoding.UTF8.GetBytes(content); var bytes = Encoding.UTF8.GetBytes(content);
@ -211,10 +186,8 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var instance = new RuntimeCompileIdentical(); var instance = new RuntimeCompileIdentical();
var length = Encoding.UTF8.GetByteCount(instance.Content); var length = Encoding.UTF8.GetByteCount(instance.Content);
var collection = new ViewCollection();
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { new ViewCollection() }, fileProvider); var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider);
var fileInfo = new TestFileInfo var fileInfo = new TestFileInfo
{ {
Length = length, Length = length,
@ -222,7 +195,6 @@ namespace Microsoft.AspNet.Mvc.Razor
Content = instance.Content Content = instance.Content
}; };
fileProvider.AddFile(ViewPath, fileInfo); fileProvider.AddFile(ViewPath, fileInfo);
var precompiledContent = new PreCompile().Content;
// Act // Act
var result = cache.GetOrAdd(ViewPath, var result = cache.GetOrAdd(ViewPath,
@ -246,9 +218,8 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var instance = (View)Activator.CreateInstance(resultViewType); var instance = (View)Activator.CreateInstance(resultViewType);
var length = Encoding.UTF8.GetByteCount(instance.Content); var length = Encoding.UTF8.GetByteCount(instance.Content);
var collection = new ViewCollection();
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var cache = new CompilerCache(new[] { new ViewCollection() }, fileProvider); var cache = new CompilerCache(new[] { new ViewCollection() }, TestLoadContext, fileProvider);
var fileInfo = new TestFileInfo var fileInfo = new TestFileInfo
{ {
@ -303,10 +274,9 @@ namespace Microsoft.AspNet.Mvc.Razor
RelativePath = "_GlobalImport.cshtml", RelativePath = "_GlobalImport.cshtml",
FullTypeName = typeof(RuntimeCompileIdentical).FullName FullTypeName = typeof(RuntimeCompileIdentical).FullName
}; };
var precompiledViews = new ViewCollection(); var precompiledViews = new ViewCollection();
precompiledViews.Add(globalRazorFileInfo); precompiledViews.Add(globalRazorFileInfo);
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider); var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
// Act // Act
var result = cache.GetOrAdd(ViewPath, var result = cache.GetOrAdd(ViewPath,
@ -325,8 +295,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var precompiledViews = new ViewCollection(); var precompiledViews = new ViewCollection();
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var precompiledView = precompiledViews.FileInfos[0]; var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider);
// Act // Act
var result = cache.GetOrAdd(ViewPath, var result = cache.GetOrAdd(ViewPath,
@ -351,7 +320,7 @@ namespace Microsoft.AspNet.Mvc.Razor
LastModified = precompiledView.LastModified, LastModified = precompiledView.LastModified,
}; };
fileProvider.AddFile(ViewPath, fileInfo); fileProvider.AddFile(ViewPath, fileInfo);
var cache = new CompilerCache(new[] { precompiledViews }, fileProvider); var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider);
// Act 1 // Act 1
var result1 = cache.GetOrAdd(ViewPath, var result1 = cache.GetOrAdd(ViewPath,
@ -381,12 +350,11 @@ namespace Microsoft.AspNet.Mvc.Razor
{ {
// Arrange // Arrange
var expectedType = typeof(RuntimeCompileDifferent); var expectedType = typeof(RuntimeCompileDifferent);
var lastModified = DateTime.UtcNow;
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var collection = new ViewCollection(); var collection = new ViewCollection();
var precompiledFile = collection.FileInfos[0]; var precompiledFile = collection.FileInfos[0];
precompiledFile.RelativePath = "Views\\home\\index.cshtml"; precompiledFile.RelativePath = "Views\\home\\index.cshtml";
var cache = new CompilerCache(new[] { collection }, fileProvider); var cache = new CompilerCache(new[] { collection }, TestLoadContext, fileProvider);
var testFile = new TestFileInfo var testFile = new TestFileInfo
{ {
Content = new PreCompile().Content, Content = new PreCompile().Content,
@ -456,7 +424,7 @@ namespace Microsoft.AspNet.Mvc.Razor
fileProvider.AddFile(globalFileInfo.PhysicalPath, globalFileInfo); fileProvider.AddFile(globalFileInfo.PhysicalPath, globalFileInfo);
viewCollection.Add(globalFile); viewCollection.Add(globalFile);
var cache = new CompilerCache(new[] { viewCollection }, fileProvider); var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider);
// Act 1 // Act 1
var result1 = cache.GetOrAdd(viewFileInfo.PhysicalPath, var result1 = cache.GetOrAdd(viewFileInfo.PhysicalPath,
@ -542,7 +510,7 @@ namespace Microsoft.AspNet.Mvc.Razor
fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo); fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo);
fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, globalFileInfo); fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, globalFileInfo);
var viewCollection = new ViewCollection(); var viewCollection = new ViewCollection();
var cache = new CompilerCache(new[] { viewCollection }, fileProvider); var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider);
// Act // Act
var result = cache.GetOrAdd(fileInfo.PhysicalPath, var result = cache.GetOrAdd(fileInfo.PhysicalPath,
@ -561,7 +529,7 @@ namespace Microsoft.AspNet.Mvc.Razor
// Arrange // Arrange
var lastModified = DateTime.UtcNow; var lastModified = DateTime.UtcNow;
var fileProvider = new TestFileProvider(); var fileProvider = new TestFileProvider();
var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), fileProvider); var cache = new CompilerCache(Enumerable.Empty<RazorFileInfoCollection>(), TestLoadContext, fileProvider);
var fileInfo = new TestFileInfo var fileInfo = new TestFileInfo
{ {
PhysicalPath = "test", PhysicalPath = "test",
@ -592,5 +560,125 @@ namespace Microsoft.AspNet.Mvc.Razor
Assert.Null(actual2.CompiledContent); Assert.Null(actual2.CompiledContent);
Assert.Same(type, actual2.CompiledType); Assert.Same(type, actual2.CompiledType);
} }
[Fact]
public void Match_ReturnsFalse_IfTypeIsAbstract()
{
// Arrange
var type = typeof(AbstractRazorFileInfoCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsFalse_IfTypeHasGenericParameters()
{
// Arrange
var type = typeof(GenericRazorFileInfoCollection<>);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsFalse_IfTypeDoesNotHaveDefaultConstructor()
{
// Arrange
var type = typeof(ParameterConstructorRazorFileInfoCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsFalse_IfTypeDoesNotDeriveFromRazorFileInfoCollection()
{
// Arrange
var type = typeof(NonSubTypeRazorFileInfoCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.False(result);
}
[Fact]
public void Match_ReturnsTrue_IfTypeDerivesFromRazorFileInfoCollection()
{
// Arrange
var type = typeof(ViewCollection);
// Act
var result = CompilerCache.Match(type);
// Assert
Assert.True(result);
}
private abstract class AbstractRazorFileInfoCollection : RazorFileInfoCollection
{
}
private class GenericRazorFileInfoCollection<TVal> : RazorFileInfoCollection
{
}
private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection
{
public ParameterConstructorRazorFileInfoCollection(string value)
{
}
}
private class NonSubTypeRazorFileInfoCollection : Controller
{
}
private class ViewCollection : RazorFileInfoCollection
{
private readonly List<RazorFileInfo> _fileInfos = new List<RazorFileInfo>();
public ViewCollection()
{
FileInfos = _fileInfos;
var content = new PreCompile().Content;
var length = Encoding.UTF8.GetByteCount(content);
Add(new RazorFileInfo()
{
FullTypeName = typeof(PreCompile).FullName,
Hash = Crc32.Calculate(GetMemoryStream(content)).ToString(CultureInfo.InvariantCulture),
HashAlgorithmVersion = 1,
LastModified = DateTime.FromFileTimeUtc(10000),
Length = length,
RelativePath = ViewPath,
});
}
public void Add(RazorFileInfo fileInfo)
{
_fileInfos.Add(fileInfo);
}
public override Assembly LoadAssembly(IAssemblyLoadContext loadContext)
{
return typeof(ViewCollection).Assembly;
}
}
} }
} }

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 System;
using Xunit;
namespace Microsoft.AspNet.Mvc.Razor
{
public class DefaultPrecompiledViewsProviderTest
{
}
}

View File

@ -0,0 +1,20 @@
// 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.Mvc;
namespace PrecompilationWebSite.Controllers
{
public class TagHelpersController : Controller
{
public IActionResult Add()
{
return View();
}
public IActionResult Remove()
{
return View();
}
}
}

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 System.ComponentModel.DataAnnotations;
namespace PrecompilationWebSite.Models
{
public class Person
{
[Range(10, 100)]
public int Age { get; set; }
}
}

View File

@ -0,0 +1,16 @@
// 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.Razor.Runtime.TagHelpers;
namespace PrecompilationWebSite.TagHelpers
{
[HtmlElementName("root")]
public class RootViewStartTagHelper : TagHelper
{
public override void Process(TagHelperContext context, TagHelperOutput output)
{
output.Attributes["data-root"] = "true";
}
}
}

View File

@ -0,0 +1,3 @@
@model PrecompilationWebSite.Models.Person
@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers"
<root><input asp-for="Age" class="form-control" /><a asp-action="Index">Back to List</a></root>

View File

@ -0,0 +1,2 @@
@removeTagHelper "*, PrecompilationWebSite"
<root>root-content</root>

View File

@ -0,0 +1 @@
@addTagHelper "PrecompilationWebSite.TagHelpers.RootViewStartTagHelper, PrecompilationWebSite"

View File

@ -0,0 +1,2 @@
@using System.Reflection
@GetType().GetTypeInfo().Assembly.FullName

View File

@ -6,6 +6,7 @@
"dependencies": { "dependencies": {
"Kestrel": "1.0.0-*", "Kestrel": "1.0.0-*",
"Microsoft.AspNet.Mvc": "6.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*",
"Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-*",
"Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0",
"Microsoft.AspNet.Server.IIS": "1.0.0-*", "Microsoft.AspNet.Server.IIS": "1.0.0-*",
"Microsoft.AspNet.Server.WebListener": "1.0.0-*", "Microsoft.AspNet.Server.WebListener": "1.0.0-*",

View File

@ -4,15 +4,17 @@
using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc;
using Microsoft.AspNet.Mvc.Razor; using Microsoft.AspNet.Mvc.Razor;
using Microsoft.Framework.OptionsModel; using Microsoft.Framework.OptionsModel;
using Microsoft.Framework.Runtime;
namespace RazorCompilerCacheWebSite namespace RazorCompilerCacheWebSite
{ {
public class CustomCompilerCache : CompilerCache public class CustomCompilerCache : CompilerCache
{ {
public CustomCompilerCache(IAssemblyProvider assemblyProvider, public CustomCompilerCache(IAssemblyProvider assemblyProvider,
IAssemblyLoadContextAccessor loadContextAccessor,
IOptions<RazorViewEngineOptions> optionsAccessor, IOptions<RazorViewEngineOptions> optionsAccessor,
CompilerCacheInitialiedService cacheInitializedService) CompilerCacheInitialiedService cacheInitializedService)
: base(assemblyProvider, optionsAccessor) : base(assemblyProvider, loadContextAccessor, optionsAccessor)
{ {
cacheInitializedService.Initialized = true; cacheInitializedService.Initialized = true;
} }