From 74e8390bdaf662f84d07b92bb843a8dae7697d00 Mon Sep 17 00:00:00 2001 From: Stephan Date: Sat, 11 Feb 2017 09:54:54 +0100 Subject: [PATCH] Scope - enable http vs call context switching --- src/Umbraco.Core/Scoping/IScopeInternal.cs | 1 + src/Umbraco.Core/Scoping/IScopeProvider.cs | 6 +- src/Umbraco.Core/Scoping/NoScope.cs | 6 +- src/Umbraco.Core/Scoping/Scope.cs | 45 ++++-- src/Umbraco.Core/Scoping/ScopeProvider.cs | 147 ++++++++++++------ .../TestHelpers/BaseDatabaseFactoryTest.cs | 2 +- 6 files changed, 141 insertions(+), 66 deletions(-) diff --git a/src/Umbraco.Core/Scoping/IScopeInternal.cs b/src/Umbraco.Core/Scoping/IScopeInternal.cs index 86276fc7f9..6b2204bfbe 100644 --- a/src/Umbraco.Core/Scoping/IScopeInternal.cs +++ b/src/Umbraco.Core/Scoping/IScopeInternal.cs @@ -7,6 +7,7 @@ namespace Umbraco.Core.Scoping internal interface IScopeInternal : IScope { IScopeInternal ParentScope { get; } + bool CallContext { get; } EventsDispatchMode DispatchMode { get; } IsolationLevel IsolationLevel { get; } UmbracoDatabase DatabaseOrNull { get; } diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 6833eac699..b47beb2475 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -24,7 +24,8 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null); + bool? scopeFileSystems = null, + bool callContext = false); /// /// Creates a detached scope. @@ -44,10 +45,11 @@ namespace Umbraco.Core.Scoping /// Attaches a scope. /// /// The scope to attach. + /// A value indicating whether to force usage of call context. /// /// Only a scope created by can be attached. /// - void AttachScope(IScope scope); + void AttachScope(IScope scope, bool callContext = false); /// /// Detaches a scope. diff --git a/src/Umbraco.Core/Scoping/NoScope.cs b/src/Umbraco.Core/Scoping/NoScope.cs index b08f10ad0e..b77fb715d2 100644 --- a/src/Umbraco.Core/Scoping/NoScope.cs +++ b/src/Umbraco.Core/Scoping/NoScope.cs @@ -29,6 +29,9 @@ namespace Umbraco.Core.Scoping public Guid InstanceId { get { return _instanceId; } } #endif + /// + public bool CallContext { get { return false; } } + /// public RepositoryCacheMode RepositoryCacheMode { @@ -100,8 +103,7 @@ namespace Umbraco.Core.Scoping if (_database != null) _database.Dispose(); - _scopeProvider.AmbientScope = null; - _scopeProvider.AmbientContext = null; + _scopeProvider.SetAmbient(null); _disposed = true; GC.SuppressFinalize(this); diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 824ccd6d80..bcb62cad90 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -19,6 +19,7 @@ namespace Umbraco.Core.Scoping private readonly EventsDispatchMode _dispatchMode; private readonly bool? _scopeFileSystem; private readonly ScopeContext _scopeContext; + private bool _callContext; private bool _disposed; private bool? _completed; @@ -36,7 +37,8 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) + bool? scopeFileSystems = null, + bool callContext = false) { _scopeProvider = scopeProvider; _scopeContext = scopeContext; @@ -44,6 +46,7 @@ namespace Umbraco.Core.Scoping _repositoryCacheMode = repositoryCacheMode; _dispatchMode = dispatchMode; _scopeFileSystem = scopeFileSystems; + _callContext = callContext; Detachable = detachable; #if DEBUG_SCOPES @@ -98,20 +101,20 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) - : this(scopeProvider, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) - { - } + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems, callContext) + { } // initializes a new scope in a nested scopes chain, with its parent public Scope(ScopeProvider scopeProvider, Scope parent, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) - : this(scopeProvider, parent, null, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) - { - } + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, parent, null, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems, callContext) + { } // initializes a new scope, replacing a NoScope instance public Scope(ScopeProvider scopeProvider, NoScope noScope, @@ -119,8 +122,9 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) - : this(scopeProvider, null, scopeContext, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) + bool? scopeFileSystems = null, + bool callContext = false) + : this(scopeProvider, null, scopeContext, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems, callContext) { // steal everything from NoScope _database = noScope.DatabaseOrNull; @@ -135,6 +139,18 @@ namespace Umbraco.Core.Scoping public Guid InstanceId { get { return _instanceId; } } #endif + // a value indicating whether to force call-context + public bool CallContext + { + get + { + if (_callContext) return true; + if (ParentScope != null) return ParentScope.CallContext; + return false; + } + set { _callContext = value; } + } + public bool ScopedFileSystems { get @@ -331,7 +347,7 @@ namespace Umbraco.Core.Scoping #endif var parent = ParentScope; - _scopeProvider.AmbientScope = parent; + _scopeProvider.SetAmbientScope(parent); if (parent != null) parent.ChildCompleted(_completed); @@ -418,7 +434,7 @@ namespace Umbraco.Core.Scoping } finally { - _scopeProvider.AmbientContext = null; + _scopeProvider.SetAmbient(null); } } }, () => @@ -426,8 +442,7 @@ namespace Umbraco.Core.Scoping if (Detachable) { // get out of the way, restore original - _scopeProvider.AmbientScope = OrigScope; - _scopeProvider.AmbientContext = OrigContext; + _scopeProvider.SetAmbient(OrigScope, OrigContext); } }); } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index 2f00e5b862..327e9fad8f 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -1,7 +1,5 @@ using System; -using System.Collections.Generic; using System.Data; -using System.Linq; using System.Runtime.Remoting.Messaging; using System.Web; using Umbraco.Core.Events; @@ -24,17 +22,17 @@ namespace Umbraco.Core.Scoping SafeCallContext.Register( () => { - var scope = StaticAmbientScope; - var context = StaticAmbientContext; - StaticAmbientScope = null; - StaticAmbientContext = null; + var scope = AmbientContextScope; + var context = AmbientContextContext; + AmbientContextScope = null; + AmbientContextContext = null; return Tuple.Create(scope, context); }, o => { // cannot re-attached over leaked scope/context // except of course over NoScope (which leaks) - var ambientScope = StaticAmbientScope; + var ambientScope = AmbientContextScope; if (ambientScope != null) { var ambientNoScope = ambientScope as NoScope; @@ -44,11 +42,11 @@ namespace Umbraco.Core.Scoping // this should rollback any pending transaction ambientNoScope.Dispose(); } - if (StaticAmbientContext != null) throw new Exception("Found leaked context when restoring call context."); + if (AmbientContextContext != null) throw new Exception("Found leaked context when restoring call context."); var t = (Tuple)o; - StaticAmbientScope = t.Item1; - StaticAmbientContext = t.Item2; + AmbientContextScope = t.Item1; + AmbientContextContext = t.Item2; }); } @@ -58,7 +56,7 @@ namespace Umbraco.Core.Scoping internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext"; - private static ScopeContext CallContextContextValue + private static ScopeContext CallContextContext { get { return (ScopeContext)CallContext.LogicalGetData(ContextItemKey); } set @@ -68,7 +66,7 @@ namespace Umbraco.Core.Scoping } } - private static ScopeContext HttpContextContextValue + private static ScopeContext HttpContextContext { get { return (ScopeContext)HttpContext.Current.Items[ContextItemKey]; } set @@ -80,23 +78,27 @@ namespace Umbraco.Core.Scoping } } - private static ScopeContext StaticAmbientContext + private static ScopeContext AmbientContextContext { - get { return HttpContext.Current == null ? CallContextContextValue : HttpContextContextValue; } + get + { + // try http context, fallback onto call context + var value = HttpContext.Current == null ? null : HttpContextContext; + return value ?? CallContextContext; + } set { - if (HttpContext.Current == null) - CallContextContextValue = value; - else - HttpContextContextValue = value; + // clear both + if (HttpContext.Current != null) + HttpContextContext = value; + CallContextContext = value; } } /// public ScopeContext AmbientContext { - get { return StaticAmbientContext; } - set { StaticAmbientContext = value; } + get { return AmbientContextContext; } } #endregion @@ -109,7 +111,7 @@ namespace Umbraco.Core.Scoping // only 1 instance which can be disposed and disposed again private static readonly ScopeReference StaticScopeReference = new ScopeReference(new ScopeProvider(null)); - private static IScopeInternal CallContextValue + private static IScopeInternal CallContextScope { get { return (IScopeInternal) CallContext.LogicalGetData(ScopeItemKey); } set @@ -125,7 +127,7 @@ namespace Umbraco.Core.Scoping } } - private static IScopeInternal HttpContextValue + private static IScopeInternal HttpContextScope { get { return (IScopeInternal) HttpContext.Current.Items[ScopeItemKey]; } set @@ -150,33 +152,79 @@ namespace Umbraco.Core.Scoping } } - private static IScopeInternal StaticAmbientScope + private static IScopeInternal AmbientContextScope { - get { return HttpContext.Current == null ? CallContextValue : HttpContextValue; } + get + { + // try http context, fallback onto call context + var value = HttpContext.Current == null ? null : HttpContextScope; + return value ?? CallContextScope; + } set { - if (HttpContext.Current == null) - CallContextValue = value; - else - HttpContextValue = value; + // clear both + if (HttpContext.Current != null) + HttpContextScope = value; + CallContextScope = value; } } /// public IScopeInternal AmbientScope { - get { return StaticAmbientScope; } - set { StaticAmbientScope = value; } + get { return AmbientContextScope; } + } + + public void SetAmbientScope(IScopeInternal value) + { + if (value != null && value.CallContext) + { + if (HttpContext.Current != null) + HttpContextScope = null; // clear http context + CallContextScope = value; // set call context + } + else + { + CallContextScope = null; // clear call context + AmbientContextScope = value; // set appropriate context (maybe null) + } } /// public IScopeInternal GetAmbientOrNoScope() { - return AmbientScope ?? (AmbientScope = new NoScope(this)); + return AmbientScope ?? (AmbientContextScope = new NoScope(this)); } #endregion + public void SetAmbient(IScopeInternal scope, ScopeContext context = null) + { + if (scope != null && scope.CallContext) + { + // clear http context + if (HttpContext.Current != null) + { + HttpContextScope = null; + HttpContextContext = null; + } + + // set call context + CallContextScope = scope; + CallContextContext = context; + } + else + { + // clear call context + CallContextScope = null; + CallContextContext = null; + + // set appropriate context (maybe null) + AmbientContextScope = scope; + AmbientContextContext = context; + } + } + /// public IScope CreateDetachedScope( IsolationLevel isolationLevel = IsolationLevel.Unspecified, @@ -188,7 +236,7 @@ namespace Umbraco.Core.Scoping } /// - public void AttachScope(IScope other) + public void AttachScope(IScope other, bool callContext = false) { var otherScope = other as Scope; if (otherScope == null) @@ -199,8 +247,9 @@ namespace Umbraco.Core.Scoping otherScope.OrigScope = AmbientScope; otherScope.OrigContext = AmbientContext; - AmbientScope = otherScope; - AmbientContext = otherScope.Context; + + otherScope.CallContext = callContext; + SetAmbient(otherScope, otherScope.Context); } /// @@ -221,8 +270,7 @@ namespace Umbraco.Core.Scoping if (scope.Detachable == false) throw new InvalidOperationException("Ambient scope is not detachable."); - AmbientScope = scope.OrigScope; - AmbientContext = scope.OrigContext; + SetAmbient(scope.OrigScope, scope.OrigContext); scope.OrigScope = null; scope.OrigContext = null; return scope; @@ -233,15 +281,18 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool? scopeFileSystems = null) + bool? scopeFileSystems = null, + bool callContext = false) { var ambient = AmbientScope; if (ambient == null) { - var context = AmbientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, false, context, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); - if (AmbientContext == null) AmbientContext = context; // assign only if scope creation did not throw! - return AmbientScope = scope; + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, false, newContext, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; } // replace noScope with a real one @@ -255,16 +306,20 @@ namespace Umbraco.Core.Scoping var database = noScope.DatabaseOrNull; if (database != null && database.InTransaction) throw new Exception("NoScope is in a transaction."); - var context = AmbientContext == null ? new ScopeContext() : null; - var scope = new Scope(this, noScope, context, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); - if (AmbientContext == null) AmbientContext = context; // assign only if scope creation did not throw! - return AmbientScope = scope; + var ambientContext = AmbientContext; + var newContext = ambientContext == null ? new ScopeContext() : null; + var scope = new Scope(this, noScope, newContext, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems, callContext); + // assign only if scope creation did not throw! + SetAmbient(scope, newContext ?? ambientContext); + return scope; } var ambientScope = ambient as Scope; if (ambientScope == null) throw new Exception("Ambient scope is not a Scope instance."); - return AmbientScope = new Scope(this, ambientScope, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); + var nested = new Scope(this, ambientScope, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems, callContext); + SetAmbient(nested, AmbientContext); + return nested; } /// diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index 70f48b12fb..43e1ae679b 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -76,7 +76,7 @@ namespace Umbraco.Tests.TestHelpers var scopeProvider = new ScopeProvider(null); if (scopeProvider.AmbientScope != null) scopeProvider.AmbientScope.Dispose(); - scopeProvider.AmbientScope = null; + scopeProvider.SetAmbientScope(null); base.Initialize();