diff --git a/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs b/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs index 892707b2f..b0368b6a7 100644 --- a/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs +++ b/src/Orchard.Tests/Stubs/StubHttpContextAccessor.cs @@ -1,4 +1,5 @@ using System.Web; +using Autofac; using Orchard.Mvc; namespace Orchard.Tests.Stubs { @@ -16,6 +17,10 @@ namespace Orchard.Tests.Stubs { return _httpContext; } + public HttpContextBase CreateContext(ILifetimeScope lifetimeScope) { + return _httpContext; + } + public void Set(HttpContextBase httpContext) { _httpContext = httpContext; } diff --git a/src/Orchard.Web/Modules/Orchard.Layouts/Handlers/LayoutPartHandler.cs b/src/Orchard.Web/Modules/Orchard.Layouts/Handlers/LayoutPartHandler.cs index 7e32d286e..04648fb23 100644 --- a/src/Orchard.Web/Modules/Orchard.Layouts/Handlers/LayoutPartHandler.cs +++ b/src/Orchard.Web/Modules/Orchard.Layouts/Handlers/LayoutPartHandler.cs @@ -12,6 +12,7 @@ namespace Orchard.Layouts.Handlers { private readonly IContentPartDisplay _contentPartDisplay; private readonly IShapeDisplay _shapeDisplay; private readonly ILayoutSerializer _serializer; + private readonly IStaticHttpContextScopeFactory _staticHttpContextScopeFactory; public LayoutPartHandler( IRepository repository, @@ -19,13 +20,15 @@ namespace Orchard.Layouts.Handlers { IContentManager contentManager, IContentPartDisplay contentPartDisplay, IShapeDisplay shapeDisplay, - ILayoutSerializer serializer) { + ILayoutSerializer serializer, + IStaticHttpContextScopeFactory staticHttpContextScopeFactory) { _layoutManager = layoutManager; _contentManager = contentManager; _contentPartDisplay = contentPartDisplay; _shapeDisplay = shapeDisplay; _serializer = serializer; + _staticHttpContextScopeFactory = staticHttpContextScopeFactory; Filters.Add(StorageFilter.For(repository)); OnPublished(UpdateTemplateClients); @@ -34,13 +37,22 @@ namespace Orchard.Layouts.Handlers { private void IndexLayout(IndexContentContext context, LayoutPart part) { var layoutShape = _contentPartDisplay.BuildDisplay(part); - var layoutHtml = _shapeDisplay.Display(layoutShape); + var layoutHtml = RenderShape(layoutShape); context.DocumentIndex .Add("body", layoutHtml).RemoveTags().Analyze() .Add("format", "html").Store(); } + /// + /// This method of rendering is safe even in background tasks. + /// + private string RenderShape(dynamic shape) { + using (_staticHttpContextScopeFactory.CreateStaticScope()) { + return _shapeDisplay.Display(shape); + } + } + private void UpdateTemplateClients(PublishContentContext context, LayoutPart part) { UpdateTemplateClients(part); } diff --git a/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs b/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs index 2aed18c8d..d752ca2e8 100644 --- a/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs +++ b/src/Orchard.Web/Modules/Orchard.Templates/Services/RazorTemplateProcessor.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.IO; using System.Text; -using System.Web; using System.Web.Mvc; using System.Web.UI; using System.Web.WebPages; @@ -15,7 +14,6 @@ namespace Orchard.Templates.Services { [OrchardFeature("Orchard.Templates.Razor")] public class RazorTemplateProcessor : TemplateProcessorImpl { private readonly IRazorCompiler _compiler; - private readonly HttpContextBase _httpContextBase; private readonly IWorkContextAccessor _wca; public override string Type { @@ -24,11 +22,9 @@ namespace Orchard.Templates.Services { public RazorTemplateProcessor( IRazorCompiler compiler, - HttpContextBase httpContextBase, IWorkContextAccessor wca) { _compiler = compiler; - _httpContextBase = httpContextBase; _wca = wca; Logger = NullLogger.Instance; } @@ -41,7 +37,7 @@ namespace Orchard.Templates.Services { public override string Process(string template, string name, DisplayContext context = null, dynamic model = null) { if (String.IsNullOrEmpty(template)) - return string.Empty; + return String.Empty; var compiledTemplate = _compiler.CompileRazor(template, name, new Dictionary()); var result = ActivateAndRenderTemplate(compiledTemplate, context, null, model); @@ -73,9 +69,10 @@ namespace Orchard.Templates.Services { // Setup a fake view context in order to support razor syntax inside of HTML attributes, // for instance: Homepage. var viewData = new ViewDataDictionary(model); + var httpContext = _wca.GetContext().HttpContext; obj.ViewContext = new ViewContext( new ControllerContext( - _httpContextBase.Request.RequestContext, + httpContext.Request.RequestContext, new StubController()), new StubView(), viewData, @@ -83,7 +80,7 @@ namespace Orchard.Templates.Services { htmlWriter); obj.ViewData = viewData; - obj.WebPageContext = new WebPageContext(_httpContextBase, obj as WebPageRenderingBase, model); + obj.WebPageContext = new WebPageContext(httpContext, obj as WebPageRenderingBase, model); obj.WorkContext = _wca.GetContext(); } diff --git a/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs b/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs index 5b04c9358..2e3e841d0 100644 --- a/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs +++ b/src/Orchard/DisplayManagement/Descriptors/ShapeTemplateStrategy/ShapeTemplateBindingStrategy.cs @@ -179,8 +179,8 @@ namespace Orchard.DisplayManagement.Descriptors.ShapeTemplateStrategy { private ControllerContext CreateControllerContext() { var controller = new StubController(); - var httpContext = _workContextAccessor.GetContext().Resolve(); - var requestContext = _workContextAccessor.GetContext().Resolve(); + var httpContext = _workContextAccessor.GetContext().HttpContext; + var requestContext = httpContext.Request.RequestContext; var routeData = requestContext.RouteData; routeData.DataTokens["IWorkContextAccessor"] = _workContextAccessor; diff --git a/src/Orchard/DisplayManagement/ShapeDisplay.cs b/src/Orchard/DisplayManagement/ShapeDisplay.cs index 1313d8a13..a432adb51 100644 --- a/src/Orchard/DisplayManagement/ShapeDisplay.cs +++ b/src/Orchard/DisplayManagement/ShapeDisplay.cs @@ -1,8 +1,6 @@ using System.Collections.Generic; using System.Linq; -using System.Web; using System.Web.Mvc; -using System.Web.Routing; using Orchard.DisplayManagement.Implementation; using Orchard.DisplayManagement.Shapes; @@ -10,28 +8,24 @@ namespace Orchard.DisplayManagement { public class ShapeDisplay : IShapeDisplay { private readonly IDisplayHelperFactory _displayHelperFactory; private readonly IWorkContextAccessor _workContextAccessor; - private readonly HttpContextBase _httpContextBase; - private readonly RequestContext _requestContext; public ShapeDisplay( - IDisplayHelperFactory displayHelperFactory, - IWorkContextAccessor workContextAccessor, - HttpContextBase httpContextBase, - RequestContext requestContext) { + IDisplayHelperFactory displayHelperFactory, + IWorkContextAccessor workContextAccessor) { _displayHelperFactory = displayHelperFactory; _workContextAccessor = workContextAccessor; - _httpContextBase = httpContextBase; - _requestContext = requestContext; } public string Display(Shape shape) { - return Display((object) shape); + return Display((object)shape); } public string Display(object shape) { + var workContext = _workContextAccessor.GetContext(); + var httpContext = workContext.HttpContext; var viewContext = new ViewContext { - HttpContext = _httpContextBase, - RequestContext = _requestContext + HttpContext = httpContext, + RequestContext = httpContext.Request.RequestContext }; viewContext.RouteData.DataTokens["IWorkContextAccessor"] = _workContextAccessor; var display = _displayHelperFactory.CreateHelper(viewContext, new ViewDataContainer()); diff --git a/src/Orchard/Environment/OrchardStarter.cs b/src/Orchard/Environment/OrchardStarter.cs index 9af6edce9..938605dbe 100644 --- a/src/Orchard/Environment/OrchardStarter.cs +++ b/src/Orchard/Environment/OrchardStarter.cs @@ -63,7 +63,7 @@ namespace Orchard.Environment { builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); - builder.RegisterType().As().InstancePerDependency(); + builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); builder.RegisterType().As().SingleInstance(); diff --git a/src/Orchard/Environment/WorkContextAccessor.cs b/src/Orchard/Environment/WorkContextAccessor.cs index af709d0d5..7e6bd0ea3 100644 --- a/src/Orchard/Environment/WorkContextAccessor.cs +++ b/src/Orchard/Environment/WorkContextAccessor.cs @@ -60,11 +60,14 @@ namespace Orchard.Environment { return CreateWorkContextScope(httpContext); var workLifetime = _lifetimeScope.BeginLifetimeScope("work"); + httpContext = _httpContextAccessor.CreateContext(workLifetime); + workLifetime.Resolve>().Value = httpContext; var events = workLifetime.Resolve>(); events.Invoke(e => e.Started(), NullLogger.Instance); return new ThreadStaticScopeImplementation( + httpContext, events, workLifetime, EnsureThreadStaticContexts(), @@ -113,8 +116,9 @@ namespace Orchard.Environment { readonly WorkContext _workContext; readonly Action _disposer; - public ThreadStaticScopeImplementation(IEnumerable events, ILifetimeScope lifetimeScope, ConcurrentDictionary contexts, object workContextKey) { + public ThreadStaticScopeImplementation(HttpContextBase httpContext, IEnumerable events, ILifetimeScope lifetimeScope, ConcurrentDictionary contexts, object workContextKey) { _workContext = lifetimeScope.Resolve(); + httpContext.Items[workContextKey] = _workContext; contexts.AddOrUpdate(workContextKey, _workContext, (a, b) => _workContext); _disposer = () => { @@ -123,6 +127,11 @@ namespace Orchard.Environment { WorkContext removedContext; contexts.TryRemove(workContextKey, out removedContext); lifetimeScope.Dispose(); + + var staticHttpContext = httpContext as IDisposable; + + if(staticHttpContext != null) + staticHttpContext.Dispose(); }; } diff --git a/src/Orchard/IStaticHttpContextScopeFactory.cs b/src/Orchard/IStaticHttpContextScopeFactory.cs new file mode 100644 index 000000000..bec2219e5 --- /dev/null +++ b/src/Orchard/IStaticHttpContextScopeFactory.cs @@ -0,0 +1,15 @@ +using System; + +namespace Orchard { + /// + /// A factory class that creates an instance and initializes the HttpContext.Current property with that instance until the scope is disposed of. + /// This is useful when rendering views from a background thread, as some Html Helpers access HttpContext.Current directly, thus preventing a NullReferenceException. + /// + public interface IStaticHttpContextScopeFactory : IDependency { + /// + /// Creates a disposable static HttpContext scope. This is safe to use even if there is an actual HttpContext.Current instance. + /// + /// + IDisposable CreateStaticScope(); + } +} diff --git a/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs b/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs deleted file mode 100644 index 9a7864887..000000000 --- a/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System.Web; - -namespace Orchard.Mvc.Extensions { - public static class HttpContextBaseExtensions { - public static bool IsBackgroundContext(this HttpContextBase httpContextBase) { - return httpContextBase == null || httpContextBase is MvcModule.HttpContextPlaceholder; - } - } -} diff --git a/src/Orchard/Mvc/Extensions/HttpContextExtensions.cs b/src/Orchard/Mvc/Extensions/HttpContextExtensions.cs new file mode 100644 index 000000000..f12ebe884 --- /dev/null +++ b/src/Orchard/Mvc/Extensions/HttpContextExtensions.cs @@ -0,0 +1,13 @@ +using System.Web; + +namespace Orchard.Mvc.Extensions { + public static class HttpContextExtensions { + public static bool IsBackgroundContext(this HttpContextBase httpContext) { + return httpContext == null || httpContext is MvcModule.HttpContextPlaceholder; + } + + public static bool IsBackgroundContext(this HttpContext httpContext) { + return httpContext == null || httpContext.Items.Contains(StaticHttpContextScopeFactory.IsBackgroundHttpContextKey); + } + } +} diff --git a/src/Orchard/Mvc/HttpContextAccessor.cs b/src/Orchard/Mvc/HttpContextAccessor.cs index 8fa47afbc..fc3d09fe5 100644 --- a/src/Orchard/Mvc/HttpContextAccessor.cs +++ b/src/Orchard/Mvc/HttpContextAccessor.cs @@ -18,16 +18,31 @@ namespace Orchard.Mvc { return httpContext == null || httpContext.Items.Contains(BackgroundHttpContextFactory.IsBackgroundHttpContextKey); } - private static HttpContext GetStaticProperty() { - var httpContext = HttpContext.Current; - if (httpContext == null) { - return null; - } + [ThreadStatic] + static ConcurrentDictionary _threadStaticContexts; public HttpContextBase Current() { - // TODO: HttpContextBase is not registred in the "shell" lifetime scope, so resolving it will cause an exception. + if (!HttpContext.Current.IsBackgroundContext()) + return new HttpContextWrapper(HttpContext.Current); + + return GetContext(); + } + + public HttpContextBase CreateContext(ILifetimeScope lifetimeScope) { + return new MvcModule.HttpContextPlaceholder(_threadStaticContexts, _contextKey, () => { + return lifetimeScope.Resolve().GetSiteSettings().BaseUrl; + }); + } + + private HttpContextBase GetContext() { + HttpContextBase context; + return ThreadStaticContexts.TryGetValue(_contextKey, out context) ? context : null; + } - return HttpContext.Current != null ? new HttpContextWrapper(HttpContext.Current) : null; + static ConcurrentDictionary ThreadStaticContexts { + get { + return _threadStaticContexts ?? (_threadStaticContexts = new ConcurrentDictionary()); + } } } } \ No newline at end of file diff --git a/src/Orchard/Mvc/IHttpContextAccessor.cs b/src/Orchard/Mvc/IHttpContextAccessor.cs index 462887fee..ba4f8fe5e 100644 --- a/src/Orchard/Mvc/IHttpContextAccessor.cs +++ b/src/Orchard/Mvc/IHttpContextAccessor.cs @@ -1,8 +1,10 @@ -using System.Web; +using System; +using System.Web; +using Autofac; namespace Orchard.Mvc { public interface IHttpContextAccessor { HttpContextBase Current(); - void Set(HttpContextBase httpContext); + HttpContextBase CreateContext(ILifetimeScope lifetimeScope); } } diff --git a/src/Orchard/Mvc/MvcModule.cs b/src/Orchard/Mvc/MvcModule.cs index d23c77fba..a7db4b266 100644 --- a/src/Orchard/Mvc/MvcModule.cs +++ b/src/Orchard/Mvc/MvcModule.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; @@ -9,6 +10,7 @@ using System.Web.Instrumentation; using System.Web.Mvc; using System.Web.Routing; using Autofac; +using Orchard.Mvc.Extensions; using Orchard.Mvc.Routes; using Orchard.Settings; @@ -50,7 +52,7 @@ namespace Orchard.Mvc { // which requires activating the Site content item, which in turn requires a UrlHelper, which in turn requires a RequestContext, // thus preventing a StackOverflowException. var baseUrl = new Func(() => siteService.GetSiteSettings().BaseUrl); - var httpContextBase = new HttpContextPlaceholder(baseUrl); + var httpContextBase = context.Resolve().Current(); context.Resolve().CreateWorkContextScope(httpContextBase); return httpContextBase; } @@ -58,7 +60,7 @@ namespace Orchard.Mvc { static RequestContext RequestContextFactory(IComponentContext context) { var httpContextAccessor = context.Resolve(); var httpContext = httpContextAccessor.Current(); - if (httpContext != null) { + if (!httpContext.IsBackgroundContext()) { var mvcHandler = httpContext.Handler as MvcHandler; if (mvcHandler != null) { @@ -85,16 +87,23 @@ namespace Orchard.Mvc { /// /// Standin context for background tasks. /// - public class HttpContextPlaceholder : HttpContextBase { + public class HttpContextPlaceholder : HttpContextBase, IDisposable { private readonly Lazy _baseUrl; private readonly IDictionary _items = new Dictionary(); + readonly Action _disposer; - public HttpContextPlaceholder(Func baseUrl) { + public HttpContextPlaceholder(ConcurrentDictionary contexts, object contextKey, Func baseUrl) { _baseUrl = new Lazy(baseUrl); + contexts.AddOrUpdate(contextKey, this, (a, b) => this); + + _disposer = () => { + HttpContextBase removedContext; + contexts.TryRemove(contextKey, out removedContext); + }; } public override HttpRequestBase Request { - get { return new HttpRequestPlaceholder(new Uri(_baseUrl.Value)); } + get { return new HttpRequestPlaceholder(this, new Uri(_baseUrl.Value)); } } public override IHttpHandler Handler { get; set; } @@ -103,6 +112,10 @@ namespace Orchard.Mvc { get { return new HttpResponsePlaceholder(); } } + public override HttpSessionStateBase Session { + get { return null; } + } + public override IDictionary Items { get { return _items; } } @@ -118,6 +131,10 @@ namespace Orchard.Mvc { public override object GetService(Type serviceType) { return null; } + + public void Dispose() { + _disposer(); + } } public class HttpResponsePlaceholder : HttpResponseBase { @@ -136,9 +153,12 @@ namespace Orchard.Mvc { /// standin context for background tasks. /// public class HttpRequestPlaceholder : HttpRequestBase { + private readonly HttpContextBase _httpContext; private readonly Uri _uri; + private RequestContext _requestContext; - public HttpRequestPlaceholder(Uri uri) { + public HttpRequestPlaceholder(HttpContextBase httpContext, Uri uri) { + _httpContext = httpContext; _uri = uri; } @@ -185,7 +205,7 @@ namespace Orchard.Mvc { return new NameValueCollection { { "SERVER_PORT", _uri.Port.ToString(CultureInfo.InvariantCulture) }, { "HTTP_HOST", _uri.Authority.ToString(CultureInfo.InvariantCulture) }, - + }; } } @@ -215,6 +235,16 @@ namespace Orchard.Mvc { return new HttpBrowserCapabilitiesPlaceholder(); } } + + public override RequestContext RequestContext { + get { + if (_requestContext == null) { + _requestContext = new RequestContext(_httpContext, new RouteData()); + } + return _requestContext; + } + set { _requestContext = value; } + } } public class HttpBrowserCapabilitiesPlaceholder : HttpBrowserCapabilitiesBase { diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index eb7303a36..02519fce4 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -148,7 +148,8 @@ - + + @@ -317,7 +318,7 @@ - + @@ -647,7 +648,7 @@ - + diff --git a/src/Orchard/StaticHttpContextScope.cs b/src/Orchard/StaticHttpContextScope.cs new file mode 100644 index 000000000..b22b181db --- /dev/null +++ b/src/Orchard/StaticHttpContextScope.cs @@ -0,0 +1,17 @@ +using System; +using System.Web; + +namespace Orchard { + public class StaticHttpContextScope : IDisposable { + private readonly HttpContext _previousHttpContext; + + public StaticHttpContextScope(HttpContext stub) { + _previousHttpContext = HttpContext.Current; + HttpContext.Current = stub; + } + + public void Dispose() { + HttpContext.Current = _previousHttpContext; + } + } +} \ No newline at end of file diff --git a/src/Orchard/StaticHttpContextScopeFactory.cs b/src/Orchard/StaticHttpContextScopeFactory.cs new file mode 100644 index 000000000..35aafda4d --- /dev/null +++ b/src/Orchard/StaticHttpContextScopeFactory.cs @@ -0,0 +1,31 @@ +using System; +using System.IO; +using System.Web; +using Orchard.Settings; + +namespace Orchard { + public class StaticHttpContextScopeFactory : IStaticHttpContextScopeFactory { + private readonly Func _siteService; + public StaticHttpContextScopeFactory(Func siteService) { + _siteService = siteService; + } + + public const string IsBackgroundHttpContextKey = "IsBackgroundHttpContext"; + + public IDisposable CreateStaticScope() { + // If there already is a current HttpContext, use that one as the stub. + if(HttpContext.Current != null) + return new StaticHttpContextScope(HttpContext.Current); + + // We're in a background task (or some other static context like the console), + // so create a stub context so that Html Helpers can still be executed when rendering shapes in background tasks + // (sadly enought some Html Helpers access HttpContext.Current directly). + var url = _siteService().GetSiteSettings().BaseUrl; + var stub = new HttpContext(new HttpRequest("", url, ""), new HttpResponse(new StringWriter())); + + stub.Items[IsBackgroundHttpContextKey] = true; + + return new StaticHttpContextScope(stub); + } + } +} \ No newline at end of file diff --git a/src/Orchard/Tasks/BackgroundService.cs b/src/Orchard/Tasks/BackgroundService.cs index b952a6a26..1775d4c19 100644 --- a/src/Orchard/Tasks/BackgroundService.cs +++ b/src/Orchard/Tasks/BackgroundService.cs @@ -22,9 +22,7 @@ namespace Orchard.Tasks { public BackgroundService( IEnumerable tasks, ITransactionManager transactionManager, - ShellSettings shellSettings, - IContentManager contentManager, - IBackgroundHttpContextFactory backgroundHttpContextFactory) { + ShellSettings shellSettings) { _tasks = tasks; _transactionManager = transactionManager; diff --git a/src/Orchard/Tasks/SweepGenerator.cs b/src/Orchard/Tasks/SweepGenerator.cs index feb8a3f0e..9c61b6740 100644 --- a/src/Orchard/Tasks/SweepGenerator.cs +++ b/src/Orchard/Tasks/SweepGenerator.cs @@ -41,7 +41,7 @@ namespace Orchard.Tasks { } void Elapsed(object sender, ElapsedEventArgs e) { - // current implementation disallows re-entrancy + // Current implementation disallows re-entrancy. if (!System.Threading.Monitor.TryEnter(_timer)) return; @@ -60,7 +60,7 @@ namespace Orchard.Tasks { public void DoWork() { using (var scope = _workContextAccessor.CreateWorkContextScope()) { - // resolve the manager and invoke it + // Resolve the manager and invoke it. var manager = scope.Resolve(); manager.Sweep(); }