diff --git a/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs b/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs index b1ad65939..9da61a392 100644 --- a/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs +++ b/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs @@ -19,6 +19,7 @@ namespace Orchard.Tests.Environment { } protected override void Register(ContainerBuilder builder) { + builder.RegisterModule(new MvcModule()); builder.RegisterModule(new WorkContextModule()); builder.RegisterType().As(); builder.RegisterAutoMocking(); diff --git a/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContextFactoryTests.cs b/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContextFactoryTests.cs index 06e12f4ec..92d248bc7 100644 --- a/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContextFactoryTests.cs +++ b/src/Orchard.Tests/Environment/ShellBuilders/DefaultShellContextFactoryTests.cs @@ -21,6 +21,7 @@ namespace Orchard.Tests.Environment.ShellBuilders { public void Init() { var builder = new ContainerBuilder(); builder.RegisterType().As(); + builder.RegisterModule(new MvcModule()); builder.RegisterModule(new WorkContextModule()); builder.RegisterType().As(); builder.RegisterAutoMocking(Moq.MockBehavior.Strict); @@ -93,4 +94,4 @@ namespace Orchard.Tests.Environment.ShellBuilders { Assert.That(context.Descriptor.Features, Has.Some.With.Property("Name").EqualTo("Orchard.Setup")); } } -} \ No newline at end of file +} diff --git a/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs b/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs index 97d91e63f..396434d12 100644 --- a/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs +++ b/src/Orchard.Tests/Tasks/SweepGeneratorTests.cs @@ -13,6 +13,7 @@ namespace Orchard.Tests.Tasks { public class SweepGeneratorTests : ContainerTestBase { protected override void Register(ContainerBuilder builder) { builder.RegisterAutoMocking(MockBehavior.Loose); + builder.RegisterModule(new MvcModule()); builder.RegisterModule(new WorkContextModule()); builder.RegisterType().As(); builder.RegisterType(); diff --git a/src/Orchard/Environment/WorkContextAccessor.cs b/src/Orchard/Environment/WorkContextAccessor.cs index d834ba701..601a0b978 100644 --- a/src/Orchard/Environment/WorkContextAccessor.cs +++ b/src/Orchard/Environment/WorkContextAccessor.cs @@ -1,6 +1,7 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; +using System.Runtime.Remoting; +using System.Runtime.Remoting.Messaging; using System.Web; using Autofac; using Orchard.Logging; @@ -15,23 +16,22 @@ namespace Orchard.Environment { // a different symbolic key is used for each tenant. // this guarantees the correct accessor is being resolved. readonly object _workContextKey = new object(); - - [ThreadStatic] - static ConcurrentDictionary _threadStaticContexts; + private readonly string _workContextSlot; public WorkContextAccessor( IHttpContextAccessor httpContextAccessor, ILifetimeScope lifetimeScope) { _httpContextAccessor = httpContextAccessor; _lifetimeScope = lifetimeScope; + _workContextSlot = "WorkContext." + Guid.NewGuid().ToString("n"); } public WorkContext GetContext(HttpContextBase httpContext) { if (!httpContext.IsBackgroundContext()) return httpContext.Items[_workContextKey] as WorkContext; - WorkContext workContext; - return EnsureThreadStaticContexts().TryGetValue(_workContextKey, out workContext) ? workContext : null; + var context = CallContext.LogicalGetData(_workContextSlot) as ObjectHandle; + return context != null ? context.Unwrap() as WorkContext : null; } public WorkContext GetContext() { @@ -39,8 +39,8 @@ namespace Orchard.Environment { if (!httpContext.IsBackgroundContext()) return GetContext(httpContext); - WorkContext workContext; - return EnsureThreadStaticContexts().TryGetValue(_workContextKey, out workContext) ? workContext : null; + var context = CallContext.LogicalGetData(_workContextSlot) as ObjectHandle; + return context != null ? context.Unwrap() as WorkContext : null; } public IWorkContextScope CreateWorkContextScope(HttpContextBase httpContext) { @@ -57,7 +57,6 @@ namespace Orchard.Environment { _workContextKey); } - public IWorkContextScope CreateWorkContextScope() { var httpContext = _httpContextAccessor.Current(); if (!httpContext.IsBackgroundContext()) @@ -68,18 +67,9 @@ namespace Orchard.Environment { var events = workLifetime.Resolve>(); events.Invoke(e => e.Started(), NullLogger.Instance); - return new ThreadStaticScopeImplementation( - events, - workLifetime, - EnsureThreadStaticContexts(), - _workContextKey); + return new CallContextScopeImplementation(events, workLifetime, _workContextSlot); } - static ConcurrentDictionary EnsureThreadStaticContexts() { - return _threadStaticContexts ?? (_threadStaticContexts = new ConcurrentDictionary()); - } - - class HttpContextScopeImplementation : IWorkContextScope { readonly WorkContext _workContext; readonly Action _disposer; @@ -113,19 +103,23 @@ namespace Orchard.Environment { } } - class ThreadStaticScopeImplementation : IWorkContextScope { + class CallContextScopeImplementation : IWorkContextScope { readonly WorkContext _workContext; readonly Action _disposer; - public ThreadStaticScopeImplementation(IEnumerable events, ILifetimeScope lifetimeScope, ConcurrentDictionary contexts, object workContextKey) { + public CallContextScopeImplementation(IEnumerable events, ILifetimeScope lifetimeScope, string workContextSlot) { + + CallContext.LogicalSetData(workContextSlot, null); + _workContext = lifetimeScope.Resolve(); - contexts.AddOrUpdate(workContextKey, _workContext, (a, b) => _workContext); + var httpContext = lifetimeScope.Resolve(); + _workContext.HttpContext = httpContext; + + CallContext.LogicalSetData(workContextSlot, new ObjectHandle(_workContext)); _disposer = () => { events.Invoke(e => e.Finished(), NullLogger.Instance); - - WorkContext removedContext; - contexts.TryRemove(workContextKey, out removedContext); + CallContext.FreeNamedDataSlot(workContextSlot); lifetimeScope.Dispose(); }; } diff --git a/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs b/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs index 9a7864887..81fb1932f 100644 --- a/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs +++ b/src/Orchard/Mvc/Extensions/HttpContextBaseExtensions.cs @@ -1,4 +1,4 @@ -using System.Web; +using System.Web; namespace Orchard.Mvc.Extensions { public static class HttpContextBaseExtensions { diff --git a/src/Orchard/Mvc/HttpContextAccessor.cs b/src/Orchard/Mvc/HttpContextAccessor.cs index b13d6fe25..0c5e3438a 100644 --- a/src/Orchard/Mvc/HttpContextAccessor.cs +++ b/src/Orchard/Mvc/HttpContextAccessor.cs @@ -1,13 +1,31 @@ -using System; +using System; using System.Web; +using Autofac; namespace Orchard.Mvc { public class HttpContextAccessor : IHttpContextAccessor { + readonly ILifetimeScope _lifetimeScope; private HttpContextBase _httpContext; + private IWorkContextAccessor _wca; + + public HttpContextAccessor(ILifetimeScope lifetimeScope) { + _lifetimeScope = lifetimeScope; + } public HttpContextBase Current() { var httpContext = GetStaticProperty(); - return !IsBackgroundHttpContext(httpContext) ? new HttpContextWrapper(httpContext) : _httpContext; + + if (!IsBackgroundHttpContext(httpContext)) + return new HttpContextWrapper(httpContext); + + if (_httpContext != null) + return _httpContext; + + if (_wca == null && _lifetimeScope.IsRegistered()) + _wca = _lifetimeScope.Resolve(); + + var workContext = _wca != null ? _wca.GetContext(null) : null; + return workContext != null ? workContext.HttpContext : null; } public void Set(HttpContextBase httpContext) { @@ -15,7 +33,7 @@ namespace Orchard.Mvc { } private static bool IsBackgroundHttpContext(HttpContext httpContext) { - return httpContext == null || httpContext.Items.Contains(BackgroundHttpContextFactory.IsBackgroundHttpContextKey); + return httpContext == null || httpContext.Items.Contains(MvcModule.IsBackgroundHttpContextKey); } private static HttpContext GetStaticProperty() { @@ -36,4 +54,4 @@ namespace Orchard.Mvc { return httpContext; } } -} \ No newline at end of file +} diff --git a/src/Orchard/Mvc/MvcModule.cs b/src/Orchard/Mvc/MvcModule.cs index d238e5fb8..5d7a84cf5 100644 --- a/src/Orchard/Mvc/MvcModule.cs +++ b/src/Orchard/Mvc/MvcModule.cs @@ -3,18 +3,20 @@ using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; +using System.IO; using System.Web; using System.Web.Caching; 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; -using Orchard.Exceptions; namespace Orchard.Mvc { public class MvcModule : Module { + public const string IsBackgroundHttpContextKey = "IsBackgroundHttpContext"; protected override void Load(ContainerBuilder moduleBuilder) { moduleBuilder.RegisterType().InstancePerDependency(); @@ -24,29 +26,11 @@ namespace Orchard.Mvc { moduleBuilder.Register(UrlHelperFactory).As().InstancePerDependency(); } - private static bool IsRequestValid() { - if (HttpContext.Current == null) - return false; - - try { - // The "Request" property throws at application startup on IIS integrated pipeline mode. - var req = HttpContext.Current.Request; - } - catch (Exception ex) { - if (ex.IsFatal()) { - throw; - } - - return false; - } - - return true; - } - static HttpContextBase HttpContextBaseFactory(IComponentContext context) { - if (IsRequestValid()) { - return new HttpContextWrapper(HttpContext.Current); - } + + var httpContext = context.Resolve().Current(); + if (httpContext != null) + return httpContext; var siteService = context.Resolve(); @@ -54,17 +38,18 @@ namespace Orchard.Mvc { // so that the RequestContext will have been established when the time comes to actually load the site settings, // 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); - context.Resolve().CreateWorkContextScope(httpContextBase); return httpContextBase; } static RequestContext RequestContextFactory(IComponentContext context) { - var httpContextAccessor = context.Resolve(); - var httpContext = httpContextAccessor.Current(); - if (httpContext != null) { + + var httpContext = HttpContextBaseFactory(context); + + if (!httpContext.IsBackgroundContext()) { var mvcHandler = httpContext.Handler as MvcHandler; if (mvcHandler != null) { @@ -77,8 +62,8 @@ namespace Orchard.Mvc { return hasRequestContext.RequestContext; } } - else { - httpContext = HttpContextBaseFactory(context); + else if (httpContext is HttpContextPlaceholder) { + return ((HttpContextPlaceholder)httpContext).RequestContext; } return new RequestContext(httpContext, new RouteData()); @@ -91,7 +76,9 @@ namespace Orchard.Mvc { /// /// Standin context for background tasks. /// - public class HttpContextPlaceholder : HttpContextBase { + public class HttpContextPlaceholder : HttpContextBase, IDisposable { + private HttpContext _httpContext; + private HttpRequestPlaceholder _request; private readonly Lazy _baseUrl; private readonly IDictionary _items = new Dictionary(); @@ -99,8 +86,54 @@ namespace Orchard.Mvc { _baseUrl = new Lazy(baseUrl); } + public void Dispose() { + _httpContext = null; + if (HttpContext.Current != null) + HttpContext.Current = null; + } + public override HttpRequestBase Request { - get { return new HttpRequestPlaceholder(new Uri(_baseUrl.Value)); } + + // Note: To fully resolve the baseUrl, some factories are needed (HttpContextBase, RequestContext...), + // so, doing this in such a factory creates a circular dependency (see HttpContextBase factory comments). + + // When rendering a view in a background task, an Html Helper can access HttpContext.Current directly, + // so, here we create a fake HttpContext based on the baseUrl, and use it to update HttpContext.Current. + // We cannot do this before in a factory (see note above), anyway, by doing this on each Request access, + // we have a better chance to maintain the HttpContext.Current state even with some asynchronous code. + + get { + if (_httpContext == null) { + var httpContext = new HttpContext(new HttpRequest("", _baseUrl.Value, ""), new HttpResponse(new StringWriter())); + httpContext.Items[IsBackgroundHttpContextKey] = true; + _httpContext = httpContext; + } + + if (HttpContext.Current != _httpContext) + HttpContext.Current = _httpContext; + + if (_request == null) { + _request = new HttpRequestPlaceholder(this, _baseUrl); + } + return _request; + } + } + + internal RequestContext RequestContext { + + // Uses the Request object but without creating an HttpContext which would need to resolve the baseUrl, + // so, can be used by the RequestContext factory without creating a circular dependency (see note above). + + get { + if (_request == null) { + _request = new HttpRequestPlaceholder(this, _baseUrl); + } + return _request.RequestContext; + } + } + + public override HttpSessionStateBase Session { + get { return new HttpSessionStatePlaceholder(); } } public override IHttpHandler Handler { get; set; } @@ -130,6 +163,14 @@ namespace Orchard.Mvc { } } + public class HttpSessionStatePlaceholder : HttpSessionStateBase { + public override object this[string name] { + get { + return null; + } + } + } + public class HttpResponsePlaceholder : HttpResponseBase { public override string ApplyAppPathModifier(string virtualPath) { return virtualPath; @@ -146,10 +187,36 @@ namespace Orchard.Mvc { /// standin context for background tasks. /// public class HttpRequestPlaceholder : HttpRequestBase { - private readonly Uri _uri; + private HttpContextBase _httpContext; + private RequestContext _requestContext; + private readonly Lazy _baseUrl; + private readonly NameValueCollection _queryString = new NameValueCollection(); + private Uri _uri; - public HttpRequestPlaceholder(Uri uri) { - _uri = uri; + public HttpRequestPlaceholder(HttpContextBase httpContext, Lazy baseUrl) { + _httpContext = httpContext; + _baseUrl = baseUrl; + } + + public override RequestContext RequestContext { + get { + if (_requestContext == null) { + _requestContext = new RequestContext(_httpContext, new RouteData()); + } + return _requestContext; + } + } + + public override NameValueCollection QueryString { + get { + return _queryString; + } + } + + public override string RawUrl { + get { + return Url.OriginalString; + } } /// @@ -179,13 +246,16 @@ namespace Orchard.Mvc { public override Uri Url { get { + if (_uri == null) { + _uri = new Uri(_baseUrl.Value); + } return _uri; } } public override NameValueCollection Headers { get { - return new NameValueCollection { { "Host", _uri.Authority } }; + return new NameValueCollection { { "Host", Url.Authority } }; } } @@ -209,16 +279,16 @@ namespace Orchard.Mvc { public override string ApplicationPath { get { - return _uri.LocalPath; + return Url.LocalPath; } } public override NameValueCollection ServerVariables { get { return new NameValueCollection { - { "SERVER_PORT", _uri.Port.ToString(CultureInfo.InvariantCulture) }, - { "HTTP_HOST", _uri.Authority.ToString(CultureInfo.InvariantCulture) }, - + { "SERVER_PORT", Url.Port.ToString(CultureInfo.InvariantCulture) }, + { "HTTP_HOST", Url.Authority.ToString(CultureInfo.InvariantCulture) }, + }; } } @@ -279,4 +349,4 @@ namespace Orchard.Mvc { public override int ScriptTimeout { get; set; } } } -} \ No newline at end of file +}