diff --git a/src/Umbraco.Core/TaskHelper.cs b/src/Umbraco.Core/FireAndForgetTasks.cs similarity index 53% rename from src/Umbraco.Core/TaskHelper.cs rename to src/Umbraco.Core/FireAndForgetTasks.cs index 113327ed88..5cd8b5cb2d 100644 --- a/src/Umbraco.Core/TaskHelper.cs +++ b/src/Umbraco.Core/FireAndForgetTasks.cs @@ -1,30 +1,36 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.Logging; namespace Umbraco.Cms.Core { /// - /// Helper class to not repeat common patterns with Task. + /// Helper class to deal with Fire and Forget tasks correctly. /// - public class TaskHelper + public class FireAndForgetTasks { - private readonly ILogger _logger; + private readonly ILogger _logger; - public TaskHelper(ILogger logger) - { - _logger = logger; - } + public FireAndForgetTasks(ILogger logger) => _logger = logger; /// /// Runs a TPL Task fire-and-forget style, the right way - in the /// background, separate from the current thread, with no risk /// of it trying to rejoin the current thread. /// - public void RunBackgroundTask(Func fn) => Task.Run(LoggingWrapper(fn)).ConfigureAwait(false); + public Task RunBackgroundTask(Func fn) + { + using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread + { + Task t = Task.Run(LoggingWrapper(fn)); + t.ConfigureAwait(false); + return t; + } + } /// /// Runs a task fire-and-forget style and notifies the TPL that this @@ -32,9 +38,15 @@ namespace Umbraco.Cms.Core /// are multiple gaps in thread use that may be long. /// Use for example when talking to a slow webservice. /// - public void RunLongRunningBackgroundTask(Func fn) => - Task.Factory.StartNew(LoggingWrapper(fn), TaskCreationOptions.LongRunning) - .ConfigureAwait(false); + public Task RunLongRunningBackgroundTask(Func fn) + { + using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread + { + Task t = Task.Factory.StartNew(LoggingWrapper(fn), TaskCreationOptions.LongRunning); + t.ConfigureAwait(false); + return t; + } + } private Func LoggingWrapper(Func fn) => async () => diff --git a/src/Umbraco.Core/HybridAccessorBase.cs b/src/Umbraco.Core/HybridAccessorBase.cs index 9843efdfe1..7cfe4a42a3 100644 --- a/src/Umbraco.Core/HybridAccessorBase.cs +++ b/src/Umbraco.Core/HybridAccessorBase.cs @@ -18,11 +18,8 @@ namespace Umbraco.Cms.Core { private readonly IRequestCache _requestCache; - // TODO: Do they need to be static?? These are singleton instances IMO they shouldn't be static - // ReSharper disable StaticMemberInGenericType - private static readonly object s_locker = new object(); - private static bool s_registered; - // ReSharper restore StaticMemberInGenericType + private readonly object _locker = new object(); + private bool _registered; private string _itemKey; @@ -53,37 +50,15 @@ namespace Umbraco.Cms.Core { _requestCache = requestCache ?? throw new ArgumentNullException(nameof(requestCache)); - lock (s_locker) + lock (_locker) { - // register the itemKey once with SafeCallContext - if (s_registered) + if (_registered) { return; } - s_registered = true; + _registered = true; } - - // ReSharper disable once VirtualMemberCallInConstructor - var itemKey = ItemKey; // virtual - SafeCallContext.Register(() => - { - T value = CallContext.GetData(itemKey); - return value; - }, o => - { - if (o == null) - { - return; - } - - if (!(o is T value)) - { - throw new ArgumentException($"Expected type {typeof(T).FullName}, got {o.GetType().FullName}", nameof(o)); - } - - CallContext.SetData(itemKey, value); - }); } protected T Value diff --git a/src/Umbraco.Core/SafeCallContext.cs b/src/Umbraco.Core/SafeCallContext.cs deleted file mode 100644 index e15ee36e33..0000000000 --- a/src/Umbraco.Core/SafeCallContext.cs +++ /dev/null @@ -1,104 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace Umbraco.Cms.Core -{ - /// - /// Provides a way to stop the data flow of a logical call context (i.e. CallContext or AsyncLocal) from within - /// a SafeCallContext and then have the original data restored to the current logical call context. - /// - /// - /// Some usages of this might be when spawning async thread or background threads in which the current logical call context will be flowed but - /// you don't want it to flow there yet you don't want to clear it either since you want the data to remain on the current thread. - /// - public class SafeCallContext : IDisposable - { - private static readonly List> EnterFuncs = new List>(); - private static readonly List> ExitActions = new List>(); - private static int _count; - private readonly List _objects; - private bool _disposed; - - public static void Register(Func enterFunc, Action exitAction) - { - if (enterFunc == null) throw new ArgumentNullException(nameof(enterFunc)); - if (exitAction == null) throw new ArgumentNullException(nameof(exitAction)); - - lock (EnterFuncs) - { - if (_count > 0) throw new InvalidOperationException("Cannot register while some SafeCallContext instances exist."); - EnterFuncs.Add(enterFunc); - ExitActions.Add(exitAction); - } - } - - // tried to make the UmbracoDatabase serializable but then it leaks to weird places - // in ReSharper and so on, where Umbraco.Core is not available. Tried to serialize - // as an object instead but then it comes *back* deserialized into the original context - // as an object and of course it breaks everything. Cannot prevent this from flowing, - // and ExecutionContext.SuppressFlow() works for threads but not domains. and we'll - // have the same issue with anything that toys with logical call context... - // - // so this class lets anything that uses the logical call context register itself, - // providing two methods: - // - an enter func that removes and returns whatever is in the logical call context - // - an exit action that restores the value into the logical call context - // whenever a SafeCallContext instance is created, it uses these methods to capture - // and clear the logical call context, and restore it when disposed. - // - // in addition, a static Clear method is provided - which uses the enter funcs to - // remove everything from logical call context - not to be used when the app runs, - // but can be useful during tests - // - // note - // see System.Transactions - // pre 4.5.1, the TransactionScope would not flow in async, and then introduced - // an option to store in the LLC so that it flows - // they are using a conditional weak table to store the data, and what they store in - // LLC is the key - which is just an empty MarshalByRefObject that is created with - // the transaction scope - that way, they can "clear current data" provided that - // they have the key - but they need to hold onto a ref to the scope... not ok for us - - public static void Clear() - { - lock (EnterFuncs) - { - foreach (var enter in EnterFuncs) - enter(); - } - } - - public SafeCallContext() - { - lock (EnterFuncs) - { - _count++; - _objects = EnterFuncs.Select(x => x()).ToList(); - } - } - - public void Dispose() - { - if (_disposed) throw new ObjectDisposedException("this"); - _disposed = true; - lock (EnterFuncs) - { - for (var i = 0; i < ExitActions.Count; i++) - ExitActions[i](_objects[i]); - _count--; - } - } - - // for unit tests ONLY - internal static void Reset() - { - lock (EnterFuncs) - { - if (_count > 0) throw new InvalidOperationException("Cannot reset while some SafeCallContext instances exist."); - EnterFuncs.Clear(); - ExitActions.Clear(); - } - } - } -} diff --git a/src/Umbraco.Core/Security/HybridBackofficeSecurityAccessor.cs b/src/Umbraco.Core/Security/HybridBackofficeSecurityAccessor.cs deleted file mode 100644 index 924f0a31a6..0000000000 --- a/src/Umbraco.Core/Security/HybridBackofficeSecurityAccessor.cs +++ /dev/null @@ -1,23 +0,0 @@ -using Umbraco.Cms.Core.Cache; - -namespace Umbraco.Cms.Core.Security -{ - public class HybridBackofficeSecurityAccessor : HybridAccessorBase, IBackOfficeSecurityAccessor - { - /// - /// Initializes a new instance of the class. - /// - public HybridBackofficeSecurityAccessor(IRequestCache requestCache) - : base(requestCache) - { } - - /// - /// Gets or sets the object. - /// - public IBackOfficeSecurity BackOfficeSecurity - { - get => Value; - set => Value = value; - } - } -} diff --git a/src/Umbraco.Core/Security/HybridUmbracoWebsiteSecurityAccessor.cs b/src/Umbraco.Core/Security/HybridUmbracoWebsiteSecurityAccessor.cs deleted file mode 100644 index 3145f400d1..0000000000 --- a/src/Umbraco.Core/Security/HybridUmbracoWebsiteSecurityAccessor.cs +++ /dev/null @@ -1,24 +0,0 @@ -using Umbraco.Cms.Core.Cache; - -namespace Umbraco.Cms.Core.Security -{ - - public class HybridUmbracoWebsiteSecurityAccessor : HybridAccessorBase, IUmbracoWebsiteSecurityAccessor - { - /// - /// Initializes a new instance of the class. - /// - public HybridUmbracoWebsiteSecurityAccessor(IRequestCache requestCache) - : base(requestCache) - { } - - /// - /// Gets or sets the object. - /// - public IUmbracoWebsiteSecurity WebsiteSecurity - { - get => Value; - set => Value = value; - } - } -} diff --git a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs index 3dc3176c11..096db978da 100644 --- a/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs +++ b/src/Umbraco.Examine.Lucene/UmbracoExamineIndex.cs @@ -1,9 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using Examine; using Examine.LuceneEngine; using Examine.LuceneEngine.Providers; @@ -35,7 +36,7 @@ namespace Umbraco.Cms.Infrastructure.Examine // context because they will fork a thread/task/whatever which should *not* capture our // call context (and the database it can contain)! ideally we should be able to override // SafelyProcessQueueItems but that's not possible in the current version of Examine. - + // TODO: Make SafelyProcessQueueItems overrideable or make this easier /// /// Create a new @@ -92,7 +93,7 @@ namespace Umbraco.Cms.Infrastructure.Examine public IEnumerable GetFields() { //we know this is a LuceneSearcher - var searcher = (LuceneSearcher) GetSearcher(); + var searcher = (LuceneSearcher)GetSearcher(); return searcher.GetAllIndexedFields(); } @@ -106,9 +107,26 @@ namespace Umbraco.Cms.Infrastructure.Examine { if (CanInitialize()) { - using (new SafeCallContext()) + // Use SafeCallContext to prevent the current Execution Context (AsyncLocal) flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class + using (ExecutionContext.SuppressFlow()) { base.PerformDeleteFromIndex(itemIds, onComplete); + } + } + } + + protected override void PerformIndexItems(IEnumerable values, Action onComplete) + { + if (CanInitialize()) + { + // Use SafeCallContext to prevent the current Execution Context (AsyncLocal) flow to child + // tasks executed in the base class so we don't leak Scopes. + // TODO: See notes at the top of this class + using (ExecutionContext.SuppressFlow()) + { + base.PerformIndexItems(values, onComplete); } } } @@ -167,9 +185,9 @@ namespace Umbraco.Cms.Infrastructure.Examine protected override void AddDocument(Document doc, ValueSet valueSet, IndexWriter writer) { _logger.LogDebug("Write lucene doc id:{DocumentId}, category:{DocumentCategory}, type:{DocumentItemType}", - valueSet.Id, - valueSet.Category, - valueSet.ItemType); + valueSet.Id, + valueSet.Category, + valueSet.ItemType); base.AddDocument(doc, valueSet, writer); } diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index f42e88b7df..4f70f28149 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -170,7 +170,7 @@ namespace Umbraco.Cms.Infrastructure.DependencyInjection // Services required to run background jobs (with out the handler) builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs index 22fa172874..7f41f199e7 100644 --- a/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Infrastructure/Runtime/SqlMainDomLock.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Data; using System.Data.SqlClient; using System.Diagnostics; @@ -253,37 +253,40 @@ _hostingEnvironment = hostingEnvironment; { var updatedTempId = tempId + UpdatedSuffix; - return Task.Run(() => + using (ExecutionContext.SuppressFlow()) { - try + return Task.Run(() => { - using var db = _dbFactory.CreateDatabase(); - - var watch = new Stopwatch(); - watch.Start(); - while (true) + try { - // poll very often, we need to take over as fast as we can - // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO - Thread.Sleep(1000); + using var db = _dbFactory.CreateDatabase(); - var acquired = TryAcquire(db, tempId, updatedTempId); - if (acquired.HasValue) - return acquired.Value; - - if (watch.ElapsedMilliseconds >= millisecondsTimeout) + var watch = new Stopwatch(); + watch.Start(); + while (true) { - return AcquireWhenMaxWaitTimeElapsed(db); + // poll very often, we need to take over as fast as we can + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); + + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; + + if (watch.ElapsedMilliseconds >= millisecondsTimeout) + { + return AcquireWhenMaxWaitTimeElapsed(db); + } } } - } - catch (Exception ex) - { - _logger.LogError(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); - return false; - } + catch (Exception ex) + { + _logger.LogError(ex, "An error occurred trying to acquire and waiting for existing SqlMainDomLock to shutdown"); + return false; + } - }, _cancellationTokenSource.Token); + }, _cancellationTokenSource.Token); + } } private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) diff --git a/src/Umbraco.Infrastructure/Scoping/Scope.cs b/src/Umbraco.Infrastructure/Scoping/Scope.cs index 7d50f5e55a..c68baa5d8c 100644 --- a/src/Umbraco.Infrastructure/Scoping/Scope.cs +++ b/src/Umbraco.Infrastructure/Scoping/Scope.cs @@ -71,7 +71,7 @@ namespace Umbraco.Cms.Core.Scoping #if DEBUG_SCOPES _scopeProvider.RegisterScope(this); - Console.WriteLine("create " + InstanceId.ToString("N").Substring(0, 8)); + logger.LogDebug("create " + InstanceId.ToString("N").Substring(0, 8)); #endif if (detachable) @@ -333,7 +333,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; } @@ -342,7 +344,9 @@ namespace Umbraco.Cms.Core.Scoping private void EnsureNotDisposed() { if (_disposed) + { throw new ObjectDisposedException(GetType().FullName); + } // TODO: safer? //if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) @@ -356,15 +360,18 @@ namespace Umbraco.Cms.Core.Scoping if (this != _scopeProvider.AmbientScope) { #if DEBUG_SCOPES - var ambient = _scopeProvider.AmbientScope; - _logger.Debug("Dispose error (" + (ambient == null ? "no" : "other") + " ambient)"); + Scope ambient = _scopeProvider.AmbientScope; + _logger.LogDebug("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); + } + + ScopeInfo ambientInfos = _scopeProvider.GetScopeInfo(ambient); + ScopeInfo 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"); + + "- ambient ->\r\n" + ambientInfos.ToString() + "\r\n" + + "- dispose ->\r\n" + disposeInfos.ToString() + "\r\n"); #else throw new InvalidOperationException("Not the ambient scope."); #endif @@ -500,13 +507,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; /// public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds); diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index b0b1868a0d..5009f316eb 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -7,8 +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; #if DEBUG_SCOPES +using System.Collections.Generic; using System.Linq; using System.Text; #endif @@ -42,31 +44,6 @@ namespace Umbraco.Cms.Core.Scoping _scopeReference = new ScopeReference(this); } - static ScopeProvider() - { - SafeCallContext.Register( - () => - { - var scope = GetCallContextObject(ScopeItemKey); - var context = GetCallContextObject(ContextItemKey); - SetCallContextObject(ScopeItemKey, null); - SetCallContextObject(ContextItemKey, null); - return Tuple.Create(scope, context); - }, - o => - { - // cannot re-attached over leaked scope/context - if (GetCallContextObject(ScopeItemKey) != null) - throw new Exception("Found leaked scope when restoring call context."); - if (GetCallContextObject(ContextItemKey) != null) - throw new Exception("Found leaked context when restoring call context."); - - var t = (Tuple) o; - SetCallContextObject(ScopeItemKey, t.Item1); - SetCallContextObject(ContextItemKey, t.Item2); - }); - } - public IUmbracoDatabaseFactory DatabaseFactory { get; } public ISqlContext SqlContext => DatabaseFactory.SqlContext; @@ -76,13 +53,17 @@ namespace Umbraco.Cms.Core.Scoping private static T GetCallContextObject(string key) where T : class, IInstanceIdentifiable { - var obj = CallContext.GetData(key); - if (obj == default(T)) return null; + T obj = CallContext.GetData(key); + if (obj == default(T)) + { + return null; + } + return obj; } - private static void SetCallContextObject(string key, T value) - where T: class, IInstanceIdentifiable + private static void SetCallContextObject(string key, T value, ILogger logger) + where T : class, IInstanceIdentifiable { #if DEBUG_SCOPES // manage the 'context' that contains the scope (null, "http" or "call") @@ -90,25 +71,34 @@ namespace Umbraco.Cms.Core.Scoping if (key == ScopeItemKey) { // first, null-register the existing value - var ambientScope = CallContext.GetData(ScopeItemKey); + IScope ambientScope = CallContext.GetData(ScopeItemKey); + + if (ambientScope != null) + { + RegisterContext(ambientScope, logger, null); + } - if (ambientScope != null) RegisterContext(ambientScope, null); // then register the new value - var scope = value as IScope; - if (scope != null) RegisterContext(scope, "call"); + if (value is IScope scope) + { + RegisterContext(scope, logger, "call"); + } } #endif if (value == null) { - var obj = CallContext.GetData(key); + T obj = CallContext.GetData(key); CallContext.SetData(key, default); // aka remove - if (obj == null) return; + if (obj == null) + { + return; + } } else { #if DEBUG_SCOPES - Current.Logger.Debug("AddObject " + value.InstanceId.ToString("N").Substring(0, 8)); + logger.LogDebug("AddObject " + value.InstanceId.ToString("N").Substring(0, 8)); #endif CallContext.SetData(key, value); @@ -121,7 +111,9 @@ namespace Umbraco.Cms.Core.Scoping { if (!_requestCache.IsAvailable && required) + { throw new Exception("Request cache is unavailable."); + } return (T)_requestCache.Get(key); } @@ -131,7 +123,10 @@ namespace Umbraco.Cms.Core.Scoping if (!_requestCache.IsAvailable) { if (required) + { throw new Exception("Request cache is unavailable."); + } + return false; } @@ -142,20 +137,31 @@ namespace Umbraco.Cms.Core.Scoping { // first, null-register the existing value var ambientScope = (IScope)_requestCache.Get(ScopeItemKey); - if (ambientScope != null) RegisterContext(ambientScope, null); + if (ambientScope != null) + { + RegisterContext(ambientScope, _logger, null); + } + // then register the new value - var scope = value as IScope; - if (scope != null) RegisterContext(scope, "http"); + if (value is IScope scope) + { + RegisterContext(scope, _logger, "http"); + } } #endif if (value == null) + { _requestCache.Remove(key); + } else + { _requestCache.Set(key, value); + } + return true; } -#endregion + #endregion #region Ambient Context @@ -166,19 +172,24 @@ namespace Umbraco.Cms.Core.Scoping get { // try http context, fallback onto call context - var value = GetHttpContextObject(ContextItemKey, false); + IScopeContext value = GetHttpContextObject(ContextItemKey, false); return value ?? GetCallContextObject(ContextItemKey); } set { // clear both SetHttpContextObject(ContextItemKey, null, false); - SetCallContextObject(ContextItemKey, null); - if (value == null) return; + SetCallContextObject(ContextItemKey, null, _logger); + if (value == null) + { + return; + } // set http/call context if (SetHttpContextObject(ContextItemKey, value, false) == false) - SetCallContextObject(ContextItemKey, value); + { + SetCallContextObject(ContextItemKey, value, _logger); + } } } @@ -186,8 +197,8 @@ namespace Umbraco.Cms.Core.Scoping #region Ambient Scope - internal const string ScopeItemKey = "Umbraco.Core.Scoping.Scope"; - internal const string ScopeRefItemKey = "Umbraco.Core.Scoping.ScopeReference"; + internal static readonly string ScopeItemKey = typeof(Scope).FullName; + internal static readonly string ScopeRefItemKey = typeof(ScopeReference).FullName; // only 1 instance which can be disposed and disposed again private readonly ScopeReference _scopeReference; @@ -206,14 +217,21 @@ namespace Umbraco.Cms.Core.Scoping // clear both SetHttpContextObject(ScopeItemKey, null, false); SetHttpContextObject(ScopeRefItemKey, null, false); - SetCallContextObject(ScopeItemKey, null); - if (value == null) return; + SetCallContextObject(ScopeItemKey, null, _logger); + if (value == null) + { + return; + } // set http/call context if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false)) + { SetHttpContextObject(ScopeRefItemKey, _scopeReference); + } else - SetCallContextObject(ScopeItemKey, value); + { + SetCallContextObject(ScopeItemKey, value, _logger); + } } } @@ -224,13 +242,16 @@ namespace Umbraco.Cms.Core.Scoping // clear all SetHttpContextObject(ScopeItemKey, null, false); SetHttpContextObject(ScopeRefItemKey, null, false); - SetCallContextObject(ScopeItemKey, null); + SetCallContextObject(ScopeItemKey, null, _logger); SetHttpContextObject(ContextItemKey, null, false); - SetCallContextObject(ContextItemKey, null); + SetCallContextObject(ContextItemKey, null, _logger); if (scope == null) { if (context != null) + { throw new ArgumentException("Must be null if scope is null.", nameof(context)); + } + return; } @@ -241,8 +262,8 @@ namespace Umbraco.Cms.Core.Scoping } else { - SetCallContextObject(ScopeItemKey, scope); - SetCallContextObject(ContextItemKey, context); + SetCallContextObject(ScopeItemKey, scope, _logger); + SetCallContextObject(ContextItemKey, context, _logger); } } @@ -304,11 +325,11 @@ 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(), _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete); // assign only if scope creation did not throw! SetAmbient(scope, newContext ?? ambientContext); @@ -356,70 +377,86 @@ namespace Umbraco.Cms.Core.Scoping //} // all scope instances that are currently being tracked - private static readonly object StaticScopeInfosLock = new object(); - private static readonly Dictionary StaticScopeInfos = new Dictionary(); + private static readonly object s_staticScopeInfosLock = new object(); + private static readonly Dictionary s_staticScopeInfos = new Dictionary(); public IEnumerable 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(message + " (" + (database == null ? "" : database.InstanceSid) + ")."); - //} - // register a scope and capture its ctor stacktrace public void RegisterScope(IScope scope) { - lock (StaticScopeInfosLock) + lock (s_staticScopeInfosLock) { - if (StaticScopeInfos.ContainsKey(scope)) throw new Exception("oops: already registered."); - _logger.Debug("Register " + scope.InstanceId.ToString("N").Substring(0, 8)); - StaticScopeInfos[scope] = new ScopeInfo(scope, Environment.StackTrace); + if (s_staticScopeInfos.ContainsKey(scope)) + { + throw new Exception("oops: already registered."); + } + + _logger.LogDebug("Register " + scope.InstanceId.ToString("N").Substring(0, 8)); + 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 static void RegisterContext(IScope scope, ILogger logger, string context) { - lock (StaticScopeInfosLock) + lock (s_staticScopeInfosLock) { - ScopeInfo info; - if (StaticScopeInfos.TryGetValue(scope, out info) == false) info = null; + 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("Register " + (context ?? "null") + " context " + sb); - if (context == null) info.NullStack = Environment.StackTrace; - //Current.Logger.Debug("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 +470,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("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! @@ -466,20 +508,38 @@ namespace Umbraco.Cms.Core.Scoping Scope = scope; Created = DateTime.Now; CtorStack = ctorStack; + CreatedThreadId = System.Threading.Thread.CurrentThread.ManagedThreadId; } 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 int CreatedThreadId { get; } // the thread id that created this scope 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("Instance Id: ") + .AppendLine(Parent.ToString()) + .Append("Created Thread Id: ") + .AppendLine(CreatedThreadId.ToInvariantString()) + .Append("Created At: ") + .AppendLine(Created.ToString("O")) + .Append("Disposed: ") + .AppendLine(Disposed.ToString()) + .Append("CTOR stack: ") + .AppendLine(CtorStack) + .ToString(); } #endif } diff --git a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs index 1eb1d3bc29..1e0fbea941 100644 --- a/src/Umbraco.Infrastructure/Search/ExamineComponent.cs +++ b/src/Umbraco.Infrastructure/Search/ExamineComponent.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Globalization; using System.Linq; +using System.Threading.Tasks; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; @@ -27,7 +28,7 @@ namespace Umbraco.Cms.Infrastructure.Search private readonly IValueSetBuilder _mediaValueSetBuilder; private readonly IValueSetBuilder _memberValueSetBuilder; private readonly BackgroundIndexRebuilder _backgroundIndexRebuilder; - private readonly TaskHelper _taskHelper; + private readonly FireAndForgetTasks _taskHelper; private readonly IScopeProvider _scopeProvider; private readonly ServiceContext _services; private readonly IMainDom _mainDom; @@ -41,16 +42,18 @@ namespace Umbraco.Cms.Infrastructure.Search private const int EnlistPriority = 80; public ExamineComponent(IMainDom mainDom, - IExamineManager examineManager, IProfilingLogger profilingLogger, + IExamineManager examineManager, + IProfilingLogger profilingLogger, ILoggerFactory loggerFactory, - IScopeProvider scopeProvider, IUmbracoIndexesCreator indexCreator, + IScopeProvider scopeProvider, + IUmbracoIndexesCreator indexCreator, ServiceContext services, IContentValueSetBuilder contentValueSetBuilder, IPublishedContentValueSetBuilder publishedContentValueSetBuilder, IValueSetBuilder mediaValueSetBuilder, IValueSetBuilder memberValueSetBuilder, BackgroundIndexRebuilder backgroundIndexRebuilder, - TaskHelper taskHelper) + FireAndForgetTasks taskHelper) { _services = services; _scopeProvider = scopeProvider; @@ -88,8 +91,10 @@ namespace Umbraco.Cms.Infrastructure.Search } //create the indexes and register them with the manager - foreach(var index in _indexCreator.Create()) + foreach (IIndex index in _indexCreator.Create()) + { _examineManager.AddIndex(index); + } _logger.LogDebug("Examine shutdown registered with MainDom"); @@ -99,7 +104,9 @@ namespace Umbraco.Cms.Infrastructure.Search // don't bind event handlers if we're not suppose to listen if (registeredIndexers == 0) + { return; + } // bind to distributed cache events - this ensures that this logic occurs on ALL servers // that are taking part in a load balanced environment. @@ -129,10 +136,14 @@ namespace Umbraco.Cms.Infrastructure.Search private void ContentCacheRefresherUpdated(ContentCacheRefresher sender, CacheRefresherEventArgs args) { if (Suspendable.ExamineEvents.CanIndex == false) + { return; + } if (args.MessageType != MessageType.RefreshByPayload) + { throw new NotSupportedException(); + } var contentService = _services.ContentService; @@ -167,10 +178,14 @@ namespace Umbraco.Cms.Infrastructure.Search IContent published = null; if (content.Published && contentService.IsPathPublished(content)) + { published = content; + } if (published == null) + { DeleteIndexForEntity(payload.Id, true); + } // just that content ReIndexForContent(content, published != null); @@ -194,9 +209,13 @@ namespace Umbraco.Cms.Infrastructure.Search if (masked != null) // else everything is masked { if (masked.Contains(descendant.ParentId) || !descendant.Published) + { masked.Add(descendant.Id); + } else + { published = descendant; + } } ReIndexForContent(descendant, published != null); @@ -221,7 +240,9 @@ namespace Umbraco.Cms.Infrastructure.Search private void MemberCacheRefresherUpdated(MemberCacheRefresher sender, CacheRefresherEventArgs args) { if (Suspendable.ExamineEvents.CanIndex == false) + { return; + } switch (args.MessageType) { @@ -256,7 +277,7 @@ namespace Umbraco.Cms.Infrastructure.Search case MessageType.RefreshByPayload: var payload = (MemberCacheRefresher.JsonPayload[])args.MessageObject; var members = payload.Select(x => _services.MemberService.GetById(x.Id)); - foreach(var m in members) + foreach (var m in members) { ReIndexForMember(m); } @@ -272,10 +293,14 @@ namespace Umbraco.Cms.Infrastructure.Search private void MediaCacheRefresherUpdated(MediaCacheRefresher sender, CacheRefresherEventArgs args) { if (Suspendable.ExamineEvents.CanIndex == false) + { return; + } if (args.MessageType != MessageType.RefreshByPayload) + { throw new NotSupportedException(); + } var mediaService = _services.MediaService; @@ -303,7 +328,9 @@ namespace Umbraco.Cms.Infrastructure.Search } if (media.Trashed) + { DeleteIndexForEntity(payload.Id, true); + } // just that media ReIndexForMedia(media, !media.Trashed); @@ -330,9 +357,14 @@ namespace Umbraco.Cms.Infrastructure.Search private void LanguageCacheRefresherUpdated(LanguageCacheRefresher sender, CacheRefresherEventArgs e) { if (!(e.MessageObject is LanguageCacheRefresher.JsonPayload[] payloads)) + { return; + } - if (payloads.Length == 0) return; + if (payloads.Length == 0) + { + return; + } var removedOrCultureChanged = payloads.Any(x => x.ChangeType == LanguageCacheRefresher.JsonPayload.LanguageChangeType.ChangeCulture @@ -354,10 +386,14 @@ namespace Umbraco.Cms.Infrastructure.Search private void ContentTypeCacheRefresherUpdated(ContentTypeCacheRefresher sender, CacheRefresherEventArgs args) { if (Suspendable.ExamineEvents.CanIndex == false) + { return; + } if (args.MessageType != MessageType.RefreshByPayload) + { throw new NotSupportedException(); + } var changedIds = new Dictionary removedIds, List refreshedIds, List otherIds)>(); @@ -370,11 +406,17 @@ namespace Umbraco.Cms.Infrastructure.Search } if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.Remove)) + { idLists.removedIds.Add(payload.Id); + } else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshMain)) + { idLists.refreshedIds.Add(payload.Id); + } else if (payload.ChangeTypes.HasType(ContentTypeChangeTypes.RefreshOther)) + { idLists.otherIds.Add(payload.Id); + } } const int pageSize = 500; @@ -413,9 +455,14 @@ namespace Umbraco.Cms.Infrastructure.Search total = results.TotalItemCount; var paged = results.Skip(page * pageSize); - foreach (var item in paged) - if (int.TryParse(item.Id, out var contentId)) + foreach (ISearchResult item in paged) + { + if (int.TryParse(item.Id, out int contentId)) + { DeleteIndexForEntity(contentId, false); + } + } + page++; } } @@ -427,18 +474,18 @@ namespace Umbraco.Cms.Infrastructure.Search { const int pageSize = 500; - var memberTypes = _services.MemberTypeService.GetAll(memberTypeIds); - foreach (var memberType in memberTypes) + IEnumerable memberTypes = _services.MemberTypeService.GetAll(memberTypeIds); + foreach (IMemberType memberType in memberTypes) { var page = 0; var total = long.MaxValue; while (page * pageSize < total) { - var memberToRefresh = _services.MemberService.GetAll( + IEnumerable memberToRefresh = _services.MemberService.GetAll( page++, pageSize, out total, "LoginName", Direction.Ascending, memberType.Alias); - foreach (var c in memberToRefresh) + foreach (IMember c in memberToRefresh) { ReIndexForMember(c); } @@ -453,13 +500,13 @@ namespace Umbraco.Cms.Infrastructure.Search var total = long.MaxValue; while (page * pageSize < total) { - var mediaToRefresh = _services.MediaService.GetPagedOfTypes( + IEnumerable mediaToRefresh = _services.MediaService.GetPagedOfTypes( //Re-index all content of these types mediaTypeIds, page++, pageSize, out total, null, Ordering.By("Path", Direction.Ascending)); - foreach (var c in mediaToRefresh) + foreach (IMedia c in mediaToRefresh) { ReIndexForMedia(c, c.Trashed == false); } @@ -473,7 +520,7 @@ namespace Umbraco.Cms.Infrastructure.Search var total = long.MaxValue; while (page * pageSize < total) { - var contentToRefresh = _services.ContentService.GetPagedOfTypes( + IEnumerable contentToRefresh = _services.ContentService.GetPagedOfTypes( //Re-index all content of these types contentTypeIds, page++, pageSize, out total, null, @@ -483,7 +530,7 @@ namespace Umbraco.Cms.Infrastructure.Search //track which Ids have their paths are published var publishChecked = new Dictionary(); - foreach (var c in contentToRefresh) + foreach (IContent c in contentToRefresh) { var isPublished = false; if (c.Published) @@ -508,27 +555,39 @@ namespace Umbraco.Cms.Infrastructure.Search { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) + { actions.Add(new DeferedReIndexForContent(_taskHelper, this, sender, isPublished)); + } else + { DeferedReIndexForContent.Execute(_taskHelper, this, sender, isPublished); + } } private void ReIndexForMember(IMember member) { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) + { actions.Add(new DeferedReIndexForMember(_taskHelper, this, member)); + } else + { DeferedReIndexForMember.Execute(_taskHelper, this, member); + } } private void ReIndexForMedia(IMedia sender, bool isPublished) { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) + { actions.Add(new DeferedReIndexForMedia(_taskHelper, this, sender, isPublished)); + } else + { DeferedReIndexForMedia.Execute(_taskHelper, this, sender, isPublished); + } } /// @@ -543,9 +602,13 @@ namespace Umbraco.Cms.Infrastructure.Search { var actions = DeferedActions.Get(_scopeProvider); if (actions != null) + { actions.Add(new DeferedDeleteIndex(this, entityId, keepIfUnpublished)); + } else + { DeferedDeleteIndex.Execute(this, entityId, keepIfUnpublished); + } } #endregion @@ -556,25 +619,27 @@ namespace Umbraco.Cms.Infrastructure.Search public static DeferedActions Get(IScopeProvider scopeProvider) { - var scopeContext = scopeProvider.Context; + IScopeContext scopeContext = scopeProvider.Context; return scopeContext?.Enlist("examineEvents", () => new DeferedActions(), // creator (completed, actions) => // action { - if (completed) actions.Execute(); + if (completed) + { + actions.Execute(); + } }, EnlistPriority); } - public void Add(DeferedAction action) - { - _actions.Add(action); - } + public void Add(DeferedAction action) => _actions.Add(action); private void Execute() { - foreach (var action in _actions) + foreach (DeferedAction action in _actions) + { action.Execute(); + } } } @@ -592,12 +657,12 @@ namespace Umbraco.Cms.Infrastructure.Search /// private class DeferedReIndexForContent : DeferedAction { - private readonly TaskHelper _taskHelper; + private readonly FireAndForgetTasks _taskHelper; private readonly ExamineComponent _examineComponent; private readonly IContent _content; private readonly bool _isPublished; - public DeferedReIndexForContent(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished) + public DeferedReIndexForContent(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished) { _taskHelper = taskHelper; _examineComponent = examineComponent; @@ -605,15 +670,13 @@ namespace Umbraco.Cms.Infrastructure.Search _isPublished = isPublished; } - public override void Execute() - { - Execute(_taskHelper, _examineComponent, _content, _isPublished); - } + public override void Execute() => Execute(_taskHelper, _examineComponent, _content, _isPublished); - public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished) - { - taskHelper.RunBackgroundTask(async () => + public static void Execute(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IContent content, bool isPublished) + => taskHelper.RunBackgroundTask(() => { + using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true); + // for content we have a different builder for published vs unpublished // we don't want to build more value sets than is needed so we'll lazily build 2 one for published one for non-published var builders = new Dictionary>> @@ -622,17 +685,16 @@ namespace Umbraco.Cms.Infrastructure.Search [false] = new Lazy>(() => examineComponent._contentValueSetBuilder.GetValueSets(content).ToList()) }; - foreach (var index in examineComponent._examineManager.Indexes.OfType() + foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType() //filter the indexers .Where(x => isPublished || !x.PublishedValuesOnly) .Where(x => x.EnableDefaultEventHandler)) { - var valueSet = builders[index.PublishedValuesOnly].Value; + List valueSet = builders[index.PublishedValuesOnly].Value; index.IndexItems(valueSet); } + return Task.CompletedTask; }); - - } } /// @@ -640,12 +702,12 @@ namespace Umbraco.Cms.Infrastructure.Search /// private class DeferedReIndexForMedia : DeferedAction { - private readonly TaskHelper _taskHelper; + private readonly FireAndForgetTasks _taskHelper; private readonly ExamineComponent _examineComponent; private readonly IMedia _media; private readonly bool _isPublished; - public DeferedReIndexForMedia(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) + public DeferedReIndexForMedia(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) { _taskHelper = taskHelper; _examineComponent = examineComponent; @@ -653,27 +715,26 @@ namespace Umbraco.Cms.Infrastructure.Search _isPublished = isPublished; } - public override void Execute() - { - Execute(_taskHelper, _examineComponent, _media, _isPublished); - } + public override void Execute() => Execute(_taskHelper, _examineComponent, _media, _isPublished); - public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) - { + public static void Execute(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMedia media, bool isPublished) => // perform the ValueSet lookup on a background thread - taskHelper.RunBackgroundTask(async () => + taskHelper.RunBackgroundTask(() => { + using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true); + var valueSet = examineComponent._mediaValueSetBuilder.GetValueSets(media).ToList(); - foreach (var index in examineComponent._examineManager.Indexes.OfType() + foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType() //filter the indexers .Where(x => isPublished || !x.PublishedValuesOnly) .Where(x => x.EnableDefaultEventHandler)) { index.IndexItems(valueSet); } + + return Task.CompletedTask; }); - } } /// @@ -683,34 +744,33 @@ namespace Umbraco.Cms.Infrastructure.Search { private readonly ExamineComponent _examineComponent; private readonly IMember _member; - private readonly TaskHelper _taskHelper; + private readonly FireAndForgetTasks _taskHelper; - public DeferedReIndexForMember(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member) + public DeferedReIndexForMember(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMember member) { _examineComponent = examineComponent; _member = member; _taskHelper = taskHelper; } - public override void Execute() - { - Execute(_taskHelper, _examineComponent, _member); - } + public override void Execute() => Execute(_taskHelper, _examineComponent, _member); - public static void Execute(TaskHelper taskHelper, ExamineComponent examineComponent, IMember member) - { + public static void Execute(FireAndForgetTasks taskHelper, ExamineComponent examineComponent, IMember member) => // perform the ValueSet lookup on a background thread - taskHelper.RunBackgroundTask(async () => + taskHelper.RunBackgroundTask(() => { + using IScope scope = examineComponent._scopeProvider.CreateScope(autoComplete: true); + var valueSet = examineComponent._memberValueSetBuilder.GetValueSets(member).ToList(); - foreach (var index in examineComponent._examineManager.Indexes.OfType() + foreach (IUmbracoIndex index in examineComponent._examineManager.Indexes.OfType() //filter the indexers .Where(x => x.EnableDefaultEventHandler)) { index.IndexItems(valueSet); } + + return Task.CompletedTask; }); - } } private class DeferedDeleteIndex : DeferedAction @@ -726,10 +786,7 @@ namespace Umbraco.Cms.Infrastructure.Search _keepIfUnpublished = keepIfUnpublished; } - public override void Execute() - { - Execute(_examineComponent, _id, _keepIfUnpublished); - } + public override void Execute() => Execute(_examineComponent, _id, _keepIfUnpublished); public static void Execute(ExamineComponent examineComponent, int id, bool keepIfUnpublished) { diff --git a/src/Umbraco.Tests.Integration/Umbraco.Core/IO/ShadowFileSystemTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Core/IO/ShadowFileSystemTests.cs index 54f5e04dd2..276d7a267e 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Core/IO/ShadowFileSystemTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Core/IO/ShadowFileSystemTests.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Text; @@ -34,7 +34,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.IO [SetUp] public void SetUp() { - SafeCallContext.Clear(); ClearFiles(HostingEnvironment); FileSystems.ResetShadowId(); } @@ -42,7 +41,6 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Core.IO [TearDown] public void TearDown() { - SafeCallContext.Clear(); ClearFiles(HostingEnvironment); FileSystems.ResetShadowId(); } diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/HttpContextRequestAppCacheTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/HttpContextRequestAppCacheTests.cs new file mode 100644 index 0000000000..1020640a37 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/HttpContextRequestAppCacheTests.cs @@ -0,0 +1,29 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using Microsoft.AspNetCore.Http; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Cache +{ + [TestFixture] + public class HttpContextRequestAppCacheTests : AppCacheTests + { + private HttpContextRequestAppCache _appCache; + private IHttpContextAccessor _httpContextAccessor; + + public override void Setup() + { + base.Setup(); + var httpContext = new DefaultHttpContext(); + _httpContextAccessor = Mock.Of(x => x.HttpContext == httpContext); + _appCache = new HttpContextRequestAppCache(_httpContextAccessor); + } + + internal override IAppCache AppCache => _appCache; + + protected override int GetTotalItemCount => _httpContextAccessor.HttpContext.Items.Count; + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/FireAndForgetTasksTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/FireAndForgetTasksTests.cs new file mode 100644 index 0000000000..2559617a62 --- /dev/null +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Core/FireAndForgetTasksTests.cs @@ -0,0 +1,77 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture.NUnit3; +using Microsoft.Extensions.Logging; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Tests.Common.TestHelpers; +using Umbraco.Cms.Tests.UnitTests.AutoFixture; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core +{ + [TestFixture] + public class FireAndForgetTasksTests + { + [Test] + [AutoMoqData] + public void RunBackgroundTask__Suppress_Execution_Context( + [Frozen] ILogger logger, + FireAndForgetTasks sut) + { + var local = new AsyncLocal + { + Value = "hello" + }; + + string taskResult = null; + + Task t = sut.RunBackgroundTask(() => + { + // FireAndForgetTasks ensure that flow is suppressed therefore this value will be null + taskResult = local.Value; + return Task.CompletedTask; + }); + + Task.WaitAll(t); + + Assert.IsNull(taskResult); + } + + [Test] + [AutoMoqData] + public void RunBackgroundTask__Must_Run_Func( + [Frozen] ILogger logger, + FireAndForgetTasks sut) + { + var i = 0; + Task t = sut.RunBackgroundTask(() => + { + Interlocked.Increment(ref i); + return Task.CompletedTask; + }); + + Task.WaitAll(t); + + Assert.AreEqual(1, i); + } + + [Test] + [AutoMoqData] + public void RunBackgroundTask__Log_Error_When_Exception_Happen_In_Background_Task( + [Frozen] ILogger logger, + Exception exception, + FireAndForgetTasks sut) + { + Task t = sut.RunBackgroundTask(() => throw exception); + + Task.WaitAll(t); + + Mock.Get(logger).VerifyLogError(exception, "Exception thrown in a background thread", Times.Once()); + } + } +} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs deleted file mode 100644 index 48c9b984ca..0000000000 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Core/TaskHelperTests.cs +++ /dev/null @@ -1,47 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System; -using System.Threading; -using System.Threading.Tasks; -using AutoFixture.NUnit3; -using Microsoft.Extensions.Logging; -using Moq; -using NUnit.Framework; -using Umbraco.Cms.Core; -using Umbraco.Cms.Tests.Common.TestHelpers; -using Umbraco.Cms.Tests.UnitTests.AutoFixture; - -namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core -{ - [TestFixture] - public class TaskHelperTests - { - [Test] - [AutoMoqData] - public void RunBackgroundTask__must_run_func([Frozen] ILogger logger, TaskHelper sut) - { - var i = 0; - sut.RunBackgroundTask(() => - { - Interlocked.Increment(ref i); - return Task.CompletedTask; - }); - - Thread.Sleep(5); // Wait for background task to execute - - Assert.AreEqual(1, i); - } - - [Test] - [AutoMoqData] - public void RunBackgroundTask__Log_error_when_exception_happen_in_background_task([Frozen] ILogger logger, Exception exception, TaskHelper sut) - { - sut.RunBackgroundTask(() => throw exception); - - Thread.Sleep(5); // Wait for background task to execute - - Mock.Get(logger).VerifyLogError(exception, "Exception thrown in a background thread", Times.Once()); - } - } -} diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs index 58614443b5..97af07d1ed 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/MigrationTests.cs @@ -1,7 +1,10 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; +#if DEBUG_SCOPES +using System.Collections.Generic; +#endif using System.Data; using Microsoft.Extensions.Logging; using Moq; diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs index d778067d22..2a810c4d18 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.BackOffice/Controllers/MemberControllerUnitTests.cs @@ -34,6 +34,7 @@ using Umbraco.Cms.Tests.UnitTests.Umbraco.Core.ShortStringHelper; using Umbraco.Cms.Web.BackOffice.Controllers; using Umbraco.Cms.Web.BackOffice.Mapping; using Umbraco.Cms.Web.Common.ActionsResults; +using Umbraco.Cms.Web.Common.Security; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; using MemberMapDefinition = Umbraco.Cms.Web.BackOffice.Mapping.MemberMapDefinition; @@ -330,6 +331,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers IDataTypeService dataTypeService, IBackOfficeSecurityAccessor backOfficeSecurityAccessor) { + var httpContextAccessor = new HttpContextAccessor(); + var mockShortStringHelper = new MockShortStringHelper(); var textService = new Mock(); @@ -337,7 +340,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers contentTypeBaseServiceProvider.Setup(x => x.GetContentTypeOf(It.IsAny())).Returns(new ContentType(mockShortStringHelper, 123)); var contentAppFactories = new Mock>(); var mockContentAppFactoryCollection = new Mock>(); - var hybridBackOfficeSecurityAccessor = new HybridBackofficeSecurityAccessor(new DictionaryAppCache()); + var hybridBackOfficeSecurityAccessor = new BackOfficeSecurityAccessor(httpContextAccessor); var contentAppFactoryCollection = new ContentAppFactoryCollection( contentAppFactories.Object, mockContentAppFactoryCollection.Object, @@ -358,7 +361,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers Mock.Get(dataEditor).Setup(x => x.GetValueEditor()).Returns(new TextOnlyValueEditor(Mock.Of(), Mock.Of(), new DataEditorAttribute(Constants.PropertyEditors.Aliases.TextBox, "Test Textbox", "textbox"), textService.Object, Mock.Of(), Mock.Of())); var propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor })); - + IMapDefinition memberMapDefinition = new MemberMapDefinition( commonMapper, new CommonTreeNodeMapper(Mock.Of()), @@ -372,7 +375,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers mockPasswordConfig.Object, contentTypeBaseServiceProvider.Object, propertyEditorCollection), - new HttpContextAccessor()); + httpContextAccessor); var map = new MapDefinitionCollection(new List() { @@ -396,7 +399,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.BackOffice.Controllers return new MemberController( new DefaultCultureDictionary( new Mock().Object, - new HttpRequestAppCache(() => null)), + NoAppCache.Instance), new LoggerFactory(), mockShortStringHelper, new DefaultEventMessagesFactory( diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunner.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunner.cs index a249185c0d..c1555a95a6 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunner.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/LegacyBackgroundTask/BackgroundTaskRunner.cs @@ -327,8 +327,10 @@ namespace Umbraco.Web.Scheduling // create a new token source since this is a new process _shutdownTokenSource = new CancellationTokenSource(); _shutdownToken = _shutdownTokenSource.Token; - _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); - + using (ExecutionContext.SuppressFlow()) // Do not flow AsyncLocal to the child thread + { + _runningTask = Task.Run(async () => await Pump().ConfigureAwait(false), _shutdownToken); + } _logger.LogDebug("{LogPrefix} Starting", _logPrefix); } @@ -350,7 +352,9 @@ namespace Umbraco.Web.Scheduling var hasTasks = TaskCount > 0; if (!force && hasTasks) + { _logger.LogInformation("{LogPrefix} Waiting for tasks to complete", _logPrefix); + } // complete the queue // will stop waiting on the queue or on a latch @@ -552,16 +556,21 @@ namespace Umbraco.Web.Scheduling try { if (bgTask.IsAsync) + { // configure await = false since we don't care about the context, we're on a background thread. await bgTask.RunAsync(token).ConfigureAwait(false); + } else + { bgTask.Run(); + } } finally // ensure we disposed - unless latched again ie wants to re-run { - var lbgTask = bgTask as ILatchedBackgroundTask; - if (lbgTask == null || lbgTask.IsLatched == false) + if (!(bgTask is ILatchedBackgroundTask lbgTask) || lbgTask.IsLatched == false) + { bgTask.Dispose(); + } } } catch (Exception e) diff --git a/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs b/src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs similarity index 53% rename from src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs rename to src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs index 17558a78d4..341d7d342c 100644 --- a/src/Umbraco.Core/Cache/GenericDictionaryRequestAppCache.cs +++ b/src/Umbraco.Web.Common/Cache/HttpContextRequestAppCache.cs @@ -1,36 +1,41 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Threading; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Events; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.Cache { /// - /// Implements a fast on top of HttpContext.Items. + /// Implements a on top of /// /// - /// If no current HttpContext items can be found (no current HttpContext, - /// or no Items...) then this cache acts as a pass-through and does not cache - /// anything. + /// The HttpContext is not thread safe and no part of it is which means we need to include our own thread + /// safety mechanisms. This relies on notifications: and + /// in order to facilitate the correct locking and releasing allocations. + /// /// - public class GenericDictionaryRequestAppCache : FastDictionaryAppCacheBase, IRequestCache + public class HttpContextRequestAppCache : FastDictionaryAppCacheBase, IRequestCache { + //private static readonly string s_contextItemsLockKey = $"{typeof(HttpContextRequestAppCache).FullName}::LockEntered"; + private readonly IHttpContextAccessor _httpContextAccessor; + /// /// Initializes a new instance of the class with a context, for unit tests! /// - public GenericDictionaryRequestAppCache(Func> requestItems) : base() - { - ContextItems = requestItems; - } - - private Func> ContextItems { get; } + public HttpContextRequestAppCache(IHttpContextAccessor httpContextAccessor) + => _httpContextAccessor = httpContextAccessor; public bool IsAvailable => TryGetContextItems(out _); private bool TryGetContextItems(out IDictionary items) { - items = ContextItems?.Invoke(); + items = _httpContextAccessor.HttpContext?.Items; return items != null; } @@ -38,7 +43,10 @@ namespace Umbraco.Cms.Core.Cache public override object Get(string key, Func factory) { //no place to cache so just return the callback result - if (!TryGetContextItems(out var items)) return factory(); + if (!TryGetContextItems(out var items)) + { + return factory(); + } key = GetCacheKey(key); @@ -140,33 +148,67 @@ namespace Umbraco.Cms.Core.Cache #region Lock - private const string ContextItemsLockKey = "Umbraco.Core.Cache.HttpRequestCache::LockEntered"; + protected override void EnterReadLock() + { + if (!TryGetContextItems(out _)) + { + return; + } - protected override void EnterReadLock() => EnterWriteLock(); + ReaderWriterLockSlim locker = GetLock(); + locker.EnterReadLock(); + } protected override void EnterWriteLock() { - if (!TryGetContextItems(out var items)) return; + if (!TryGetContextItems(out _)) + { + return; + } - // note: cannot keep 'entered' as a class variable here, - // since there is one per request - so storing it within - // ContextItems - which is locked, so this should be safe + ReaderWriterLockSlim locker = GetLock(); + locker.EnterWriteLock(); - var entered = false; - Monitor.Enter(items, ref entered); - items[ContextItemsLockKey] = entered; + //// note: cannot keep 'entered' as a class variable here, + //// since there is one per request - so storing it within + //// ContextItems - which is locked, so this should be safe + + //var entered = false; + //Monitor.Enter(items, ref entered); + //items[s_contextItemsLockKey] = entered; } - protected override void ExitReadLock() => ExitWriteLock(); + protected override void ExitReadLock() + { + if (!TryGetContextItems(out _)) + { + return; + } + + ReaderWriterLockSlim locker = GetLock(); + if (locker.IsReadLockHeld) + { + locker.ExitReadLock(); + } + } protected override void ExitWriteLock() { - if (!TryGetContextItems(out var items)) return; + if (!TryGetContextItems(out _)) + { + return; + } - var entered = (bool?)items[ContextItemsLockKey] ?? false; - if (entered) - Monitor.Exit(items); - items.Remove(ContextItemsLockKey); + ReaderWriterLockSlim locker = GetLock(); + if (locker.IsWriteLockHeld) + { + locker.ExitWriteLock(); + } + + //var entered = (bool?)items[s_contextItemsLockKey] ?? false; + //if (entered) + // Monitor.Exit(items); + //items.Remove(s_contextItemsLockKey); } #endregion @@ -185,5 +227,40 @@ namespace Umbraco.Cms.Core.Cache } IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + /// + /// Ensures and returns the current lock + /// + /// + private ReaderWriterLockSlim GetLock() => _httpContextAccessor.GetRequiredHttpContext().RequestServices.GetRequiredService().Locker; + + /// + /// Used as Scoped instance to allow locking within a request + /// + internal class RequestLock : IDisposable + { + private bool _disposedValue; + + public ReaderWriterLockSlim Locker { get; } = new ReaderWriterLockSlim(); + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + Locker.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + } + } } } diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 1d636706f7..d66cbc7e3a 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -54,6 +54,7 @@ using Umbraco.Cms.Web.Common.Security; using Umbraco.Cms.Web.Common.Templates; using Umbraco.Cms.Web.Common.UmbracoContext; using Umbraco.Core.Security; +using static Umbraco.Cms.Core.Cache.HttpContextRequestAppCache; using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Extensions @@ -86,15 +87,18 @@ namespace Umbraco.Extensions IHostingEnvironment tempHostingEnvironment = GetTemporaryHostingEnvironment(webHostEnvironment, config); - var loggingDir = tempHostingEnvironment.MapPathContentRoot(Cms.Core.Constants.SystemDirectories.LogFiles); + var loggingDir = tempHostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.LogFiles); var loggingConfig = new LoggingConfiguration(loggingDir); services.AddLogger(tempHostingEnvironment, loggingConfig, config); + // TODO: This doesn't seem right? The HttpContextAccessor is normally added to the container + // with ASP.NET Core's own ext methods. Is there a chance we can end up with a different + // accessor registered and resolved? IHttpContextAccessor httpContextAccessor = new HttpContextAccessor(); services.AddSingleton(httpContextAccessor); - var requestCache = new GenericDictionaryRequestAppCache(() => httpContextAccessor.HttpContext?.Items); + var requestCache = new HttpContextRequestAppCache(httpContextAccessor); var appCaches = AppCaches.Create(requestCache); services.AddUnique(appCaches); @@ -263,9 +267,9 @@ namespace Umbraco.Extensions builder.Services.AddUnique(); builder.Services.AddUnique(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); builder.AddNotificationHandler(); - builder.Services.AddUnique(); + builder.Services.AddUnique(); var umbracoApiControllerTypes = builder.TypeLoader.GetUmbracoApiControllers().ToList(); builder.WithCollectionBuilder() @@ -285,6 +289,7 @@ namespace Umbraco.Extensions builder.Services.AddSingleton(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.AddHttpClients(); diff --git a/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs b/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs index 8a17419964..bd4ccf3d60 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/PureLiveModelFactory.cs @@ -774,9 +774,10 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder } public void Stop(bool immediate) - { + { _watcher.EnableRaisingEvents = false; _watcher.Dispose(); + _locker.Dispose(); _hostingLifetime.UnregisterObject(this); } diff --git a/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs index 048a6e2965..c49668451a 100644 --- a/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs +++ b/src/Umbraco.Web.Common/ModelsBuilder/RefreshingRazorViewEngine.cs @@ -69,9 +69,10 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder /// This is used so that when new PureLive models are built, the entire razor stack is re-constructed so all razor /// caches and assembly references, etc... are cleared. /// - internal class RefreshingRazorViewEngine : IRazorViewEngine + internal class RefreshingRazorViewEngine : IRazorViewEngine, IDisposable { private IRazorViewEngine _current; + private bool _disposedValue; private readonly PureLiveModelFactory _pureLiveModelFactory; private readonly Func _defaultRazorViewEngineFactory; private readonly ReaderWriterLockSlim _locker = new ReaderWriterLockSlim(); @@ -172,5 +173,24 @@ namespace Umbraco.Cms.Web.Common.ModelsBuilder _locker.ExitReadLock(); } } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _locker.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + } } } diff --git a/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs b/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs new file mode 100644 index 0000000000..ea2fc8c3e7 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/BackOfficeSecurityAccessor.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + public class BackOfficeSecurityAccessor : IBackOfficeSecurityAccessor + { + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + public BackOfficeSecurityAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; + + /// + /// Gets or sets the object. + /// + public IBackOfficeSecurity BackOfficeSecurity + { + get => _httpContextAccessor.HttpContext?.Features.Get(); + set => _httpContextAccessor.HttpContext?.Features.Set(value); + } + } +} diff --git a/src/Umbraco.Web.Common/Security/UmbracoWebsiteSecurityAccessor.cs b/src/Umbraco.Web.Common/Security/UmbracoWebsiteSecurityAccessor.cs new file mode 100644 index 0000000000..2f323c8512 --- /dev/null +++ b/src/Umbraco.Web.Common/Security/UmbracoWebsiteSecurityAccessor.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Http; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.Web.Common.Security +{ + + public class UmbracoWebsiteSecurityAccessor : IUmbracoWebsiteSecurityAccessor + { + private readonly IHttpContextAccessor _httpContextAccessor; + + /// + /// Initializes a new instance of the class. + /// + public UmbracoWebsiteSecurityAccessor(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; + + /// + /// Gets or sets the object. + /// + public IUmbracoWebsiteSecurity WebsiteSecurity + { + get => _httpContextAccessor.HttpContext?.Features.Get(); + set => _httpContextAccessor.HttpContext?.Features.Set(value); + } + } +}