From 7d2910866c1a63d48c1d588890d487785fabcdc7 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Fri, 16 Jun 2017 11:16:40 +0200 Subject: [PATCH 01/15] Adding a page rendering http handler that does not rely on building a WebForms control tree --- Composite/Composite.csproj | 1 + Composite/Core/PageTemplates/IPageRenderer.cs | 20 +++- .../Core/Routing/Pages/C1PageRouteHander.cs | 19 ++- .../Core/Routing/Pages/CmsPageHttpHandler.cs | 110 ++++++++++++++++++ .../PageTemplates/Razor/RazorPageRenderer.cs | 2 +- 5 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 Composite/Core/Routing/Pages/CmsPageHttpHandler.cs diff --git a/Composite/Composite.csproj b/Composite/Composite.csproj index 7a111b4d12..9008ad24a6 100644 --- a/Composite/Composite.csproj +++ b/Composite/Composite.csproj @@ -252,6 +252,7 @@ + diff --git a/Composite/Core/PageTemplates/IPageRenderer.cs b/Composite/Core/PageTemplates/IPageRenderer.cs index 4b2f5fb846..eeebe6fc03 100644 --- a/Composite/Core/PageTemplates/IPageRenderer.cs +++ b/Composite/Core/PageTemplates/IPageRenderer.cs @@ -1,4 +1,7 @@ -namespace Composite.Core.PageTemplates +using System.Xml.Linq; +using Composite.Functions; + +namespace Composite.Core.PageTemplates { /// /// This class is responsible for rendering the provided job onto the provided asp.net web forms page. @@ -13,4 +16,19 @@ public interface IPageRenderer /// The render job. void AttachToPage(System.Web.UI.Page renderTaget, PageContentToRender contentToRender); } + + /// + /// A page renderer that does not rely of request being handled by ASP.NET Web Forms + /// and does not support UserControl functions + /// + public interface ISlimPageRenderer : IPageRenderer + { + /// + /// Rendering the content into an . + /// + /// The render job. + /// The function context container. + XDocument Render(PageContentToRender contentToRender, + FunctionContextContainer functionContextContainer); + } } diff --git a/Composite/Core/Routing/Pages/C1PageRouteHander.cs b/Composite/Core/Routing/Pages/C1PageRouteHander.cs index 949233e213..5c842df1cb 100644 --- a/Composite/Core/Routing/Pages/C1PageRouteHander.cs +++ b/Composite/Core/Routing/Pages/C1PageRouteHander.cs @@ -1,5 +1,6 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Collections.Concurrent; using System.Linq; using System.Web; using System.Web.Compilation; @@ -10,6 +11,8 @@ using System.Xml.Linq; using Composite.Core.Extensions; using Composite.Core.Linq; +using Composite.Core.PageTemplates; + namespace Composite.Core.Routing.Pages { @@ -51,7 +54,7 @@ static C1PageRouteHandler() _handlerType = Type.GetType(typeAttr.Value); if(_handlerType == null) { - Log.LogError(typeof(C1PageRouteHandler).Name, "Failed to load type '{0}'", typeAttr.Value); + Log.LogError(typeof(C1PageRouteHandler).Name, $"Failed to load type '{typeAttr.Value}'"); } } } @@ -90,6 +93,11 @@ public IHttpHandler GetHttpHandler(RequestContext requestContext) context.Response.Cache.SetCacheability(HttpCacheability.NoCache); } + if (IsSlimPageRenderer(_pageUrlData.GetPage().TemplateId)) + { + return new CmsPageHttpHandler(); + } + if (_handlerType != null) { return (IHttpHandler)Activator.CreateInstance(_handlerType); @@ -97,5 +105,14 @@ public IHttpHandler GetHttpHandler(RequestContext requestContext) return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath("~/Renderers/Page.aspx", typeof(Page)); } + + private static readonly ConcurrentDictionary _pageRendererTypCache + = new ConcurrentDictionary(); + + private bool IsSlimPageRenderer(Guid pageTemplate) + { + return _pageRendererTypCache.GetOrAdd(pageTemplate, + templateId => PageTemplateFacade.BuildPageRenderer(templateId) is ISlimPageRenderer); + } } } diff --git a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs new file mode 100644 index 0000000000..3c38a29565 --- /dev/null +++ b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs @@ -0,0 +1,110 @@ +using System; +using System.Web; +using System.Web.UI; +using System.Xml.Linq; +using Composite.Core.Configuration; +using Composite.Core.Instrumentation; +using Composite.Core.PageTemplates; +using Composite.Core.WebClient.Renderings; +using Composite.Core.WebClient.Renderings.Page; +using Composite.Core.Xml; + +namespace Composite.Core.Routing.Pages +{ + internal class CmsPageHttpHandler: IHttpHandler + { + private const string CacheProfileName = "C1Page"; + + public void ProcessRequest(HttpContext context) + { + InitializeFullPageCaching(context); + + using (var renderingContext = RenderingContext.InitializeFromHttpContext()) + { + if (renderingContext.RunResponseHandlers()) + { + return; + } + + var renderer = PageTemplateFacade.BuildPageRenderer(renderingContext.Page.TemplateId); + + var slimRenderer = (ISlimPageRenderer) renderer; + + var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); + + XDocument document; + + using (Profiler.Measure($"{nameof(ISlimPageRenderer)}.Render")) + { + document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); + } + + PageRenderer.ProcessPageDocument(document, functionContext, renderingContext.Page); + + string xhtml; + if (document.Root.Name == RenderingElementNames.Html) + { + var xhtmlDocument = new XhtmlDocument(document); + + PageRenderer.ProcessXhtmlDocument(xhtmlDocument, renderingContext.Page); + + xhtml = xhtmlDocument.ToString(); + } + else + { + xhtml = document.ToString(); + } + + if (renderingContext.PreRenderRedirectCheck()) + { + return; + } + + xhtml = renderingContext.ConvertInternalLinks(xhtml); + + if (GlobalSettingsFacade.PrettifyPublicMarkup) + { + xhtml = renderingContext.FormatXhtml(xhtml); + } + + var response = context.Response; + + // Inserting perfomance profiling information + if (renderingContext.ProfilingEnabled) + { + xhtml = renderingContext.BuildProfilerReport(); + + response.ContentType = "text/xml"; + } + + response.Write(xhtml); + } + } + + void InitializeFullPageCaching(HttpContext context) + { + using (var page = new CachableEmptyPage()) + { + page.ProcessRequest(context); + } + } + + public bool IsReusable => true; + + private class CachableEmptyPage : Page + { + protected override void FrameworkInitialize() + { + base.FrameworkInitialize(); + + // That's an equivalent of having <%@ OutputCache CacheProfile="C1Page" %> + // on an *.aspx page + + InitOutputCache(new OutputCacheParameters + { + CacheProfile = CacheProfileName + }); + } + } + } +} diff --git a/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs b/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs index 8e77fe2e9b..25476cacfa 100644 --- a/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs +++ b/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs @@ -14,7 +14,7 @@ namespace Composite.Plugins.PageTemplates.Razor { - internal class RazorPageRenderer : IPageRenderer + internal class RazorPageRenderer : IPageRenderer, ISlimPageRenderer { private readonly Hashtable _renderingInfo; private readonly Hashtable _loadingExceptions; From 4e9410419c6f708e33f8585f5298164954f5f64b Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Fri, 16 Jun 2017 17:32:38 +0200 Subject: [PATCH 02/15] A prototype for donut caching implementation for razor templates and razor functions. --- Composite/AspNet/Razor/RazorFunction.cs | 16 +- Composite/Composite.csproj | 1 + .../Core/Caching/FileRelatedDataCache.cs | 2 +- .../PageTemplates/TemplateDefinitionHelper.cs | 11 +- .../Core/Routing/Pages/CmsPageHttpHandler.cs | 108 +++++++- .../WebClient/Renderings/Page/PageRenderer.cs | 232 ++++++++++-------- .../PluginFacades/FunctionWrapper.cs | 18 +- Composite/Functions/IDynamicFunction.cs | 13 + .../FileBasedFunctionProvider.cs | 21 +- .../RazorBasedFunction.cs | 42 ++-- .../RazorFunctionProvider.cs | 13 +- .../UserControlFunctionProvider.cs | 2 +- 12 files changed, 327 insertions(+), 152 deletions(-) create mode 100644 Composite/Functions/IDynamicFunction.cs diff --git a/Composite/AspNet/Razor/RazorFunction.cs b/Composite/AspNet/Razor/RazorFunction.cs index 80afb7347c..17235edf46 100644 --- a/Composite/AspNet/Razor/RazorFunction.cs +++ b/Composite/AspNet/Razor/RazorFunction.cs @@ -10,7 +10,6 @@ namespace Composite.AspNet.Razor /// Base class for c1 functions based on razor /// public abstract class RazorFunction : CompositeC1WebPage, IParameterWidgetsProvider - { /// /// Gets the function description. Override this to make a custom description. @@ -21,10 +20,7 @@ public abstract class RazorFunction : CompositeC1WebPage, IParameterWidgetsProvi /// get { return "Will show recent Twitter activity for a given keyword."; } ///} /// - public virtual string FunctionDescription - { - get { return string.Empty; } - } + public virtual string FunctionDescription => string.Empty; /// /// Gets the return type. By default this is XhtmlDocument (html). Override this to set another return type, like string or XElement. @@ -35,10 +31,12 @@ public virtual string FunctionDescription /// get { return typeof(string); } ///} /// - public virtual Type FunctionReturnType - { - get { return typeof (XhtmlDocument); } - } + public virtual Type FunctionReturnType => typeof (XhtmlDocument); + + /// + /// Determines whether the function output can be cached. + /// + public virtual bool PreventFunctionOutputCaching => false; /// public virtual IDictionary GetParameterWidgets() diff --git a/Composite/Composite.csproj b/Composite/Composite.csproj index 9008ad24a6..cfd1a06c26 100644 --- a/Composite/Composite.csproj +++ b/Composite/Composite.csproj @@ -256,6 +256,7 @@ + diff --git a/Composite/Core/Caching/FileRelatedDataCache.cs b/Composite/Core/Caching/FileRelatedDataCache.cs index 5ea00703dd..2e3bed177d 100644 --- a/Composite/Core/Caching/FileRelatedDataCache.cs +++ b/Composite/Core/Caching/FileRelatedDataCache.cs @@ -125,7 +125,7 @@ private bool Get(string key, DateTime lastModifiedUtc, out CachedData cachedData } catch (Exception ex) { - Log.LogWarning(LogTitle, "Failed to load cached data. Cache '{0}', file: '{1}'", cacheFileName); + Log.LogWarning(LogTitle, $"Failed to load cached data. Cache '{key}', file: '{cacheFileName}'"); Log.LogWarning(LogTitle, ex); cachedData = null; diff --git a/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs b/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs index 946b95e455..f763bd11f6 100644 --- a/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs +++ b/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs @@ -125,14 +125,19 @@ public static class TemplateDefinitionHelper if (functionContextContainer != null) { + bool allFunctionsExecuted = false; + using (Profiler.Measure($"Evaluating placeholder '{placeholderId}'")) { - PageRenderer.ExecuteEmbeddedFunctions(placeholderXhtml.Root, functionContextContainer); + allFunctionsExecuted = PageRenderer.ExecuteCachebleFuctions(placeholderXhtml.Root, functionContextContainer); } - using (Profiler.Measure("Normalizing XHTML document")) + if (allFunctionsExecuted) { - PageRenderer.NormalizeXhtmlDocument(placeholderXhtml); + using (Profiler.Measure("Normalizing XHTML document")) + { + PageRenderer.NormalizeXhtmlDocument(placeholderXhtml); + } } } diff --git a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs index 3c38a29565..b28ec90ec5 100644 --- a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs +++ b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs @@ -1,4 +1,6 @@ using System; +using System.Reflection; +using System.Runtime.Caching; using System.Web; using System.Web.UI; using System.Xml.Linq; @@ -11,35 +13,79 @@ namespace Composite.Core.Routing.Pages { + /// + /// Renders page tempates without building a Web Form's control tree. + /// Contains a custom implementation of "donut caching". + /// internal class CmsPageHttpHandler: IHttpHandler { private const string CacheProfileName = "C1Page"; + private static readonly FieldInfo CacheabilityFieldInfo; + + static CmsPageHttpHandler() + { + CacheabilityFieldInfo = typeof(HttpCachePolicy).GetField("_cacheability", BindingFlags.Instance | BindingFlags.NonPublic); + } public void ProcessRequest(HttpContext context) { InitializeFullPageCaching(context); + var cacheKey = GetCacheKey(context); + using (var renderingContext = RenderingContext.InitializeFromHttpContext()) { - if (renderingContext.RunResponseHandlers()) + var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); + + XDocument document; + using (Profiler.Measure("Cache lookup")) { - return; + document = GetFromCache(cacheKey); } - var renderer = PageTemplateFacade.BuildPageRenderer(renderingContext.Page.TemplateId); + bool allFunctionsExecuted = false; + if (document == null) + { + if (renderingContext.RunResponseHandlers()) + { + return; + } - var slimRenderer = (ISlimPageRenderer) renderer; + var renderer = PageTemplateFacade.BuildPageRenderer(renderingContext.Page.TemplateId); - var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); + var slimRenderer = (ISlimPageRenderer) renderer; - XDocument document; + using (Profiler.Measure($"{nameof(ISlimPageRenderer)}.Render")) + { + document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); + } + + allFunctionsExecuted = PageRenderer.ExecuteCachebleFuctions(document.Root, functionContext); + + if (!allFunctionsExecuted && ServerSideCachingEnabled(context)) + { + context.Response.Cache.SetNoServerCaching(); + + AddToCache(cacheKey, document); + } + } + else + { + context.Response.Cache.SetNoServerCaching(); + } - using (Profiler.Measure($"{nameof(ISlimPageRenderer)}.Render")) + if (!allFunctionsExecuted) { - document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); + using (Profiler.Measure("Executing embedded functions")) + { + PageRenderer.ExecuteEmbeddedFunctions(document.Root, functionContext); + } } - PageRenderer.ProcessPageDocument(document, functionContext, renderingContext.Page); + using (Profiler.Measure("Resolving page fields")) + { + PageRenderer.ResolvePageFields(document, renderingContext.Page); + } string xhtml; if (document.Root.Name == RenderingElementNames.Html) @@ -81,6 +127,17 @@ public void ProcessRequest(HttpContext context) } } + private bool ServerSideCachingEnabled(HttpContext context) + { + var cacheability = GetPageCacheablity(context); + + // TODO: a proper check here + return cacheability > HttpCacheability.NoCache; + } + + private HttpCacheability GetPageCacheablity(HttpContext context) + => (HttpCacheability) CacheabilityFieldInfo.GetValue(context.Response.Cache); + void InitializeFullPageCaching(HttpContext context) { using (var page = new CachableEmptyPage()) @@ -89,6 +146,37 @@ void InitializeFullPageCaching(HttpContext context) } } + private XDocument GetFromCache(string cacheKey) + { + // TODO: set the response headers as well + + var result = MemoryCache.Default.Get(cacheKey) as XDocument; + if (result != null) + { + result = new XDocument(result); + } + + return result; + } + + private void AddToCache(string cacheKey, XDocument document) + { + // TODO: use the standard ASP.NET cache storage providers + // TODO: preserve the response headers as well + + var copy = new XDocument(document); + MemoryCache.Default.Add(cacheKey, copy, new CacheItemPolicy + { + SlidingExpiration = TimeSpan.FromSeconds(60) + }); + } + + private string GetCacheKey(HttpContext context) + { + // TODO: implement properly + return context.Request.Url.ToString(); + } + public bool IsReusable => true; private class CachableEmptyPage : Page @@ -107,4 +195,4 @@ protected override void FrameworkInitialize() } } } -} +} \ No newline at end of file diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index 22df48a2f8..ae37fae37b 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -30,6 +30,7 @@ public static class PageRenderer private static readonly string LogTitle = typeof(PageRenderer).Name; private static readonly NameBasedAttributeComparer _nameBasedAttributeComparer = new NameBasedAttributeComparer(); + private static readonly XName XName_function = Namespaces.Function10 + "function"; /// public static FunctionContextContainer GetPageRenderFunctionContextContainer() @@ -236,27 +237,6 @@ public static IEnumerable GetCurrentPageAssociatedData(Type type) } - internal static void ProcessPageDocument( - XDocument document, - FunctionContextContainer contextContainer, - IPage page) - { - using (Profiler.Measure("Executing embedded functions")) - { - ExecuteEmbeddedFunctions(document.Root, contextContainer); - } - - using (Profiler.Measure("Resolving page fields")) - { - ResolvePageFields(document, page); - } - - using (Profiler.Measure("Normalizing ASP.NET forms")) - { - NormalizeAspNetForms(document); - } - } - internal static void ProcessXhtmlDocument(XhtmlDocument xhtmlDocument, IPage page) { using (Profiler.Measure("Normalizing XHTML document")) @@ -291,7 +271,20 @@ public static Control Render(XDocument document, FunctionContextContainer contex { using (TimerProfilerFacade.CreateTimerProfiler()) { - ProcessPageDocument(document, contextContainer, page); + using (Profiler.Measure("Executing embedded functions")) + { + ExecuteEmbeddedFunctions(document.Root, contextContainer); + } + + using (Profiler.Measure("Resolving page fields")) + { + ResolvePageFields(document, page); + } + + using (Profiler.Measure("Normalizing ASP.NET forms")) + { + NormalizeAspNetForms(document); + } if (document.Root.Name != RenderingElementNames.Html) { @@ -445,7 +438,6 @@ private static void NormalizeAspNetForms(XDocument document) aspNetFormElement.ReplaceWith(aspNetFormElement.Nodes()); } } - } @@ -472,111 +464,151 @@ public static void ResolvePageFields(XDocument document, IPage page) } elem.ReplaceWith(new XElement(Namespaces.Xhtml + "meta", - new XAttribute("name", "description"), - new XAttribute("content", page.Description))); + new XAttribute("name", "description"), + new XAttribute("content", page.Description))); } } - - - /// - public static void ExecuteEmbeddedFunctions(XElement element, FunctionContextContainer contextContainer) + /// + /// Executes functions that match the predicate recursively, + /// + /// + /// + /// + /// A predicate that defines whether a function should be executed based on its name. + /// True if all of the functions has matched the predicate + internal static bool ExecuteFunctionsRec( + XElement element, + FunctionContextContainer functionContext, + Predicate functionShouldBeExecuted = null) { - using (TimerProfilerFacade.CreateTimerProfiler()) + if (element.Name != XName_function) { - IEnumerable functionCallDefinitions = element.DescendantsAndSelf(Namespaces.Function10 + "function") - .Where(f => !f.Ancestors(Namespaces.Function10 + "function").Any()); - - var functionCalls = functionCallDefinitions.ToList(); - if (functionCalls.Count == 0) return; - - object[] functionExecutionResults = new object[functionCalls.Count]; - - for (int i = 0; i < functionCalls.Count; i++) + var children = element.Elements(); + if (element.Elements(XName_function).Any()) { - XElement functionCallDefinition = functionCalls[i]; - string functionName = null; + // Allows replacing the function elements without breaking the iterator + children = children.ToList(); + } - object functionResult; - try + bool allChildrenExecuted = true; + foreach (var childElement in children) + { + if (!ExecuteFunctionsRec(childElement, functionContext, functionShouldBeExecuted)) { - // Evaluating function calls in parameters - IEnumerable parameters = functionCallDefinition.Elements(); - - foreach (XElement parameterNode in parameters) - { - ExecuteEmbeddedFunctions(parameterNode, contextContainer); - } - - - // Executing a function call - BaseRuntimeTreeNode runtimeTreeNode = FunctionTreeBuilder.Build(functionCallDefinition); - - functionName = runtimeTreeNode.GetAllSubFunctionNames().FirstOrDefault(); - - object result = runtimeTreeNode.GetValue(contextContainer); + allChildrenExecuted = false; + } + } + return allChildrenExecuted; + } - if (result != null) - { - // Evaluating functions in a result of a function call - object embedableResult = contextContainer.MakeXEmbedable(result); + bool allRecFunctionsExecuted = true; - foreach (XElement xelement in GetXElements(embedableResult)) - { - ExecuteEmbeddedFunctions(xelement, contextContainer); - } + string functionName = (string) element.Attribute("name"); + object result; + try + { + // Evaluating function calls in parameters + IEnumerable parameters = element.Elements(); - functionResult = embedableResult; - } - else - { - functionResult = null; - } - } - catch (Exception ex) + bool allParametersEvaluated = true; + foreach (XElement parameterNode in parameters.ToList()) + { + if (!ExecuteFunctionsRec(parameterNode, functionContext, functionShouldBeExecuted)) { - using (Profiler.Measure("PageRenderer. Loggin an exception")) - { - XElement errorBoxHtml; + allParametersEvaluated = false; + } + } - if (!contextContainer.ProcessException(functionName, ex, LogTitle, out errorBoxHtml)) - { - throw; - } + if (!allParametersEvaluated) + { + return false; + } - functionResult = errorBoxHtml; - } - } + if (functionShouldBeExecuted != null && + !functionShouldBeExecuted(functionName)) + { + return false; + } - functionExecutionResults[i] = functionResult; - }; + // Executing a function call + BaseRuntimeTreeNode runtimeTreeNode = FunctionTreeBuilder.Build(element); + result = runtimeTreeNode.GetValue(functionContext); - // Applying changes - for (int i = 0; i < functionCalls.Count; i++) + if (result != null) { - XElement functionCall = functionCalls[i]; - object functionCallResult = functionExecutionResults[i]; - if (functionCallResult != null) + // Evaluating functions in a result of a function call + result = functionContext.MakeXEmbedable(result); + + foreach (XElement xelement in GetXElements(result).ToList()) { - if (functionCallResult is XAttribute && functionCall.Parent != null) + if (!ExecuteFunctionsRec(xelement, functionContext, functionShouldBeExecuted)) { - functionCall.Parent.Add(functionCallResult); - functionCall.Remove(); - } - else - { - functionCall.ReplaceWith(functionCallResult); + allRecFunctionsExecuted = false; } } - else + } + } + catch (Exception ex) + { + using (Profiler.Measure("PageRenderer. Logging an exception")) + { + XElement errorBoxHtml; + + if (!functionContext.ProcessException(functionName, ex, LogTitle, out errorBoxHtml)) { - functionCall.Remove(); + throw; } + + result = errorBoxHtml; } } + + ReplaceFunctionWithResult(element, result); + + return allRecFunctionsExecuted; + } + + /// + public static void ExecuteEmbeddedFunctions(XElement element, FunctionContextContainer functionContext) + { + ExecuteFunctionsRec(element, functionContext, null); } + /// + /// Executes all cacheble (not dynamic) functions and returs True + /// if all of the functions were cacheble. + /// + /// + /// + /// + internal static bool ExecuteCachebleFuctions(XElement element, FunctionContextContainer functionContext) + { + return ExecuteFunctionsRec(element, functionContext, name => + { + var function = FunctionFacade.GetFunction(name) as IDynamicFunction; + return function == null || !function.PreventFunctionOutputCaching; + }); + } + private static void ReplaceFunctionWithResult(XElement functionCall, object result) + { + if (result == null) + { + functionCall.Remove(); + return; + } + + if (result is XAttribute && functionCall.Parent != null) + { + functionCall.Parent.Add(result); + functionCall.Remove(); + } + else + { + functionCall.ReplaceWith(result); + } + } private static IEnumerable GetXElements(object source) { diff --git a/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs b/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs index 6e31908801..9c9b3999d8 100644 --- a/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs +++ b/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Web; @@ -14,7 +14,7 @@ namespace Composite.Functions.Foundation.PluginFacades /// This class is used for catching exceptions from plugins and handling them correctly /// [DebuggerDisplay("Name = {Name}, Namespace = {Namespace}")] - internal sealed class FunctionWrapper : IDowncastableFunction, ICompoundFunction, IFunctionInitializationInfo + internal sealed class FunctionWrapper : IDowncastableFunction, ICompoundFunction, IFunctionInitializationInfo, IDynamicFunction { private static readonly string LogTitle = typeof (FunctionWrapper).Name; private readonly IFunction _functionToWrap; @@ -139,8 +139,8 @@ public bool AllowRecursiveCall { get { - return _functionToWrap is ICompoundFunction - && (_functionToWrap as ICompoundFunction).AllowRecursiveCall; + return _functionToWrap is ICompoundFunction compoundFunction + && compoundFunction.AllowRecursiveCall; } } @@ -156,5 +156,15 @@ bool IFunctionInitializationInfo.FunctionInitializedCorrectly return ((IFunctionInitializationInfo) _functionToWrap).FunctionInitializedCorrectly; } } + + public bool PreventFunctionOutputCaching + { + get + { + var dynamicFunction = _functionToWrap as IDynamicFunction; + return dynamicFunction != null && dynamicFunction.PreventFunctionOutputCaching; + } + } + } } diff --git a/Composite/Functions/IDynamicFunction.cs b/Composite/Functions/IDynamicFunction.cs new file mode 100644 index 0000000000..d2a01d746a --- /dev/null +++ b/Composite/Functions/IDynamicFunction.cs @@ -0,0 +1,13 @@ +namespace Composite.Functions +{ + /// + /// Allows defining whether function results should be cached during the donut caching. + /// + public interface IDynamicFunction : IFunction + { + /// + /// Indicates whether the function output can be cached. + /// + bool PreventFunctionOutputCaching { get; } + } +} \ No newline at end of file diff --git a/Composite/Plugins/Functions/FunctionProviders/FileBasedFunctionProvider/FileBasedFunctionProvider.cs b/Composite/Plugins/Functions/FunctionProviders/FileBasedFunctionProvider/FileBasedFunctionProvider.cs index e8adb0b19e..ab10600679 100644 --- a/Composite/Plugins/Functions/FunctionProviders/FileBasedFunctionProvider/FileBasedFunctionProvider.cs +++ b/Composite/Plugins/Functions/FunctionProviders/FileBasedFunctionProvider/FileBasedFunctionProvider.cs @@ -109,7 +109,8 @@ public IEnumerable Functions function = InstantiateFunctionFromCache(virtualPath, @namespace, name, cachedFunctionInfo.ReturnType, - cachedFunctionInfo.Description); + cachedFunctionInfo.Description, + cachedFunctionInfo.PreventCaching); } } catch (ThreadAbortException) @@ -213,13 +214,15 @@ protected FileBasedFunctionProvider(string name, string folder) /// The name. /// Cached value of return type. /// Cached value of the description. + /// Cached PreventFunctionOutputCache property value. /// protected virtual IFunction InstantiateFunctionFromCache( string virtualPath, string @namespace, string name, Type returnType, - string cachedDescription) + string cachedDescription, + bool preventCaching) { return InstantiateFunction(virtualPath, @namespace, name); } @@ -280,6 +283,7 @@ private class CachedFunctionInformation { public Type ReturnType { get; private set; } public string Description { get; private set; } + public bool PreventCaching { get; private set; } private CachedFunctionInformation() {} @@ -287,6 +291,8 @@ public CachedFunctionInformation(IFunction function) { ReturnType = function.ReturnType; Description = function.Description; + PreventCaching = function is IDynamicFunction dynamicFunction + && dynamicFunction.PreventFunctionOutputCaching; } public static void Serialize(CachedFunctionInformation data, string filePath) @@ -296,6 +302,7 @@ public static void Serialize(CachedFunctionInformation data, string filePath) if (data != null) { lines.Add(TypeManager.SerializeType(data.ReturnType)); + lines.Add(data.PreventCaching.ToString()); lines.AddRange(data.Description.Split(new [] { Environment.NewLine }, StringSplitOptions.None)); } @@ -313,9 +320,15 @@ public static CachedFunctionInformation Deserialize(string filePath) Type type = TypeManager.TryGetType(lines[0]); if (type == null) return null; - string description = string.Join(Environment.NewLine, lines.Skip(1)); + bool preventCaching = bool.Parse(lines[1]); + string description = string.Join(Environment.NewLine, lines.Skip(2)); - return new CachedFunctionInformation { Description = description, ReturnType = type }; + return new CachedFunctionInformation + { + Description = description, + PreventCaching = preventCaching, + ReturnType = type + }; } } } diff --git a/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorBasedFunction.cs b/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorBasedFunction.cs index 8501dacbdb..d57a0d56df 100644 --- a/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorBasedFunction.cs +++ b/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorBasedFunction.cs @@ -13,16 +13,21 @@ namespace Composite.Plugins.Functions.FunctionProviders.RazorFunctionProvider { [DebuggerDisplay("Razor function: {Namespace + '.' + Name}")] - internal class RazorBasedFunction : FileBasedFunction + internal class RazorBasedFunction : FileBasedFunction, IDynamicFunction { - public RazorBasedFunction(string ns, string name, string description, IDictionary parameters, Type returnType, string virtualPath, FileBasedFunctionProvider provider) + public RazorBasedFunction(string ns, string name, string description, + IDictionary parameters, Type returnType, string virtualPath, + bool preventCaching, + FileBasedFunctionProvider provider) : base(ns, name, description, parameters, returnType, virtualPath, provider) { - } + PreventFunctionOutputCaching = preventCaching; + } - public RazorBasedFunction(string ns, string name, string description, Type returnType, string virtualPath, FileBasedFunctionProvider provider) + public RazorBasedFunction(string ns, string name, string description, Type returnType, string virtualPath, bool preventCaching, FileBasedFunctionProvider provider) : base(ns, name, description, returnType, virtualPath, provider) { + PreventFunctionOutputCaching = preventCaching; } protected override void InitializeParameters() @@ -36,12 +41,14 @@ protected override void InitializeParameters() razorPage = WebPage.CreateInstanceFromVirtualPath(VirtualPath); } - if (!(razorPage is RazorFunction)) + var razorFunction = razorPage as RazorFunction; + if (razorFunction == null) { throw new InvalidOperationException($"Failed to initialize function from cache. Path: '{VirtualPath}'"); } - Parameters = FunctionBasedFunctionProviderHelper.GetParameters(razorPage as RazorFunction, typeof (RazorFunction), VirtualPath); + Parameters = FunctionBasedFunctionProviderHelper.GetParameters(razorFunction, typeof (RazorFunction), VirtualPath); + PreventFunctionOutputCaching = razorFunction.PreventFunctionOutputCaching; } finally { @@ -51,21 +58,21 @@ protected override void InitializeParameters() public override object Execute(ParameterList parameters, FunctionContextContainer context) { - Action setParametersAction = webPageBase => + void SetParametersAction(WebPageBase webPageBase) { - foreach (var param in parameters.AllParameterNames) - { - var parameter = Parameters[param]; + foreach (var param in parameters.AllParameterNames) + { + var parameter = Parameters[param]; - object parameterValue = parameters.GetParameter(param); + object parameterValue = parameters.GetParameter(param); - parameter.SetValue(webPageBase, parameterValue); - } - }; + parameter.SetValue(webPageBase, parameterValue); + } + } - try + try { - return RazorHelper.ExecuteRazorPage(VirtualPath, setParametersAction, ReturnType, context); + return RazorHelper.ExecuteRazorPage(VirtualPath, SetParametersAction, ReturnType, context); } catch (Exception ex) { @@ -102,5 +109,8 @@ private void EmbedExecutionExceptionSourceCode(Exception ex) } } } + + /// + public bool PreventFunctionOutputCaching { get; protected set; } } } diff --git a/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorFunctionProvider.cs b/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorFunctionProvider.cs index 81e6e9fcda..3f2ab541e0 100644 --- a/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorFunctionProvider.cs +++ b/Composite/Plugins/Functions/FunctionProviders/RazorFunctionProvider/RazorFunctionProvider.cs @@ -40,8 +40,13 @@ protected override IFunction InstantiateFunction(string virtualPath, string @nam var functionParameters = FunctionBasedFunctionProviderHelper.GetParameters( razorFunction, typeof(RazorFunction), virtualPath); - return new RazorBasedFunction(@namespace, name, razorFunction.FunctionDescription, functionParameters, - razorFunction.FunctionReturnType, virtualPath, this); + return new RazorBasedFunction(@namespace, name, + razorFunction.FunctionDescription, + functionParameters, + razorFunction.FunctionReturnType, + virtualPath, + razorFunction.PreventFunctionOutputCaching, + this); } finally { @@ -49,11 +54,11 @@ protected override IFunction InstantiateFunction(string virtualPath, string @nam } } - protected override IFunction InstantiateFunctionFromCache(string virtualPath, string @namespace, string name, Type returnType, string cachedDescription) + protected override IFunction InstantiateFunctionFromCache(string virtualPath, string @namespace, string name, Type returnType, string cachedDescription, bool preventCaching) { if (returnType != null) { - return new RazorBasedFunction(@namespace, name, cachedDescription, returnType, virtualPath, this); + return new RazorBasedFunction(@namespace, name, cachedDescription, returnType, virtualPath, preventCaching, this); } return InstantiateFunction(virtualPath, @namespace, name); diff --git a/Composite/Plugins/Functions/FunctionProviders/UserControlFunctionProvider/UserControlFunctionProvider.cs b/Composite/Plugins/Functions/FunctionProviders/UserControlFunctionProvider/UserControlFunctionProvider.cs index a900f60fc5..3746513bba 100644 --- a/Composite/Plugins/Functions/FunctionProviders/UserControlFunctionProvider/UserControlFunctionProvider.cs +++ b/Composite/Plugins/Functions/FunctionProviders/UserControlFunctionProvider/UserControlFunctionProvider.cs @@ -58,7 +58,7 @@ protected override IFunction InstantiateFunction(string virtualPath, string @nam return new UserControlBasedFunction(@namespace, name, description, parameters, typeof(UserControl), virtualPath, this); } - protected override IFunction InstantiateFunctionFromCache(string virtualPath, string @namespace, string name, Type returnType, string cachedDescription) + protected override IFunction InstantiateFunctionFromCache(string virtualPath, string @namespace, string name, Type returnType, string cachedDescription, bool preventCaching) { return new UserControlBasedFunction(@namespace, name, cachedDescription, virtualPath, this); } From bae5a49c1f8a0a51538f49a8141e2788a3b2c019 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Mon, 19 Jun 2017 14:11:14 +0200 Subject: [PATCH 03/15] Fixing thread culture not being set for CmsPageHttpHandler --- Composite/Core/WebClient/Renderings/RenderingContext.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Composite/Core/WebClient/Renderings/RenderingContext.cs b/Composite/Core/WebClient/Renderings/RenderingContext.cs index 81b5360349..f39e2f3579 100644 --- a/Composite/Core/WebClient/Renderings/RenderingContext.cs +++ b/Composite/Core/WebClient/Renderings/RenderingContext.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Text; +using System.Threading; using System.Web; using Composite.C1Console.Security; using Composite.Core.Extensions; @@ -240,7 +241,11 @@ private void InitializeFromHttpContextInternal() PageRenderer.CurrentPage = Page; - _dataScope = new DataScope(Page.DataSourceId.PublicationScope, Page.DataSourceId.LocaleScope); + var culture = Page.DataSourceId.LocaleScope; + + _dataScope = new DataScope(Page.DataSourceId.PublicationScope, culture); + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; var pagePlaceholderContents = GetPagePlaceholderContents(); PageContentToRender = new PageContentToRender(Page, pagePlaceholderContents, PreviewMode); From 46b05309178122ca1fd6c821db857309a88e3316 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Tue, 20 Jun 2017 10:21:58 +0200 Subject: [PATCH 04/15] Implementing donut caching --- .../Core/Routing/Pages/CmsPageHttpHandler.cs | 113 ++++++++++++++---- 1 file changed, 92 insertions(+), 21 deletions(-) diff --git a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs index b28ec90ec5..52ff947c08 100644 --- a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs +++ b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Generic; using System.Reflection; using System.Runtime.Caching; using System.Web; +using System.Web.Caching; using System.Web.UI; using System.Xml.Linq; using Composite.Core.Configuration; @@ -13,6 +15,39 @@ namespace Composite.Core.Routing.Pages { + [Serializable] + internal class DonutCacheEntry + { + private XDocument _document; + + public DonutCacheEntry() + { + } + + public DonutCacheEntry(HttpContext context, XDocument document) + { + Document = new XDocument(document); + + var headers = context.Response.Headers; + + var headersCopy = new List(headers.Count); + foreach (var name in headers.AllKeys) + { + headersCopy.Add(new HeaderElement(name, headers[name])); + } + + OutputHeaders = headersCopy; + } + + public XDocument Document + { + get => new XDocument(_document); + set => _document = value; + } + + public IReadOnlyCollection OutputHeaders { get; set; } + } + /// /// Renders page tempates without building a Web Form's control tree. /// Contains a custom implementation of "donut caching". @@ -38,13 +73,27 @@ public void ProcessRequest(HttpContext context) var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); XDocument document; + DonutCacheEntry cacheEntry; using (Profiler.Measure("Cache lookup")) { - document = GetFromCache(cacheKey); + cacheEntry = GetFromCache(context, cacheKey); } bool allFunctionsExecuted = false; - if (document == null) + bool preventResponseCaching = false; + + if (cacheEntry != null) + { + document = cacheEntry.Document; + foreach (var header in cacheEntry.OutputHeaders) + { + context.Response.Headers[header.Name] = header.Value; + } + + // Making sure this response will not go to the output cache + preventResponseCaching = true; + } + else { if (renderingContext.RunResponseHandlers()) { @@ -64,15 +113,14 @@ public void ProcessRequest(HttpContext context) if (!allFunctionsExecuted && ServerSideCachingEnabled(context)) { - context.Response.Cache.SetNoServerCaching(); + preventResponseCaching = true; - AddToCache(cacheKey, document); + using (Profiler.Measure("Adding to cache")) + { + AddToCache(context, cacheKey, new DonutCacheEntry(context, document)); + } } } - else - { - context.Response.Cache.SetNoServerCaching(); - } if (!allFunctionsExecuted) { @@ -115,6 +163,11 @@ public void ProcessRequest(HttpContext context) var response = context.Response; + if (preventResponseCaching) + { + context.Response.Cache.SetNoServerCaching(); + } + // Inserting perfomance profiling information if (renderingContext.ProfilingEnabled) { @@ -129,10 +182,18 @@ public void ProcessRequest(HttpContext context) private bool ServerSideCachingEnabled(HttpContext context) { + if (context.Response.StatusCode != 200) + { + return false; + } + +#if !DEBUG var cacheability = GetPageCacheablity(context); // TODO: a proper check here return cacheability > HttpCacheability.NoCache; +#endif + return true; } private HttpCacheability GetPageCacheablity(HttpContext context) @@ -146,29 +207,39 @@ void InitializeFullPageCaching(HttpContext context) } } - private XDocument GetFromCache(string cacheKey) + private DonutCacheEntry GetFromCache(HttpContext context, string cacheKey) { - // TODO: set the response headers as well + var provider = GetCacheProvider(context); - var result = MemoryCache.Default.Get(cacheKey) as XDocument; - if (result != null) + if (provider == null) { - result = new XDocument(result); + return MemoryCache.Default.Get(cacheKey) as DonutCacheEntry; } - return result; + return provider.Get(cacheKey) as DonutCacheEntry; } - private void AddToCache(string cacheKey, XDocument document) + private void AddToCache(HttpContext context, string cacheKey, DonutCacheEntry entry) { - // TODO: use the standard ASP.NET cache storage providers - // TODO: preserve the response headers as well + var provider = GetCacheProvider(context); - var copy = new XDocument(document); - MemoryCache.Default.Add(cacheKey, copy, new CacheItemPolicy + if (provider == null) { - SlidingExpiration = TimeSpan.FromSeconds(60) - }); + MemoryCache.Default.Add(cacheKey, entry, new CacheItemPolicy + { + SlidingExpiration = TimeSpan.FromSeconds(60) + }); + return; + } + + provider.Add(cacheKey, entry, DateTime.UtcNow.AddSeconds(60)); + } + + OutputCacheProvider GetCacheProvider(HttpContext context) + { + var cacheName = context.ApplicationInstance.GetOutputCacheProviderName(context); + + return cacheName != "AspNetInternalProvider" ? OutputCache.Providers?[cacheName] : null; } private string GetCacheKey(HttpContext context) From 7b44238cabe6466b881d2fcd5c135adfb7ec0d30 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Thu, 22 Jun 2017 13:40:06 +0200 Subject: [PATCH 05/15] Donut caching - support for varyByParam/varyByHeader caching profile settings; refactoring --- Composite/AspNet/Caching/DonutCacheEntry.cs | 41 +++ Composite/AspNet/Caching/OutputCacheHelper.cs | 202 +++++++++++++ Composite/AspNet/CmsPageHttpHandler.cs | 143 ++++++++++ Composite/Composite.csproj | 4 +- .../Core/Routing/Pages/C1PageRouteHander.cs | 3 +- .../Core/Routing/Pages/CmsPageHttpHandler.cs | 269 ------------------ 6 files changed, 391 insertions(+), 271 deletions(-) create mode 100644 Composite/AspNet/Caching/DonutCacheEntry.cs create mode 100644 Composite/AspNet/Caching/OutputCacheHelper.cs create mode 100644 Composite/AspNet/CmsPageHttpHandler.cs delete mode 100644 Composite/Core/Routing/Pages/CmsPageHttpHandler.cs diff --git a/Composite/AspNet/Caching/DonutCacheEntry.cs b/Composite/AspNet/Caching/DonutCacheEntry.cs new file mode 100644 index 0000000000..b981259d2d --- /dev/null +++ b/Composite/AspNet/Caching/DonutCacheEntry.cs @@ -0,0 +1,41 @@ +using System; +using System.Collections.Generic; +using System.Web; +using System.Web.Caching; +using System.Xml.Linq; + +namespace Composite.AspNet.Caching +{ + [Serializable] + internal class DonutCacheEntry + { + private XDocument _document; + + public DonutCacheEntry() + { + } + + public DonutCacheEntry(HttpContext context, XDocument document) + { + Document = new XDocument(document); + + var headers = context.Response.Headers; + + var headersCopy = new List(headers.Count); + foreach (var name in headers.AllKeys) + { + headersCopy.Add(new HeaderElement(name, headers[name])); + } + + OutputHeaders = headersCopy; + } + + public XDocument Document + { + get => new XDocument(_document); + set => _document = value; + } + + public IReadOnlyCollection OutputHeaders { get; set; } + } +} diff --git a/Composite/AspNet/Caching/OutputCacheHelper.cs b/Composite/AspNet/Caching/OutputCacheHelper.cs new file mode 100644 index 0000000000..2b69f408c4 --- /dev/null +++ b/Composite/AspNet/Caching/OutputCacheHelper.cs @@ -0,0 +1,202 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Linq; +using System.Reflection; +using System.Runtime.Caching; +using System.Text; +using System.Web; +using System.Web.Caching; +using System.Web.Configuration; +using System.Web.Hosting; +using System.Web.UI; + +namespace Composite.AspNet.Caching +{ + internal static class OutputCacheHelper + { + private const string CacheProfileName = "C1Page"; + private static readonly FieldInfo CacheabilityFieldInfo; + + private static readonly Dictionary _outputCacheProfiles; + + static OutputCacheHelper() + { + CacheabilityFieldInfo = typeof(HttpCachePolicy).GetField("_cacheability", BindingFlags.Instance | BindingFlags.NonPublic); + + _outputCacheProfiles = new Dictionary(); + + var settings = WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath) + .GetSection("system.web/caching/outputCacheSettings") as OutputCacheSettingsSection; + + if (settings != null) + { + foreach (OutputCacheProfile profile in settings.OutputCacheProfiles) + { + _outputCacheProfiles[profile.Name] = profile; + } + } + } + + /// + /// Returns true and sets the cache key value for the current request if + /// ASP.NET full page caching is enabled. + /// + /// + /// + /// + public static bool TryGetCacheKey(HttpContext context, out string cacheKey) + { + var cacheProfile = _outputCacheProfiles[CacheProfileName]; + + if (!cacheProfile.Enabled || cacheProfile.Duration <= 0 + || !(cacheProfile.Location == (OutputCacheLocation) (-1) /* Unspecified */ + || cacheProfile.Location == OutputCacheLocation.Any + || cacheProfile.Location == OutputCacheLocation.Server + || cacheProfile.Location == OutputCacheLocation.ServerAndClient)) + { + cacheKey = null; + return false; + } + + var request = context.Request; + + var sb = new StringBuilder(1 + request.Path.Length + (request.PathInfo ?? "").Length ); + + sb.Append(request.HttpMethod[0]).Append(request.Path).Append(request.PathInfo); + + if (cacheProfile.VaryByCustom != null) + { + string custom = context.ApplicationInstance.GetVaryByCustomString(context, cacheProfile.VaryByCustom); + sb.Append("c").Append(custom); + } + + if (!string.IsNullOrEmpty(cacheProfile.VaryByParam)) + { + var filter = GetVaryByFilter(cacheProfile.VaryByParam); + + AppendParameters(sb, "Q", request.QueryString, filter); + + if (request.HttpMethod == "POST") + { + AppendParameters(sb, "F", request.Form, filter); + } + } + + if (!string.IsNullOrEmpty(cacheProfile.VaryByHeader)) + { + var filter = GetVaryByFilter(cacheProfile.VaryByHeader); + + AppendParameters(sb, "H", request.Headers, filter); + } + + cacheKey = sb.ToString(); + return true; + } + + private static Func GetVaryByFilter(string varyBy) + { + if (varyBy == "*") + { + return parameter => true; + } + + var list = varyBy.Split(';'); + return parameter => list.Contains(parameter); + } + + + private static void AppendParameters(StringBuilder sb, string cacheKeyDelimiter, NameValueCollection collection, Func filter) + { + foreach (string key in collection.OfType().Where(filter)) + { + sb.Append(cacheKeyDelimiter).Append(key).Append("=").Append(collection[key]); + } + } + + + public static DonutCacheEntry GetFromCache(HttpContext context, string cacheKey) + { + var provider = GetCacheProvider(context); + + if (provider == null) + { + return MemoryCache.Default.Get(cacheKey) as DonutCacheEntry; + } + + return provider.Get(cacheKey) as DonutCacheEntry; + } + + + public static void AddToCache(HttpContext context, string cacheKey, DonutCacheEntry entry) + { + var provider = GetCacheProvider(context); + + if (provider == null) + { + MemoryCache.Default.Add(cacheKey, entry, new CacheItemPolicy + { + SlidingExpiration = TimeSpan.FromSeconds(60) + }); + return; + } + + provider.Add(cacheKey, entry, DateTime.UtcNow.AddSeconds(60)); + } + + + static OutputCacheProvider GetCacheProvider(HttpContext context) + { + var cacheName = context.ApplicationInstance.GetOutputCacheProviderName(context); + + return cacheName != "AspNetInternalProvider" ? OutputCache.Providers?[cacheName] : null; + } + + + public static bool ResponseCachebale(HttpContext context) + { + if (context.Response.StatusCode != 200) + { + return false; + } + +#if !DEBUG + var cacheability = GetPageCacheablity(context); + + return cacheability > HttpCacheability.NoCache; +#endif + return true; + } + + + private static HttpCacheability GetPageCacheablity(HttpContext context) + => (HttpCacheability)CacheabilityFieldInfo.GetValue(context.Response.Cache); + + + + public static void InitializeFullPageCaching(HttpContext context) + { + using (var page = new CachableEmptyPage()) + { + page.ProcessRequest(context); + } + } + + + private class CachableEmptyPage : Page + { + protected override void FrameworkInitialize() + { + base.FrameworkInitialize(); + + // That's an equivalent of having <%@ OutputCache CacheProfile="C1Page" %> + // on an *.aspx page + + InitOutputCache(new OutputCacheParameters + { + CacheProfile = CacheProfileName + }); + } + } + } +} diff --git a/Composite/AspNet/CmsPageHttpHandler.cs b/Composite/AspNet/CmsPageHttpHandler.cs new file mode 100644 index 0000000000..79caeb53fa --- /dev/null +++ b/Composite/AspNet/CmsPageHttpHandler.cs @@ -0,0 +1,143 @@ +using System.Web; +using System.Xml.Linq; +using Composite.AspNet.Caching; +using Composite.Core.Configuration; +using Composite.Core.Instrumentation; +using Composite.Core.PageTemplates; +using Composite.Core.WebClient.Renderings; +using Composite.Core.WebClient.Renderings.Page; +using Composite.Core.Xml; + +namespace Composite.AspNet +{ + /// + /// Renders page tempates without building a Web Form's control tree. + /// Contains a custom implementation of "donut caching". + /// + internal class CmsPageHttpHandler: IHttpHandler + { + public void ProcessRequest(HttpContext context) + { + OutputCacheHelper.InitializeFullPageCaching(context); + + bool cachingEnabled = OutputCacheHelper.TryGetCacheKey(context, out string cacheKey); + + using (var renderingContext = RenderingContext.InitializeFromHttpContext()) + { + var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); + + XDocument document; + DonutCacheEntry cacheEntry = null; + if (cachingEnabled) + { + using (Profiler.Measure("Cache lookup")) + { + cacheEntry = OutputCacheHelper.GetFromCache(context, cacheKey); + } + } + + bool allFunctionsExecuted = false; + bool preventResponseCaching = false; + + if (cacheEntry != null) + { + document = cacheEntry.Document; + foreach (var header in cacheEntry.OutputHeaders) + { + context.Response.Headers[header.Name] = header.Value; + } + + // Making sure this response will not go to the output cache + preventResponseCaching = true; + } + else + { + if (renderingContext.RunResponseHandlers()) + { + return; + } + + var renderer = PageTemplateFacade.BuildPageRenderer(renderingContext.Page.TemplateId); + + var slimRenderer = (ISlimPageRenderer) renderer; + + using (Profiler.Measure($"{nameof(ISlimPageRenderer)}.Render")) + { + document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); + } + + allFunctionsExecuted = PageRenderer.ExecuteCachebleFuctions(document.Root, functionContext); + + if (cachingEnabled && !allFunctionsExecuted && OutputCacheHelper.ResponseCachebale(context)) + { + preventResponseCaching = true; + + using (Profiler.Measure("Adding to cache")) + { + OutputCacheHelper.AddToCache(context, cacheKey, new DonutCacheEntry(context, document)); + } + } + } + + if (!allFunctionsExecuted) + { + using (Profiler.Measure("Executing embedded functions")) + { + PageRenderer.ExecuteEmbeddedFunctions(document.Root, functionContext); + } + } + + using (Profiler.Measure("Resolving page fields")) + { + PageRenderer.ResolvePageFields(document, renderingContext.Page); + } + + string xhtml; + if (document.Root.Name == RenderingElementNames.Html) + { + var xhtmlDocument = new XhtmlDocument(document); + + PageRenderer.ProcessXhtmlDocument(xhtmlDocument, renderingContext.Page); + + xhtml = xhtmlDocument.ToString(); + } + else + { + xhtml = document.ToString(); + } + + if (renderingContext.PreRenderRedirectCheck()) + { + return; + } + + xhtml = renderingContext.ConvertInternalLinks(xhtml); + + if (GlobalSettingsFacade.PrettifyPublicMarkup) + { + xhtml = renderingContext.FormatXhtml(xhtml); + } + + var response = context.Response; + + if (preventResponseCaching) + { + context.Response.Cache.SetNoServerCaching(); + } + + // Inserting perfomance profiling information + if (renderingContext.ProfilingEnabled) + { + xhtml = renderingContext.BuildProfilerReport(); + + response.ContentType = "text/xml"; + } + + response.Write(xhtml); + } + } + + + public bool IsReusable => true; + } +} \ No newline at end of file diff --git a/Composite/Composite.csproj b/Composite/Composite.csproj index cfd1a06c26..6a35e8695d 100644 --- a/Composite/Composite.csproj +++ b/Composite/Composite.csproj @@ -251,8 +251,10 @@ + + - + diff --git a/Composite/Core/Routing/Pages/C1PageRouteHander.cs b/Composite/Core/Routing/Pages/C1PageRouteHander.cs index 5c842df1cb..2cf13d13af 100644 --- a/Composite/Core/Routing/Pages/C1PageRouteHander.cs +++ b/Composite/Core/Routing/Pages/C1PageRouteHander.cs @@ -9,6 +9,7 @@ using System.Web.Routing; using System.Web.UI; using System.Xml.Linq; +using Composite.AspNet; using Composite.Core.Extensions; using Composite.Core.Linq; using Composite.Core.PageTemplates; @@ -54,7 +55,7 @@ static C1PageRouteHandler() _handlerType = Type.GetType(typeAttr.Value); if(_handlerType == null) { - Log.LogError(typeof(C1PageRouteHandler).Name, $"Failed to load type '{typeAttr.Value}'"); + Log.LogError(nameof(C1PageRouteHandler), $"Failed to load type '{typeAttr.Value}'"); } } } diff --git a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs b/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs deleted file mode 100644 index 52ff947c08..0000000000 --- a/Composite/Core/Routing/Pages/CmsPageHttpHandler.cs +++ /dev/null @@ -1,269 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Reflection; -using System.Runtime.Caching; -using System.Web; -using System.Web.Caching; -using System.Web.UI; -using System.Xml.Linq; -using Composite.Core.Configuration; -using Composite.Core.Instrumentation; -using Composite.Core.PageTemplates; -using Composite.Core.WebClient.Renderings; -using Composite.Core.WebClient.Renderings.Page; -using Composite.Core.Xml; - -namespace Composite.Core.Routing.Pages -{ - [Serializable] - internal class DonutCacheEntry - { - private XDocument _document; - - public DonutCacheEntry() - { - } - - public DonutCacheEntry(HttpContext context, XDocument document) - { - Document = new XDocument(document); - - var headers = context.Response.Headers; - - var headersCopy = new List(headers.Count); - foreach (var name in headers.AllKeys) - { - headersCopy.Add(new HeaderElement(name, headers[name])); - } - - OutputHeaders = headersCopy; - } - - public XDocument Document - { - get => new XDocument(_document); - set => _document = value; - } - - public IReadOnlyCollection OutputHeaders { get; set; } - } - - /// - /// Renders page tempates without building a Web Form's control tree. - /// Contains a custom implementation of "donut caching". - /// - internal class CmsPageHttpHandler: IHttpHandler - { - private const string CacheProfileName = "C1Page"; - private static readonly FieldInfo CacheabilityFieldInfo; - - static CmsPageHttpHandler() - { - CacheabilityFieldInfo = typeof(HttpCachePolicy).GetField("_cacheability", BindingFlags.Instance | BindingFlags.NonPublic); - } - - public void ProcessRequest(HttpContext context) - { - InitializeFullPageCaching(context); - - var cacheKey = GetCacheKey(context); - - using (var renderingContext = RenderingContext.InitializeFromHttpContext()) - { - var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); - - XDocument document; - DonutCacheEntry cacheEntry; - using (Profiler.Measure("Cache lookup")) - { - cacheEntry = GetFromCache(context, cacheKey); - } - - bool allFunctionsExecuted = false; - bool preventResponseCaching = false; - - if (cacheEntry != null) - { - document = cacheEntry.Document; - foreach (var header in cacheEntry.OutputHeaders) - { - context.Response.Headers[header.Name] = header.Value; - } - - // Making sure this response will not go to the output cache - preventResponseCaching = true; - } - else - { - if (renderingContext.RunResponseHandlers()) - { - return; - } - - var renderer = PageTemplateFacade.BuildPageRenderer(renderingContext.Page.TemplateId); - - var slimRenderer = (ISlimPageRenderer) renderer; - - using (Profiler.Measure($"{nameof(ISlimPageRenderer)}.Render")) - { - document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); - } - - allFunctionsExecuted = PageRenderer.ExecuteCachebleFuctions(document.Root, functionContext); - - if (!allFunctionsExecuted && ServerSideCachingEnabled(context)) - { - preventResponseCaching = true; - - using (Profiler.Measure("Adding to cache")) - { - AddToCache(context, cacheKey, new DonutCacheEntry(context, document)); - } - } - } - - if (!allFunctionsExecuted) - { - using (Profiler.Measure("Executing embedded functions")) - { - PageRenderer.ExecuteEmbeddedFunctions(document.Root, functionContext); - } - } - - using (Profiler.Measure("Resolving page fields")) - { - PageRenderer.ResolvePageFields(document, renderingContext.Page); - } - - string xhtml; - if (document.Root.Name == RenderingElementNames.Html) - { - var xhtmlDocument = new XhtmlDocument(document); - - PageRenderer.ProcessXhtmlDocument(xhtmlDocument, renderingContext.Page); - - xhtml = xhtmlDocument.ToString(); - } - else - { - xhtml = document.ToString(); - } - - if (renderingContext.PreRenderRedirectCheck()) - { - return; - } - - xhtml = renderingContext.ConvertInternalLinks(xhtml); - - if (GlobalSettingsFacade.PrettifyPublicMarkup) - { - xhtml = renderingContext.FormatXhtml(xhtml); - } - - var response = context.Response; - - if (preventResponseCaching) - { - context.Response.Cache.SetNoServerCaching(); - } - - // Inserting perfomance profiling information - if (renderingContext.ProfilingEnabled) - { - xhtml = renderingContext.BuildProfilerReport(); - - response.ContentType = "text/xml"; - } - - response.Write(xhtml); - } - } - - private bool ServerSideCachingEnabled(HttpContext context) - { - if (context.Response.StatusCode != 200) - { - return false; - } - -#if !DEBUG - var cacheability = GetPageCacheablity(context); - - // TODO: a proper check here - return cacheability > HttpCacheability.NoCache; -#endif - return true; - } - - private HttpCacheability GetPageCacheablity(HttpContext context) - => (HttpCacheability) CacheabilityFieldInfo.GetValue(context.Response.Cache); - - void InitializeFullPageCaching(HttpContext context) - { - using (var page = new CachableEmptyPage()) - { - page.ProcessRequest(context); - } - } - - private DonutCacheEntry GetFromCache(HttpContext context, string cacheKey) - { - var provider = GetCacheProvider(context); - - if (provider == null) - { - return MemoryCache.Default.Get(cacheKey) as DonutCacheEntry; - } - - return provider.Get(cacheKey) as DonutCacheEntry; - } - - private void AddToCache(HttpContext context, string cacheKey, DonutCacheEntry entry) - { - var provider = GetCacheProvider(context); - - if (provider == null) - { - MemoryCache.Default.Add(cacheKey, entry, new CacheItemPolicy - { - SlidingExpiration = TimeSpan.FromSeconds(60) - }); - return; - } - - provider.Add(cacheKey, entry, DateTime.UtcNow.AddSeconds(60)); - } - - OutputCacheProvider GetCacheProvider(HttpContext context) - { - var cacheName = context.ApplicationInstance.GetOutputCacheProviderName(context); - - return cacheName != "AspNetInternalProvider" ? OutputCache.Providers?[cacheName] : null; - } - - private string GetCacheKey(HttpContext context) - { - // TODO: implement properly - return context.Request.Url.ToString(); - } - - public bool IsReusable => true; - - private class CachableEmptyPage : Page - { - protected override void FrameworkInitialize() - { - base.FrameworkInitialize(); - - // That's an equivalent of having <%@ OutputCache CacheProfile="C1Page" %> - // on an *.aspx page - - InitOutputCache(new OutputCacheParameters - { - CacheProfile = CacheProfileName - }); - } - } - } -} \ No newline at end of file From 4cd010cfb5a54c54a3c3efa9a37b1d1c27a0af40 Mon Sep 17 00:00:00 2001 From: Marcus Wendt Date: Thu, 22 Jun 2017 14:51:48 +0200 Subject: [PATCH 06/15] cleanup.bat encoding being utf8, where byte order mark os confusing cmd.exe in first line. now a comment. --- Website/App_Data/Composite/reset-installation.bat | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Website/App_Data/Composite/reset-installation.bat b/Website/App_Data/Composite/reset-installation.bat index 855974e8d0..160fe60fd5 100644 --- a/Website/App_Data/Composite/reset-installation.bat +++ b/Website/App_Data/Composite/reset-installation.bat @@ -1,5 +1,7 @@ -del DataMetaData\*.xml -del DataStores\*.xml +:: clean up +del DataMetaData\*.* /q +del DataStores\*.* /q +pause del Configuration\DynamicSqlDataProvider.config del Configuration\DynamicXmlDataProvider.config del Configuration\InstallationInformation.xml From 5edf2cff9fe712eefd8d97fc7148aeef1a4bebe9 Mon Sep 17 00:00:00 2001 From: Marcus Wendt Date: Tue, 11 Jul 2017 11:17:52 +0200 Subject: [PATCH 07/15] cleanup.bat fix :/ --- Website/App_Data/Composite/reset-installation.bat | 1 - 1 file changed, 1 deletion(-) diff --git a/Website/App_Data/Composite/reset-installation.bat b/Website/App_Data/Composite/reset-installation.bat index 160fe60fd5..6a6bf10f89 100644 --- a/Website/App_Data/Composite/reset-installation.bat +++ b/Website/App_Data/Composite/reset-installation.bat @@ -1,7 +1,6 @@ :: clean up del DataMetaData\*.* /q del DataStores\*.* /q -pause del Configuration\DynamicSqlDataProvider.config del Configuration\DynamicXmlDataProvider.config del Configuration\InstallationInformation.xml From e9728a55fcc1eab7806f270923b71709d6422c94 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Thu, 2 Jan 2020 17:21:13 +0100 Subject: [PATCH 08/15] CmsPageHttpHandler - removing duplicated tags from the head element --- Composite/AspNet/CmsPageHttpHandler.cs | 7 +- .../WebClient/Renderings/Page/PageRenderer.cs | 79 ++++++++++++++++++- 2 files changed, 82 insertions(+), 4 deletions(-) diff --git a/Composite/AspNet/CmsPageHttpHandler.cs b/Composite/AspNet/CmsPageHttpHandler.cs index 79caeb53fa..f530dc0ef0 100644 --- a/Composite/AspNet/CmsPageHttpHandler.cs +++ b/Composite/AspNet/CmsPageHttpHandler.cs @@ -1,4 +1,4 @@ -using System.Web; +using System.Web; using System.Xml.Linq; using Composite.AspNet.Caching; using Composite.Core.Configuration; @@ -11,7 +11,7 @@ namespace Composite.AspNet { /// - /// Renders page tempates without building a Web Form's control tree. + /// Renders page templates without building a Web Form's control tree. /// Contains a custom implementation of "donut caching". /// internal class CmsPageHttpHandler: IHttpHandler @@ -98,6 +98,7 @@ public void ProcessRequest(HttpContext context) var xhtmlDocument = new XhtmlDocument(document); PageRenderer.ProcessXhtmlDocument(xhtmlDocument, renderingContext.Page); + PageRenderer.ProcessDocumentHead(xhtmlDocument); xhtml = xhtmlDocument.ToString(); } @@ -125,7 +126,7 @@ public void ProcessRequest(HttpContext context) context.Response.Cache.SetNoServerCaching(); } - // Inserting perfomance profiling information + // Inserting performance profiling information if (renderingContext.ProfilingEnabled) { xhtml = renderingContext.BuildProfilerReport(); diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index c8ac8407e1..f00a41c943 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Text; using System.Web; using System.Web.UI; using System.Web.UI.HtmlControls; @@ -31,6 +32,8 @@ public static class PageRenderer private static readonly NameBasedAttributeComparer _nameBasedAttributeComparer = new NameBasedAttributeComparer(); private static readonly XName XName_function = Namespaces.Function10 + "function"; + private static readonly XName XName_Id = "id"; + private static readonly XName XName_Name = "name"; /// public static FunctionContextContainer GetPageRenderFunctionContextContainer() @@ -55,7 +58,7 @@ public static Control Render(this IPage page, IEnumerable e.Name.LocalName.Equals("meta", StringComparison.OrdinalIgnoreCase); + + private static bool CheckForDuplication(HashSet values, string value) + { + if (!string.IsNullOrWhiteSpace(value)) + { + if (values.Contains(value)) return true; + + values.Add(value); + } + + return false; + } + + private static string AttributesAsString(this XElement e) + { + var str = new StringBuilder(); + foreach (var attr in e.Attributes().OrderBy(a => a.Name.NamespaceName).ThenBy(a => a.Name.LocalName)) + { + str.Append(attr.Name.LocalName); + str.Append("=\""); + str.Append(attr.Value); + str.Append("\" "); + } + + return str.ToString(); + } + + /// + public static void ProcessDocumentHead(XhtmlDocument xhtmlDocument) + { + var head = xhtmlDocument.Head; + + var uniqueIdValues = new HashSet(StringComparer.OrdinalIgnoreCase); + var uniqueMetaNameValues = new HashSet(StringComparer.OrdinalIgnoreCase); + var uniqueScriptAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); + var uniqueLinkAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); + + var priorityOrderedElements = new List(); + + priorityOrderedElements.AddRange(head.Elements().Where(IsMetaTag)); + priorityOrderedElements.Reverse(); + priorityOrderedElements.AddRange(head.Elements().Where(e => !IsMetaTag(e))); + + foreach (var e in priorityOrderedElements) + { + var id = (string) e.Attribute(XName_Id); + + bool toBeRemoved = CheckForDuplication(uniqueIdValues, id); + + if (!toBeRemoved && !e.Nodes().Any()) + { + switch (e.Name.LocalName.ToLowerInvariant()) + { + case "meta": + var name = (string) e.Attribute(XName_Name); + toBeRemoved = CheckForDuplication(uniqueMetaNameValues, name); + break; + case "script": + toBeRemoved = CheckForDuplication(uniqueScriptAttributes, e.AttributesAsString()); + break; + case "link": + toBeRemoved = CheckForDuplication(uniqueLinkAttributes, e.AttributesAsString()); + break; + } + } + + if (toBeRemoved) + { + e.Remove(); + } + } + } + /// public static Control Render(XDocument document, FunctionContextContainer contextContainer, IXElementToControlMapper mapper, IPage page) From a7714c06b6a5706994411e7199dedc89813b51a9 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Fri, 3 Jan 2020 14:43:06 +0100 Subject: [PATCH 09/15] Adding a configuration setting 'omitAspNetWebFormsSupport', which switches on the usage of the CmsPageHttpHandler --- .../BuildinGlobalSettingsProvider.cs | 2 ++ .../GlobalSettingsProviderPluginFacade.cs | 3 ++- .../Configuration/GlobalSettingsFacade.cs | 11 ++++++++- .../Configuration/GlobalSettingsFacadeImpl.cs | 4 +++- .../Configuration/IGlobalSettingsFacade.cs | 3 ++- .../IGlobalSettingsProvider.cs | 4 +++- .../Core/Routing/Pages/C1PageRouteHander.cs | 23 +++++++++++-------- .../WebClient/Renderings/Page/PageRenderer.cs | 17 ++++++++------ .../ConfigBasedGlobalSettingsProvider.cs | 13 +++++++++-- .../Composite/DebugBuild.Composite.config | 1 + .../Composite/ReleaseBuild.Composite.config | 3 ++- 11 files changed, 59 insertions(+), 25 deletions(-) diff --git a/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs b/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs index 5f6238a023..3207ada8ef 100644 --- a/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs +++ b/Composite/Core/Configuration/BuildinPlugins/GlobalSettingsProvider/BuildinGlobalSettingsProvider.cs @@ -139,5 +139,7 @@ public string SerializedWorkflowsDirectory public TimeZoneInfo TimeZone => _timezone; public bool InheritGlobalReadPermissionOnHiddenPerspectives => false; + + public bool OmitAspNetWebFormsSupport => false; } } diff --git a/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs b/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs index 2bd6a1985b..26f5d0fe2c 100644 --- a/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs +++ b/Composite/Core/Configuration/Foundation/PluginFacades/GlobalSettingsProviderPluginFacade.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Configuration; using Composite.Core.Collections.Generic; @@ -326,6 +326,7 @@ public static bool PrettifyRenderFunctionExceptions public static bool InheritGlobalReadPermissionOnHiddenPerspectives => UseReaderLock(p => p.InheritGlobalReadPermissionOnHiddenPerspectives); + public static bool OmitAspNetWebFormsSupport => UseReaderLock(p => p.OmitAspNetWebFormsSupport); private static void Flush() { diff --git a/Composite/Core/Configuration/GlobalSettingsFacade.cs b/Composite/Core/Configuration/GlobalSettingsFacade.cs index 5ed1639cb6..969c30ab72 100644 --- a/Composite/Core/Configuration/GlobalSettingsFacade.cs +++ b/Composite/Core/Configuration/GlobalSettingsFacade.cs @@ -1,7 +1,9 @@ -using System; +using System; using System.Linq; using System.Collections.Generic; using System.Globalization; +using Composite.Core.PageTemplates; +using Composite.Plugins.PageTemplates.Razor; namespace Composite.Core.Configuration @@ -248,6 +250,13 @@ public static void RemoveNonProbableAssemblyName(string assemblyNamePatern) public static bool InheritGlobalReadPermissionOnHiddenPerspectives => _globalSettingsFacade.InheritGlobalReadPermissionOnHiddenPerspectives; + /// + /// When true, a page request handler that doesn't support UserControl functions will be used. + /// Applicable for -s, that return renderer-s implementing interface + /// (f.e. ). + /// + public static bool OmitAspNetWebFormsSupport => _globalSettingsFacade.OmitAspNetWebFormsSupport; + // Overload /// public static CachingSettings GetNamedCaching(string name) diff --git a/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs b/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs index f5f07a068b..f2e7884204 100644 --- a/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs +++ b/Composite/Core/Configuration/GlobalSettingsFacadeImpl.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; using Composite.Core.Configuration.Foundation.PluginFacades; @@ -166,5 +166,7 @@ public void RemoveNonProbableAssemblyName(string assemblyNamePatern) public bool InheritGlobalReadPermissionOnHiddenPerspectives => GlobalSettingsProviderPluginFacade.InheritGlobalReadPermissionOnHiddenPerspectives; + + public bool OmitAspNetWebFormsSupport => GlobalSettingsProviderPluginFacade.OmitAspNetWebFormsSupport; } } \ No newline at end of file diff --git a/Composite/Core/Configuration/IGlobalSettingsFacade.cs b/Composite/Core/Configuration/IGlobalSettingsFacade.cs index ee3495bb83..ecaa4a690c 100644 --- a/Composite/Core/Configuration/IGlobalSettingsFacade.cs +++ b/Composite/Core/Configuration/IGlobalSettingsFacade.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Globalization; @@ -44,5 +44,6 @@ internal interface IGlobalSettingsFacade bool FunctionPreviewEnabled { get; } TimeZoneInfo TimeZone { get; } bool InheritGlobalReadPermissionOnHiddenPerspectives { get; } + bool OmitAspNetWebFormsSupport { get; } } } diff --git a/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs b/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs index 1540c192b3..6482b8b219 100644 --- a/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs +++ b/Composite/Core/Configuration/Plugins/GlobalSettingsProvider/IGlobalSettingsProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using Composite.Core.Configuration.Plugins.GlobalSettingsProvider.Runtime; using Microsoft.Practices.EnterpriseLibrary.Common.Configuration.ObjectBuilder; @@ -87,5 +87,7 @@ internal interface IGlobalSettingsProvider TimeZoneInfo TimeZone { get; } bool InheritGlobalReadPermissionOnHiddenPerspectives { get; } + + bool OmitAspNetWebFormsSupport { get; } } } diff --git a/Composite/Core/Routing/Pages/C1PageRouteHander.cs b/Composite/Core/Routing/Pages/C1PageRouteHander.cs index 2cf13d13af..cc119c6d49 100644 --- a/Composite/Core/Routing/Pages/C1PageRouteHander.cs +++ b/Composite/Core/Routing/Pages/C1PageRouteHander.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics.CodeAnalysis; using System.Collections.Concurrent; using System.Linq; @@ -10,6 +10,7 @@ using System.Web.UI; using System.Xml.Linq; using Composite.AspNet; +using Composite.Core.Configuration; using Composite.Core.Extensions; using Composite.Core.Linq; using Composite.Core.PageTemplates; @@ -19,6 +20,9 @@ namespace Composite.Core.Routing.Pages { internal class C1PageRouteHandler : IRouteHandler { + private const string PageHandlerPath = "Renderers/Page.aspx"; + private const string PageHandlerVirtualPath = "~/" + PageHandlerPath; + private readonly PageUrlData _pageUrlData; private static readonly Type _handlerType; @@ -43,9 +47,8 @@ static C1PageRouteHandler() var handler = handlers .Elements("add") - .Where(e => e.Attribute("path") != null - && e.Attribute("path").Value.Equals("Renderers/Page.aspx", StringComparison.OrdinalIgnoreCase)) - .SingleOrDefaultOrException("Multiple handlers for 'Renderers/Page.aspx' were found'"); + .Where(e => e.Attribute("path")?.Value.Equals(PageHandlerPath, StringComparison.OrdinalIgnoreCase) ?? false) + .SingleOrDefaultOrException($"Multiple handlers for '{PageHandlerPath}' were found'"); if (handler != null) { @@ -94,17 +97,17 @@ public IHttpHandler GetHttpHandler(RequestContext requestContext) context.Response.Cache.SetCacheability(HttpCacheability.NoCache); } - if (IsSlimPageRenderer(_pageUrlData.GetPage().TemplateId)) + if (_handlerType != null) { - return new CmsPageHttpHandler(); + return (IHttpHandler)Activator.CreateInstance(_handlerType); } - if (_handlerType != null) + if (GlobalSettingsFacade.OmitAspNetWebFormsSupport && IsSlimPageRenderer(_pageUrlData.GetPage().TemplateId)) { - return (IHttpHandler)Activator.CreateInstance(_handlerType); + return new CmsPageHttpHandler(); } - - return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath("~/Renderers/Page.aspx", typeof(Page)); + + return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(PageHandlerVirtualPath, typeof(Page)); } private static readonly ConcurrentDictionary _pageRendererTypCache diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index f00a41c943..c4868ffc76 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -252,14 +252,14 @@ internal static void ProcessXhtmlDocument(XhtmlDocument xhtmlDocument, IPage pag ResolveRelativePaths(xhtmlDocument); } - using (Profiler.Measure("Sorting elements")) + using (Profiler.Measure("Appending C1 meta tags")) { - PrioritizeHeadNodes(xhtmlDocument); + AppendC1MetaTags(page, xhtmlDocument); } - using (Profiler.Measure("Appending C1 meta tags")) + using (Profiler.Measure("Sorting elements")) { - AppendC1MetaTags(page, xhtmlDocument); + PrioritizeHeadNodes(xhtmlDocument); } using (Profiler.Measure("Parsing localization strings")) @@ -313,8 +313,11 @@ private static string AttributesAsString(this XElement e) /// public static void ProcessDocumentHead(XhtmlDocument xhtmlDocument) { - var head = xhtmlDocument.Head; + RemoveDuplicates(xhtmlDocument.Head); + } + private static void RemoveDuplicates(XElement head) + { var uniqueIdValues = new HashSet(StringComparer.OrdinalIgnoreCase); var uniqueMetaNameValues = new HashSet(StringComparer.OrdinalIgnoreCase); var uniqueScriptAttributes = new HashSet(StringComparer.OrdinalIgnoreCase); @@ -328,7 +331,7 @@ public static void ProcessDocumentHead(XhtmlDocument xhtmlDocument) foreach (var e in priorityOrderedElements) { - var id = (string) e.Attribute(XName_Id); + var id = (string)e.Attribute(XName_Id); bool toBeRemoved = CheckForDuplication(uniqueIdValues, id); @@ -337,7 +340,7 @@ public static void ProcessDocumentHead(XhtmlDocument xhtmlDocument) switch (e.Name.LocalName.ToLowerInvariant()) { case "meta": - var name = (string) e.Attribute(XName_Name); + var name = (string)e.Attribute(XName_Name); toBeRemoved = CheckForDuplication(uniqueMetaNameValues, name); break; case "script": diff --git a/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs b/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs index 8d5c9ac0f3..5a2789d55f 100644 --- a/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs +++ b/Composite/Plugins/GlobalSettings/GlobalSettingsProviders/ConfigBasedGlobalSettingsProvider.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Configuration; using System.Linq; @@ -101,6 +101,8 @@ public ConfigBasedGlobalSettingsProvider(ConfigBasedGlobalSettingsProviderData c public bool InheritGlobalReadPermissionOnHiddenPerspectives => _configurationData.InheritGlobalReadPermissionOnHiddenPerspectives; + + public bool OmitAspNetWebFormsSupport => _configurationData.OmitAspNetWebFormsSupport; } internal class ConfigCachingSettings: ICachingSettings @@ -520,7 +522,14 @@ public bool InheritGlobalReadPermissionOnHiddenPerspectives get { return (bool)base[InheritGlobalReadPermissionOnHiddenPerspectivesPropertyName]; } set { base[InheritGlobalReadPermissionOnHiddenPerspectivesPropertyName] = value; } } - + + private const string _omitAspNetWebFormsSupportPropertyName = "omitAspNetWebFormsSupport"; + [ConfigurationProperty(_omitAspNetWebFormsSupportPropertyName, DefaultValue = false)] + public bool OmitAspNetWebFormsSupport + { + get { return (bool)base[_omitAspNetWebFormsSupportPropertyName]; } + set { base[_omitAspNetWebFormsSupportPropertyName] = value; } + } } diff --git a/Website/App_Data/Composite/DebugBuild.Composite.config b/Website/App_Data/Composite/DebugBuild.Composite.config index c1c5c02345..58dd780f4a 100644 --- a/Website/App_Data/Composite/DebugBuild.Composite.config +++ b/Website/App_Data/Composite/DebugBuild.Composite.config @@ -98,6 +98,7 @@ prettifyRenderFunctionExceptions="true" functionPreviewEnabled="true" inheritGlobalReadPermissionOnHiddenPerspectives="false" + omitAspNetWebFormsSupport="false" > diff --git a/Website/App_Data/Composite/ReleaseBuild.Composite.config b/Website/App_Data/Composite/ReleaseBuild.Composite.config index b257af27c6..7b8ff8b4b9 100644 --- a/Website/App_Data/Composite/ReleaseBuild.Composite.config +++ b/Website/App_Data/Composite/ReleaseBuild.Composite.config @@ -91,8 +91,9 @@ imageQuality="80" prettifyPublicMarkup="true" prettifyRenderFunctionExceptions="true" - functionPreviewEnabled="true" + functionPreviewEnabled="true" inheritGlobalReadPermissionOnHiddenPerspectives="false" + omitAspNetWebFormsSupport="false" > From d41c5d38f4bbe55f602a6209cfca0ae1f6c5a0d6 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Mon, 6 Jan 2020 11:10:42 +0100 Subject: [PATCH 10/15] Fixing typos, refactoring, fixing appearing before in html . --- Composite/AspNet/CmsPageHttpHandler.cs | 2 +- .../PageTemplates/TemplateDefinitionHelper.cs | 10 +++---- .../WebClient/Renderings/Page/PageRenderer.cs | 12 ++++---- .../PluginFacades/FunctionWrapper.cs | 12 ++------ .../Functions/FunctionRuntimeTreeNode.cs | 29 +++++++++++-------- 5 files changed, 31 insertions(+), 34 deletions(-) diff --git a/Composite/AspNet/CmsPageHttpHandler.cs b/Composite/AspNet/CmsPageHttpHandler.cs index f530dc0ef0..667f0f6506 100644 --- a/Composite/AspNet/CmsPageHttpHandler.cs +++ b/Composite/AspNet/CmsPageHttpHandler.cs @@ -66,7 +66,7 @@ public void ProcessRequest(HttpContext context) document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); } - allFunctionsExecuted = PageRenderer.ExecuteCachebleFuctions(document.Root, functionContext); + allFunctionsExecuted = PageRenderer.ExecuteCacheableFunctions(document.Root, functionContext); if (cachingEnabled && !allFunctionsExecuted && OutputCacheHelper.ResponseCachebale(context)) { diff --git a/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs b/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs index f763bd11f6..e391f5a2d1 100644 --- a/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs +++ b/Composite/Core/PageTemplates/TemplateDefinitionHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; @@ -58,7 +58,7 @@ public static class TemplateDefinitionHelper var placeholderAttributes = property.GetCustomAttributes(typeof(PlaceholderAttribute), true); if (placeholderAttributes.Length == 0) continue; - Verify.That(placeholderAttributes.Length == 1, "Multiple '{0}' attributes defined on property", typeof(PlaceholderAttribute), property.Name); + Verify.That(placeholderAttributes.Length == 1, $"Multiple '{typeof(PlaceholderAttribute)}' attributes defined on property '{property.Name}'"); var placeholderAttribute = (PlaceholderAttribute)placeholderAttributes[0]; @@ -125,11 +125,11 @@ public static class TemplateDefinitionHelper if (functionContextContainer != null) { - bool allFunctionsExecuted = false; + bool allFunctionsExecuted; using (Profiler.Measure($"Evaluating placeholder '{placeholderId}'")) { - allFunctionsExecuted = PageRenderer.ExecuteCachebleFuctions(placeholderXhtml.Root, functionContextContainer); + allFunctionsExecuted = PageRenderer.ExecuteCacheableFunctions(placeholderXhtml.Root, functionContextContainer); } if (allFunctionsExecuted) @@ -152,7 +152,7 @@ public static class TemplateDefinitionHelper Verify.IsNotNull(property, "Failed to find placeholder property '{0}'", propertyName); } - property.SetValue(template, placeholderXhtml, new object[0]); + property.SetValue(template, placeholderXhtml); } } } diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index c4868ffc76..12f3270ced 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -430,7 +430,7 @@ private static int GetHeadNodePriority(XNode headNode) if (headElement.Attribute("name") != null) return 20; - if (headElement.Attribute("property") != null) return 30; + if (headElement.Attribute("property") != null) return 25; return 20; } @@ -670,18 +670,18 @@ public static void ExecuteEmbeddedFunctions(XElement element, FunctionContextCon } /// - /// Executes all cacheble (not dynamic) functions and returs True - /// if all of the functions were cacheble. + /// Executes all cacheable (not dynamic) functions and returns True + /// if all of the functions were cacheable. /// /// /// /// - internal static bool ExecuteCachebleFuctions(XElement element, FunctionContextContainer functionContext) + internal static bool ExecuteCacheableFunctions(XElement element, FunctionContextContainer functionContext) { return ExecuteFunctionsRec(element, functionContext, name => { - var function = FunctionFacade.GetFunction(name) as IDynamicFunction; - return function == null || !function.PreventFunctionOutputCaching; + var function = FunctionFacade.GetFunction(name); + return !(function is IDynamicFunction df && df.PreventFunctionOutputCaching); }); } diff --git a/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs b/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs index 9c9b3999d8..7d536e0323 100644 --- a/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs +++ b/Composite/Functions/Foundation/PluginFacades/FunctionWrapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Diagnostics; using System.Web; @@ -157,14 +157,6 @@ bool IFunctionInitializationInfo.FunctionInitializedCorrectly } } - public bool PreventFunctionOutputCaching - { - get - { - var dynamicFunction = _functionToWrap as IDynamicFunction; - return dynamicFunction != null && dynamicFunction.PreventFunctionOutputCaching; - } - } - + public bool PreventFunctionOutputCaching => _functionToWrap is IDynamicFunction df && df.PreventFunctionOutputCaching; } } diff --git a/Composite/Functions/FunctionRuntimeTreeNode.cs b/Composite/Functions/FunctionRuntimeTreeNode.cs index d7c6d98b3e..2a121694e5 100644 --- a/Composite/Functions/FunctionRuntimeTreeNode.cs +++ b/Composite/Functions/FunctionRuntimeTreeNode.cs @@ -49,14 +49,16 @@ internal FunctionRuntimeTreeNode(IFunction function, List public override object GetValue(FunctionContextContainer contextContainer) { - using (TimerProfilerFacade.CreateTimerProfiler(this.GetNamespace() + "." + this.GetName())) - { - if (contextContainer == null) throw new ArgumentNullException("contextContainer"); + if (contextContainer == null) throw new ArgumentNullException("contextContainer"); + + string functionName = _function.CompositeName() ?? ""; + using (TimerProfilerFacade.CreateTimerProfiler(functionName)) + { ValidateNotSelfCalling(); try - { + { var parameters = new ParameterList(contextContainer); foreach (ParameterProfile parameterProfile in _function.ParameterProfiles) @@ -98,7 +100,7 @@ public override object GetValue(FunctionContextContainer contextContainer) } catch (Exception ex) { - throw new InvalidOperationException(string.Format("Failed to get value for parameter '{0}' in function '{1}'.", parameterProfile.Name, _function.CompositeName()), ex); + throw new InvalidOperationException($"Failed to get value for parameter '{parameterProfile.Name}' in function '{functionName}'.", ex); } parameters.AddConstantParameter(parameterProfile.Name, value, parameterProfile.Type, true); } @@ -108,20 +110,23 @@ public override object GetValue(FunctionContextContainer contextContainer) IDisposable measurement = null; try { - string functionName = _function.CompositeName(); if (functionName != "Composite.Utils.GetInputParameter") { - measurement = Profiler.Measure(functionName ?? "", () => _function.EntityToken); + var nodeToLog = functionName; + + if (_function is IDynamicFunction df && df.PreventFunctionOutputCaching) + { + nodeToLog += " (PreventCaching)"; + } + + measurement = Profiler.Measure(nodeToLog, () => _function.EntityToken); } result = _function.Execute(parameters, contextContainer); } finally { - if (measurement != null) - { - measurement.Dispose(); - } + measurement?.Dispose(); } return result; @@ -132,7 +137,7 @@ public override object GetValue(FunctionContextContainer contextContainer) } catch (Exception ex) { - throw new InvalidOperationException("Failed to get value for function '{0}'".FormatWith(_function.CompositeName()), ex); + throw new InvalidOperationException($"Failed to get value for function '{functionName}'", ex); } } } From da430141727bbdb0a6dd633ab74432235e65787b Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Mon, 6 Jan 2020 16:10:18 +0100 Subject: [PATCH 11/15] Preventing donut caching for pages with suppressed exceptions shown --- Composite/AspNet/CmsPageHttpHandler.cs | 9 ++++++--- Composite/Functions/FunctionContextContainer.cs | 5 ++++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Composite/AspNet/CmsPageHttpHandler.cs b/Composite/AspNet/CmsPageHttpHandler.cs index 667f0f6506..4ba35b8673 100644 --- a/Composite/AspNet/CmsPageHttpHandler.cs +++ b/Composite/AspNet/CmsPageHttpHandler.cs @@ -65,16 +65,19 @@ public void ProcessRequest(HttpContext context) { document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); } - + allFunctionsExecuted = PageRenderer.ExecuteCacheableFunctions(document.Root, functionContext); if (cachingEnabled && !allFunctionsExecuted && OutputCacheHelper.ResponseCachebale(context)) { preventResponseCaching = true; - using (Profiler.Measure("Adding to cache")) + if (!functionContext.ExceptionsSuppressed) { - OutputCacheHelper.AddToCache(context, cacheKey, new DonutCacheEntry(context, document)); + using (Profiler.Measure("Adding to cache")) + { + OutputCacheHelper.AddToCache(context, cacheKey, new DonutCacheEntry(context, document)); + } } } } diff --git a/Composite/Functions/FunctionContextContainer.cs b/Composite/Functions/FunctionContextContainer.cs index c8838e0e05..ff0ec83ad1 100644 --- a/Composite/Functions/FunctionContextContainer.cs +++ b/Composite/Functions/FunctionContextContainer.cs @@ -23,6 +23,8 @@ public sealed class FunctionContextContainer private readonly ParameterList _parameterList; private readonly Dictionary _parameterDictionary; + internal bool ExceptionsSuppressed { get; private set; } + #region constructors /// public FunctionContextContainer() @@ -59,7 +61,7 @@ public FunctionContextContainer(FunctionContextContainer inheritFromContainer, D /// - /// Used for embeding ASP.NET controls into xhtml markup. + /// Used for embedding ASP.NET controls into xhtml markup. /// public IFunctionResultToXEmbedableMapper XEmbedableMapper { get; set; } @@ -143,6 +145,7 @@ public bool ProcessException(string functionName, Exception exception, string lo Log.LogError("Function: " + functionName, exception); errorBoxHtml = XhtmlErrorFormatter.GetErrorDescriptionHtmlElement(exception, functionName); + ExceptionsSuppressed = true; return true; } From 86bda59951e5ef3d99e01fe7fca27108b27b60c9 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Mon, 6 Jan 2020 16:35:36 +0100 Subject: [PATCH 12/15] 404 for internal links pointing to not existing pages; PageRederer refactoring/cleanup --- .../Core/Routing/Pages/C1PageRouteHander.cs | 10 ++++- .../WebClient/Renderings/Page/PageRenderer.cs | 44 ++----------------- 2 files changed, 11 insertions(+), 43 deletions(-) diff --git a/Composite/Core/Routing/Pages/C1PageRouteHander.cs b/Composite/Core/Routing/Pages/C1PageRouteHander.cs index cc119c6d49..3d52c0f17f 100644 --- a/Composite/Core/Routing/Pages/C1PageRouteHander.cs +++ b/Composite/Core/Routing/Pages/C1PageRouteHander.cs @@ -102,9 +102,15 @@ public IHttpHandler GetHttpHandler(RequestContext requestContext) return (IHttpHandler)Activator.CreateInstance(_handlerType); } - if (GlobalSettingsFacade.OmitAspNetWebFormsSupport && IsSlimPageRenderer(_pageUrlData.GetPage().TemplateId)) + if (GlobalSettingsFacade.OmitAspNetWebFormsSupport) { - return new CmsPageHttpHandler(); + var page = _pageUrlData.GetPage() + ?? throw new HttpException(404, "Page not found - either this page has not been published yet or it has been deleted."); + + if (IsSlimPageRenderer(page.TemplateId)) + { + return new CmsPageHttpHandler(); + } } return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(PageHandlerVirtualPath, typeof(Page)); diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index 12f3270ced..cb4814b823 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -125,15 +125,6 @@ private static void ResolvePlaceholders(XDocument document, IEnumerable - public static Guid CurrentPageId - { - get - { - if (!RequestLifetimeCache.HasKey("PageRenderer.IPage")) - { - return Guid.Empty; - } - - return RequestLifetimeCache.TryGet("PageRenderer.IPage").Id; - } - } + public static Guid CurrentPageId => CurrentPage?.Id ?? Guid.Empty; /// @@ -183,11 +163,6 @@ public static IPage CurrentPage { get { - if (!RequestLifetimeCache.HasKey("PageRenderer.IPage")) - { - return null; - } - return RequestLifetimeCache.TryGet("PageRenderer.IPage"); } set @@ -207,20 +182,7 @@ public static IPage CurrentPage /// - public static CultureInfo CurrentPageCulture - { - get - { - if (!RequestLifetimeCache.HasKey("PageRenderer.IPage")) - { - return null; - } - - var page = RequestLifetimeCache.TryGet("PageRenderer.IPage"); - return page.DataSourceId.LocaleScope; - } - } - + public static CultureInfo CurrentPageCulture => CurrentPage?.DataSourceId.LocaleScope; /// @@ -645,7 +607,7 @@ public static void ResolvePageFields(XDocument document, IPage page) } catch (Exception ex) { - using (Profiler.Measure("PageRenderer. Logging an exception")) + using (Profiler.Measure("PageRenderer. Logging exception: " + ex.Message)) { XElement errorBoxHtml; From 19841c67724f325e929a3ce2e521b18487abb5b8 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Wed, 8 Jan 2020 11:54:08 +0100 Subject: [PATCH 13/15] Implementing ISlimPageRenderer for XmlPageRenderer --- Composite/Core/PageTemplates/IPageRenderer.cs | 10 +++++----- .../WebClient/Renderings/Page/PageRenderer.cs | 9 ++++++++- .../PageTemplates/Razor/RazorPageRenderer.cs | 6 +++--- .../XmlPageTemplates/XmlPageRenderer.cs | 20 +++++++++++++++---- 4 files changed, 32 insertions(+), 13 deletions(-) diff --git a/Composite/Core/PageTemplates/IPageRenderer.cs b/Composite/Core/PageTemplates/IPageRenderer.cs index eeebe6fc03..c914c7abb0 100644 --- a/Composite/Core/PageTemplates/IPageRenderer.cs +++ b/Composite/Core/PageTemplates/IPageRenderer.cs @@ -1,20 +1,20 @@ -using System.Xml.Linq; +using System.Xml.Linq; using Composite.Functions; namespace Composite.Core.PageTemplates { /// - /// This class is responsible for rendering the provided job onto the provided asp.net web forms page. + /// This class is responsible for rendering the provided job onto the provided ASP.NET Web Forms page. /// The AttachToPage method is called at page construction and is expected to hook on to asp.net page events (like PreInit) to drive the rendering. /// public interface IPageRenderer { /// - /// Attaches rendering code to an instace of . + /// Attaches rendering code to an instance of . /// - /// The render taget. + /// The render target. /// The render job. - void AttachToPage(System.Web.UI.Page renderTaget, PageContentToRender contentToRender); + void AttachToPage(System.Web.UI.Page renderTarget, PageContentToRender contentToRender); } /// diff --git a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs index cb4814b823..ec81ff8ae3 100644 --- a/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs +++ b/Composite/Core/WebClient/Renderings/Page/PageRenderer.cs @@ -19,6 +19,7 @@ using Composite.Core.Xml; using Composite.C1Console.Security; using Composite.Core.Configuration; +using Composite.Plugins.PageTemplates.XmlPageTemplates; namespace Composite.Core.WebClient.Renderings.Page { @@ -105,7 +106,13 @@ public static XhtmlDocument ParsePlaceholderContent(IPagePlaceholderContent plac return XhtmlDocument.Parse($"{placeholderContent.Content}"); } - private static void ResolvePlaceholders(XDocument document, IEnumerable placeholderContents) + + /// + /// Replaces <rendering:placeholder ... /> tags with provided placeholder contents. Used by . + /// + /// The document to be updated. + /// The placeholder content to be used. + internal static void ResolvePlaceholders(XDocument document, IEnumerable placeholderContents) { using (TimerProfilerFacade.CreateTimerProfiler()) { diff --git a/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs b/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs index 25476cacfa..ba464dc7d9 100644 --- a/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs +++ b/Composite/Plugins/PageTemplates/Razor/RazorPageRenderer.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Text; using System.Web; @@ -30,9 +30,9 @@ internal class RazorPageRenderer : IPageRenderer, ISlimPageRenderer private Page _aspnetPage; private PageContentToRender _job; - public void AttachToPage(Page renderTaget, PageContentToRender contentToRender) + public void AttachToPage(Page renderTarget, PageContentToRender contentToRender) { - _aspnetPage = renderTaget; + _aspnetPage = renderTarget; _job = contentToRender; _aspnetPage.Init += RendererPage; diff --git a/Composite/Plugins/PageTemplates/XmlPageTemplates/XmlPageRenderer.cs b/Composite/Plugins/PageTemplates/XmlPageTemplates/XmlPageRenderer.cs index 8e4181c00d..1b0757b394 100644 --- a/Composite/Plugins/PageTemplates/XmlPageTemplates/XmlPageRenderer.cs +++ b/Composite/Plugins/PageTemplates/XmlPageTemplates/XmlPageRenderer.cs @@ -1,24 +1,36 @@ -using System; +using System; using System.Web.UI; +using System.Xml.Linq; using Composite.Core.PageTemplates; using Composite.Core.Instrumentation; using Composite.Core.WebClient.Renderings.Page; +using Composite.Core.WebClient.Renderings.Template; +using Composite.Functions; namespace Composite.Plugins.PageTemplates.XmlPageTemplates { - internal class XmlPageRenderer: IPageRenderer + internal class XmlPageRenderer: IPageRenderer, ISlimPageRenderer { private Page _aspnetPage; private PageContentToRender _job; - public void AttachToPage(Page renderTaget, PageContentToRender contentToRender) + public void AttachToPage(Page renderTarget, PageContentToRender contentToRender) { - _aspnetPage = renderTaget; + _aspnetPage = renderTarget; _job = contentToRender; _aspnetPage.Init += RendererPage; } + public XDocument Render(PageContentToRender contentToRender, FunctionContextContainer functionContextContainer) + { + var document = TemplateInfo.GetTemplateDocument(contentToRender.Page.TemplateId); + + PageRenderer.ResolvePlaceholders(document, contentToRender.Contents); + + return document; + } + private void RendererPage(object sender, EventArgs e) { if (_aspnetPage.Master != null) From 78b472aa8ca012fb2eaa38c2f9863cf8c0ed2794 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Wed, 8 Jan 2020 16:02:15 +0100 Subject: [PATCH 14/15] Making C1PageRoute/CmsPageHttpRequest handle page preview requests --- Composite/Composite.csproj | 5 +- Composite/Core/Routing/Pages/C1PageRoute.cs | 24 +++++-- .../Renderings/Page/PagePreviewBuilder.cs | 18 ++--- .../Renderings/Page/PagePreviewContext.cs | 69 +++++++++++++++++++ .../WebClient/Renderings/RenderingContext.cs | 19 ++--- 5 files changed, 105 insertions(+), 30 deletions(-) create mode 100644 Composite/Core/WebClient/Renderings/Page/PagePreviewContext.cs diff --git a/Composite/Composite.csproj b/Composite/Composite.csproj index 5fb3af357b..d0bd2ca32c 100644 --- a/Composite/Composite.csproj +++ b/Composite/Composite.csproj @@ -256,6 +256,7 @@ + @@ -263,7 +264,7 @@ - + @@ -2694,7 +2695,7 @@ --> - + $(ProjectDir)git_branch.txt $(ProjectDir)git_commithash.txt diff --git a/Composite/Core/Routing/Pages/C1PageRoute.cs b/Composite/Core/Routing/Pages/C1PageRoute.cs index fbaf87025e..cab1a4b8ec 100644 --- a/Composite/Core/Routing/Pages/C1PageRoute.cs +++ b/Composite/Core/Routing/Pages/C1PageRoute.cs @@ -1,10 +1,13 @@ -using System; +using System; using System.Globalization; using System.Web; using System.Web.Routing; using Composite.Core.WebClient; using Composite.Core.Configuration; using Composite.Core.Extensions; +using Composite.Core.WebClient.Renderings; +using Composite.Core.WebClient.Renderings.Page; +using Composite.Search.DocumentSources; namespace Composite.Core.Routing.Pages { @@ -42,8 +45,7 @@ public static PageUrlData PageUrlData /// The PathInfo url part. public static string GetPathInfo() { - var urlData = PageUrlData; - return urlData != null ? urlData.PathInfo : null; + return PageUrlData?.PathInfo; } /// @@ -94,13 +96,22 @@ public override RouteData GetRouteData(HttpContextBase context) string localPath = context.Request.Url.LocalPath; - var urlProvider = PageUrls.UrlProvider; + if (IsPagePreviewPath(localPath) && + PagePreviewContext.TryGetPreviewKey(context.Request, out Guid previewKey)) + { + var page = PagePreviewContext.GetPage(previewKey); + if (page == null) throw new InvalidOperationException("Not preview information found by key: " + previewKey); + + return new RouteData(this, new C1PageRouteHandler(new PageUrlData(page))); + } if (UrlUtils.IsAdminConsoleRequest(localPath) || IsRenderersPath(localPath)) { return null; } + var urlProvider = PageUrls.UrlProvider; + string currentUrl = context.Request.Url.OriginalString; UrlKind urlKind; @@ -195,6 +206,11 @@ private static bool IsRenderersPath(string relativeUrl) return relativeUrl.StartsWith(UrlUtils.RenderersRootPath + "/", true); } + private static bool IsPagePreviewPath(string relativeUrl) + { + return relativeUrl.StartsWith($"{UrlUtils.RenderersRootPath}/PagePreview", true); + } + private RouteData GetRedirectRoute(string url) { return new RouteData(this, new SeoFriendlyRedirectRouteHandler(url)); diff --git a/Composite/Core/WebClient/Renderings/Page/PagePreviewBuilder.cs b/Composite/Core/WebClient/Renderings/Page/PagePreviewBuilder.cs index a5d50c6175..803f44f508 100644 --- a/Composite/Core/WebClient/Renderings/Page/PagePreviewBuilder.cs +++ b/Composite/Core/WebClient/Renderings/Page/PagePreviewBuilder.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Web; -using System.Web.Caching; using Composite.Data.Types; namespace Composite.Core.WebClient.Renderings.Page @@ -13,8 +12,6 @@ namespace Composite.Core.WebClient.Renderings.Page [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] public class PagePreviewBuilder { - private static readonly TimeSpan PreviewExpirationTimeSpan = new TimeSpan(0, 20, 0); - /// /// Execute an 'im mem' preview request of the provided page and content. Requires IIS to run in "Integrated" pipeline mode. /// @@ -41,19 +38,14 @@ public static string RenderPreview(IPage selectedPage, IList public static string RenderPreview(IPage selectedPage, IList contents, RenderingReason renderingReason) { - HttpContext ctx = HttpContext.Current; - string key = Guid.NewGuid().ToString(); - string query = "previewKey=" + key; - - ctx.Cache.Add(key + "_SelectedPage", selectedPage, null, Cache.NoAbsoluteExpiration, PreviewExpirationTimeSpan, CacheItemPriority.NotRemovable, null); - ctx.Cache.Add(key + "_SelectedContents", contents, null, Cache.NoAbsoluteExpiration, PreviewExpirationTimeSpan, CacheItemPriority.NotRemovable, null); - ctx.Cache.Add(key + "_RenderingReason", renderingReason, null, Cache.NoAbsoluteExpiration, PreviewExpirationTimeSpan, CacheItemPriority.NotRemovable, null); - if (!HttpRuntime.UsingIntegratedPipeline) { throw new InvalidOperationException("IIS classic mode not supported"); } + var previewKey = Guid.NewGuid(); + PagePreviewContext.Save(previewKey, selectedPage, contents, renderingReason); + // The header trick here is to work around (what seems to be) a bug in .net 4.5, where preserveForm=false is ignored // asp.net 4.5 request validation will see the 'page edit http post' data and start bitching. It really should not. var headers = new System.Collections.Specialized.NameValueCollection @@ -61,13 +53,15 @@ public static string RenderPreview(IPage selectedPage, IList key + "_SelectedPage"; + private static string CacheKey_Contents(Guid key) => key + "_SelectedContents"; + private static string CacheKey_RenderingReason(Guid key) => key + "_RenderingReason"; + + public static void Save(Guid previewKey, IPage selectedPage, IList contents, RenderingReason renderingReason) + { + var cache = HttpRuntime.Cache; + + cache.Add(CacheKey_Page(previewKey), selectedPage, null, Cache.NoAbsoluteExpiration, PreviewExpirationTimeSpan, CacheItemPriority.NotRemovable, null); + cache.Add(CacheKey_Contents(previewKey), contents, null, Cache.NoAbsoluteExpiration, PreviewExpirationTimeSpan, CacheItemPriority.NotRemovable, null); + cache.Add(CacheKey_RenderingReason(previewKey), renderingReason, null, Cache.NoAbsoluteExpiration, PreviewExpirationTimeSpan, CacheItemPriority.NotRemovable, null); + } + + public static bool TryGetPreviewKey(HttpRequest request, out Guid previewKey) + { + return TryGetPreviewKey(request.QueryString, out previewKey); + } + + public static bool TryGetPreviewKey(HttpRequestBase request, out Guid previewKey) + { + return TryGetPreviewKey(request.QueryString, out previewKey); + } + + private static bool TryGetPreviewKey(NameValueCollection queryString, out Guid previewKey) + { + var value = queryString[PreviewKeyUrlParameter]; + if (!string.IsNullOrWhiteSpace(value) && Guid.TryParse(value, out previewKey)) + { + return true; + } + + previewKey = Guid.Empty; + return false; + } + + public static IPage GetPage(Guid previewKey) + => (IPage) HttpRuntime.Cache.Get(CacheKey_Page(previewKey)); + + public static IList GetPageContents(Guid previewKey) + => (IList)HttpRuntime.Cache.Get(CacheKey_Contents(previewKey)); + + public static RenderingReason GetRenderingReason(Guid previewKey) + => (RenderingReason)HttpRuntime.Cache.Get(CacheKey_RenderingReason(previewKey)); + + public static void Remove(Guid previewKey) + { + var cache = HttpRuntime.Cache; + + cache.Remove(CacheKey_Page(previewKey)); + cache.Remove(CacheKey_Contents(previewKey)); + cache.Remove(CacheKey_RenderingReason(previewKey)); + } + } +} diff --git a/Composite/Core/WebClient/Renderings/RenderingContext.cs b/Composite/Core/WebClient/Renderings/RenderingContext.cs index f39e2f3579..84f0a89a4d 100644 --- a/Composite/Core/WebClient/Renderings/RenderingContext.cs +++ b/Composite/Core/WebClient/Renderings/RenderingContext.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Text; using System.Threading; @@ -54,7 +54,7 @@ public sealed class RenderingContext: IDisposable private static readonly List _prettifyErrorUrls = new List(); private static int _prettifyErrorCount; - private string _previewKey; + private Guid _previewKey; private IDisposable _pagePerfMeasuring; private string _cachedUrl; private IDisposable _dataScope; @@ -115,7 +115,7 @@ public bool RunResponseHandlers() /// public IEnumerable GetPagePlaceholderContents() { - return PreviewMode ? (IEnumerable)HttpRuntime.Cache.Get(_previewKey + "_SelectedContents") + return PreviewMode ? PagePreviewContext.GetPageContents(_previewKey) : PageManager.GetPlaceholderContent(Page.Id, Page.VersionId); } @@ -204,15 +204,13 @@ private void InitializeFromHttpContextInternal() _pagePerfMeasuring = Profiler.Measure("C1 Page"); } - _previewKey = request.QueryString["previewKey"]; - PreviewMode = !_previewKey.IsNullOrEmpty(); + PreviewMode = PagePreviewContext.TryGetPreviewKey(request, out _previewKey); if (PreviewMode) { - Page = (IPage)HttpRuntime.Cache.Get(_previewKey + "_SelectedPage"); + Page = PagePreviewContext.GetPage(_previewKey); C1PageRoute.PageUrlData = new PageUrlData(Page); - - PageRenderer.RenderingReason = (RenderingReason) HttpRuntime.Cache.Get(_previewKey + "_RenderingReason"); + PageRenderer.RenderingReason = PagePreviewContext.GetRenderingReason(_previewKey); } else { @@ -326,10 +324,7 @@ public void Dispose() if (PreviewMode) { - var cache = HttpRuntime.Cache; - - cache.Remove(_previewKey + "_SelectedPage"); - cache.Remove(_previewKey + "_SelectedContents"); + PagePreviewContext.Remove(_previewKey); } } } From db7a19aca3c5c425dff7a68e277dcf2d51e122c9 Mon Sep 17 00:00:00 2001 From: Dmitry Dzygin Date: Mon, 13 Jan 2020 17:07:50 +0100 Subject: [PATCH 15/15] Disabling caching for unpublished pages; code review fixes --- Composite/AspNet/Caching/OutputCacheHelper.cs | 29 +++++++------------ Composite/AspNet/CmsPageHttpHandler.cs | 28 +++++++++++++----- .../Core/Routing/Pages/C1PageRouteHander.cs | 22 +++++++------- 3 files changed, 42 insertions(+), 37 deletions(-) diff --git a/Composite/AspNet/Caching/OutputCacheHelper.cs b/Composite/AspNet/Caching/OutputCacheHelper.cs index 2b69f408c4..3c5a9e8bbd 100644 --- a/Composite/AspNet/Caching/OutputCacheHelper.cs +++ b/Composite/AspNet/Caching/OutputCacheHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; @@ -24,17 +24,13 @@ static OutputCacheHelper() { CacheabilityFieldInfo = typeof(HttpCachePolicy).GetField("_cacheability", BindingFlags.Instance | BindingFlags.NonPublic); - _outputCacheProfiles = new Dictionary(); + var section = WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath) + .GetSection("system.web/caching/outputCacheSettings"); - var settings = WebConfigurationManager.OpenWebConfiguration(HostingEnvironment.ApplicationVirtualPath) - .GetSection("system.web/caching/outputCacheSettings") as OutputCacheSettingsSection; - - if (settings != null) + if (section is OutputCacheSettingsSection settings) { - foreach (OutputCacheProfile profile in settings.OutputCacheProfiles) - { - _outputCacheProfiles[profile.Name] = profile; - } + _outputCacheProfiles = settings.OutputCacheProfiles.OfType() + .ToDictionary(_ => _.Name); } } @@ -153,37 +149,34 @@ static OutputCacheProvider GetCacheProvider(HttpContext context) } - public static bool ResponseCachebale(HttpContext context) + public static bool ResponseCacheable(HttpContext context) { if (context.Response.StatusCode != 200) { return false; } -#if !DEBUG - var cacheability = GetPageCacheablity(context); + var cacheability = GetPageCacheability(context); return cacheability > HttpCacheability.NoCache; -#endif - return true; } - private static HttpCacheability GetPageCacheablity(HttpContext context) + private static HttpCacheability GetPageCacheability(HttpContext context) => (HttpCacheability)CacheabilityFieldInfo.GetValue(context.Response.Cache); public static void InitializeFullPageCaching(HttpContext context) { - using (var page = new CachableEmptyPage()) + using (var page = new CacheableEmptyPage()) { page.ProcessRequest(context); } } - private class CachableEmptyPage : Page + private class CacheableEmptyPage : Page { protected override void FrameworkInitialize() { diff --git a/Composite/AspNet/CmsPageHttpHandler.cs b/Composite/AspNet/CmsPageHttpHandler.cs index 4ba35b8673..49b953f023 100644 --- a/Composite/AspNet/CmsPageHttpHandler.cs +++ b/Composite/AspNet/CmsPageHttpHandler.cs @@ -20,22 +20,28 @@ public void ProcessRequest(HttpContext context) { OutputCacheHelper.InitializeFullPageCaching(context); - bool cachingEnabled = OutputCacheHelper.TryGetCacheKey(context, out string cacheKey); - using (var renderingContext = RenderingContext.InitializeFromHttpContext()) { - var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); - - XDocument document; + bool cachingEnabled = false; + string cacheKey = null; DonutCacheEntry cacheEntry = null; - if (cachingEnabled) + + bool consoleUserLoggedIn = Composite.C1Console.Security.UserValidationFacade.IsLoggedIn(); + + // "Donut caching" is enabled for logged in users, only if profiling is enabled as well. + if (!renderingContext.CachingDisabled + && (!consoleUserLoggedIn || renderingContext.ProfilingEnabled)) { + cachingEnabled = OutputCacheHelper.TryGetCacheKey(context, out cacheKey); using (Profiler.Measure("Cache lookup")) { cacheEntry = OutputCacheHelper.GetFromCache(context, cacheKey); } } + XDocument document; + var functionContext = PageRenderer.GetPageRenderFunctionContextContainer(); + bool allFunctionsExecuted = false; bool preventResponseCaching = false; @@ -65,10 +71,10 @@ public void ProcessRequest(HttpContext context) { document = slimRenderer.Render(renderingContext.PageContentToRender, functionContext); } - + allFunctionsExecuted = PageRenderer.ExecuteCacheableFunctions(document.Root, functionContext); - if (cachingEnabled && !allFunctionsExecuted && OutputCacheHelper.ResponseCachebale(context)) + if (cachingEnabled && !allFunctionsExecuted && OutputCacheHelper.ResponseCacheable(context)) { preventResponseCaching = true; @@ -129,6 +135,12 @@ public void ProcessRequest(HttpContext context) context.Response.Cache.SetNoServerCaching(); } + // Disabling ASP.NET cache if there's a logged-in user + if (consoleUserLoggedIn) + { + context.Response.Cache.SetCacheability(HttpCacheability.NoCache); + } + // Inserting performance profiling information if (renderingContext.ProfilingEnabled) { diff --git a/Composite/Core/Routing/Pages/C1PageRouteHander.cs b/Composite/Core/Routing/Pages/C1PageRouteHander.cs index 3d52c0f17f..014fb10211 100644 --- a/Composite/Core/Routing/Pages/C1PageRouteHander.cs +++ b/Composite/Core/Routing/Pages/C1PageRouteHander.cs @@ -91,6 +91,17 @@ public IHttpHandler GetHttpHandler(RequestContext requestContext) context.RewritePath(filePath, pathInfo, queryString); } + if (_handlerType == null && GlobalSettingsFacade.OmitAspNetWebFormsSupport) + { + var page = _pageUrlData.GetPage() + ?? throw new HttpException(404, "Page not found - either this page has not been published yet or it has been deleted."); + + if (IsSlimPageRenderer(page.TemplateId)) + { + return new CmsPageHttpHandler(); + } + } + // Disabling ASP.NET cache if there's a logged-in user if (Composite.C1Console.Security.UserValidationFacade.IsLoggedIn()) { @@ -102,17 +113,6 @@ public IHttpHandler GetHttpHandler(RequestContext requestContext) return (IHttpHandler)Activator.CreateInstance(_handlerType); } - if (GlobalSettingsFacade.OmitAspNetWebFormsSupport) - { - var page = _pageUrlData.GetPage() - ?? throw new HttpException(404, "Page not found - either this page has not been published yet or it has been deleted."); - - if (IsSlimPageRenderer(page.TemplateId)) - { - return new CmsPageHttpHandler(); - } - } - return (IHttpHandler)BuildManager.CreateInstanceFromVirtualPath(PageHandlerVirtualPath, typeof(Page)); }