// Copyright (c) Microsoft Corporation. All rights reserved. See License.txt in the project root for license information. using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Web.Caching; using System.Web.Compilation; using System.Web.Hosting; using System.Web.Util; using System.Xml.Linq; using Microsoft.Internal.Web.Utils; namespace System.Web.WebPages { /// /// Wraps the caching and instantiation of paths of the BuildManager. /// In case of precompiled non-updateable sites, the only way to verify if a file exists is to call BuildManager.GetObjectFactory. However this method is less performant than /// VirtualPathProvider.FileExists which is used for all other scenarios. In this class, we optimize for the first scenario by storing the results of GetObjectFactory for a /// long duration. /// internal sealed class BuildManagerWrapper : IVirtualPathFactory { internal static readonly Guid KeyGuid = Guid.NewGuid(); private static readonly TimeSpan _objectFactoryCacheDuration = TimeSpan.FromMinutes(1); private readonly IVirtualPathUtility _virtualPathUtility; private readonly VirtualPathProvider _vpp; private readonly bool _isPrecompiled; private readonly FileExistenceCache _vppCache; private IEnumerable _supportedExtensions; public BuildManagerWrapper() : this(HostingEnvironment.VirtualPathProvider, new VirtualPathUtilityWrapper()) { } public BuildManagerWrapper(VirtualPathProvider vpp, IVirtualPathUtility virtualPathUtility) { _vpp = vpp; _virtualPathUtility = virtualPathUtility; _isPrecompiled = IsNonUpdatablePrecompiledApp(); if (!_isPrecompiled) { _vppCache = new FileExistenceCache(vpp); } } public IEnumerable SupportedExtensions { get { return _supportedExtensions ?? WebPageHttpHandler.GetRegisteredExtensions(); } set { _supportedExtensions = value; } } /// /// Determines if a page exists in the website. /// This method switches between a long duration cache or a short duration FileExistenceCache depending on whether the site is precompiled. /// This is an optimization because BuildManager.GetObjectFactory is comparably slower than performing VirtualPathFactory.Exists /// public bool Exists(string virtualPath) { if (_isPrecompiled) { return ExistsInPrecompiledSite(virtualPath); } return ExistsInVpp(virtualPath); } /// /// An app's is precompiled for our purposes if /// (a) it has a PreCompiledApp.config file in the site root, /// (b) The PreCompiledApp.config says that the app is not Updatable. /// /// /// This code is based on System.Web.DynamicData.Misc.IsNonUpdatablePrecompiledAppNoCache (DynamicData) /// [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "We want to replicate the behavior of BuildManager which catches all exceptions.")] internal bool IsNonUpdatablePrecompiledApp() { if (_vpp == null) { return false; } var virtualPath = _virtualPathUtility.ToAbsolute("~/PrecompiledApp.config"); if (!_vpp.FileExists(virtualPath)) { return false; } XDocument document; using (var stream = _vpp.GetFile(virtualPath).Open()) { try { document = XDocument.Load(_vpp.GetFile(virtualPath).Open()); } catch { // If we are unable to load the file, ignore it. The BuildManager behaves identically. return false; } } if (document.Root == null || !document.Root.Name.LocalName.Equals("precompiledApp", StringComparison.OrdinalIgnoreCase)) { return false; } var updatableAttribute = document.Root.Attribute("updatable"); if (updatableAttribute != null) { bool result; return Boolean.TryParse(updatableAttribute.Value, out result) && (result == false); } return false; } private bool ExistsInPrecompiledSite(string virtualPath) { var key = GetKeyFromVirtualPath(virtualPath); // We assume that the key is unique enough to avoid collisions. var buildManagerResult = (BuildManagerResult)HttpRuntime.Cache.Get(key); if (buildManagerResult == null) { // For precompiled apps, we cache the ObjectFactory and use it in the CreateInstance method. var objectFactory = GetObjectFactory(virtualPath); buildManagerResult = new BuildManagerResult { ObjectFactory = objectFactory, Exists = objectFactory != null }; // Cache the result with a sliding expiration for a long duration. HttpRuntime.Cache.Add(key, buildManagerResult, null, Cache.NoAbsoluteExpiration, _objectFactoryCacheDuration, CacheItemPriority.Low, null); } return buildManagerResult.Exists; } /// /// Determines if a site exists in the VirtualPathProvider. /// Results of hits are cached for a very short amount of time in the FileExistenceCache. /// private bool ExistsInVpp(string virtualPath) { Debug.Assert(_vppCache != null); return _vppCache.FileExists(virtualPath); } /// /// Determines if an ObjectFactory exists for the virtualPath. /// The BuildManager complains if we pass in extensions that aren't registered for compilation. So we ensure that the virtual path is not /// extensionless and that it is one of the extension /// private IWebObjectFactory GetObjectFactory(string virtualPath) { if (IsPathExtensionSupported(virtualPath)) { return BuildManager.GetObjectFactory(virtualPath, throwIfNotFound: false); } return null; } public object CreateInstance(string virtualPath) { return CreateInstanceOfType(virtualPath); } public T CreateInstanceOfType(string virtualPath) where T : class { if (_isPrecompiled) { var buildManagerResult = (BuildManagerResult)HttpRuntime.Cache.Get(GetKeyFromVirtualPath(virtualPath)); // The cache could have evicted our results. In this case, we'll simply fall through to CreateInstanceFromVirtualPath if (buildManagerResult != null) { Debug.Assert(buildManagerResult.Exists && buildManagerResult.ObjectFactory != null, "This method must only be called if the file exists."); return buildManagerResult.ObjectFactory.CreateInstance() as T; } } return (T)BuildManager.CreateInstanceFromVirtualPath(virtualPath, typeof(T)); } /// /// Determines if the extension is one of the extensions registered with WebPageHttpHandler. /// public bool IsPathExtensionSupported(string virtualPath) { string extension = PathUtil.GetExtension(virtualPath); return !String.IsNullOrEmpty(extension) && SupportedExtensions.Contains(extension.Substring(1), StringComparer.OrdinalIgnoreCase); } /// /// Creates a reasonably unique key for a given virtual path by concatenating it with a Guid. /// private static string GetKeyFromVirtualPath(string virtualPath) { return KeyGuid.ToString() + "_" + virtualPath; } private class BuildManagerResult { public bool Exists { get; set; } public IWebObjectFactory ObjectFactory { get; set; } } } }