// 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