diff --git a/src/Orchard.Tests/DisplayManagement/Descriptors/ContainerTestBase.cs b/src/Orchard.Tests/ContainerTestBase.cs similarity index 87% rename from src/Orchard.Tests/DisplayManagement/Descriptors/ContainerTestBase.cs rename to src/Orchard.Tests/ContainerTestBase.cs index 48103c1da..8d4f42f0e 100644 --- a/src/Orchard.Tests/DisplayManagement/Descriptors/ContainerTestBase.cs +++ b/src/Orchard.Tests/ContainerTestBase.cs @@ -2,7 +2,7 @@ using NUnit.Framework; using Orchard.Tests.Utility; -namespace Orchard.Tests.DisplayManagement.Descriptors { +namespace Orchard.Tests { public class ContainerTestBase { protected IContainer _container; diff --git a/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs b/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs new file mode 100644 index 000000000..7165afc1e --- /dev/null +++ b/src/Orchard.Tests/Environment/DefaultWorkContextAccessorTests.cs @@ -0,0 +1,182 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Web; +using Autofac; +using NUnit.Framework; +using Orchard.Environment; +using Orchard.Mvc; +using Orchard.Tests.Stubs; +using Orchard.Tests.Utility; + +namespace Orchard.Tests.Environment { + [TestFixture] + public class DefaultWorkContextAccessorTests : ContainerTestBase { + + HttpContextBase _httpContextCurrent; + + public override void Init() { + _httpContextCurrent = null; + base.Init(); + } + + protected override void Register(ContainerBuilder builder) { + builder.RegisterType().As(); + builder.RegisterAutoMocking(); + } + + protected override void Resolve(IContainer container) { + container.Mock() + .Setup(x => x.Current()) + .Returns(() => _httpContextCurrent); + } + + [Test] + public void ScopeIsCreatedAndCanBeRetrievedFromHttpContextBase() { + var accessor = _container.Resolve(); + var httpContext = new StubHttpContext(); + + var workContextScope = accessor.CreateContextScope(httpContext); + Assert.That(workContextScope.WorkContext, Is.Not.Null); + + var workContext = accessor.GetContext(httpContext); + Assert.That(workContext, Is.SameAs(workContextScope.WorkContext)); + } + + [Test] + public void DifferentHttpContextWillHoldDifferentWorkContext() { + var accessor = _container.Resolve(); + var httpContext1 = new StubHttpContext(); + var workContextScope1 = accessor.CreateContextScope(httpContext1); + var workContext1 = accessor.GetContext(httpContext1); + + var httpContext2 = new StubHttpContext(); + var workContextScope2 = accessor.CreateContextScope(httpContext2); + var workContext2 = accessor.GetContext(httpContext2); + + Assert.That(workContext1, Is.Not.Null); + Assert.That(workContext1, Is.SameAs(workContextScope1.WorkContext)); + Assert.That(workContext2, Is.Not.Null); + Assert.That(workContext2, Is.SameAs(workContextScope2.WorkContext)); + Assert.That(workContext1, Is.Not.SameAs(workContext2)); + } + + [Test] + public void ContextIsNullAfterDisposingScope() { + var accessor = _container.Resolve(); + var httpContext = new StubHttpContext(); + + Assert.That(accessor.GetContext(httpContext), Is.Null); + + var scope = accessor.CreateContextScope(httpContext); + Assert.That(accessor.GetContext(httpContext), Is.Not.Null); + + scope.Dispose(); + Assert.That(accessor.GetContext(httpContext), Is.Null); + } + + [Test] + public void DifferentChildScopesWillNotCollideInTheSameHttpContext() { + var shell1 = _container.BeginLifetimeScope(); + var accessor1 = shell1.Resolve(); + + var shell2 = _container.BeginLifetimeScope(); + var accessor2 = shell2.Resolve(); + + var httpContext = new StubHttpContext(); + + Assert.That(accessor1.GetContext(httpContext), Is.Null); + Assert.That(accessor2.GetContext(httpContext), Is.Null); + + var scope1 = accessor1.CreateContextScope(httpContext); + Assert.That(accessor1.GetContext(httpContext), Is.Not.Null); + Assert.That(accessor2.GetContext(httpContext), Is.Null); + + var scope2 = accessor2.CreateContextScope(httpContext); + Assert.That(accessor1.GetContext(httpContext), Is.Not.Null); + Assert.That(accessor2.GetContext(httpContext), Is.Not.Null); + + scope1.Dispose(); + Assert.That(accessor1.GetContext(httpContext), Is.Null); + Assert.That(accessor2.GetContext(httpContext), Is.Not.Null); + + scope2.Dispose(); + Assert.That(accessor1.GetContext(httpContext), Is.Null); + Assert.That(accessor2.GetContext(httpContext), Is.Null); + } + + + [Test] + public void FunctionsByDefaultAgainstAmbientHttpContext() { + var accessor = _container.Resolve(); + + var explicitHttpContext = new StubHttpContext(); + var ambientHttpContext = new StubHttpContext(); + + _httpContextCurrent = ambientHttpContext; + + Assert.That(accessor.GetContext(), Is.Null); + Assert.That(accessor.GetContext(ambientHttpContext), Is.Null); + Assert.That(accessor.GetContext(explicitHttpContext), Is.Null); + + var scope = accessor.CreateContextScope(); + Assert.That(accessor.GetContext(), Is.Not.Null); + Assert.That(accessor.GetContext(ambientHttpContext), Is.Not.Null); + Assert.That(accessor.GetContext(explicitHttpContext), Is.Null); + Assert.That(accessor.GetContext(), Is.SameAs(accessor.GetContext(ambientHttpContext))); + + _httpContextCurrent = explicitHttpContext; + Assert.That(accessor.GetContext(), Is.Null); + + _httpContextCurrent = ambientHttpContext; + Assert.That(accessor.GetContext(), Is.Not.Null); + + scope.Dispose(); + Assert.That(accessor.GetContext(), Is.Null); + } + + + [Test] + public void StillFunctionsWithoutAmbientHttpContext() { + var accessor = _container.Resolve(); + + Assert.That(accessor.GetContext(), Is.Null); + + var scope = accessor.CreateContextScope(); + Assert.That(accessor.GetContext(), Is.Not.Null); + + scope.Dispose(); + Assert.That(accessor.GetContext(), Is.Null); + } + + [Test] + public void DifferentChildScopesWillNotCollideWithoutAmbientHttpContext() { + var shell1 = _container.BeginLifetimeScope(); + var accessor1 = shell1.Resolve(); + + var shell2 = _container.BeginLifetimeScope(); + var accessor2 = shell2.Resolve(); + + Assert.That(accessor1.GetContext(), Is.Null); + Assert.That(accessor2.GetContext(), Is.Null); + + var scope1 = accessor1.CreateContextScope(); + Assert.That(accessor1.GetContext(), Is.Not.Null); + Assert.That(accessor2.GetContext(), Is.Null); + + var scope2 = accessor2.CreateContextScope(); + Assert.That(accessor1.GetContext(), Is.Not.Null); + Assert.That(accessor2.GetContext(), Is.Not.Null); + + scope1.Dispose(); + Assert.That(accessor1.GetContext(), Is.Null); + Assert.That(accessor2.GetContext(), Is.Not.Null); + + scope2.Dispose(); + Assert.That(accessor1.GetContext(), Is.Null); + Assert.That(accessor2.GetContext(), Is.Null); + } + + } +} diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index a5776bbba..7c1875fba 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -194,7 +194,7 @@ - + @@ -204,6 +204,7 @@ + diff --git a/src/Orchard/Environment/DefaultWorkContextAccessor.cs b/src/Orchard/Environment/DefaultWorkContextAccessor.cs new file mode 100644 index 000000000..6c0c13e7d --- /dev/null +++ b/src/Orchard/Environment/DefaultWorkContextAccessor.cs @@ -0,0 +1,146 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Web; +using Autofac; +using Orchard.Mvc; + +namespace Orchard.Environment { + public class DefaultWorkContextAccessor : IWorkContextAccessor { + readonly ILifetimeScope _lifetimeScope; + + readonly IHttpContextAccessor _httpContextAccessor; + // a different symbolic key is used for each tenant. + // this guarantees the correct accessor is being resolved. + readonly object _workContextKey = new object(); + + [ThreadStatic] + static readonly ConcurrentDictionary _threadStaticContexts = new ConcurrentDictionary(); + + public DefaultWorkContextAccessor( + IHttpContextAccessor httpContextAccessor, + ILifetimeScope lifetimeScope) { + _httpContextAccessor = httpContextAccessor; + _lifetimeScope = lifetimeScope; + } + + + + public WorkContext GetContext(HttpContextBase httpContext) { + return httpContext.Items[_workContextKey] as WorkContext; + } + + public WorkContext GetContext() { + var httpContext = _httpContextAccessor.Current(); + if (httpContext != null) + return GetContext(httpContext); + + WorkContext workContext; + return _threadStaticContexts.TryGetValue(_workContextKey, out workContext) ? workContext : null; + } + + public IWorkContextScope CreateContextScope(HttpContextBase httpContext) { + + var workLifetime = SpawnWorkLifetime(builder => { + builder.Register(ctx => httpContext) + .As(); + + builder.Register(ctx => new WorkContextImplementation(ctx)) + .As() + .InstancePerMatchingLifetimeScope("work"); + }); + return new HttpContextScopeImplementation( + workLifetime, + httpContext, + _workContextKey); + } + + + public IWorkContextScope CreateContextScope() { + var httpContext = _httpContextAccessor.Current(); + if (httpContext != null) + return CreateContextScope(httpContext); + + var workLifetime = SpawnWorkLifetime(builder => { + builder.Register(ctx => httpContext) + .As(); + + builder.Register(ctx => new WorkContextImplementation(ctx)) + .As() + .InstancePerMatchingLifetimeScope("work"); + }); + return new ThreadStaticScopeImplementation( + workLifetime, + _threadStaticContexts, + _workContextKey); + } + + private ILifetimeScope SpawnWorkLifetime(Action configurationAction) { + return _lifetimeScope.BeginLifetimeScope("work", configurationAction); + } + + + class WorkContextImplementation : WorkContext { + private readonly IComponentContext _componentContext; + + public WorkContextImplementation(IComponentContext componentContext) { + _componentContext = componentContext; + } + + public override T Service() { + throw new NotImplementedException(); + } + + public override T State() { + throw new NotImplementedException(); + } + } + + class HttpContextScopeImplementation : IWorkContextScope { + readonly WorkContext _workContext; + readonly Action _disposer; + + public HttpContextScopeImplementation(ILifetimeScope lifetimeScope, HttpContextBase httpContext, object workContextKey) { + _workContext = lifetimeScope.Resolve(); + httpContext.Items[workContextKey] = _workContext; + _disposer = () => { + httpContext.Items.Remove(workContextKey); + lifetimeScope.Dispose(); + }; + } + + void IDisposable.Dispose() { + _disposer(); + } + + public WorkContext WorkContext { + get { return _workContext; } + } + } + + class ThreadStaticScopeImplementation : IWorkContextScope { + readonly WorkContext _workContext; + readonly Action _disposer; + + public ThreadStaticScopeImplementation(ILifetimeScope lifetimeScope, ConcurrentDictionary contexts, object workContextKey) { + _workContext = lifetimeScope.Resolve(); + contexts.AddOrUpdate(workContextKey, _workContext, (a, b) => _workContext); + _disposer = () => { + WorkContext removedContext; + contexts.TryRemove(workContextKey, out removedContext); + lifetimeScope.Dispose(); + }; + } + + void IDisposable.Dispose() { + _disposer(); + } + + public WorkContext WorkContext { + get { return _workContext; } + } + } + } + + +} diff --git a/src/Orchard/IWorkContextAccessor.cs b/src/Orchard/IWorkContextAccessor.cs index 1f29e844c..f4e1bc166 100644 --- a/src/Orchard/IWorkContextAccessor.cs +++ b/src/Orchard/IWorkContextAccessor.cs @@ -1,8 +1,16 @@ -using System.Web; +using System; +using System.Web; namespace Orchard { - public interface IWorkContextAccessor : IDependency { - WorkContext GetContext(); + public interface IWorkContextAccessor : ISingletonDependency { WorkContext GetContext(HttpContextBase httpContext); + IWorkContextScope CreateContextScope(HttpContextBase httpContext); + + WorkContext GetContext(); + IWorkContextScope CreateContextScope(); } -} \ No newline at end of file + + public interface IWorkContextScope : IDisposable { + WorkContext WorkContext { get; } + } +} diff --git a/src/Orchard/Mvc/IHttpContextAccessor.cs b/src/Orchard/Mvc/IHttpContextAccessor.cs new file mode 100644 index 000000000..d456bce2e --- /dev/null +++ b/src/Orchard/Mvc/IHttpContextAccessor.cs @@ -0,0 +1,7 @@ +using System.Web; + +namespace Orchard.Mvc { + public interface IHttpContextAccessor { + HttpContextBase Current(); + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 424da0136..d2020d883 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -402,10 +402,12 @@ + + diff --git a/src/Orchard/WorkContext.cs b/src/Orchard/WorkContext.cs index 4e407d27e..466aaa637 100644 --- a/src/Orchard/WorkContext.cs +++ b/src/Orchard/WorkContext.cs @@ -1,4 +1,6 @@ -using Orchard.Security; +using System; +using Autofac; +using Orchard.Security; using Orchard.Settings; using Orchard.UI;