// 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.Linq; using System.Reflection; using Microsoft.AspNet.FileProviders; using Microsoft.Framework.Cache.Memory; namespace Microsoft.AspNet.Mvc.Razor { /// /// Caches the result of runtime compilation of Razor files for the duration of the app lifetime. /// public class CompilerCache : ICompilerCache { private readonly IFileProvider _fileProvider; private readonly IMemoryCache _cache; /// /// Initializes a new instance of populated with precompiled views /// discovered using . /// /// /// An representing the assemblies /// used to search for pre-compiled views. /// /// An instance that represents the application's /// file system. /// public CompilerCache(IAssemblyProvider provider, IRazorFileProviderCache fileProvider) : this(GetFileInfos(provider.CandidateAssemblies), fileProvider) { } // Internal for unit testing internal CompilerCache(IEnumerable razorFileInfoCollection, IFileProvider fileProvider) { _fileProvider = fileProvider; _cache = new MemoryCache(new MemoryCacheOptions { ListenForMemoryPressure = false }); var cacheEntries = new List(); foreach (var viewCollection in razorFileInfoCollection) { var containingAssembly = viewCollection.GetType().GetTypeInfo().Assembly; foreach (var fileInfo in viewCollection.FileInfos) { var viewType = containingAssembly.GetType(fileInfo.FullTypeName); var cacheEntry = new CompilerCacheEntry(fileInfo, viewType); // There shouldn't be any duplicates and if there are any the first will win. // If the result doesn't match the one on disk its going to recompile anyways. _cache.Set(NormalizePath(fileInfo.RelativePath), cacheEntry, PopulateCacheSetContext); cacheEntries.Add(cacheEntry); } } // Set up ViewStarts foreach (var entry in cacheEntries) { var viewStartLocations = ViewStartUtility.GetViewStartLocations(entry.RelativePath); foreach (var location in viewStartLocations) { var viewStartEntry = _cache.Get(location); if (viewStartEntry != null) { // Add the the composite _ViewStart entry as a dependency. entry.AssociatedViewStartEntry = viewStartEntry; break; } } } } internal static IEnumerable GetFileInfos(IEnumerable assemblies) { return assemblies.SelectMany(a => a.ExportedTypes) .Where(Match) .Select(c => (RazorFileInfoCollection)Activator.CreateInstance(c)); } private static bool Match(Type t) { var inAssemblyType = typeof(RazorFileInfoCollection); if (inAssemblyType.IsAssignableFrom(t)) { var hasParameterlessConstructor = t.GetConstructor(Type.EmptyTypes) != null; return hasParameterlessConstructor && !t.GetTypeInfo().IsAbstract && !t.GetTypeInfo().ContainsGenericParameters; } return false; } /// public CompilationResult GetOrAdd([NotNull] RelativeFileInfo fileInfo, [NotNull] Func compile) { CompilationResult result; var entry = GetOrAdd(fileInfo, compile, out result); return result; } private CompilerCacheEntry GetOrAdd(RelativeFileInfo relativeFileInfo, Func compile, out CompilationResult result) { var normalizedPath = NormalizePath(relativeFileInfo.RelativePath); var cacheEntry = _cache.Get(normalizedPath); if (cacheEntry == null) { return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); } else if (cacheEntry.IsPreCompiled && !cacheEntry.IsValidatedPreCompiled) { // For precompiled views, the first time the entry is read, we need to ensure that no changes were made // either to the file associated with this entry, or any _ViewStart associated with it between the time // the View was precompiled and the time EnsureInitialized was called. For later iterations, we can // rely on expiration triggers ensuring the validity of the entry. var fileInfo = relativeFileInfo.FileInfo; if (cacheEntry.Length != fileInfo.Length) { // Recompile if the file lengths differ return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); } if (AssociatedViewStartsChanged(cacheEntry, compile)) { // Recompile if the view starts have changed since the entry was created. return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); } if (cacheEntry.LastModified == fileInfo.LastModified) { result = CompilationResult.Successful(cacheEntry.CompiledType); // Assigning to IsValidatedPreCompiled is an atomic operation and will result in a safe race // if it is being concurrently updated and read. cacheEntry.IsValidatedPreCompiled = true; return cacheEntry; } // Timestamp doesn't match but it might be because of deployment, compare the hash. if (cacheEntry.IsPreCompiled && string.Equals(cacheEntry.Hash, RazorFileHash.GetHash(fileInfo, cacheEntry.HashAlgorithmVersion), StringComparison.Ordinal)) { // Cache hit, but we need to update the entry. // Assigning to LastModified and IsValidatedPreCompiled are atomic operations and will result in safe races // if the entry is being concurrently read or updated. cacheEntry.LastModified = fileInfo.LastModified; cacheEntry.IsValidatedPreCompiled = true; result = CompilationResult.Successful(cacheEntry.CompiledType); return cacheEntry; } // it's not a match, recompile return OnCacheMiss(relativeFileInfo, normalizedPath, compile, out result); } result = CompilationResult.Successful(cacheEntry.CompiledType); return cacheEntry; } private CompilerCacheEntry OnCacheMiss(RelativeFileInfo file, string normalizedPath, Func compile, out CompilationResult result) { result = compile(file); var cacheEntry = new CompilerCacheEntry(file, result.CompiledType); // Concurrent addition to MemoryCache with the same key result in safe race. return _cache.Set(normalizedPath, cacheEntry, PopulateCacheSetContext); } private CompilerCacheEntry PopulateCacheSetContext(ICacheSetContext cacheSetContext) { var entry = (CompilerCacheEntry)cacheSetContext.State; cacheSetContext.AddExpirationTrigger(_fileProvider.Watch(entry.RelativePath)); var viewStartLocations = ViewStartUtility.GetViewStartLocations(cacheSetContext.Key); foreach (var location in viewStartLocations) { cacheSetContext.AddExpirationTrigger(_fileProvider.Watch(location)); } return entry; } private bool AssociatedViewStartsChanged(CompilerCacheEntry entry, Func compile) { var viewStartEntry = GetCompositeViewStartEntry(entry.RelativePath, compile); return entry.AssociatedViewStartEntry != viewStartEntry; } // Returns the entry for the nearest _ViewStart that the file inherits directives from. Since _ViewStart // entries are affected by other _ViewStart entries that are in the path hierarchy, the returned value // represents the composite result of performing a cache check on individual _ViewStart entries. private CompilerCacheEntry GetCompositeViewStartEntry(string relativePath, Func compile) { var viewStartLocations = ViewStartUtility.GetViewStartLocations(relativePath); foreach (var viewStartLocation in viewStartLocations) { var viewStartFileInfo = _fileProvider.GetFileInfo(viewStartLocation); if (viewStartFileInfo.Exists) { var relativeFileInfo = new RelativeFileInfo(viewStartFileInfo, viewStartLocation); CompilationResult result; return GetOrAdd(relativeFileInfo, compile, out result); } } // No _ViewStarts discovered. return null; } private static string NormalizePath(string path) { // We need to allow for scenarios where the application was precompiled on a machine with forward slashes // but is being run in one with backslashes (or vice versa). To this effect, we'll normalize paths to // use backslashes for lookups and storage in the dictionary. path = path.Replace('/', '\\'); path = path.TrimStart('\\'); return path; } } }