From 57f5b19f25950ab90669fe9a37bab7941f39892d Mon Sep 17 00:00:00 2001 From: Pranav K Date: Mon, 2 Feb 2015 07:57:23 -0800 Subject: [PATCH] * 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 --- .../Preprocess/RazorPreCompilation.cs | 8 +- .../Preprocess/RazorPreCompilation.cs | 21 ++ .../Compilation/CompilerCache.cs | 28 +-- .../Compilation/RoslynCompilationService.cs | 30 +-- .../Internal/SymbolsUtility.cs | 45 +++++ .../Properties/Resources.Designer.cs | 16 ++ ...ecompilationTagHelperDescriptorResolver.cs | 80 ++++++++ .../RazorFileInfoCollection.cs | 51 +++++ .../RazorFileInfoCollectionGenerator.cs | 77 +++++--- .../Razor/PreCompileViews/RazorPreCompiler.cs | 144 ++++++++++++-- src/Microsoft.AspNet.Mvc.Razor/Resources.resx | 3 + src/Microsoft.AspNet.Mvc/MvcServices.cs | 2 +- .../RazorPreCompileModule.cs | 12 +- .../PrecompilationTest.cs | 74 +++++-- .../Compilation/CompilerCacheTest.cs | 186 +++++++++++++----- .../DefaultPrecompiledViewsProviderTest.cs | 13 ++ .../Controllers/TagHelpersController.cs | 20 ++ .../PrecompilationWebSite/Models/Person.cs | 13 ++ .../TagHelpers/RootViewStartTagHelper.cs | 16 ++ .../Views/TagHelpers/Add.cshtml | 3 + .../Views/TagHelpers/Remove.cshtml | 2 + .../Views/TagHelpers/_GlobalImport.cshtml | 1 + .../Views/TagHelpers/_ViewStart.cshtml | 2 + .../PrecompilationWebSite/project.json | 1 + .../Services/CustomCompilerCache.cs | 4 +- 25 files changed, 698 insertions(+), 154 deletions(-) create mode 100644 samples/TagHelperSample.Web/Compiler/Preprocess/RazorPreCompilation.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Internal/SymbolsUtility.cs create mode 100644 src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationTagHelperDescriptorResolver.cs create mode 100644 test/Microsoft.AspNet.Mvc.Razor.Test/DefaultPrecompiledViewsProviderTest.cs create mode 100644 test/WebSites/PrecompilationWebSite/Controllers/TagHelpersController.cs create mode 100644 test/WebSites/PrecompilationWebSite/Models/Person.cs create mode 100644 test/WebSites/PrecompilationWebSite/TagHelpers/RootViewStartTagHelper.cs create mode 100644 test/WebSites/PrecompilationWebSite/Views/TagHelpers/Add.cshtml create mode 100644 test/WebSites/PrecompilationWebSite/Views/TagHelpers/Remove.cshtml create mode 100644 test/WebSites/PrecompilationWebSite/Views/TagHelpers/_GlobalImport.cshtml create mode 100644 test/WebSites/PrecompilationWebSite/Views/TagHelpers/_ViewStart.cshtml diff --git a/samples/MvcSample.Web/Compiler/Preprocess/RazorPreCompilation.cs b/samples/MvcSample.Web/Compiler/Preprocess/RazorPreCompilation.cs index 014a3350cc..03f29c1685 100644 --- a/samples/MvcSample.Web/Compiler/Preprocess/RazorPreCompilation.cs +++ b/samples/MvcSample.Web/Compiler/Preprocess/RazorPreCompilation.cs @@ -3,13 +3,19 @@ using System; using Microsoft.AspNet.Mvc; +using Microsoft.Framework.Runtime; namespace MvcSample.Web { 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); } } } \ No newline at end of file diff --git a/samples/TagHelperSample.Web/Compiler/Preprocess/RazorPreCompilation.cs b/samples/TagHelperSample.Web/Compiler/Preprocess/RazorPreCompilation.cs new file mode 100644 index 0000000000..d7b5b87908 --- /dev/null +++ b/samples/TagHelperSample.Web/Compiler/Preprocess/RazorPreCompilation.cs @@ -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); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs index 26b705421b..4c9dcf4c7f 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/CompilerCache.cs @@ -8,6 +8,7 @@ using System.Reflection; using Microsoft.AspNet.FileProviders; using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor { @@ -16,6 +17,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class CompilerCache : ICompilerCache { + private static readonly TypeInfo RazorFileInfoCollectionType = typeof(RazorFileInfoCollection).GetTypeInfo(); private readonly IFileProvider _fileProvider; private readonly IMemoryCache _cache; @@ -23,27 +25,30 @@ namespace Microsoft.AspNet.Mvc.Razor /// Initializes a new instance of populated with precompiled views /// discovered using . /// - /// - /// An representing the assemblies - /// used to search for pre-compiled views. - /// + /// The that provides assemblies + /// for precompiled view discovery. + /// The . /// An accessor to the . - public CompilerCache(IAssemblyProvider provider, + public CompilerCache(IAssemblyProvider assemblyProvider, + IAssemblyLoadContextAccessor loadContextAccessor, IOptions 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, + internal CompilerCache(IEnumerable razorFileInfoCollections, + IAssemblyLoadContext loadContext, IFileProvider fileProvider) { _fileProvider = fileProvider; _cache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false }); + var cacheEntries = new List(); - 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) { var viewType = containingAssembly.GetType(fileInfo.FullTypeName); @@ -247,7 +252,7 @@ namespace Microsoft.AspNet.Mvc.Razor .Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c)); } - private static bool Match(Type t) + internal static bool Match(Type t) { var inAssemblyType = typeof(RazorFileInfoCollection); if (inAssemblyType.IsAssignableFrom(t)) @@ -262,7 +267,6 @@ namespace Microsoft.AspNet.Mvc.Razor return false; } - private class GetOrAddResult { public CompilerCacheEntry CompilerCacheEntry { get; set; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs index a3bec60d63..3861756ac9 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Compilation/RoslynCompilationService.cs @@ -9,8 +9,8 @@ using System.IO; using System.Linq; using System.Reflection; using System.Reflection.PortableExecutable; -using System.Runtime.InteropServices; using Microsoft.AspNet.FileProviders; +using Microsoft.AspNet.Mvc.Razor.Internal; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.Emit; @@ -24,7 +24,7 @@ namespace Microsoft.AspNet.Mvc.Razor /// public class RoslynCompilationService : ICompilationService { - private readonly Lazy _supportsPdbGeneration = new Lazy(SupportsPdbGeneration); + private readonly Lazy _supportsPdbGeneration = new Lazy(SymbolsUtility.SupportsSymbolsGeneration); private readonly ConcurrentDictionary _metadataFileCache = new ConcurrentDictionary(StringComparer.OrdinalIgnoreCase); @@ -229,32 +229,6 @@ namespace Microsoft.AspNet.Mvc.Razor 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; - } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Internal/SymbolsUtility.cs b/src/Microsoft.AspNet.Mvc.Razor/Internal/SymbolsUtility.cs new file mode 100644 index 0000000000..d444e33973 --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Internal/SymbolsUtility.cs @@ -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 +{ + /// + /// Utility type for determining if a platform supports symbol file generation. + /// + public class SymbolsUtility + { + private const string SymWriterGuid = "0AE2DEB0-F901-478b-BB9F-881EE8066788"; + + /// + /// Determines if the current platform supports symbols (pdb) generation. + /// + /// true if pdb generation is supported; false otherwise. + 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; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs index e6822d8316..c6e9045f13 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Properties/Resources.Designer.cs @@ -378,6 +378,22 @@ namespace Microsoft.AspNet.Mvc.Razor return GetString("RazorHash_UnsupportedHashAlgorithm"); } + /// + /// The resource '{0}' specified by '{1}' could not be found. + /// + internal static string RazorFileInfoCollection_ResourceCouldNotBeFound + { + get { return GetString("RazorFileInfoCollection_ResourceCouldNotBeFound"); } + } + + /// + /// The resource '{0}' specified by '{1}' could not be found. + /// + 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) { var value = _resourceManager.GetString(name); diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationTagHelperDescriptorResolver.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationTagHelperDescriptorResolver.cs new file mode 100644 index 0000000000..5e4af6a28c --- /dev/null +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/PrecompilationTagHelperDescriptorResolver.cs @@ -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 +{ + /// + /// used during Razor precompilation. + /// + 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; + + /// + /// Initializes a new instance of . + /// + /// The . + /// The . + public PrecompilationTagHelperTypeResolver([NotNull] IBeforeCompileContext compileContext, + [NotNull] IAssemblyLoadContext loadContext) + { + _compileContext = compileContext; + _loadContext = loadContext; + } + + /// + protected override IEnumerable 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(); + } + } + } +} \ 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 index 412f628d71..b1d9aa55d0 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollection.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollection.cs @@ -1,12 +1,63 @@ // 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.Reflection; +using Microsoft.Framework.Runtime; namespace Microsoft.AspNet.Mvc.Razor { + /// + /// Specifies metadata about precompiled views. + /// public abstract class RazorFileInfoCollection { + /// + /// Gets or sets the name of the resource containing the precompiled binary. + /// + public string AssemblyResourceName { get; protected set; } + + /// + /// Gets or sets the name of the resource that contains the symbols (pdb). + /// + public string SymbolsResourceName { get; protected set; } + + /// + /// Gets the of s. + /// public IReadOnlyList FileInfos { get; protected set; } + + /// + /// Loads the assembly containing precompiled views. + /// + /// The . + /// The containing precompiled views. + 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); + } + } + } } } \ 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 index a09a252830..ae2ee25a44 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorFileInfoCollectionGenerator.cs @@ -1,9 +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 System.Collections.Generic; +using System.Globalization; using System.Text; using Microsoft.CodeAnalysis; +using Microsoft.Framework.Runtime; using Microsoft.Framework.Runtime.Roslyn; namespace Microsoft.AspNet.Mvc.Razor @@ -12,23 +13,26 @@ namespace Microsoft.AspNet.Mvc.Razor { private string _fileFormat; - public RazorFileInfoCollectionGenerator([NotNull] IEnumerable fileInfos, + public RazorFileInfoCollectionGenerator([NotNull] RazorFileInfoCollection fileInfoCollection, [NotNull] CompilationSettings compilationSettings) { - FileInfos = fileInfos; + RazorFileInfoCollection = fileInfoCollection; CompilationSettings = compilationSettings; } - protected IEnumerable FileInfos { get; } + protected RazorFileInfoCollection RazorFileInfoCollection { get; } protected CompilationSettings CompilationSettings { get; } public virtual SyntaxTree GenerateCollection() { 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); builder.Append(perFileEntry); @@ -55,25 +59,27 @@ namespace Microsoft.AspNet.Mvc.Razor fileInfo.HashAlgorithmVersion); } - protected virtual string Top + protected virtual string TopFormat { get { return -@"using System; +$@"using System; using System.Collections.Generic; -using Microsoft.AspNet.Mvc.Razor; +using System.Reflection; +using {typeof(RazorFileInfoCollection).Namespace}; namespace __ASP_ASSEMBLY -{ - public class __PreGeneratedViewCollection : " + nameof(RazorFileInfoCollection) + @" - { +{{{{ + public class __PreGeneratedViewCollection : {nameof(RazorFileInfoCollection)} + {{{{ public __PreGeneratedViewCollection() - { - var fileInfos = new List<" + nameof(RazorFileInfo) + @">(); - " + nameof(RazorFileInfoCollection.FileInfos) + @" = fileInfos; - " + nameof(RazorFileInfo) + @" info; - + {{{{ + {nameof(RazorFileInfoCollection.AssemblyResourceName)} = ""{{0}}""; + {nameof(RazorFileInfoCollection.SymbolsResourceName)} = ""{{1}}""; + var fileInfos = new List<{nameof(RazorFileInfo)}>(); + {nameof(RazorFileInfoCollection.FileInfos)} = fileInfos; + {nameof(RazorFileInfo)} info; "; } } @@ -83,9 +89,20 @@ namespace __ASP_ASSEMBLY get { 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) { _fileFormat = - " info = new " - + nameof(RazorFileInfo) + @" - {{ - " + nameof(RazorFileInfo.LastModified) + @" = DateTime.FromFileTimeUtc({0:D}).ToLocalTime(), - " + nameof(RazorFileInfo.Length) + @" = {1:D}, - " + nameof(RazorFileInfo.RelativePath) + @" = @""{2}"", - " + nameof(RazorFileInfo.FullTypeName) + @" = @""{3}"", - " + nameof(RazorFileInfo.Hash) + @" = ""{4}"", - " + nameof(RazorFileInfo.HashAlgorithmVersion) + @" = {5}, - }}; + $@" + info = new {nameof(RazorFileInfo)} + {{{{ + {nameof(RazorFileInfo.LastModified)} = DateTime.FromFileTimeUtc({{0:D}}).ToLocalTime(), + {nameof(RazorFileInfo.Length)} = {{1:D}}, + {nameof(RazorFileInfo.RelativePath)} = @""{{2}}"", + {nameof(RazorFileInfo.FullTypeName)} = @""{{3}}"", + {nameof(RazorFileInfo.Hash)} = ""{{4}}"", + {nameof(RazorFileInfo.HashAlgorithmVersion)} = {{5}}, + }}}}; fileInfos.Add(info); "; } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs index 06920098a8..2560c55a96 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs +++ b/src/Microsoft.AspNet.Mvc.Razor/Razor/PreCompileViews/RazorPreCompiler.cs @@ -5,12 +5,18 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Reflection; using System.Threading.Tasks; 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.CSharp; using Microsoft.Framework.Cache.Memory; using Microsoft.Framework.DependencyInjection; using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Runtime; using Microsoft.Framework.Runtime.Roslyn; namespace Microsoft.AspNet.Mvc.Razor @@ -21,9 +27,12 @@ namespace Microsoft.AspNet.Mvc.Razor private readonly IFileProvider _fileProvider; public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, + [NotNull] IBeforeCompileContext compileContext, [NotNull] IMemoryCache precompilationCache, [NotNull] CompilationSettings compilationSettings) : this(designTimeServiceProvider, + compileContext, + designTimeServiceProvider.GetRequiredService(), designTimeServiceProvider.GetRequiredService>(), precompilationCache, compilationSettings) @@ -31,16 +40,30 @@ namespace Microsoft.AspNet.Mvc.Razor } public RazorPreCompiler([NotNull] IServiceProvider designTimeServiceProvider, + [NotNull] IBeforeCompileContext compileContext, + [NotNull] IAssemblyLoadContextAccessor loadContextAccessor, [NotNull] IOptions optionsAccessor, [NotNull] IMemoryCache precompilationCache, [NotNull] CompilationSettings compilationSettings) { _serviceProvider = designTimeServiceProvider; + CompileContext = compileContext; + LoadContext = loadContextAccessor.GetLoadContext(GetType().GetTypeInfo().Assembly); _fileProvider = optionsAccessor.Options.FileProvider; CompilationSettings = compilationSettings; PreCompilationCache = precompilationCache; + TagHelperTypeResolver = new PrecompilationTagHelperTypeResolver(CompileContext, LoadContext); } + /// + /// Gets or sets a value that determines if symbols (.pdb) file for the precompiled views. + /// + public bool GenerateSymbols { get; set; } + + protected IBeforeCompileContext CompileContext { get; } + + protected IAssemblyLoadContext LoadContext { get; } + protected CompilationSettings CompilationSettings { get; } protected IMemoryCache PreCompilationCache { get; } @@ -49,32 +72,37 @@ namespace Microsoft.AspNet.Mvc.Razor 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( - descriptors, + result, CompilationSettings); var tree = collectionGenerator.GenerateCollection(); - context.Compilation = context.Compilation.AddSyntaxTrees(tree); + CompileContext.Compilation = CompileContext.Compilation.AddSyntaxTrees(tree); } } - protected virtual IEnumerable CreateCompilationDescriptors( - [NotNull] IBeforeCompileContext context) + protected virtual RazorFileInfoCollection CreateFileInfoCollection() { var filesToProcess = new List(); GetFileInfosRecursive(root: string.Empty, razorFiles: filesToProcess); + if (filesToProcess.Count == 0) + { + return null; + } var razorFiles = new RazorFileInfo[filesToProcess.Count]; var syntaxTrees = new SyntaxTree[filesToProcess.Count]; var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = MaxDegreesOfParallelism }; var diagnosticsLock = new object(); + var hasErrors = false; Parallel.For(0, filesToProcess.Count, parallelOptions, index => { @@ -91,25 +119,91 @@ namespace Microsoft.AspNet.Mvc.Razor } else { + hasErrors = true; lock (diagnosticsLock) { - foreach (var diagnostic in cacheEntry.Diagnostics) - { - context.Diagnostics.Add(diagnostic); - } + AddRange(CompileContext.Diagnostics, cacheEntry.Diagnostics); } } } }); - context.Compilation = context.Compilation - .AddSyntaxTrees(syntaxTrees.Where(tree => tree != null)); - return razorFiles.Where(file => file != null); + if (hasErrors) + { + // 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 syntaxTrees, + [NotNull] IEnumerable 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() { - return _serviceProvider.GetRequiredService(); + var descriptorResolver = new TagHelperDescriptorResolver(TagHelperTypeResolver); + return new MvcRazorHost(new DefaultCodeTreeCache(_fileProvider)) + { + TagHelperDescriptorResolver = descriptorResolver + }; } private PrecompilationCacheEntry OnCacheMiss(ICacheSetContext cacheSetContext) @@ -193,5 +287,25 @@ namespace Microsoft.AspNet.Mvc.Razor return null; } + + private static void AddRange(IList target, IEnumerable source) + { + foreach (var diagnostic in source) + { + target.Add(diagnostic); + } + } + + private class PrecompileRazorFileInfoCollection : RazorFileInfoCollection + { + public PrecompileRazorFileInfoCollection(string assemblyResourceName, + string symbolsResourceName, + IReadOnlyList fileInfos) + { + AssemblyResourceName = assemblyResourceName; + SymbolsResourceName = symbolsResourceName; + FileInfos = fileInfos; + } + } } } diff --git a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx index b032e9039c..5aaf28b864 100644 --- a/src/Microsoft.AspNet.Mvc.Razor/Resources.resx +++ b/src/Microsoft.AspNet.Mvc.Razor/Resources.resx @@ -186,4 +186,7 @@ Unsupported hash algorithm. + + The resource '{0}' specified by '{1}' could not be found. + \ No newline at end of file diff --git a/src/Microsoft.AspNet.Mvc/MvcServices.cs b/src/Microsoft.AspNet.Mvc/MvcServices.cs index 276b72b062..35cfdc9e3f 100644 --- a/src/Microsoft.AspNet.Mvc/MvcServices.cs +++ b/src/Microsoft.AspNet.Mvc/MvcServices.cs @@ -124,7 +124,7 @@ namespace Microsoft.AspNet.Mvc // The host is designed to be discarded after consumption and is very inexpensive to initialize. yield return describe.Transient(); - + // Caches compilation artifacts across the lifetime of the application. yield return describe.Singleton(); diff --git a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs index 0c1d67bfcd..91c2fb6665 100644 --- a/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs +++ b/src/Microsoft.AspNet.Mvc/RazorPreCompileModule.cs @@ -31,6 +31,11 @@ namespace Microsoft.AspNet.Mvc _memoryCache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false }); } + /// + /// Gets or sets a value that determines if symbols (.pdb) file for the precompiled views. + /// + public bool GenerateSymbols { get; protected set; } + protected virtual string FileExtension { get; } = ".cshtml"; public virtual void BeforeCompile(IBeforeCompileContext context) @@ -45,8 +50,11 @@ namespace Microsoft.AspNet.Mvc sc.AddMvc(); var serviceProvider = BuildFallbackServiceProvider(sc, _appServices); - var viewCompiler = new RazorPreCompiler(serviceProvider, _memoryCache, compilationSettings); - viewCompiler.CompileViews(context); + var viewCompiler = new RazorPreCompiler(serviceProvider, context, _memoryCache, compilationSettings) + { + GenerateSymbols = GenerateSymbols + }; + viewCompiler.CompileViews(); } public void AfterCompile(IAfterCompileContext context) diff --git a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs index d41c55b839..75201cccf5 100644 --- a/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs +++ b/test/Microsoft.AspNet.Mvc.FunctionalTests/PrecompilationTest.cs @@ -8,6 +8,7 @@ using System.Net; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNet.Builder; +using Microsoft.AspNet.Mvc.Razor; using Microsoft.AspNet.TestHost; using Microsoft.Framework.DependencyInjection; 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 // the view. If the view is precompiled, this assembly will be PrecompilationWebsite. - var assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); + var assemblyNamePrefix = GetAssemblyNamePrefix(); try { @@ -50,9 +51,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert - 1 Assert.Equal(HttpStatusCode.OK, response.StatusCode); var parsedResponse1 = new ParsedResponse(responseContent); - Assert.Equal(assemblyName, parsedResponse1.ViewStart); - Assert.Equal(assemblyName, parsedResponse1.Layout); - Assert.Equal(assemblyName, parsedResponse1.Index); + Assert.StartsWith(assemblyNamePrefix, parsedResponse1.ViewStart); + Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Layout); + Assert.StartsWith(assemblyNamePrefix, parsedResponse1.Index); // Act - 2 // Touch the Layout file and verify it is now dynamically compiled. @@ -61,9 +62,9 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert - 2 var response2 = new ParsedResponse(responseContent); - Assert.NotEqual(assemblyName, response2.Layout); - Assert.Equal(assemblyName, response2.ViewStart); - Assert.Equal(assemblyName, response2.Index); + Assert.StartsWith(assemblyNamePrefix, response2.ViewStart); + Assert.StartsWith(assemblyNamePrefix, response2.Index); + Assert.DoesNotContain(assemblyNamePrefix, response2.Layout); // Act - 3 // Touch the _ViewStart file and verify it is is dynamically compiled. @@ -72,8 +73,8 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests // Assert - 3 var response3 = new ParsedResponse(responseContent); - Assert.NotEqual(assemblyName, response3.ViewStart); - Assert.Equal(assemblyName, response3.Index); + Assert.NotEqual(assemblyNamePrefix, response3.ViewStart); + Assert.Equal(response2.Index, response3.Index); Assert.Equal(response2.Layout, response3.Layout); // Act - 4 @@ -159,13 +160,13 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests public async Task PrecompiledView_UsesCompilationOptionsFromApplication() { // Arrange - var assemblyName = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); + var assemblyNamePrefix = GetAssemblyNamePrefix(); #if ASPNET50 var expected = -@"Value set inside ASPNET50 " + assemblyName; +@"Value set inside ASPNET50 " + assemblyNamePrefix; #elif ASPNETCORE50 var expected = -@"Value set inside ASPNETCORE50 " + assemblyName; +@"Value set inside ASPNETCORE50 " + assemblyNamePrefix; #endif var server = TestServer.Create(_services, _app); @@ -176,14 +177,14 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var responseContent = await response.Content.ReadAsStringAsync(); // Assert - Assert.Equal(expected, responseContent.Trim()); + Assert.StartsWith(expected, responseContent.Trim()); } [Fact] public async Task DeletingPrecompiledGlobalFile_PriorToFirstRequestToAView_CausesViewToBeRecompiled() { // Arrange - var expected = typeof(Startup).GetTypeInfo().Assembly.GetName().ToString(); + var expected = GetAssemblyNamePrefix(); var server = TestServer.Create(_services, _app); var client = server.CreateClient(); @@ -209,7 +210,7 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests var response2 = await client.GetStringAsync("http://localhost/Home/GlobalDeletedPriorToFirstRequest"); // Assert - 2 - Assert.NotEqual(expected, response2.Trim()); + Assert.DoesNotContain(expected, response2.Trim()); } finally { @@ -217,6 +218,49 @@ namespace Microsoft.AspNet.Mvc.FunctionalTests } } + [Fact] + public async Task TagHelpersFromTheApplication_CanBeAdded() + { + // Arrange + var assemblyNamePrefix = GetAssemblyNamePrefix(); + var expected = @"Back to List"; + 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-content"; + 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 TouchFile(string viewsDir, string file) { var path = Path.Combine(viewsDir, file); diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs index b79820e1db..00ba6b46b5 100644 --- a/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/Compilation/CompilerCacheTest.cs @@ -6,7 +6,9 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Reflection; using System.Text; +using Microsoft.Framework.Runtime; using Moq; using Xunit; @@ -15,13 +17,14 @@ namespace Microsoft.AspNet.Mvc.Razor public class CompilerCacheTest { private const string ViewPath = "view-path"; + private readonly IAssemblyLoadContext TestLoadContext = Mock.Of(); [Fact] public void GetOrAdd_ReturnsFileNotFoundResult_IfFileIsNotFoundInFileSystem() { // Arrange var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(Enumerable.Empty(), fileProvider); + var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = GetType(); // Act @@ -38,7 +41,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var fileProvider = new TestFileProvider(); fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(Enumerable.Empty(), fileProvider); + var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = GetType(); var expected = UncachedCompilationResult.Successful(type, "hello world"); @@ -60,7 +63,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var fileProvider = new TestFileProvider(); fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(Enumerable.Empty(), fileProvider); + var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = typeof(RuntimeCompileIdentical); var expected = UncachedCompilationResult.Successful(type, "hello world"); @@ -88,7 +91,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var fileProvider = new TestFileProvider(); fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(Enumerable.Empty(), fileProvider); + var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = typeof(RuntimeCompileIdentical); var expected1 = UncachedCompilationResult.Successful(type, "hello world"); var expected2 = UncachedCompilationResult.Successful(type, "different content"); @@ -116,7 +119,7 @@ namespace Microsoft.AspNet.Mvc.Razor var mockFileProvider = new Mock { CallBase = true }; var fileProvider = mockFileProvider.Object; fileProvider.AddFile(ViewPath, "some content"); - var cache = new CompilerCache(Enumerable.Empty(), fileProvider); + var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var type = typeof(RuntimeCompileIdentical); var expected = UncachedCompilationResult.Successful(type, "hello world"); @@ -168,34 +171,6 @@ namespace Microsoft.AspNet.Mvc.Razor } } - private class ViewCollection : RazorFileInfoCollection - { - private readonly List _fileInfos = new List(); - - 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) { var bytes = Encoding.UTF8.GetBytes(content); @@ -211,10 +186,8 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var instance = new RuntimeCompileIdentical(); var length = Encoding.UTF8.GetByteCount(instance.Content); - var collection = new ViewCollection(); 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 { Length = length, @@ -222,7 +195,6 @@ namespace Microsoft.AspNet.Mvc.Razor Content = instance.Content }; fileProvider.AddFile(ViewPath, fileInfo); - var precompiledContent = new PreCompile().Content; // Act var result = cache.GetOrAdd(ViewPath, @@ -246,9 +218,8 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var instance = (View)Activator.CreateInstance(resultViewType); var length = Encoding.UTF8.GetByteCount(instance.Content); - var collection = new ViewCollection(); 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 { @@ -303,10 +274,9 @@ namespace Microsoft.AspNet.Mvc.Razor RelativePath = "_GlobalImport.cshtml", FullTypeName = typeof(RuntimeCompileIdentical).FullName }; - var precompiledViews = new ViewCollection(); precompiledViews.Add(globalRazorFileInfo); - var cache = new CompilerCache(new[] { precompiledViews }, fileProvider); + var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider); // Act var result = cache.GetOrAdd(ViewPath, @@ -325,8 +295,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var precompiledViews = new ViewCollection(); var fileProvider = new TestFileProvider(); - var precompiledView = precompiledViews.FileInfos[0]; - var cache = new CompilerCache(new[] { precompiledViews }, fileProvider); + var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider); // Act var result = cache.GetOrAdd(ViewPath, @@ -351,7 +320,7 @@ namespace Microsoft.AspNet.Mvc.Razor LastModified = precompiledView.LastModified, }; fileProvider.AddFile(ViewPath, fileInfo); - var cache = new CompilerCache(new[] { precompiledViews }, fileProvider); + var cache = new CompilerCache(new[] { precompiledViews }, TestLoadContext, fileProvider); // Act 1 var result1 = cache.GetOrAdd(ViewPath, @@ -381,12 +350,11 @@ namespace Microsoft.AspNet.Mvc.Razor { // Arrange var expectedType = typeof(RuntimeCompileDifferent); - var lastModified = DateTime.UtcNow; var fileProvider = new TestFileProvider(); var collection = new ViewCollection(); var precompiledFile = collection.FileInfos[0]; 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 { Content = new PreCompile().Content, @@ -456,7 +424,7 @@ namespace Microsoft.AspNet.Mvc.Razor fileProvider.AddFile(globalFileInfo.PhysicalPath, globalFileInfo); viewCollection.Add(globalFile); - var cache = new CompilerCache(new[] { viewCollection }, fileProvider); + var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider); // Act 1 var result1 = cache.GetOrAdd(viewFileInfo.PhysicalPath, @@ -542,7 +510,7 @@ namespace Microsoft.AspNet.Mvc.Razor fileProvider.AddFile(fileInfo.PhysicalPath, fileInfo); fileProvider.AddFile(viewStartRazorFileInfo.RelativePath, globalFileInfo); var viewCollection = new ViewCollection(); - var cache = new CompilerCache(new[] { viewCollection }, fileProvider); + var cache = new CompilerCache(new[] { viewCollection }, TestLoadContext, fileProvider); // Act var result = cache.GetOrAdd(fileInfo.PhysicalPath, @@ -561,7 +529,7 @@ namespace Microsoft.AspNet.Mvc.Razor // Arrange var lastModified = DateTime.UtcNow; var fileProvider = new TestFileProvider(); - var cache = new CompilerCache(Enumerable.Empty(), fileProvider); + var cache = new CompilerCache(Enumerable.Empty(), TestLoadContext, fileProvider); var fileInfo = new TestFileInfo { PhysicalPath = "test", @@ -592,5 +560,125 @@ namespace Microsoft.AspNet.Mvc.Razor Assert.Null(actual2.CompiledContent); 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 : RazorFileInfoCollection + { + + } + + private class ParameterConstructorRazorFileInfoCollection : RazorFileInfoCollection + { + public ParameterConstructorRazorFileInfoCollection(string value) + { + } + } + + private class NonSubTypeRazorFileInfoCollection : Controller + { + + } + + private class ViewCollection : RazorFileInfoCollection + { + private readonly List _fileInfos = new List(); + + 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; + } + } } } \ No newline at end of file diff --git a/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultPrecompiledViewsProviderTest.cs b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultPrecompiledViewsProviderTest.cs new file mode 100644 index 0000000000..b59c9d3938 --- /dev/null +++ b/test/Microsoft.AspNet.Mvc.Razor.Test/DefaultPrecompiledViewsProviderTest.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 System; +using Xunit; + +namespace Microsoft.AspNet.Mvc.Razor +{ + public class DefaultPrecompiledViewsProviderTest + { + + } +} \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Controllers/TagHelpersController.cs b/test/WebSites/PrecompilationWebSite/Controllers/TagHelpersController.cs new file mode 100644 index 0000000000..60c60c1827 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Controllers/TagHelpersController.cs @@ -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(); + } + } +} \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Models/Person.cs b/test/WebSites/PrecompilationWebSite/Models/Person.cs new file mode 100644 index 0000000000..9ec8d58f16 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Models/Person.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 System.ComponentModel.DataAnnotations; + +namespace PrecompilationWebSite.Models +{ + public class Person + { + [Range(10, 100)] + public int Age { get; set; } + } +} \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/TagHelpers/RootViewStartTagHelper.cs b/test/WebSites/PrecompilationWebSite/TagHelpers/RootViewStartTagHelper.cs new file mode 100644 index 0000000000..25137e43b7 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/TagHelpers/RootViewStartTagHelper.cs @@ -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"; + } + } +} \ No newline at end of file diff --git a/test/WebSites/PrecompilationWebSite/Views/TagHelpers/Add.cshtml b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/Add.cshtml new file mode 100644 index 0000000000..55726c8d2e --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/Add.cshtml @@ -0,0 +1,3 @@ +@model PrecompilationWebSite.Models.Person +@addTagHelper "*, Microsoft.AspNet.Mvc.TagHelpers" +Back to List diff --git a/test/WebSites/PrecompilationWebSite/Views/TagHelpers/Remove.cshtml b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/Remove.cshtml new file mode 100644 index 0000000000..11c91d0c88 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/Remove.cshtml @@ -0,0 +1,2 @@ +@removeTagHelper "*, PrecompilationWebSite" +root-content diff --git a/test/WebSites/PrecompilationWebSite/Views/TagHelpers/_GlobalImport.cshtml b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/_GlobalImport.cshtml new file mode 100644 index 0000000000..8560f82d73 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/_GlobalImport.cshtml @@ -0,0 +1 @@ +@addTagHelper "PrecompilationWebSite.TagHelpers.RootViewStartTagHelper, PrecompilationWebSite" diff --git a/test/WebSites/PrecompilationWebSite/Views/TagHelpers/_ViewStart.cshtml b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/_ViewStart.cshtml new file mode 100644 index 0000000000..4a10269678 --- /dev/null +++ b/test/WebSites/PrecompilationWebSite/Views/TagHelpers/_ViewStart.cshtml @@ -0,0 +1,2 @@ +@using System.Reflection +@GetType().GetTypeInfo().Assembly.FullName diff --git a/test/WebSites/PrecompilationWebSite/project.json b/test/WebSites/PrecompilationWebSite/project.json index 3f6e124d78..baa2940d45 100644 --- a/test/WebSites/PrecompilationWebSite/project.json +++ b/test/WebSites/PrecompilationWebSite/project.json @@ -6,6 +6,7 @@ "dependencies": { "Kestrel": "1.0.0-*", "Microsoft.AspNet.Mvc": "6.0.0-*", + "Microsoft.AspNet.Mvc.TagHelpers": "6.0.0-*", "Microsoft.AspNet.Mvc.TestConfiguration": "1.0.0", "Microsoft.AspNet.Server.IIS": "1.0.0-*", "Microsoft.AspNet.Server.WebListener": "1.0.0-*", diff --git a/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs b/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs index af0df88b97..288b9ab3e7 100644 --- a/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs +++ b/test/WebSites/RazorCompilerCacheWebSite/Services/CustomCompilerCache.cs @@ -4,15 +4,17 @@ using Microsoft.AspNet.Mvc; using Microsoft.AspNet.Mvc.Razor; using Microsoft.Framework.OptionsModel; +using Microsoft.Framework.Runtime; namespace RazorCompilerCacheWebSite { public class CustomCompilerCache : CompilerCache { public CustomCompilerCache(IAssemblyProvider assemblyProvider, + IAssemblyLoadContextAccessor loadContextAccessor, IOptions optionsAccessor, CompilerCacheInitialiedService cacheInitializedService) - : base(assemblyProvider, optionsAccessor) + : base(assemblyProvider, loadContextAccessor, optionsAccessor) { cacheInitializedService.Initialized = true; }