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);
+ }
+ }
+}