summaryrefslogtreecommitdiff
path: root/external/aspnetwebstack/src/System.Web.WebPages/BuildManagerWrapper.cs
blob: 7e11aa2a2ce537c430cb0e1d3dec47799336d284 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
// 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
{
    /// <summary>
    /// 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.
    /// </summary>
    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<string> _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<string> SupportedExtensions
        {
            get { return _supportedExtensions ?? WebPageHttpHandler.GetRegisteredExtensions(); }
            set { _supportedExtensions = value; }
        }

        /// <summary>
        /// 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
        /// </summary>
        public bool Exists(string virtualPath)
        {
            if (_isPrecompiled)
            {
                return ExistsInPrecompiledSite(virtualPath);
            }
            return ExistsInVpp(virtualPath);
        }

        /// <summary>
        /// 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.
        /// </summary>
        /// <remarks>
        /// This code is based on System.Web.DynamicData.Misc.IsNonUpdatablePrecompiledAppNoCache (DynamicData)
        /// </remarks>
        [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;
        }

        /// <summary>
        /// Determines if a site exists in the VirtualPathProvider.
        /// Results of hits are cached for a very short amount of time in the FileExistenceCache.
        /// </summary>
        private bool ExistsInVpp(string virtualPath)
        {
            Debug.Assert(_vppCache != null);
            return _vppCache.FileExists(virtualPath);
        }

        /// <summary>
        /// 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
        /// </summary>
        private IWebObjectFactory GetObjectFactory(string virtualPath)
        {
            if (IsPathExtensionSupported(virtualPath))
            {
                return BuildManager.GetObjectFactory(virtualPath, throwIfNotFound: false);
            }
            return null;
        }

        public object CreateInstance(string virtualPath)
        {
            return CreateInstanceOfType<object>(virtualPath);
        }

        public T CreateInstanceOfType<T>(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));
        }

        /// <summary>
        /// Determines if the extension is one of the extensions registered with WebPageHttpHandler. 
        /// </summary>
        public bool IsPathExtensionSupported(string virtualPath)
        {
            string extension = PathUtil.GetExtension(virtualPath);
            return !String.IsNullOrEmpty(extension)
                   && SupportedExtensions.Contains(extension.Substring(1), StringComparer.OrdinalIgnoreCase);
        }

        /// <summary>
        /// Creates a reasonably unique key for a given virtual path by concatenating it with a Guid.
        /// </summary>
        private static string GetKeyFromVirtualPath(string virtualPath)
        {
            return KeyGuid.ToString() + "_" + virtualPath;
        }

        private class BuildManagerResult
        {
            public bool Exists { get; set; }

            public IWebObjectFactory ObjectFactory { get; set; }
        }
    }
}