// Copyright (c) .NET Foundation. 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.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Text; using Microsoft.AspNet.FileProviders; using Microsoft.Framework.Caching.Memory; namespace Microsoft.AspNet.Mvc.Razor.Compilation { /// /// Caches the result of runtime compilation of Razor files for the duration of the application lifetime. /// public class CompilerCache : ICompilerCache { private readonly IFileProvider _fileProvider; private readonly IMemoryCache _cache; private readonly ConcurrentDictionary _normalizedPathLookup = new ConcurrentDictionary(StringComparer.Ordinal); /// /// Initializes a new instance of . /// /// used to locate Razor views. public CompilerCache(IFileProvider fileProvider) { if (fileProvider == null) { throw new ArgumentNullException(nameof(fileProvider)); } _fileProvider = fileProvider; _cache = new MemoryCache(new MemoryCacheOptions { CompactOnMemoryPressure = false }); } /// /// Initializes a new instance of populated with precompiled views /// specified by . /// /// used to locate Razor views. /// A mapping of application relative paths of view to the precompiled view /// s. public CompilerCache( IFileProvider fileProvider, IDictionary precompiledViews) : this(fileProvider) { if (fileProvider == null) { throw new ArgumentNullException(nameof(fileProvider)); } if (precompiledViews == null) { throw new ArgumentNullException(nameof(precompiledViews)); } foreach (var item in precompiledViews) { var cacheEntry = new CompilerCacheResult(CompilationResult.Successful(item.Value)); _cache.Set(GetNormalizedPath(item.Key), cacheEntry); } } /// public CompilerCacheResult GetOrAdd( string relativePath, Func compile) { if (relativePath == null) { throw new ArgumentNullException(nameof(relativePath)); } if (compile == null) { throw new ArgumentNullException(nameof(compile)); } CompilerCacheResult cacheResult; // Attempt to lookup the cache entry using the passed in path. This will succeed if the path is already // normalized and a cache entry exists. if (!_cache.TryGetValue(relativePath, out cacheResult)) { var normalizedPath = GetNormalizedPath(relativePath); if (!_cache.TryGetValue(normalizedPath, out cacheResult)) { cacheResult = CreateCacheEntry(normalizedPath, compile); } } return cacheResult; } private CompilerCacheResult CreateCacheEntry( string normalizedPath, Func compile) { CompilerCacheResult cacheResult; var fileInfo = _fileProvider.GetFileInfo(normalizedPath); MemoryCacheEntryOptions cacheEntryOptions; CompilerCacheResult cacheResultToCache; if (!fileInfo.Exists) { cacheResultToCache = CompilerCacheResult.FileNotFound; cacheResult = CompilerCacheResult.FileNotFound; cacheEntryOptions = new MemoryCacheEntryOptions(); cacheEntryOptions.AddExpirationTrigger(_fileProvider.Watch(normalizedPath)); } else { var relativeFileInfo = new RelativeFileInfo(fileInfo, normalizedPath); var compilationResult = compile(relativeFileInfo).EnsureSuccessful(); cacheEntryOptions = GetMemoryCacheEntryOptions(normalizedPath); // By default the CompilationResult returned by IRoslynCompiler is an instance of // UncachedCompilationResult. This type has the generated code as a string property and do not want // to cache it. We'll instead cache the unwrapped result. cacheResultToCache = new CompilerCacheResult( CompilationResult.Successful(compilationResult.CompiledType)); cacheResult = new CompilerCacheResult(compilationResult); } _cache.Set(normalizedPath, cacheResultToCache, cacheEntryOptions); return cacheResult; } private MemoryCacheEntryOptions GetMemoryCacheEntryOptions(string relativePath) { var options = new MemoryCacheEntryOptions(); options.AddExpirationTrigger(_fileProvider.Watch(relativePath)); var viewImportsPaths = ViewHierarchyUtility.GetViewImportsLocations(relativePath); foreach (var location in viewImportsPaths) { options.AddExpirationTrigger(_fileProvider.Watch(location)); } return options; } private string GetNormalizedPath(string relativePath) { Debug.Assert(relativePath != null); if (relativePath.Length == 0) { return relativePath; } string normalizedPath; if (!_normalizedPathLookup.TryGetValue(relativePath, out normalizedPath)) { var builder = new StringBuilder(relativePath); builder.Replace('\\', '/'); if (builder[0] != '/') { builder.Insert(0, '/'); } normalizedPath = builder.ToString(); _normalizedPathLookup.TryAdd(relativePath, normalizedPath); } return normalizedPath; } } }