Merge pull request #5966 from jtkech/patch-2

Experimental work on HttpContext, WorkContext and async code in background tasks
This commit is contained in:
Sébastien Ros
2015-12-17 12:28:44 -08:00
7 changed files with 156 additions and 71 deletions

View File

@@ -19,6 +19,7 @@ namespace Orchard.Tests.Environment {
}
protected override void Register(ContainerBuilder builder) {
builder.RegisterModule(new MvcModule());
builder.RegisterModule(new WorkContextModule());
builder.RegisterType<WorkContextAccessor>().As<IWorkContextAccessor>();
builder.RegisterAutoMocking();

View File

@@ -21,6 +21,7 @@ namespace Orchard.Tests.Environment.ShellBuilders {
public void Init() {
var builder = new ContainerBuilder();
builder.RegisterType<ShellContextFactory>().As<IShellContextFactory>();
builder.RegisterModule(new MvcModule());
builder.RegisterModule(new WorkContextModule());
builder.RegisterType<WorkContextAccessor>().As<IWorkContextAccessor>();
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"));
}
}
}
}

View File

@@ -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<WorkContextAccessor>().As<IWorkContextAccessor>();
builder.RegisterType<SweepGenerator>();

View File

@@ -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<object, WorkContext> _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<IEnumerable<IWorkContextEvents>>();
events.Invoke(e => e.Started(), NullLogger.Instance);
return new ThreadStaticScopeImplementation(
events,
workLifetime,
EnsureThreadStaticContexts(),
_workContextKey);
return new CallContextScopeImplementation(events, workLifetime, _workContextSlot);
}
static ConcurrentDictionary<object, WorkContext> EnsureThreadStaticContexts() {
return _threadStaticContexts ?? (_threadStaticContexts = new ConcurrentDictionary<object, WorkContext>());
}
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<IWorkContextEvents> events, ILifetimeScope lifetimeScope, ConcurrentDictionary<object, WorkContext> contexts, object workContextKey) {
public CallContextScopeImplementation(IEnumerable<IWorkContextEvents> events, ILifetimeScope lifetimeScope, string workContextSlot) {
CallContext.LogicalSetData(workContextSlot, null);
_workContext = lifetimeScope.Resolve<WorkContext>();
contexts.AddOrUpdate(workContextKey, _workContext, (a, b) => _workContext);
var httpContext = lifetimeScope.Resolve<HttpContextBase>();
_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();
};
}

View File

@@ -1,4 +1,4 @@
using System.Web;
using System.Web;
namespace Orchard.Mvc.Extensions {
public static class HttpContextBaseExtensions {

View File

@@ -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<IWorkContextAccessor>())
_wca = _lifetimeScope.Resolve<IWorkContextAccessor>();
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;
}
}
}
}

View File

@@ -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<ShellRoute>().InstancePerDependency();
@@ -24,29 +26,11 @@ namespace Orchard.Mvc {
moduleBuilder.Register(UrlHelperFactory).As<UrlHelper>().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<IHttpContextAccessor>().Current();
if (httpContext != null)
return httpContext;
var siteService = context.Resolve<ISiteService>();
@@ -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<string>(() => siteService.GetSiteSettings().BaseUrl);
var httpContextBase = new HttpContextPlaceholder(baseUrl);
context.Resolve<IWorkContextAccessor>().CreateWorkContextScope(httpContextBase);
return httpContextBase;
}
static RequestContext RequestContextFactory(IComponentContext context) {
var httpContextAccessor = context.Resolve<IHttpContextAccessor>();
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 {
/// <summary>
/// Standin context for background tasks.
/// </summary>
public class HttpContextPlaceholder : HttpContextBase {
public class HttpContextPlaceholder : HttpContextBase, IDisposable {
private HttpContext _httpContext;
private HttpRequestPlaceholder _request;
private readonly Lazy<string> _baseUrl;
private readonly IDictionary _items = new Dictionary<object, object>();
@@ -99,8 +86,54 @@ namespace Orchard.Mvc {
_baseUrl = new Lazy<string>(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.
/// </summary>
public class HttpRequestPlaceholder : HttpRequestBase {
private readonly Uri _uri;
private HttpContextBase _httpContext;
private RequestContext _requestContext;
private readonly Lazy<string> _baseUrl;
private readonly NameValueCollection _queryString = new NameValueCollection();
private Uri _uri;
public HttpRequestPlaceholder(Uri uri) {
_uri = uri;
public HttpRequestPlaceholder(HttpContextBase httpContext, Lazy<string> 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;
}
}
/// <summary>
@@ -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; }
}
}
}
}