Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/merge-v8-05032021
# Conflicts: # src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs # src/Umbraco.Infrastructure/Search/ExamineComponent.cs
This commit is contained in:
48
src/Umbraco.Infrastructure/Scoping/HttpScopeReference.cs
Normal file
48
src/Umbraco.Infrastructure/Scoping/HttpScopeReference.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Disposed at the end of the request to cleanup any orphaned Scopes.
|
||||
/// </summary>
|
||||
/// <remarks>Registered as Scoped in DI (per request)</remarks>
|
||||
internal class HttpScopeReference : IHttpScopeReference
|
||||
{
|
||||
private readonly ScopeProvider _scopeProvider;
|
||||
private bool _disposedValue;
|
||||
private bool _registered = false;
|
||||
|
||||
public HttpScopeReference(ScopeProvider scopeProvider) => _scopeProvider = scopeProvider;
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
if (_registered)
|
||||
{
|
||||
// dispose the entire chain (if any)
|
||||
// reset (don't commit by default)
|
||||
Scope scope;
|
||||
while ((scope = _scopeProvider.AmbientScope) != null)
|
||||
{
|
||||
scope.Reset();
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() =>
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
Dispose(disposing: true);
|
||||
|
||||
public void Register() => _registered = true;
|
||||
}
|
||||
}
|
||||
18
src/Umbraco.Infrastructure/Scoping/IHttpScopeReference.cs
Normal file
18
src/Umbraco.Infrastructure/Scoping/IHttpScopeReference.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System;
|
||||
|
||||
namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
/// <summary>
|
||||
/// Cleans up orphaned <see cref="IScope"/> references at the end of a request
|
||||
/// </summary>
|
||||
public interface IHttpScopeReference : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// Register for cleanup in the request
|
||||
/// </summary>
|
||||
void Register();
|
||||
}
|
||||
}
|
||||
@@ -51,6 +51,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
/// <para>A detached scope is not ambient and has no parent.</para>
|
||||
/// <para>It is meant to be attached by <see cref="AttachScope"/>.</para>
|
||||
/// </remarks>
|
||||
// TODO: This is not actually used apart from unit tests - I'm assuming it's maybe used by Deploy?
|
||||
IScope CreateDetachedScope(
|
||||
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
|
||||
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.Threading;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Extensions;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Extensions;
|
||||
using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings;
|
||||
|
||||
namespace Umbraco.Cms.Core.Scoping
|
||||
@@ -71,21 +72,34 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
_scopeProvider.RegisterScope(this);
|
||||
Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8));
|
||||
#endif
|
||||
logger.LogTrace("Create {InstanceId} on thread {ThreadId}", InstanceId.ToString("N").Substring(0, 8), Thread.CurrentThread.ManagedThreadId);
|
||||
|
||||
if (detachable)
|
||||
{
|
||||
if (parent != null) throw new ArgumentException("Cannot set parent on detachable scope.", nameof(parent));
|
||||
if (scopeContext != null) throw new ArgumentException("Cannot set context on detachable scope.", nameof(scopeContext));
|
||||
if (autoComplete) throw new ArgumentException("Cannot auto-complete a detachable scope.", nameof(autoComplete));
|
||||
if (parent != null)
|
||||
{
|
||||
throw new ArgumentException("Cannot set parent on detachable scope.", nameof(parent));
|
||||
}
|
||||
|
||||
if (scopeContext != null)
|
||||
{
|
||||
throw new ArgumentException("Cannot set context on detachable scope.", nameof(scopeContext));
|
||||
}
|
||||
|
||||
if (autoComplete)
|
||||
{
|
||||
throw new ArgumentException("Cannot auto-complete a detachable scope.", nameof(autoComplete));
|
||||
}
|
||||
|
||||
// detachable creates its own scope context
|
||||
Context = new ScopeContext();
|
||||
|
||||
// see note below
|
||||
if (scopeFileSystems == true)
|
||||
{
|
||||
_fscope = fileSystems.Shadow();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -98,16 +112,22 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
// TODO: means that it's OK to go from L2 to None for reading purposes, but writing would be BAD!
|
||||
// this is for XmlStore that wants to bypass caches when rebuilding XML (same for NuCache)
|
||||
if (repositoryCacheMode != RepositoryCacheMode.Unspecified && parent.RepositoryCacheMode > repositoryCacheMode)
|
||||
{
|
||||
throw new ArgumentException($"Value '{repositoryCacheMode}' cannot be lower than parent value '{parent.RepositoryCacheMode}'.", nameof(repositoryCacheMode));
|
||||
}
|
||||
|
||||
// cannot specify a dispatcher!
|
||||
if (_eventDispatcher != null)
|
||||
{
|
||||
throw new ArgumentException("Value cannot be specified on nested scope.", nameof(eventDispatcher));
|
||||
}
|
||||
|
||||
// cannot specify a different fs scope!
|
||||
// can be 'true' only on outer scope (and false does not make much sense)
|
||||
if (scopeFileSystems != null && parent._scopeFileSystem != scopeFileSystems)
|
||||
{
|
||||
throw new ArgumentException($"Value '{scopeFileSystems.Value}' be different from parent value '{parent._scopeFileSystem}'.", nameof(scopeFileSystems));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -115,7 +135,9 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
// 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 = fileSystems.Shadow();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,6 +178,8 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
public Guid InstanceId { get; } = Guid.NewGuid();
|
||||
|
||||
public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
public ISqlContext SqlContext => _scopeProvider.SqlContext;
|
||||
|
||||
// a value indicating whether to force call-context
|
||||
@@ -163,8 +187,16 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_callContext) return true;
|
||||
if (ParentScope != null) return ParentScope.CallContext;
|
||||
if (_callContext)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.CallContext;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
set => _callContext = value;
|
||||
@@ -174,7 +206,11 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ParentScope != null) return ParentScope.ScopedFileSystems;
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.ScopedFileSystems;
|
||||
}
|
||||
|
||||
return _fscope != null;
|
||||
}
|
||||
}
|
||||
@@ -184,8 +220,16 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified) return _repositoryCacheMode;
|
||||
if (ParentScope != null) return ParentScope.RepositoryCacheMode;
|
||||
if (_repositoryCacheMode != RepositoryCacheMode.Unspecified)
|
||||
{
|
||||
return _repositoryCacheMode;
|
||||
}
|
||||
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.RepositoryCacheMode;
|
||||
}
|
||||
|
||||
return RepositoryCacheMode.Default;
|
||||
}
|
||||
}
|
||||
@@ -195,10 +239,12 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
get
|
||||
{
|
||||
if (ParentScope != null) return ParentScope.IsolatedCaches;
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.IsolatedCaches;
|
||||
}
|
||||
|
||||
return _isolatedCaches ?? (_isolatedCaches
|
||||
= new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache())));
|
||||
return _isolatedCaches ??= new IsolatedCaches(type => new DeepCloneAppCache(new ObjectCacheAppCache()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,8 +270,16 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_isolationLevel != IsolationLevel.Unspecified) return _isolationLevel;
|
||||
if (ParentScope != null) return ParentScope.IsolationLevel;
|
||||
if (_isolationLevel != IsolationLevel.Unspecified)
|
||||
{
|
||||
return _isolationLevel;
|
||||
}
|
||||
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.IsolationLevel;
|
||||
}
|
||||
|
||||
return Database.SqlContext.SqlSyntax.DefaultIsolationLevel;
|
||||
}
|
||||
}
|
||||
@@ -238,14 +292,19 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
EnsureNotDisposed();
|
||||
|
||||
if (_database != null)
|
||||
{
|
||||
return _database;
|
||||
}
|
||||
|
||||
if (ParentScope != null)
|
||||
{
|
||||
var database = ParentScope.Database;
|
||||
var currentLevel = database.GetCurrentTransactionIsolationLevel();
|
||||
IUmbracoDatabase database = ParentScope.Database;
|
||||
IsolationLevel currentLevel = database.GetCurrentTransactionIsolationLevel();
|
||||
if (_isolationLevel > IsolationLevel.Unspecified && currentLevel < _isolationLevel)
|
||||
{
|
||||
throw new Exception("Scope requires isolation level " + _isolationLevel + ", but got " + currentLevel + " from parent.");
|
||||
}
|
||||
|
||||
return _database = database;
|
||||
}
|
||||
|
||||
@@ -282,8 +341,12 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
get
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (ParentScope != null) return ParentScope.Messages;
|
||||
return _messages ?? (_messages = new EventMessages());
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.Messages;
|
||||
}
|
||||
|
||||
return _messages ??= new EventMessages();
|
||||
|
||||
// TODO: event messages?
|
||||
// this may be a problem: the messages collection will be cleared at the end of the scope
|
||||
@@ -309,8 +372,12 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
get
|
||||
{
|
||||
EnsureNotDisposed();
|
||||
if (ParentScope != null) return ParentScope.Events;
|
||||
return _eventDispatcher ?? (_eventDispatcher = new QueuingEventDispatcher(_mediaFileSystem));
|
||||
if (ParentScope != null)
|
||||
{
|
||||
return ParentScope.Events;
|
||||
}
|
||||
|
||||
return _eventDispatcher ??= new QueuingEventDispatcher(_mediaFileSystem);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,14 +385,14 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
public bool Complete()
|
||||
{
|
||||
if (_completed.HasValue == false)
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
return _completed.Value;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
_completed = null;
|
||||
}
|
||||
public void Reset() => _completed = null;
|
||||
|
||||
public void ChildCompleted(bool? completed)
|
||||
{
|
||||
@@ -333,7 +400,9 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
if (completed.HasValue == false || completed.Value == false)
|
||||
{
|
||||
if (LogUncompletedScopes)
|
||||
_logger.LogDebug("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
|
||||
{
|
||||
_logger.LogWarning("Uncompleted Child Scope at\r\n {StackTrace}", Environment.StackTrace);
|
||||
}
|
||||
|
||||
_completed = false;
|
||||
}
|
||||
@@ -341,8 +410,17 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
private void EnsureNotDisposed()
|
||||
{
|
||||
// We can't be disposed
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException(GetType().FullName);
|
||||
{
|
||||
throw new ObjectDisposedException($"The {nameof(Scope)} ({this.GetDebugInfo()}) is already disposed");
|
||||
}
|
||||
|
||||
// And neither can our ancestors if we're trying to be disposed since
|
||||
// a child must always be disposed before it's parent.
|
||||
// This is a safety check, it's actually not entirely possible that a parent can be
|
||||
// disposed before the child since that will end up with a "not the Ambient" exception.
|
||||
ParentScope?.EnsureNotDisposed();
|
||||
|
||||
// TODO: safer?
|
||||
//if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
|
||||
@@ -355,38 +433,49 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
if (this != _scopeProvider.AmbientScope)
|
||||
{
|
||||
var failedMessage = $"The {nameof(Scope)} {this.GetDebugInfo()} being disposed is not the Ambient {nameof(Scope)} {_scopeProvider.AmbientScope.GetDebugInfo()}. This typically indicates that a child {nameof(Scope)} was not disposed, or flowed to a child thread that was not awaited, or concurrent threads are accessing the same {nameof(Scope)} (Ambient context) which is not supported. If using Task.Run (or similar) as a fire and forget tasks or to run threads in parallel you must suppress execution context flow with ExecutionContext.SuppressFlow() and ExecutionContext.RestoreFlow().";
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
var ambient = _scopeProvider.AmbientScope;
|
||||
_logger.Debug<Scope>("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
|
||||
Scope ambient = _scopeProvider.AmbientScope;
|
||||
_logger.LogWarning("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)");
|
||||
if (ambient == null)
|
||||
{
|
||||
throw new InvalidOperationException("Not the ambient scope (no ambient scope).");
|
||||
var ambientInfos = _scopeProvider.GetScopeInfo(ambient);
|
||||
var disposeInfos = _scopeProvider.GetScopeInfo(this);
|
||||
throw new InvalidOperationException("Not the ambient scope (see ctor stack traces).\r\n"
|
||||
+ "- ambient ctor ->\r\n" + ambientInfos.CtorStack + "\r\n"
|
||||
+ "- dispose ctor ->\r\n" + disposeInfos.CtorStack + "\r\n");
|
||||
}
|
||||
|
||||
ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient);
|
||||
ScopeInfo disposeInfos = _scopeProvider.GetScopeInfo(this);
|
||||
throw new InvalidOperationException($"{failedMessage} (see ctor stack traces).\r\n"
|
||||
+ "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n"
|
||||
+ "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n");
|
||||
#else
|
||||
throw new InvalidOperationException("Not the ambient scope.");
|
||||
throw new InvalidOperationException(failedMessage);
|
||||
#endif
|
||||
}
|
||||
|
||||
var parent = ParentScope;
|
||||
_scopeProvider.AmbientScope = parent; // might be null = this is how scopes are removed from context objects
|
||||
// Replace the Ambient scope with the parent
|
||||
Scope parent = ParentScope;
|
||||
_scopeProvider.PopAmbientScope(this); // pop, the parent is on the stack so is now current
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
_scopeProvider.Disposed(this);
|
||||
#endif
|
||||
|
||||
if (_autoComplete && _completed == null)
|
||||
{
|
||||
_completed = true;
|
||||
}
|
||||
|
||||
if (parent != null)
|
||||
{
|
||||
parent.ChildCompleted(_completed);
|
||||
}
|
||||
else
|
||||
{
|
||||
DisposeLastScope();
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
private void DisposeLastScope()
|
||||
@@ -401,9 +490,13 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
try
|
||||
{
|
||||
if (completed)
|
||||
{
|
||||
_database.CompleteTransaction();
|
||||
}
|
||||
else
|
||||
{
|
||||
_database.AbortTransaction();
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -416,7 +509,9 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
_database = null;
|
||||
|
||||
if (databaseException)
|
||||
{
|
||||
RobustExit(false, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -438,14 +533,20 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
// to ensure we don't leave a scope around, etc
|
||||
private void RobustExit(bool completed, bool onException)
|
||||
{
|
||||
if (onException) completed = false;
|
||||
if (onException)
|
||||
{
|
||||
completed = false;
|
||||
}
|
||||
|
||||
TryFinally(() =>
|
||||
{
|
||||
if (_scopeFileSystem == true)
|
||||
{
|
||||
if (completed)
|
||||
{
|
||||
_fscope.Complete();
|
||||
}
|
||||
|
||||
_fscope.Dispose();
|
||||
_fscope = null;
|
||||
}
|
||||
@@ -453,7 +554,9 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
// deal with events
|
||||
if (onException == false)
|
||||
{
|
||||
_eventDispatcher?.ScopeExit(completed);
|
||||
}
|
||||
}, () =>
|
||||
{
|
||||
// if *we* created it, then get rid of it
|
||||
@@ -466,7 +569,7 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
finally
|
||||
{
|
||||
// removes the ambient context (ambient scope already gone)
|
||||
_scopeProvider.SetAmbient(null);
|
||||
_scopeProvider.PopAmbientScopeContext();
|
||||
}
|
||||
}
|
||||
}, () =>
|
||||
@@ -474,7 +577,18 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
if (Detachable)
|
||||
{
|
||||
// get out of the way, restore original
|
||||
_scopeProvider.SetAmbient(OrigScope, OrigContext);
|
||||
|
||||
// TODO: Difficult to know if this is correct since this is all required
|
||||
// by Deploy which I don't fully understand since there is limited tests on this in the CMS
|
||||
if (OrigScope != _scopeProvider.AmbientScope)
|
||||
{
|
||||
_scopeProvider.PopAmbientScope(_scopeProvider.AmbientScope);
|
||||
}
|
||||
if (OrigContext != _scopeProvider.AmbientContext)
|
||||
{
|
||||
_scopeProvider.PopAmbientScopeContext();
|
||||
}
|
||||
|
||||
Attached = false;
|
||||
OrigScope = null;
|
||||
OrigContext = null;
|
||||
@@ -482,14 +596,15 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
});
|
||||
}
|
||||
|
||||
private static void TryFinally(params Action[] actions)
|
||||
{
|
||||
TryFinally(0, actions);
|
||||
}
|
||||
private static void TryFinally(params Action[] actions) => TryFinally(0, actions);
|
||||
|
||||
private static void TryFinally(int index, Action[] actions)
|
||||
{
|
||||
if (index == actions.Length) return;
|
||||
if (index == actions.Length)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
actions[index]();
|
||||
@@ -500,13 +615,8 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
}
|
||||
}
|
||||
|
||||
// backing field for LogUncompletedScopes
|
||||
private static bool? _logUncompletedScopes;
|
||||
|
||||
// caching config
|
||||
// true if Umbraco.CoreDebugSettings.LogUncompletedScope appSetting is set to "true"
|
||||
private bool LogUncompletedScopes => (_logUncompletedScopes
|
||||
?? (_logUncompletedScopes = _coreDebugSettings.LogIncompletedScopes)).Value;
|
||||
private bool LogUncompletedScopes => _coreDebugSettings.LogIncompletedScopes;
|
||||
|
||||
/// <inheritdoc />
|
||||
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Cms.Core.Scoping
|
||||
@@ -41,6 +42,8 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
public Guid InstanceId { get; } = Guid.NewGuid();
|
||||
|
||||
public int CreatedThreadId { get; } = Thread.CurrentThread.ManagedThreadId;
|
||||
|
||||
private IDictionary<string, IEnlistedObject> Enlisted => _enlisted
|
||||
?? (_enlisted = new Dictionary<string, IEnlistedObject>());
|
||||
|
||||
|
||||
@@ -7,6 +7,10 @@ using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using CoreDebugSettings = Umbraco.Cms.Core.Configuration.Models.CoreDebugSettings;
|
||||
using Umbraco.Extensions;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
using System.Linq;
|
||||
@@ -26,6 +30,10 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
private readonly FileSystems _fileSystems;
|
||||
private readonly CoreDebugSettings _coreDebugSettings;
|
||||
private readonly IMediaFileSystem _mediaFileSystem;
|
||||
private static readonly AsyncLocal<ConcurrentStack<IScope>> s_scopeStack = new AsyncLocal<ConcurrentStack<IScope>>();
|
||||
private static readonly AsyncLocal<ConcurrentStack<IScopeContext>> s_scopeContextStack = new AsyncLocal<ConcurrentStack<IScopeContext>>();
|
||||
private static readonly string s_scopeItemKey = typeof(Scope).FullName;
|
||||
private static readonly string s_contextItemKey = typeof(ScopeProvider).FullName;
|
||||
|
||||
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, IOptions<CoreDebugSettings> coreDebugSettings, IMediaFileSystem mediaFileSystem, ILogger<ScopeProvider> logger, ILoggerFactory loggerFactory, IRequestCache requestCache)
|
||||
{
|
||||
@@ -38,33 +46,6 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
_requestCache = requestCache;
|
||||
// take control of the FileSystems
|
||||
_fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems;
|
||||
|
||||
_scopeReference = new ScopeReference(this);
|
||||
}
|
||||
|
||||
static ScopeProvider()
|
||||
{
|
||||
SafeCallContext.Register(
|
||||
() =>
|
||||
{
|
||||
var scope = GetCallContextObject<IScope>(ScopeItemKey);
|
||||
var context = GetCallContextObject<IScopeContext>(ContextItemKey);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, null);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, null);
|
||||
return Tuple.Create(scope, context);
|
||||
},
|
||||
o =>
|
||||
{
|
||||
// cannot re-attached over leaked scope/context
|
||||
if (GetCallContextObject<IScope>(ScopeItemKey) != null)
|
||||
throw new Exception("Found leaked scope when restoring call context.");
|
||||
if (GetCallContextObject<IScopeContext>(ContextItemKey) != null)
|
||||
throw new Exception("Found leaked context when restoring call context.");
|
||||
|
||||
var t = (Tuple<IScope, IScopeContext>) o;
|
||||
SetCallContextObject<IScope>(ScopeItemKey, t.Item1);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, t.Item2);
|
||||
});
|
||||
}
|
||||
|
||||
public IUmbracoDatabaseFactory DatabaseFactory { get; }
|
||||
@@ -73,45 +54,123 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
|
||||
#region Context
|
||||
|
||||
private static T GetCallContextObject<T>(string key)
|
||||
where T : class, IInstanceIdentifiable
|
||||
private void MoveHttpContextScopeToCallContext()
|
||||
{
|
||||
var obj = CallContext<T>.GetData(key);
|
||||
if (obj == default(T)) return null;
|
||||
return obj;
|
||||
var source = (ConcurrentStack<IScope>)_requestCache.Get(s_scopeItemKey);
|
||||
ConcurrentStack<IScope> stack = s_scopeStack.Value;
|
||||
MoveContexts(s_scopeItemKey, source, stack, (_, v) => s_scopeStack.Value = v);
|
||||
}
|
||||
|
||||
private static void SetCallContextObject<T>(string key, T value)
|
||||
where T: class, IInstanceIdentifiable
|
||||
private void MoveHttpContextScopeContextToCallContext()
|
||||
{
|
||||
#if DEBUG_SCOPES
|
||||
// manage the 'context' that contains the scope (null, "http" or "call")
|
||||
// only for scopes of course!
|
||||
if (key == ScopeItemKey)
|
||||
{
|
||||
// first, null-register the existing value
|
||||
var ambientScope = CallContext<IScope>.GetData(ScopeItemKey);
|
||||
var source = (ConcurrentStack<IScopeContext>)_requestCache.Get(s_contextItemKey);
|
||||
ConcurrentStack<IScopeContext> stack = s_scopeContextStack.Value;
|
||||
MoveContexts(s_contextItemKey, source, stack, (_, v) => s_scopeContextStack.Value = v);
|
||||
}
|
||||
|
||||
if (ambientScope != null) RegisterContext(ambientScope, null);
|
||||
// then register the new value
|
||||
var scope = value as IScope;
|
||||
if (scope != null) RegisterContext(scope, "call");
|
||||
private void MoveCallContextScopeToHttpContext()
|
||||
{
|
||||
ConcurrentStack<IScope> source = s_scopeStack.Value;
|
||||
var stack = (ConcurrentStack<IScope>)_requestCache.Get(s_scopeItemKey);
|
||||
MoveContexts(s_scopeItemKey, source, stack, (k, v) => _requestCache.Set(k, v));
|
||||
}
|
||||
|
||||
private void MoveCallContextScopeContextToHttpContext()
|
||||
{
|
||||
ConcurrentStack<IScopeContext> source = s_scopeContextStack.Value;
|
||||
var stack = (ConcurrentStack<IScopeContext>)_requestCache.Get(s_contextItemKey);
|
||||
MoveContexts(s_contextItemKey, source, stack, (k, v) => _requestCache.Set(k, v));
|
||||
}
|
||||
|
||||
private void MoveContexts<T>(string key, ConcurrentStack<T> source, ConcurrentStack<T> stack, Action<string, ConcurrentStack<T>> setter)
|
||||
where T : class, IInstanceIdentifiable
|
||||
{
|
||||
if (source == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (stack != null)
|
||||
{
|
||||
stack.Clear();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: This isn't going to copy it back up the execution context chain
|
||||
stack = new ConcurrentStack<T>();
|
||||
setter(key, stack);
|
||||
}
|
||||
|
||||
var arr = new T[source.Count];
|
||||
source.CopyTo(arr, 0);
|
||||
Array.Reverse(arr);
|
||||
foreach (T a in arr)
|
||||
{
|
||||
stack.Push(a);
|
||||
}
|
||||
|
||||
source.Clear();
|
||||
}
|
||||
|
||||
private void SetCallContextScope(IScope value)
|
||||
{
|
||||
ConcurrentStack<IScope> stack = s_scopeStack.Value;
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
// first, null-register the existing value
|
||||
if (stack != null && stack.TryPeek(out IScope ambientScope))
|
||||
{
|
||||
RegisterContext(ambientScope, null);
|
||||
}
|
||||
|
||||
// then register the new value
|
||||
if (value != null)
|
||||
{
|
||||
RegisterContext(value, "call");
|
||||
}
|
||||
#endif
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
var obj = CallContext<T>.GetData(key);
|
||||
CallContext<T>.SetData(key, default); // aka remove
|
||||
if (obj == null) return;
|
||||
if (stack != null)
|
||||
{
|
||||
stack.TryPop(out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
Current.Logger.Debug<ScopeProvider>("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
|
||||
_logger.LogDebug("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
|
||||
#endif
|
||||
if (stack == null)
|
||||
{
|
||||
stack = new ConcurrentStack<IScope>();
|
||||
}
|
||||
stack.Push(value);
|
||||
s_scopeStack.Value = stack;
|
||||
}
|
||||
}
|
||||
|
||||
CallContext<T>.SetData(key, value);
|
||||
private void SetCallContextScopeContext(IScopeContext value)
|
||||
{
|
||||
ConcurrentStack<IScopeContext> stack = s_scopeContextStack.Value;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
if (stack != null)
|
||||
{
|
||||
stack.TryPop(out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (stack == null)
|
||||
{
|
||||
stack = new ConcurrentStack<IScopeContext>();
|
||||
}
|
||||
stack.Push(value);
|
||||
s_scopeContextStack.Value = stack;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,131 +178,199 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
private T GetHttpContextObject<T>(string key, bool required = true)
|
||||
where T : class
|
||||
{
|
||||
|
||||
if (!_requestCache.IsAvailable && required)
|
||||
{
|
||||
throw new Exception("Request cache is unavailable.");
|
||||
}
|
||||
|
||||
return (T)_requestCache.Get(key);
|
||||
var stack = (ConcurrentStack<T>)_requestCache.Get(key);
|
||||
return stack != null && stack.TryPeek(out T peek) ? peek : null;
|
||||
}
|
||||
|
||||
private bool SetHttpContextObject(string key, object value, bool required = true)
|
||||
private bool SetHttpContextObject<T>(string key, T value, bool required = true)
|
||||
{
|
||||
if (!_requestCache.IsAvailable)
|
||||
{
|
||||
if (required)
|
||||
{
|
||||
throw new Exception("Request cache is unavailable.");
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
// manage the 'context' that contains the scope (null, "http" or "call")
|
||||
// only for scopes of course!
|
||||
if (key == ScopeItemKey)
|
||||
if (key == s_scopeItemKey)
|
||||
{
|
||||
// first, null-register the existing value
|
||||
var ambientScope = (IScope)_requestCache.Get(ScopeItemKey);
|
||||
if (ambientScope != null) RegisterContext(ambientScope, null);
|
||||
var ambientScope = (IScope)_requestCache.Get(s_scopeItemKey);
|
||||
if (ambientScope != null)
|
||||
{
|
||||
RegisterContext(ambientScope, null);
|
||||
}
|
||||
|
||||
// then register the new value
|
||||
var scope = value as IScope;
|
||||
if (scope != null) RegisterContext(scope, "http");
|
||||
if (value is IScope scope)
|
||||
{
|
||||
RegisterContext(scope, "http");
|
||||
}
|
||||
}
|
||||
#endif
|
||||
var stack = (ConcurrentStack<T>)_requestCache.Get(key);
|
||||
|
||||
if (value == null)
|
||||
_requestCache.Remove(key);
|
||||
{
|
||||
if (stack != null)
|
||||
{
|
||||
stack.TryPop(out _);
|
||||
}
|
||||
}
|
||||
else
|
||||
_requestCache.Set(key, value);
|
||||
{
|
||||
if (stack == null)
|
||||
{
|
||||
stack = new ConcurrentStack<T>();
|
||||
}
|
||||
stack.Push(value);
|
||||
_requestCache.Set(key, stack);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
#endregion
|
||||
|
||||
#region Ambient Context
|
||||
|
||||
internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext";
|
||||
|
||||
/// <summary>
|
||||
/// Get the Ambient (Current) <see cref="IScopeContext"/> for the current execution context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal)
|
||||
/// </remarks>
|
||||
public IScopeContext AmbientContext
|
||||
{
|
||||
get
|
||||
{
|
||||
// try http context, fallback onto call context
|
||||
var value = GetHttpContextObject<IScopeContext>(ContextItemKey, false);
|
||||
return value ?? GetCallContextObject<IScopeContext>(ContextItemKey);
|
||||
}
|
||||
set
|
||||
{
|
||||
// clear both
|
||||
SetHttpContextObject(ContextItemKey, null, false);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, null);
|
||||
if (value == null) return;
|
||||
IScopeContext value = GetHttpContextObject<IScopeContext>(s_contextItemKey, false);
|
||||
if (value != null)
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
// set http/call context
|
||||
if (SetHttpContextObject(ContextItemKey, value, false) == false)
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, value);
|
||||
ConcurrentStack<IScopeContext> stack = s_scopeContextStack.Value;
|
||||
if (stack == null || !stack.TryPeek(out IScopeContext peek))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return peek;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Ambient Scope
|
||||
|
||||
internal const string ScopeItemKey = "Umbraco.Core.Scoping.Scope";
|
||||
internal const string ScopeRefItemKey = "Umbraco.Core.Scoping.ScopeReference";
|
||||
|
||||
// only 1 instance which can be disposed and disposed again
|
||||
private readonly ScopeReference _scopeReference;
|
||||
|
||||
|
||||
IScope IScopeAccessor.AmbientScope => AmbientScope;
|
||||
|
||||
// null if there is none
|
||||
/// <summary>
|
||||
/// Get or set the Ambient (Current) <see cref="Scope"/> for the current execution context.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The current execution context may be request based (HttpContext) or on a background thread (AsyncLocal)
|
||||
/// </remarks>
|
||||
public Scope AmbientScope
|
||||
{
|
||||
// try http context, fallback onto call context
|
||||
// we are casting here because we know its a concrete type
|
||||
get => (Scope)GetHttpContextObject<IScope>(ScopeItemKey, false)
|
||||
?? (Scope)GetCallContextObject<IScope>(ScopeItemKey);
|
||||
set
|
||||
get
|
||||
{
|
||||
// clear both
|
||||
SetHttpContextObject(ScopeItemKey, null, false);
|
||||
SetHttpContextObject(ScopeRefItemKey, null, false);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, null);
|
||||
if (value == null) return;
|
||||
// try http context, fallback onto call context
|
||||
IScope value = GetHttpContextObject<IScope>(s_scopeItemKey, false);
|
||||
if (value != null)
|
||||
{
|
||||
return (Scope)value;
|
||||
}
|
||||
|
||||
// set http/call context
|
||||
if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false))
|
||||
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
|
||||
else
|
||||
SetCallContextObject<IScope>(ScopeItemKey, value);
|
||||
ConcurrentStack<IScope> stack = s_scopeStack.Value;
|
||||
if (stack == null || !stack.TryPeek(out IScope peek))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (Scope)peek;
|
||||
}
|
||||
}
|
||||
|
||||
public void PopAmbientScope(Scope scope)
|
||||
{
|
||||
// pop the stack from all contexts
|
||||
SetHttpContextObject<IScope>(s_scopeItemKey, null, false);
|
||||
SetCallContextScope(null);
|
||||
|
||||
// We need to move the stack to a different context if the parent scope
|
||||
// is flagged with a different CallContext flag. This is required
|
||||
// if creating a child scope with callContext: true (thus forcing CallContext)
|
||||
// when there is actually a current HttpContext available.
|
||||
// It's weird but is required for Deploy somehow.
|
||||
bool parentScopeCallContext = (scope.ParentScope?.CallContext ?? false);
|
||||
if (scope.CallContext && !parentScopeCallContext)
|
||||
{
|
||||
MoveCallContextScopeToHttpContext();
|
||||
MoveCallContextScopeContextToHttpContext();
|
||||
}
|
||||
else if (!scope.CallContext && parentScopeCallContext)
|
||||
{
|
||||
MoveHttpContextScopeToCallContext();
|
||||
MoveHttpContextScopeContextToCallContext();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
public void SetAmbient(Scope scope, IScopeContext context = null)
|
||||
public void PushAmbientScope(Scope scope)
|
||||
{
|
||||
// clear all
|
||||
SetHttpContextObject(ScopeItemKey, null, false);
|
||||
SetHttpContextObject(ScopeRefItemKey, null, false);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, null);
|
||||
SetHttpContextObject(ContextItemKey, null, false);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, null);
|
||||
if (scope == null)
|
||||
if (scope is null)
|
||||
{
|
||||
if (context != null)
|
||||
throw new ArgumentException("Must be null if scope is null.", nameof(context));
|
||||
return;
|
||||
throw new ArgumentNullException(nameof(scope));
|
||||
}
|
||||
|
||||
if (scope.CallContext == false && SetHttpContextObject(ScopeItemKey, scope, false))
|
||||
if (scope.CallContext != false || !SetHttpContextObject<IScope>(s_scopeItemKey, scope, false))
|
||||
{
|
||||
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
|
||||
SetHttpContextObject(ContextItemKey, context);
|
||||
// In this case, always ensure that the HttpContext items
|
||||
// is transfered to CallContext and then cleared since we
|
||||
// may be migrating context with the callContext = true flag.
|
||||
// This is a weird case when forcing callContext when HttpContext
|
||||
// is available. Required by Deploy.
|
||||
|
||||
if (_requestCache.IsAvailable)
|
||||
{
|
||||
MoveHttpContextScopeToCallContext();
|
||||
MoveHttpContextScopeContextToCallContext();
|
||||
}
|
||||
|
||||
SetCallContextScope(scope);
|
||||
}
|
||||
else
|
||||
}
|
||||
|
||||
public void PushAmbientScopeContext(IScopeContext scopeContext)
|
||||
{
|
||||
if (scopeContext is null)
|
||||
{
|
||||
SetCallContextObject<IScope>(ScopeItemKey, scope);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, context);
|
||||
throw new ArgumentNullException(nameof(scopeContext));
|
||||
}
|
||||
|
||||
SetHttpContextObject<IScopeContext>(s_contextItemKey, scopeContext, false);
|
||||
SetCallContextScopeContext(scopeContext);
|
||||
}
|
||||
|
||||
public void PopAmbientScopeContext()
|
||||
{
|
||||
// pop stack from all contexts
|
||||
SetHttpContextObject<IScopeContext>(s_contextItemKey, null, false);
|
||||
SetCallContextScopeContext(null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
@@ -252,43 +379,65 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
|
||||
IEventDispatcher eventDispatcher = null,
|
||||
bool? scopeFileSystems = null)
|
||||
{
|
||||
return new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
|
||||
}
|
||||
=> new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AttachScope(IScope other, bool callContext = false)
|
||||
{
|
||||
// IScopeProvider.AttachScope works with an IScope
|
||||
// but here we can only deal with our own Scope class
|
||||
if (!(other is Scope otherScope))
|
||||
if (other is not Scope otherScope)
|
||||
{
|
||||
throw new ArgumentException("Not a Scope instance.");
|
||||
}
|
||||
|
||||
if (otherScope.Detachable == false)
|
||||
{
|
||||
throw new ArgumentException("Not a detachable scope.");
|
||||
}
|
||||
|
||||
if (otherScope.Attached)
|
||||
{
|
||||
throw new InvalidOperationException("Already attached.");
|
||||
}
|
||||
|
||||
otherScope.Attached = true;
|
||||
otherScope.OrigScope = AmbientScope;
|
||||
otherScope.OrigContext = AmbientContext;
|
||||
|
||||
otherScope.CallContext = callContext;
|
||||
SetAmbient(otherScope, otherScope.Context);
|
||||
|
||||
PushAmbientScopeContext(otherScope.Context);
|
||||
PushAmbientScope(otherScope);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IScope DetachScope()
|
||||
{
|
||||
var ambientScope = AmbientScope;
|
||||
Scope ambientScope = AmbientScope;
|
||||
if (ambientScope == null)
|
||||
{
|
||||
throw new InvalidOperationException("There is no ambient scope.");
|
||||
}
|
||||
|
||||
if (ambientScope.Detachable == false)
|
||||
{
|
||||
throw new InvalidOperationException("Ambient scope is not detachable.");
|
||||
}
|
||||
|
||||
SetAmbient(ambientScope.OrigScope, ambientScope.OrigContext);
|
||||
PopAmbientScope(ambientScope);
|
||||
PopAmbientScopeContext();
|
||||
|
||||
Scope originalScope = AmbientScope;
|
||||
if (originalScope != ambientScope.OrigScope)
|
||||
{
|
||||
throw new InvalidOperationException($"The detatched scope ({ambientScope.GetDebugInfo()}) does not match the original ({originalScope.GetDebugInfo()})");
|
||||
}
|
||||
IScopeContext originalScopeContext = AmbientContext;
|
||||
if (originalScopeContext != ambientScope.OrigContext)
|
||||
{
|
||||
throw new InvalidOperationException($"The detatched scope context does not match the original");
|
||||
}
|
||||
|
||||
ambientScope.OrigScope = null;
|
||||
ambientScope.OrigContext = null;
|
||||
ambientScope.Attached = false;
|
||||
@@ -304,33 +453,32 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
bool callContext = false,
|
||||
bool autoComplete = false)
|
||||
{
|
||||
var ambientScope = AmbientScope;
|
||||
Scope ambientScope = AmbientScope;
|
||||
if (ambientScope == null)
|
||||
{
|
||||
var ambientContext = AmbientContext;
|
||||
var newContext = ambientContext == null ? new ScopeContext() : null;
|
||||
IScopeContext ambientContext = AmbientContext;
|
||||
ScopeContext newContext = ambientContext == null ? new ScopeContext() : null;
|
||||
var scope = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
|
||||
// assign only if scope creation did not throw!
|
||||
SetAmbient(scope, newContext ?? ambientContext);
|
||||
PushAmbientScope(scope);
|
||||
if (newContext != null)
|
||||
{
|
||||
PushAmbientScopeContext(newContext);
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
var nested = new Scope(this, _coreDebugSettings, _mediaFileSystem, _loggerFactory.CreateLogger<Scope>(), _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
|
||||
SetAmbient(nested, AmbientContext);
|
||||
PushAmbientScope(nested);
|
||||
return nested;
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
var scope = AmbientScope;
|
||||
scope?.Reset();
|
||||
|
||||
_scopeReference.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IScopeContext Context => AmbientContext;
|
||||
|
||||
// for testing
|
||||
internal ConcurrentStack<IScope> GetCallContextScopeValue() => s_scopeStack.Value;
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
// this code needs TLC
|
||||
//
|
||||
@@ -356,70 +504,96 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
//}
|
||||
|
||||
// all scope instances that are currently being tracked
|
||||
private static readonly object StaticScopeInfosLock = new object();
|
||||
private static readonly Dictionary<IScope, ScopeInfo> StaticScopeInfos = new Dictionary<IScope, ScopeInfo>();
|
||||
private static readonly object s_staticScopeInfosLock = new object();
|
||||
private static readonly Dictionary<IScope, ScopeInfo> s_staticScopeInfos = new Dictionary<IScope, ScopeInfo>();
|
||||
|
||||
public IEnumerable<ScopeInfo> ScopeInfos
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (StaticScopeInfosLock)
|
||||
lock (s_staticScopeInfosLock)
|
||||
{
|
||||
return StaticScopeInfos.Values.ToArray(); // capture in an array
|
||||
return s_staticScopeInfos.Values.ToArray(); // capture in an array
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ScopeInfo GetScopeInfo(IScope scope)
|
||||
{
|
||||
lock (StaticScopeInfosLock)
|
||||
lock (s_staticScopeInfosLock)
|
||||
{
|
||||
ScopeInfo scopeInfo;
|
||||
return StaticScopeInfos.TryGetValue(scope, out scopeInfo) ? scopeInfo : null;
|
||||
return s_staticScopeInfos.TryGetValue(scope, out ScopeInfo scopeInfo) ? scopeInfo : null;
|
||||
}
|
||||
}
|
||||
|
||||
//private static void Log(string message, UmbracoDatabase database)
|
||||
//{
|
||||
// LogHelper.Debug<ScopeProvider>(message + " (" + (database == null ? "" : database.InstanceSid) + ").");
|
||||
//}
|
||||
|
||||
// register a scope and capture its ctor stacktrace
|
||||
public void RegisterScope(IScope scope)
|
||||
{
|
||||
lock (StaticScopeInfosLock)
|
||||
if (scope is null)
|
||||
{
|
||||
if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered.");
|
||||
_logger.Debug<ScopeProvider>("Register " + scope.InstanceId.ToString("N").Substring(0, 8));
|
||||
StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace);
|
||||
throw new ArgumentNullException(nameof(scope));
|
||||
}
|
||||
|
||||
lock (s_staticScopeInfosLock)
|
||||
{
|
||||
if (s_staticScopeInfos.ContainsKey(scope))
|
||||
{
|
||||
throw new Exception("oops: already registered.");
|
||||
}
|
||||
|
||||
_logger.LogDebug("Register {ScopeId} on Thread {ThreadId}", scope.InstanceId.ToString("N").Substring(0, 8), Thread.CurrentThread.ManagedThreadId);
|
||||
s_staticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
// register that a scope is in a 'context'
|
||||
// 'context' that contains the scope (null, "http" or "call")
|
||||
public static void RegisterContext(IScope scope, string context)
|
||||
public void RegisterContext(IScope scope, string context)
|
||||
{
|
||||
lock (StaticScopeInfosLock)
|
||||
if (scope is null)
|
||||
{
|
||||
ScopeInfo info;
|
||||
if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null;
|
||||
throw new ArgumentNullException(nameof(scope));
|
||||
}
|
||||
|
||||
lock (s_staticScopeInfosLock)
|
||||
{
|
||||
if (s_staticScopeInfos.TryGetValue(scope, out ScopeInfo info) == false)
|
||||
{
|
||||
info = null;
|
||||
}
|
||||
|
||||
if (info == null)
|
||||
{
|
||||
if (context == null) return;
|
||||
if (context == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Exception("oops: unregistered scope.");
|
||||
}
|
||||
var sb = new StringBuilder();
|
||||
var s = scope;
|
||||
IScope s = scope;
|
||||
while (s != null)
|
||||
{
|
||||
if (sb.Length > 0) sb.Append(" < ");
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
sb.Append(" < ");
|
||||
}
|
||||
|
||||
sb.Append(s.InstanceId.ToString("N").Substring(0, 8));
|
||||
var ss = s as Scope;
|
||||
s = ss?.ParentScope;
|
||||
}
|
||||
Current.Logger.Debug<ScopeProvider>("Register " + (context ?? "null") + " context " + sb);
|
||||
if (context == null) info.NullStack = Environment.StackTrace;
|
||||
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 16));
|
||||
|
||||
_logger.LogTrace("Register " + (context ?? "null") + " context " + sb);
|
||||
|
||||
if (context == null)
|
||||
{
|
||||
info.NullStack = Environment.StackTrace;
|
||||
}
|
||||
|
||||
_logger.LogTrace("At:\r\n" + Head(Environment.StackTrace, 16));
|
||||
|
||||
info.Context = context;
|
||||
}
|
||||
}
|
||||
@@ -433,20 +607,25 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
pos = s.IndexOf("\r\n", pos + 1, StringComparison.OrdinalIgnoreCase);
|
||||
i++;
|
||||
}
|
||||
if (pos < 0) return s;
|
||||
|
||||
if (pos < 0)
|
||||
{
|
||||
return s;
|
||||
}
|
||||
|
||||
return s.Substring(0, pos);
|
||||
}
|
||||
|
||||
public void Disposed(IScope scope)
|
||||
{
|
||||
lock (StaticScopeInfosLock)
|
||||
lock (s_staticScopeInfosLock)
|
||||
{
|
||||
if (StaticScopeInfos.ContainsKey(scope))
|
||||
if (s_staticScopeInfos.ContainsKey(scope))
|
||||
{
|
||||
// enable this by default
|
||||
//Console.WriteLine("unregister " + scope.InstanceId.ToString("N").Substring(0, 8));
|
||||
StaticScopeInfos.Remove(scope);
|
||||
_logger.Debug<ScopeProvider>("Remove " + scope.InstanceId.ToString("N").Substring(0, 8));
|
||||
s_staticScopeInfos.Remove(scope);
|
||||
_logger.LogDebug("Remove " + scope.InstanceId.ToString("N").Substring(0, 8));
|
||||
|
||||
// instead, enable this to keep *all* scopes
|
||||
// beware, there can be a lot of scopes!
|
||||
@@ -471,15 +650,31 @@ namespace Umbraco.Cms.Core.Scoping
|
||||
public IScope Scope { get; } // the scope itself
|
||||
|
||||
// the scope's parent identifier
|
||||
public Guid Parent => ((Scope) Scope).ParentScope == null ? Guid.Empty : ((Scope) Scope).ParentScope.InstanceId;
|
||||
public Guid Parent => ((Scope)Scope).ParentScope == null ? Guid.Empty : ((Scope)Scope).ParentScope.InstanceId;
|
||||
|
||||
public DateTime Created { get; } // the date time the scope was created
|
||||
public bool Disposed { get; set; } // whether the scope has been disposed already
|
||||
public string Context { get; set; } // the current 'context' that contains the scope (null, "http" or "lcc")
|
||||
|
||||
public string CtorStack { get; } // the stacktrace of the scope ctor
|
||||
public string DisposedStack { get; set; } // the stacktrace when disposed
|
||||
//public string DisposedStack { get; set; } // the stacktrace when disposed
|
||||
public string NullStack { get; set; } // the stacktrace when the 'context' that contains the scope went null
|
||||
|
||||
public override string ToString() => new StringBuilder()
|
||||
.AppendLine("ScopeInfo:")
|
||||
.Append("Instance Id: ")
|
||||
.AppendLine(Scope.InstanceId.ToString())
|
||||
.Append("Parent Id: ")
|
||||
.AppendLine(Parent.ToString())
|
||||
.Append("Created Thread Id: ")
|
||||
.AppendLine(Scope.CreatedThreadId.ToInvariantString())
|
||||
.Append("Created At: ")
|
||||
.AppendLine(Created.ToString("O"))
|
||||
.Append("Disposed: ")
|
||||
.AppendLine(Disposed.ToString())
|
||||
.Append("CTOR stack: ")
|
||||
.AppendLine(CtorStack)
|
||||
.ToString();
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.Scoping
|
||||
{
|
||||
/// <summary>
|
||||
/// References a scope.
|
||||
/// </summary>
|
||||
/// <remarks>Should go into HttpContext to indicate there is also an IScope in context
|
||||
/// that needs to be disposed at the end of the request (the scope, and the entire scopes
|
||||
/// chain).</remarks>
|
||||
internal class ScopeReference : IDisposeOnRequestEnd // implies IDisposable
|
||||
{
|
||||
private readonly ScopeProvider _scopeProvider;
|
||||
|
||||
public ScopeReference(ScopeProvider scopeProvider)
|
||||
{
|
||||
_scopeProvider = scopeProvider;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// dispose the entire chain (if any)
|
||||
// reset (don't commit by default)
|
||||
Scope scope;
|
||||
while ((scope = _scopeProvider.AmbientScope) != null)
|
||||
{
|
||||
scope.Reset();
|
||||
scope.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user