From 820069d5d5cd3d0da0c3c1829ff428113e94de6c Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 3 Feb 2017 20:01:43 +0100 Subject: [PATCH] U4-9322 - filesystems & cleanup --- src/Umbraco.Core/ApplicationContext.cs | 2 +- .../IO/FileSystemProviderManager.cs | 57 +++--- src/Umbraco.Core/IO/ShadowFileSystems.cs | 73 +++++++ src/Umbraco.Core/IO/ShadowFileSystemsScope.cs | 114 ----------- src/Umbraco.Core/IO/ShadowWrapper.cs | 14 +- src/Umbraco.Core/Scoping/IScope.cs | 1 - src/Umbraco.Core/Scoping/IScopeInternal.cs | 18 ++ src/Umbraco.Core/Scoping/IScopeProvider.cs | 4 +- .../Scoping/IScopeProviderInternal.cs | 4 +- src/Umbraco.Core/Scoping/NoScope.cs | 10 +- src/Umbraco.Core/Scoping/Scope.cs | 187 ++++++++++++++---- src/Umbraco.Core/Scoping/ScopeProvider.cs | 58 +++--- src/Umbraco.Core/Scoping/ScopeReference.cs | 5 +- src/Umbraco.Core/Umbraco.Core.csproj | 3 +- src/Umbraco.Tests/IO/ShadowFileSystemTests.cs | 104 +++++++--- .../Scoping/ScopeFileSystemsTests.cs | 157 +++++++++++++++ src/Umbraco.Tests/Scoping/ScopeTests.cs | 103 ++++++++++ src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + 18 files changed, 664 insertions(+), 251 deletions(-) create mode 100644 src/Umbraco.Core/IO/ShadowFileSystems.cs delete mode 100644 src/Umbraco.Core/IO/ShadowFileSystemsScope.cs create mode 100644 src/Umbraco.Core/Scoping/IScopeInternal.cs create mode 100644 src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs diff --git a/src/Umbraco.Core/ApplicationContext.cs b/src/Umbraco.Core/ApplicationContext.cs index 07d4508279..5b2caf409f 100644 --- a/src/Umbraco.Core/ApplicationContext.cs +++ b/src/Umbraco.Core/ApplicationContext.cs @@ -164,7 +164,7 @@ namespace Umbraco.Core public static ApplicationContext Current { get; internal set; } // fixme - internal IScopeProvider ScopeProvider { get { return DatabaseContext.ScopeProvider; } } + internal IScopeProvider ScopeProvider { get { return _databaseContext == null ? null : _databaseContext.ScopeProvider; } } /// /// Returns the application wide cache accessor diff --git a/src/Umbraco.Core/IO/FileSystemProviderManager.cs b/src/Umbraco.Core/IO/FileSystemProviderManager.cs index d807832bcb..5f0e014012 100644 --- a/src/Umbraco.Core/IO/FileSystemProviderManager.cs +++ b/src/Umbraco.Core/IO/FileSystemProviderManager.cs @@ -5,9 +5,10 @@ using System.Configuration; using System.Linq; using System.Reflection; using Umbraco.Core.Configuration; +using Umbraco.Core.Scoping; namespace Umbraco.Core.IO -{ +{ public class FileSystemProviderManager { private readonly FileSystemProvidersSection _config; @@ -40,6 +41,12 @@ namespace Umbraco.Core.IO get { return Instance; } } + private IScopeProviderInternal ScopeProvider + { + // fixme - 'course this is bad, but enough for now + get { return ApplicationContext.Current == null ? null : ApplicationContext.Current.ScopeProvider as IScopeProviderInternal; } + } + internal FileSystemProviderManager() { _config = (FileSystemProvidersSection) ConfigurationManager.GetSection("umbracoConfiguration/FileSystemProviders"); @@ -52,13 +59,13 @@ namespace Umbraco.Core.IO _masterPagesFileSystem = new PhysicalFileSystem(SystemDirectories.Masterpages); _mvcViewsFileSystem = new PhysicalFileSystem(SystemDirectories.MvcViews); - _macroPartialFileSystem = _macroPartialFileSystemWrapper = new ShadowWrapper(_macroPartialFileSystem, "Views/MacroPartials"); - _partialViewsFileSystem = _partialViewsFileSystemWrapper = new ShadowWrapper(_partialViewsFileSystem, "Views/Partials"); - _stylesheetsFileSystem = _stylesheetsFileSystemWrapper = new ShadowWrapper(_stylesheetsFileSystem, "css"); - _scriptsFileSystem = _scriptsFileSystemWrapper = new ShadowWrapper(_scriptsFileSystem, "scripts"); - _xsltFileSystem = _xsltFileSystemWrapper = new ShadowWrapper(_xsltFileSystem, "xslt"); - _masterPagesFileSystem = _masterPagesFileSystemWrapper = new ShadowWrapper(_masterPagesFileSystem, "masterpages"); - _mvcViewsFileSystem = _mvcViewsFileSystemWrapper = new ShadowWrapper(_mvcViewsFileSystem, "Views"); + _macroPartialFileSystem = _macroPartialFileSystemWrapper = new ShadowWrapper(_macroPartialFileSystem, "Views/MacroPartials", ScopeProvider); + _partialViewsFileSystem = _partialViewsFileSystemWrapper = new ShadowWrapper(_partialViewsFileSystem, "Views/Partials", ScopeProvider); + _stylesheetsFileSystem = _stylesheetsFileSystemWrapper = new ShadowWrapper(_stylesheetsFileSystem, "css", ScopeProvider); + _scriptsFileSystem = _scriptsFileSystemWrapper = new ShadowWrapper(_scriptsFileSystem, "scripts", ScopeProvider); + _xsltFileSystem = _xsltFileSystemWrapper = new ShadowWrapper(_xsltFileSystem, "xslt", ScopeProvider); + _masterPagesFileSystem = _masterPagesFileSystemWrapper = new ShadowWrapper(_masterPagesFileSystem, "masterpages", ScopeProvider); + _mvcViewsFileSystem = _mvcViewsFileSystemWrapper = new ShadowWrapper(_mvcViewsFileSystem, "Views", ScopeProvider); // filesystems obtained from GetFileSystemProvider are already wrapped and do not need to be wrapped again MediaFileSystem = GetFileSystemProvider(); @@ -92,7 +99,7 @@ namespace Umbraco.Core.IO } private readonly ConcurrentDictionary _providerLookup = new ConcurrentDictionary(); - private readonly ConcurrentDictionary _aliases = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _aliases = new ConcurrentDictionary(); /// /// Gets an underlying (non-typed) filesystem supporting a strongly-typed filesystem. @@ -121,7 +128,7 @@ namespace Umbraco.Core.IO // find a ctor matching the config parameters var paramCount = providerConfig.Parameters != null ? providerConfig.Parameters.Count : 0; - var constructor = providerType.GetConstructors().SingleOrDefault(x + var constructor = providerType.GetConstructors().SingleOrDefault(x => x.GetParameters().Length == paramCount && x.GetParameters().All(y => providerConfig.Parameters.AllKeys.Contains(y.Name))); if (constructor == null) throw new InvalidOperationException(string.Format("Type {0} has no ctor matching the {1} configuration parameter(s).", providerType.FullName, paramCount)); @@ -129,7 +136,7 @@ namespace Umbraco.Core.IO var parameters = new object[paramCount]; if (providerConfig.Parameters != null) // keeps ReSharper happy for (var i = 0; i < paramCount; i++) - parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; + parameters[i] = providerConfig.Parameters[providerConfig.Parameters.AllKeys[i]].Value; return new ProviderConstructionInfo { @@ -159,7 +166,7 @@ namespace Umbraco.Core.IO var alias = _aliases.GetOrAdd(typeof (TFileSystem), fsType => { // validate the ctor - var constructor = fsType.GetConstructors().SingleOrDefault(x + var constructor = fsType.GetConstructors().SingleOrDefault(x => x.GetParameters().Length == 1 && TypeHelper.IsTypeAssignableFrom(x.GetParameters().Single().ParameterType)); if (constructor == null) throw new InvalidOperationException("Type " + fsType.FullName + " must inherit from FileSystemWrapper and have a constructor that accepts one parameter of type " + typeof(IFileSystem).FullName + "."); @@ -176,8 +183,8 @@ namespace Umbraco.Core.IO // so we are double-wrapping here // could be optimized by having FileSystemWrapper inherit from ShadowWrapper, maybe var innerFs = GetUnderlyingFileSystemProvider(alias); - var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias); - var fs = (TFileSystem) Activator.CreateInstance(typeof (TFileSystem), innerFs); + var shadowWrapper = new ShadowWrapper(innerFs, "typed/" + alias, ScopeProvider); + var fs = (TFileSystem) Activator.CreateInstance(typeof (TFileSystem), shadowWrapper); _wrappers.Add(shadowWrapper); // keeping a weak reference to the wrapper return fs; } @@ -186,25 +193,7 @@ namespace Umbraco.Core.IO #region Shadow - // note - // shadowing is thread-safe, but entering and exiting shadow mode is not, and there is only one - // global shadow for the entire application, so great care should be taken to ensure that the - // application is *not* doing anything else when using a shadow. - // shadow applies to well-known filesystems *only* - at the moment, any other filesystem that would - // be created directly (via ctor) or via GetFileSystemProvider is *not* shadowed. - - // shadow must be enabled in an app event handler before anything else ie before any filesystem - // is actually created and used - after, it is too late - enabling shadow has a neglictible perfs - // impact. - // NO! by the time an app event handler is instanciated it is already too late, see note in ctor. - //internal void EnableShadow() - //{ - // if (_mvcViewsFileSystem != null) // test one of the fs... - // throw new InvalidOperationException("Cannot enable shadow once filesystems have been created."); - // _shadowEnabled = true; - //} - - public ICompletable Shadow(Guid id) + internal ICompletable Shadow(Guid id) { var typed = _wrappers.ToArray(); var wrappers = new ShadowWrapper[typed.Length + 7]; @@ -218,7 +207,7 @@ namespace Umbraco.Core.IO wrappers[i++] = _masterPagesFileSystemWrapper; wrappers[i] = _mvcViewsFileSystemWrapper; - return ShadowFileSystemsScope.CreateScope(id, wrappers); + return new ShadowFileSystems(id, wrappers); } #endregion diff --git a/src/Umbraco.Core/IO/ShadowFileSystems.cs b/src/Umbraco.Core/IO/ShadowFileSystems.cs new file mode 100644 index 0000000000..2fe703c3df --- /dev/null +++ b/src/Umbraco.Core/IO/ShadowFileSystems.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Logging; + +namespace Umbraco.Core.IO +{ + internal class ShadowFileSystems : ICompletable + { + // note: taking a reference to the _manager instead of using manager.Current + // to avoid using Current everywhere but really, we support only 1 scope at + // a time, not multiple scopes in case of multiple managers (not supported) + + private static readonly object Locker = new object(); + private static Guid _currentId = Guid.Empty; + private readonly Guid _id; + private readonly ShadowWrapper[] _wrappers; + private bool _completed; + + public ShadowFileSystems(Guid id, ShadowWrapper[] wrappers) + { + lock (Locker) + { + if (_currentId != Guid.Empty) + throw new InvalidOperationException("Already shadowing."); + _currentId = id; + + LogHelper.Debug("Shadow " + id + "."); + _id = id; + _wrappers = wrappers; + foreach (var wrapper in _wrappers) + wrapper.Shadow(id); + } + } + + public void Complete() + { + _completed = true; + } + + public void Dispose() + { + lock (Locker) + { + LogHelper.Debug("UnShadow " + _id + " (" + (_completed ? "complete" : "abort") + ")."); + + var exceptions = new List(); + foreach (var wrapper in _wrappers) + { + try + { + // this may throw an AggregateException if some of the changes could not be applied + wrapper.UnShadow(_completed); + } + catch (AggregateException ae) + { + exceptions.Add(ae); + } + } + + _currentId = Guid.Empty; + + if (exceptions.Count > 0) + throw new AggregateException(_completed ? "Failed to apply all changes (see exceptions)." : "Failed to abort (see exceptions).", exceptions); + } + } + + // for tests + internal static void ResetId() + { + _currentId = Guid.Empty; + } + } +} diff --git a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs b/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs deleted file mode 100644 index 0793946b62..0000000000 --- a/src/Umbraco.Core/IO/ShadowFileSystemsScope.cs +++ /dev/null @@ -1,114 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Runtime.Remoting.Messaging; -using Umbraco.Core.Logging; - -namespace Umbraco.Core.IO -{ - internal class ShadowFileSystemsScope : ICompletable - { - // note: taking a reference to the _manager instead of using manager.Current - // to avoid using Current everywhere but really, we support only 1 scope at - // a time, not multiple scopes in case of multiple managers (not supported) - - private const string ItemKey = "Umbraco.Core.IO.ShadowFileSystemsScope"; - private static readonly object Locker = new object(); - private readonly Guid _id; - private readonly ShadowWrapper[] _wrappers; - - static ShadowFileSystemsScope() - { - SafeCallContext.Register( - () => - { - var scope = CallContext.LogicalGetData(ItemKey); - CallContext.FreeNamedDataSlot(ItemKey); - return scope; - }, - o => - { - if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException(); - if (o != null) CallContext.LogicalSetData(ItemKey, o); - }); - } - - private ShadowFileSystemsScope(Guid id, ShadowWrapper[] wrappers) - { - LogHelper.Debug("Shadow " + id + "."); - _id = id; - _wrappers = wrappers; - foreach (var wrapper in _wrappers) - wrapper.Shadow(id); - } - - // internal for tests + FileSystemProviderManager - // do NOT use otherwise - internal static ShadowFileSystemsScope CreateScope(Guid id, ShadowWrapper[] wrappers) - { - lock (Locker) - { - if (CallContext.LogicalGetData(ItemKey) != null) throw new InvalidOperationException("Already shadowing."); - CallContext.LogicalSetData(ItemKey, ItemKey); // value does not matter - } - return new ShadowFileSystemsScope(id, wrappers); - } - - internal static bool InScope - { - get { return NoScope == false; } - } - - internal static bool NoScope - { - get { return CallContext.LogicalGetData(ItemKey) == null; } - } - - public void Complete() - { - lock (Locker) - { - LogHelper.Debug("UnShadow " + _id + " (complete)."); - - var exceptions = new List(); - foreach (var wrapper in _wrappers) - { - try - { - // this may throw an AggregateException if some of the changes could not be applied - wrapper.UnShadow(true); - } - catch (AggregateException ae) - { - exceptions.Add(ae); - } - } - - if (exceptions.Count > 0) - throw new AggregateException("Failed to apply all changes (see exceptions).", exceptions); - - // last, & *only* if successful (otherwise we'll unshadow & cleanup as best as we can) - CallContext.FreeNamedDataSlot(ItemKey); - } - } - - public void Dispose() - { - lock (Locker) - { - if (CallContext.LogicalGetData(ItemKey) == null) return; - - try - { - LogHelper.Debug("UnShadow " + _id + " (abort)"); - foreach (var wrapper in _wrappers) - wrapper.UnShadow(false); // should not throw - } - finally - { - // last, & always - CallContext.FreeNamedDataSlot(ItemKey); - } - } - } - } -} diff --git a/src/Umbraco.Core/IO/ShadowWrapper.cs b/src/Umbraco.Core/IO/ShadowWrapper.cs index 87d2b8db9f..7c8bd55830 100644 --- a/src/Umbraco.Core/IO/ShadowWrapper.cs +++ b/src/Umbraco.Core/IO/ShadowWrapper.cs @@ -2,20 +2,23 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using Umbraco.Core.Scoping; namespace Umbraco.Core.IO { internal class ShadowWrapper : IFileSystem2 { + private readonly IScopeProviderInternal _scopeProvider; private readonly IFileSystem _innerFileSystem; private readonly string _shadowPath; private ShadowFileSystem _shadowFileSystem; private string _shadowDir; - public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath) + public ShadowWrapper(IFileSystem innerFileSystem, string shadowPath, IScopeProviderInternal scopeProvider) { _innerFileSystem = innerFileSystem; _shadowPath = shadowPath; + _scopeProvider = scopeProvider; } internal void Shadow(Guid id) @@ -62,7 +65,14 @@ namespace Umbraco.Core.IO private IFileSystem FileSystem { - get { return ShadowFileSystemsScope.NoScope ? _innerFileSystem : _shadowFileSystem; } + get + { + var isScoped = _scopeProvider != null && _scopeProvider.AmbientScope != null && _scopeProvider.AmbientScope.ScopedFileSystems; + + return isScoped + ? _shadowFileSystem + : _innerFileSystem; + } } public IEnumerable GetDirectories(string path) diff --git a/src/Umbraco.Core/Scoping/IScope.cs b/src/Umbraco.Core/Scoping/IScope.cs index 9fca60fb95..a0d994b2f2 100644 --- a/src/Umbraco.Core/Scoping/IScope.cs +++ b/src/Umbraco.Core/Scoping/IScope.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Persistence; diff --git a/src/Umbraco.Core/Scoping/IScopeInternal.cs b/src/Umbraco.Core/Scoping/IScopeInternal.cs new file mode 100644 index 0000000000..86276fc7f9 --- /dev/null +++ b/src/Umbraco.Core/Scoping/IScopeInternal.cs @@ -0,0 +1,18 @@ +using System.Data; +using Umbraco.Core.Events; +using Umbraco.Core.Persistence; + +namespace Umbraco.Core.Scoping +{ + internal interface IScopeInternal : IScope + { + IScopeInternal ParentScope { get; } + EventsDispatchMode DispatchMode { get; } + IsolationLevel IsolationLevel { get; } + UmbracoDatabase DatabaseOrNull { get; } + EventMessages MessagesOrNull { get; } + bool ScopedFileSystems { get; } + void ChildCompleted(bool? completed); + void Reset(); + } +} diff --git a/src/Umbraco.Core/Scoping/IScopeProvider.cs b/src/Umbraco.Core/Scoping/IScopeProvider.cs index 3384d0489d..7cce737a89 100644 --- a/src/Umbraco.Core/Scoping/IScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/IScopeProvider.cs @@ -21,7 +21,7 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool scopeFileSystems = false); + bool? scopeFileSystems = null); /// /// Creates a detached scope. @@ -35,7 +35,7 @@ namespace Umbraco.Core.Scoping IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, - bool scopeFileSystems = false); + bool? scopeFileSystems = null); /// /// Attaches a scope. diff --git a/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs b/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs index 8267112d81..0cd03117eb 100644 --- a/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs +++ b/src/Umbraco.Core/Scoping/IScopeProviderInternal.cs @@ -14,12 +14,12 @@ /// /// Gets the ambient scope. /// - IScope AmbientScope { get; } + IScopeInternal AmbientScope { get; } /// /// Gets the ambient scope if any, else creates and returns a . /// - IScope GetAmbientOrNoScope(); + IScopeInternal GetAmbientOrNoScope(); /// /// Resets the ambient scope. diff --git a/src/Umbraco.Core/Scoping/NoScope.cs b/src/Umbraco.Core/Scoping/NoScope.cs index e99ded6b3b..2efcaecf90 100644 --- a/src/Umbraco.Core/Scoping/NoScope.cs +++ b/src/Umbraco.Core/Scoping/NoScope.cs @@ -1,4 +1,5 @@ using System; +using System.Data; using Umbraco.Core.Cache; using Umbraco.Core.Events; using Umbraco.Core.Persistence; @@ -8,7 +9,7 @@ namespace Umbraco.Core.Scoping /// /// Implements when there is no scope. /// - internal class NoScope : IScope + internal class NoScope : IScopeInternal { private readonly ScopeProvider _scopeProvider; private bool _disposed; @@ -105,5 +106,12 @@ namespace Umbraco.Core.Scoping _disposed = true; GC.SuppressFinalize(this); } + + public IScopeInternal ParentScope { get { return null; } } + public EventsDispatchMode DispatchMode { get {return EventsDispatchMode.Unspecified; } } + public IsolationLevel IsolationLevel { get {return IsolationLevel.Unspecified; } } + public bool ScopedFileSystems { get { return false; } } + public void ChildCompleted(bool? completed) { } + public void Reset() { } } } diff --git a/src/Umbraco.Core/Scoping/Scope.cs b/src/Umbraco.Core/Scoping/Scope.cs index 47f6e1499c..109c8dbba8 100644 --- a/src/Umbraco.Core/Scoping/Scope.cs +++ b/src/Umbraco.Core/Scoping/Scope.cs @@ -1,8 +1,8 @@ using System; -using System.Collections.Generic; using System.Data; using Umbraco.Core.Cache; using Umbraco.Core.Events; +using Umbraco.Core.IO; using Umbraco.Core.Persistence; namespace Umbraco.Core.Scoping @@ -11,7 +11,7 @@ namespace Umbraco.Core.Scoping /// Implements . /// /// Not thread-safe obviously. - internal class Scope : IScope + internal class Scope : IScopeInternal { private readonly ScopeProvider _scopeProvider; private readonly IsolationLevel _isolationLevel; @@ -24,14 +24,15 @@ namespace Umbraco.Core.Scoping private IsolatedRuntimeCache _isolatedRuntimeCache; private UmbracoDatabase _database; - private IEventDispatcher _eventDispatcher; + private ICompletable _fscope; + private IEventDispatcher _eventDispatcher; // this is v7, in v8 this has to change to RepeatableRead private const IsolationLevel DefaultIsolationLevel = IsolationLevel.ReadCommitted; // initializes a new scope - public Scope(ScopeProvider scopeProvider, bool detachable, - ScopeContext scopeContext, + private Scope(ScopeProvider scopeProvider, + Scope parent, ScopeContext scopeContext, bool detachable, IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, @@ -44,10 +45,62 @@ namespace Umbraco.Core.Scoping _dispatchMode = dispatchMode; _scopeFileSystem = scopeFileSystems; Detachable = detachable; + #if DEBUG_SCOPES _scopeProvider.Register(this); Console.WriteLine("create " + _instanceId.ToString("N").Substring(0, 8)); #endif + + if (detachable) + { + if (parent != null) throw new ArgumentException("Cannot set parent on detachable scope.", "parent"); + if (scopeContext != null) throw new ArgumentException("Cannot set context on detachable scope.", "scopeContext"); + + // detachable creates its own scope context + _scopeContext = new ScopeContext(); + + // see note below + if (scopeFileSystems == true) + _fscope = FileSystemProviderManager.Current.Shadow(Guid.NewGuid()); + + return; + } + + if (parent != null) + { + ParentScope = parent; + + // cannot specify a different mode! + if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode) + throw new ArgumentException("Cannot be different from parent.", "repositoryCacheMode"); + + // cannot specify a different mode! + if (_dispatchMode != EventsDispatchMode.Unspecified && parent._dispatchMode != dispatchMode) + throw new ArgumentException("Cannot be different from parent.", "dispatchMode"); + + // cannot specify a different fs scope! + if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems) + throw new ArgumentException("Cannot be different from parent.", "scopeFileSystems"); + } + else + { + // the FS scope cannot be "on demand" like the rest, because we would need to hook into + // every scoped FS to trigger the creation of shadow FS "on demand", and that would be + // pretty pointless since if scopeFileSystems is true, we *know* we want to shadow + if (scopeFileSystems == true) + _fscope = FileSystemProviderManager.Current.Shadow(Guid.NewGuid()); + } + } + + // initializes a new scope + public Scope(ScopeProvider scopeProvider, bool detachable, + ScopeContext scopeContext, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, + RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, + EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, + bool? scopeFileSystems = null) + : this(scopeProvider, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) + { } // initializes a new scope in a nested scopes chain, with its parent @@ -56,38 +109,25 @@ namespace Umbraco.Core.Scoping RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, bool? scopeFileSystems = null) - : this(scopeProvider, false, null, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) + : this(scopeProvider, parent, null, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) { - ParentScope = parent; - - // cannot specify a different mode! - if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode != repositoryCacheMode) - throw new ArgumentException("Cannot be different from parent.", "repositoryCacheMode"); - - // cannot specify a different mode! - if (_dispatchMode != EventsDispatchMode.Unspecified && parent._dispatchMode != dispatchMode) - throw new ArgumentException("Cannot be different from parent.", "dispatchMode"); - - // cannot specify a different fs scope! - if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems) - throw new ArgumentException("Cannot be different from parent.", "scopeFileSystems"); } // initializes a new scope, replacing a NoScope instance public Scope(ScopeProvider scopeProvider, NoScope noScope, ScopeContext scopeContext, - IsolationLevel isolationLevel = IsolationLevel.Unspecified, + IsolationLevel isolationLevel = IsolationLevel.Unspecified, RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified, EventsDispatchMode dispatchMode = EventsDispatchMode.Unspecified, bool? scopeFileSystems = null) - : this(scopeProvider, false, scopeContext, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) + : this(scopeProvider, null, scopeContext, false, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems) { // steal everything from NoScope _database = noScope.DatabaseOrNull; // make sure the NoScope can be replaced ie not in a transaction if (_database != null && _database.InTransaction) - throw new Exception("NoScope instance is not free."); + throw new Exception("NoScope instance is not free."); } #if DEBUG_SCOPES @@ -95,7 +135,16 @@ namespace Umbraco.Core.Scoping public Guid InstanceId { get { return _instanceId; } } #endif - private EventsDispatchMode DispatchMode + public bool ScopedFileSystems + { + get + { + if (ParentScope != null) return ParentScope.ScopedFileSystems; + return _fscope != null; + } + } + + public EventsDispatchMode DispatchMode { get { @@ -124,7 +173,7 @@ namespace Umbraco.Core.Scoping if (ParentScope != null) return ParentScope.IsolatedRuntimeCache; return _isolatedRuntimeCache ?? (_isolatedRuntimeCache - = new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); + = new IsolatedRuntimeCache(type => new DeepCloneRuntimeCacheProvider(new ObjectCacheRuntimeCacheProvider()))); } } @@ -133,12 +182,21 @@ namespace Umbraco.Core.Scoping public bool Detachable { get; private set; } // the parent scope (in a nested scopes chain) - public Scope ParentScope { get; set; } + public IScopeInternal ParentScope { get; set; } // the original scope (when attaching a detachable scope) - public IScope OrigScope { get; set; } + public IScopeInternal OrigScope { get; set; } - private IsolationLevel IsolationLevel + // the original context (when attaching a detachable scope) + public ScopeContext OrigContext { get; set; } + + // the context (for attaching & detaching only) + public ScopeContext Context + { + get { return _scopeContext; } + } + + public IsolationLevel IsolationLevel { get { @@ -286,8 +344,11 @@ namespace Umbraco.Core.Scoping private void DisposeLastScope() { + // figure out completed var completed = _completed.HasValue && _completed.Value; + // deal with database + bool ex = false; if (_database != null) { try @@ -297,28 +358,82 @@ namespace Umbraco.Core.Scoping else _database.AbortTransaction(); } + catch + { + ex = true; + throw; + } finally { _database.Dispose(); _database = null; + + if (ex) + RobustExit(false, true); } } - // deal with events - if (_eventDispatcher != null) - _eventDispatcher.ScopeExit(completed); + RobustExit(completed, false); + } - // if *we* created it, then get rid of it - if (_scopeProvider.AmbientContext == _scopeContext) + private void RobustExit(bool completed, bool kabum) + { + if (kabum) completed = false; + + TryFinally(() => { - try + if (_scopeFileSystem == true) { - _scopeProvider.AmbientContext.ScopeExit(completed); + if (completed) + _fscope.Complete(); + _fscope.Dispose(); + _fscope = null; } - finally + }, () => + { + // deal with events + if (kabum == false && _eventDispatcher != null) + _eventDispatcher.ScopeExit(completed); + }, () => + { + // if *we* created it, then get rid of it + if (_scopeProvider.AmbientContext == _scopeContext) { - _scopeProvider.AmbientContext = null; + try + { + _scopeProvider.AmbientContext.ScopeExit(completed); + } + finally + { + _scopeProvider.AmbientContext = null; + } } + }, () => + { + if (Detachable) + { + // get out of the way, restore original + _scopeProvider.AmbientScope = OrigScope; + _scopeProvider.AmbientContext = OrigContext; + } + }); + } + + private static void TryFinally(params Action[] actions) + { + TryFinally(0, actions); + } + + private static void TryFinally(int index, Action[] actions) + { + if (index == actions.Length) return; + try + { + actions[index](); + } + finally + { + TryFinally(index + 1, actions); } } } diff --git a/src/Umbraco.Core/Scoping/ScopeProvider.cs b/src/Umbraco.Core/Scoping/ScopeProvider.cs index 4da987fce4..0992d4cb17 100644 --- a/src/Umbraco.Core/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Core/Scoping/ScopeProvider.cs @@ -25,15 +25,20 @@ namespace Umbraco.Core.Scoping () => { var scope = StaticAmbientScope; + var context = StaticAmbientContext; StaticAmbientScope = null; - return scope; + StaticAmbientContext = null; + return Tuple.Create(scope, context); }, - scope => + o => { - var ambient = StaticAmbientScope; - if (ambient != null) - ambient.Dispose(); - StaticAmbientScope = (IScope)scope; + // cannot re-attached over leaked scope/context + if (StaticAmbientScope != null) throw new Exception("Found leaked scope when restoring call context."); + if (StaticAmbientContext != null) throw new Exception("Found leaked context when restoring call context."); + + var t = (Tuple)o; + StaticAmbientScope = t.Item1; + StaticAmbientContext = t.Item2; }); } @@ -94,9 +99,9 @@ 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 IScope CallContextValue + private static IScopeInternal CallContextValue { - get { return (IScope) CallContext.LogicalGetData(ScopeItemKey); } + get { return (IScopeInternal) CallContext.LogicalGetData(ScopeItemKey); } set { #if DEBUG_SCOPES @@ -110,9 +115,9 @@ namespace Umbraco.Core.Scoping } } - private static IScope HttpContextValue + private static IScopeInternal HttpContextValue { - get { return (IScope) HttpContext.Current.Items[ScopeItemKey]; } + get { return (IScopeInternal) HttpContext.Current.Items[ScopeItemKey]; } set { #if DEBUG_SCOPES @@ -135,7 +140,7 @@ namespace Umbraco.Core.Scoping } } - private static IScope StaticAmbientScope + private static IScopeInternal StaticAmbientScope { get { return HttpContext.Current == null ? CallContextValue : HttpContextValue; } set @@ -148,14 +153,14 @@ namespace Umbraco.Core.Scoping } /// - public IScope AmbientScope + public IScopeInternal AmbientScope { get { return StaticAmbientScope; } set { StaticAmbientScope = value; } } /// - public IScope GetAmbientOrNoScope() + public IScopeInternal GetAmbientOrNoScope() { return AmbientScope ?? (AmbientScope = new NoScope(this)); } @@ -184,7 +189,9 @@ namespace Umbraco.Core.Scoping throw new ArgumentException("Not a detachable scope."); otherScope.OrigScope = AmbientScope; + otherScope.OrigContext = AmbientContext; AmbientScope = otherScope; + AmbientContext = otherScope.Context; } /// @@ -206,7 +213,9 @@ namespace Umbraco.Core.Scoping throw new InvalidOperationException("Ambient scope is not detachable."); AmbientScope = scope.OrigScope; + AmbientContext = scope.OrigContext; scope.OrigScope = null; + scope.OrigContext = null; return scope; } @@ -220,7 +229,10 @@ namespace Umbraco.Core.Scoping var ambient = AmbientScope; if (ambient == null) { - return AmbientScope = new Scope(this, false, GetNewContext(), isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); + 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; } // replace noScope with a real one @@ -234,20 +246,16 @@ namespace Umbraco.Core.Scoping var database = noScope.DatabaseOrNull; if (database != null && database.InTransaction) throw new Exception("NoScope is in a transaction."); - return AmbientScope = new Scope(this, noScope, GetNewContext(), isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); + 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 scope = ambient as Scope; - if (scope == null) throw new Exception("Ambient scope is not a Scope instance."); + var ambientScope = ambient as Scope; + if (ambientScope == null) throw new Exception("Ambient scope is not a Scope instance."); - return AmbientScope = new Scope(this, scope, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); - } - - private ScopeContext GetNewContext() - { - return AmbientContext == null - ? AmbientContext = new ScopeContext() - : null; + return AmbientScope = new Scope(this, ambientScope, isolationLevel, repositoryCacheMode, dispatchMode, scopeFileSystems); } /// diff --git a/src/Umbraco.Core/Scoping/ScopeReference.cs b/src/Umbraco.Core/Scoping/ScopeReference.cs index 74db47e9c4..998f21c587 100644 --- a/src/Umbraco.Core/Scoping/ScopeReference.cs +++ b/src/Umbraco.Core/Scoping/ScopeReference.cs @@ -19,11 +19,10 @@ { // dispose the entire chain (if any) // reset (don't commit by default) - IScope scope; + IScopeInternal scope; while ((scope = _scopeProvider.AmbientScope) != null) { - if (scope is Scope) - ((Scope) scope).Reset(); + scope.Reset(); scope.Dispose(); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 832ac2fea5..8ebe0c4cb0 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -354,7 +354,7 @@ - + @@ -525,6 +525,7 @@ + diff --git a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs index 540070df31..8e79544052 100644 --- a/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests/IO/ShadowFileSystemTests.cs @@ -2,10 +2,11 @@ using System.IO; using System.Linq; using System.Text; -using System.Threading; +using Moq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.IO; +using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; namespace Umbraco.Tests.IO @@ -23,6 +24,7 @@ namespace Umbraco.Tests.IO { SafeCallContext.Clear(); ClearFiles(); + ShadowFileSystems.ResetId(); } [TearDown] @@ -30,6 +32,7 @@ namespace Umbraco.Tests.IO { SafeCallContext.Clear(); ClearFiles(); + ShadowFileSystems.ResetId(); } private static void ClearFiles() @@ -373,8 +376,11 @@ namespace Umbraco.Tests.IO var appdata = IOHelper.MapPath("App_Data"); Directory.CreateDirectory(path); + var scopedFileSystems = false; + var scopeProvider = MockScopeProvider(() => scopedFileSystems); + var fs = new PhysicalFileSystem(path, "ignore"); - var sw = new ShadowWrapper(fs, "shadow"); + var sw = new ShadowWrapper(fs, "shadow", scopeProvider); var swa = new[] { sw }; using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) @@ -394,46 +400,52 @@ namespace Umbraco.Tests.IO Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); // shadow with scope but no complete does not complete - var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + scopedFileSystems = true; // pretend we have a scope + var scope = new ShadowFileSystems(id = Guid.NewGuid(), swa); Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) sw.AddFile("sub/f3.txt", ms); Assert.IsFalse(fs.FileExists("sub/f3.txt")); Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); scope.Dispose(); + scopedFileSystems = false; Assert.IsFalse(fs.FileExists("sub/f3.txt")); Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); // shadow with scope and complete does complete - scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + scopedFileSystems = true; // pretend we have a scope + scope = new ShadowFileSystems(id = Guid.NewGuid(), swa); Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) sw.AddFile("sub/f4.txt", ms); Assert.IsFalse(fs.FileExists("sub/f4.txt")); Assert.AreEqual(1, Directory.GetDirectories(appdata + "/Shadow").Length); scope.Complete(); - Assert.IsTrue(fs.FileExists("sub/f4.txt")); - TestHelper.TryAssert(() => Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length)); scope.Dispose(); + scopedFileSystems = false; + TestHelper.TryAssert(() => Assert.AreEqual(0, Directory.GetDirectories(appdata + "/Shadow").Length)); Assert.IsTrue(fs.FileExists("sub/f4.txt")); Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); // test scope for "another thread" - scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + scopedFileSystems = true; // pretend we have a scope + scope = new ShadowFileSystems(id = Guid.NewGuid(), swa); Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) sw.AddFile("sub/f5.txt", ms); Assert.IsFalse(fs.FileExists("sub/f5.txt")); - using (new SafeCallContext()) // pretend we're another thread w/out scope - { - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) - sw.AddFile("sub/f6.txt", ms); - } + + // pretend we're another thread w/out scope + scopedFileSystems = false; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + sw.AddFile("sub/f6.txt", ms); + scopedFileSystems = true; // pretend we have a scope + Assert.IsTrue(fs.FileExists("sub/f6.txt")); // other thread has written out to fs scope.Complete(); - Assert.IsTrue(fs.FileExists("sub/f5.txt")); scope.Dispose(); + scopedFileSystems = false; Assert.IsTrue(fs.FileExists("sub/f5.txt")); Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id)); } @@ -445,8 +457,11 @@ namespace Umbraco.Tests.IO var appdata = IOHelper.MapPath("App_Data"); Directory.CreateDirectory(path); + var scopedFileSystems = false; + var scopeProvider = MockScopeProvider(() => scopedFileSystems); + var fs = new PhysicalFileSystem(path, "ignore"); - var sw = new ShadowWrapper(fs, "shadow"); + var sw = new ShadowWrapper(fs, "shadow", scopeProvider); var swa = new[] { sw }; using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) @@ -455,20 +470,23 @@ namespace Umbraco.Tests.IO Guid id; - var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + scopedFileSystems = true; // pretend we have a scope + var scope = new ShadowFileSystems(id = Guid.NewGuid(), swa); Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) sw.AddFile("sub/f2.txt", ms); Assert.IsFalse(fs.FileExists("sub/f2.txt")); - using (new SafeCallContext()) // pretend we're another thread w/out scope - { - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) - sw.AddFile("sub/f2.txt", ms); - } + + // pretend we're another thread w/out scope + scopedFileSystems = false; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) + sw.AddFile("sub/f2.txt", ms); + scopedFileSystems = true; // pretend we have a scope + Assert.IsTrue(fs.FileExists("sub/f2.txt")); // other thread has written out to fs scope.Complete(); - Assert.IsTrue(fs.FileExists("sub/f2.txt")); scope.Dispose(); + scopedFileSystems = false; Assert.IsTrue(fs.FileExists("sub/f2.txt")); TestHelper.TryAssert(() => Assert.IsFalse(Directory.Exists(appdata + "/Shadow/" + id))); @@ -488,8 +506,11 @@ namespace Umbraco.Tests.IO var appdata = IOHelper.MapPath("App_Data"); Directory.CreateDirectory(path); + var scopedFileSystems = false; + var scopeProvider = MockScopeProvider(() => scopedFileSystems); + var fs = new PhysicalFileSystem(path, "ignore"); - var sw = new ShadowWrapper(fs, "shadow"); + var sw = new ShadowWrapper(fs, "shadow", scopeProvider); var swa = new[] { sw }; using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) @@ -498,27 +519,32 @@ namespace Umbraco.Tests.IO Guid id; - var scope = ShadowFileSystemsScope.CreateScope(id = Guid.NewGuid(), swa); + scopedFileSystems = true; // pretend we have a scope + var scope = new ShadowFileSystems(id = Guid.NewGuid(), swa); Assert.IsTrue(Directory.Exists(appdata + "/Shadow/" + id)); using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) sw.AddFile("sub/f2.txt", ms); Assert.IsFalse(fs.FileExists("sub/f2.txt")); - using (new SafeCallContext()) // pretend we're another thread w/out scope - { - using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) - sw.AddFile("sub/f2.txt/f2.txt", ms); - } + + // pretend we're another thread w/out scope + scopedFileSystems = false; + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("bar"))) + sw.AddFile("sub/f2.txt/f2.txt", ms); + scopedFileSystems = true; // pretend we have a scope + Assert.IsTrue(fs.FileExists("sub/f2.txt/f2.txt")); // other thread has written out to fs using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) sw.AddFile("sub/f3.txt", ms); Assert.IsFalse(fs.FileExists("sub/f3.txt")); + scope.Complete(); + try { // no way this can work since we're trying to write a file // but there's now a directory with the same name on the real fs - scope.Complete(); + scope.Dispose(); Assert.Fail("Expected AggregateException."); } catch (AggregateException ae) @@ -576,5 +602,25 @@ namespace Umbraco.Tests.IO TestHelper.Try(() => Directory.Delete(path, true)); TestHelper.TryAssert(() => Assert.IsFalse(File.Exists(path + "/test/inner/f3.txt"))); } + + [Test] + public void MockTest() + { + var scoped = false; + var provider = MockScopeProvider(() => scoped); + + Assert.IsFalse(provider.AmbientScope.ScopedFileSystems); + scoped = true; + Assert.IsTrue(provider.AmbientScope.ScopedFileSystems); + } + + private static IScopeProviderInternal MockScopeProvider(Func f) + { + var scopeMock = new Mock(); + scopeMock.Setup(x => x.ScopedFileSystems).Returns(f); + var providerMock = new Mock(); + providerMock.Setup(x => x.AmbientScope).Returns(scopeMock.Object); + return providerMock.Object; + } } } diff --git a/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs b/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs new file mode 100644 index 0000000000..e2a9b2f05b --- /dev/null +++ b/src/Umbraco.Tests/Scoping/ScopeFileSystemsTests.cs @@ -0,0 +1,157 @@ +using System; +using System.IO; +using System.Text; +using NUnit.Framework; +using Umbraco.Core; +using Umbraco.Core.IO; +using Umbraco.Core.Scoping; +using Umbraco.Tests.TestHelpers; + +namespace Umbraco.Tests.Scoping +{ + [TestFixture] + [DatabaseTestBehavior(DatabaseBehavior.EmptyDbFilePerTest)] + public class ScopeFileSystemsTests : BaseDatabaseFactoryTest + { + [SetUp] + public override void Initialize() + { + base.Initialize(); + + SafeCallContext.Clear(); + ClearFiles(); + } + + [TearDown] + public override void TearDown() + { + base.TearDown(); + SafeCallContext.Clear(); + ClearFiles(); + } + + private static void ClearFiles() + { + TestHelper.DeleteDirectory(IOHelper.MapPath("FileSysTests")); + TestHelper.DeleteDirectory(IOHelper.MapPath("App_Data")); + } + + [TestCase(true)] + [TestCase(false)] + public void CreateMediaTest(bool complete) + { + var physMediaFileSystem = new PhysicalFileSystem(IOHelper.MapPath("media"), "ignore"); + var mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; + + var scopeProvider = ApplicationContext.ScopeProvider; + using (var scope = scopeProvider.CreateScope(scopeFileSystems: true)) + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + mediaFileSystem.AddFile("f1.txt", ms); + Assert.IsTrue(mediaFileSystem.FileExists("f1.txt")); + Assert.IsFalse(physMediaFileSystem.FileExists("f1.txt")); + if (complete) + scope.Complete(); + Assert.IsTrue(mediaFileSystem.FileExists("f1.txt")); + Assert.IsFalse(physMediaFileSystem.FileExists("f1.txt")); + } + + if (complete) + { + Assert.IsTrue(FileSystemProviderManager.Current.MediaFileSystem.FileExists("f1.txt")); + Assert.IsTrue(physMediaFileSystem.FileExists("f1.txt")); + } + else + { + Assert.IsFalse(FileSystemProviderManager.Current.MediaFileSystem.FileExists("f1.txt")); + Assert.IsFalse(physMediaFileSystem.FileExists("f1.txt")); + } + } + + [Test] + public void MultiThread() + { + var physMediaFileSystem = new PhysicalFileSystem(IOHelper.MapPath("media"), "ignore"); + var mediaFileSystem = FileSystemProviderManager.Current.MediaFileSystem; + + var scopeProvider = ApplicationContext.ScopeProvider; + using (var scope = scopeProvider.CreateScope(scopeFileSystems: true)) + { + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + mediaFileSystem.AddFile("f1.txt", ms); + Assert.IsTrue(mediaFileSystem.FileExists("f1.txt")); + Assert.IsFalse(physMediaFileSystem.FileExists("f1.txt")); + + using (new SafeCallContext()) + { + Assert.IsFalse(mediaFileSystem.FileExists("f1.txt")); + + using (var ms = new MemoryStream(Encoding.UTF8.GetBytes("foo"))) + mediaFileSystem.AddFile("f2.txt", ms); + Assert.IsTrue(mediaFileSystem.FileExists("f2.txt")); + Assert.IsTrue(physMediaFileSystem.FileExists("f2.txt")); + } + + Assert.IsTrue(mediaFileSystem.FileExists("f2.txt")); + Assert.IsTrue(physMediaFileSystem.FileExists("f2.txt")); + } + } + + [Test] + public void SingleShadow() + { + var scopeProvider = ApplicationContext.ScopeProvider; + using (var scope = scopeProvider.CreateScope(scopeFileSystems: true)) + { + using (new SafeCallContext()) // not nesting! + { + // ok to create a 'normal' other scope + using (var other = scopeProvider.CreateScope()) + { + other.Complete(); + } + + // not ok to create a 'scoped filesystems' other scope + // because at the moment we don't support concurrent scoped filesystems + Assert.Throws(() => + { + var other = scopeProvider.CreateScope(scopeFileSystems: true); + }); + } + } + } + + [Test] + public void SingleShadowEvenDetached() + { + var scopeProvider = ApplicationContext.ScopeProvider as IScopeProviderInternal; + using (var scope = scopeProvider.CreateScope(scopeFileSystems: true)) + { + using (new SafeCallContext()) // not nesting! + { + // not ok to create a 'scoped filesystems' other scope + // because at the moment we don't support concurrent scoped filesystems + // even a detached one + Assert.Throws(() => + { + var other = scopeProvider.CreateDetachedScope(scopeFileSystems: true); + }); + } + } + + var detached = scopeProvider.CreateDetachedScope(scopeFileSystems: true); + + Assert.IsNull(scopeProvider.AmbientScope); + + Assert.Throws(() => + { + // even if there is no ambient scope, there's a single shadow + using (var other = scopeProvider.CreateScope(scopeFileSystems: true)) + { } + }); + + scopeProvider.AttachScope(detached); + detached.Dispose(); + } + } +} diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index fc9dedf264..446b7ea9f0 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -540,5 +540,108 @@ namespace Umbraco.Tests.Scoping Assert.IsNull(ambientScope); // the scope is gone Assert.IsNotNull(ambientContext); // the context is still there } + + [Test] + public void ScopeContextException() + { + var scopeProvider = DatabaseContext.ScopeProvider; + + bool? completed = null; + + Assert.IsNull(scopeProvider.AmbientScope); + using (var scope = scopeProvider.CreateScope()) + { + var detached = scopeProvider.CreateDetachedScope(); + scopeProvider.AttachScope(detached); + // the exception does not prevent other enlisted items to run + // *and* it does not prevent the scope from properly going down + scopeProvider.Context.Enlist("name", c => + { + throw new Exception("bang"); + }); + scopeProvider.Context.Enlist("other", c => + { + completed = c; + }); + detached.Complete(); + Assert.Throws(() => + { + detached.Dispose(); + }); + + // even though disposing of the scope has thrown, it has exited + // properly ie it has removed itself, and the app remains clean + + Assert.AreSame(scope, scopeProvider.AmbientScope); + scope.Complete(); + } + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + + Assert.IsNotNull(completed); + Assert.AreEqual(true, completed); + } + + [Test] + public void DetachableScope() + { + var scopeProvider = DatabaseContext.ScopeProvider; + + Assert.IsNull(scopeProvider.AmbientScope); + using (var scope = scopeProvider.CreateScope()) + { + Assert.IsInstanceOf(scope); + Assert.IsNotNull(scopeProvider.AmbientScope); + Assert.AreSame(scope, scopeProvider.AmbientScope); + + Assert.IsNotNull(scopeProvider.AmbientContext); // the ambient context + Assert.IsNotNull(scopeProvider.Context); // the ambient context too (getter only) + var context = scopeProvider.Context; + + var detached = scopeProvider.CreateDetachedScope(); + scopeProvider.AttachScope(detached); + + Assert.AreEqual(detached, scopeProvider.AmbientScope); + Assert.AreNotSame(context, scopeProvider.Context); + + // nesting under detached! + using (var nested = scopeProvider.CreateScope()) + { + Assert.Throws(() => + { + // cannot detach a non-detachable scope + scopeProvider.DetachScope(); + }); + nested.Complete(); + } + + Assert.AreEqual(detached, scopeProvider.AmbientScope); + Assert.AreNotSame(context, scopeProvider.Context); + + // can detach + Assert.AreSame(detached, scopeProvider.DetachScope()); + + Assert.AreSame(scope, scopeProvider.AmbientScope); + Assert.AreSame(context, scopeProvider.AmbientContext); + + Assert.Throws(() => + { + // cannot disposed a non-attached scope + // in fact, only the ambient scope can be disposed + detached.Dispose(); + }); + + scopeProvider.AttachScope(detached); + detached.Complete(); + detached.Dispose(); + + // has self-detached, and is gone! + + Assert.AreSame(scope, scopeProvider.AmbientScope); + Assert.AreSame(context, scopeProvider.AmbientContext); + } + Assert.IsNull(scopeProvider.AmbientScope); + Assert.IsNull(scopeProvider.AmbientContext); + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 67dd83aa50..8297cc8d40 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -175,6 +175,7 @@ +