diff --git a/src/Umbraco.Abstractions/SafeCallContext.cs b/src/Umbraco.Abstractions/SafeCallContext.cs index 7a64f3f7b5..ee1416e833 100644 --- a/src/Umbraco.Abstractions/SafeCallContext.cs +++ b/src/Umbraco.Abstractions/SafeCallContext.cs @@ -4,6 +4,11 @@ using System.Collections.Generic; namespace Umbraco.Core { + // TODO: This may no longer be necessary but I'm unsure. This is based on the premise of using the old CallContext which + // requires all objects to be serializable. Stephane wrote a blog post here https://www.zpqrtbnk.net/posts/putting-things-in-contexts/ + // about this. But now we are not using the CallContext since it doesn't exist and instead using AsyncLocal which probably + // doesn't have this problem... but that would require some testing. For now we'll leave this class here until we + // can acquire/discover more info. public class SafeCallContext : IDisposable { private static readonly List> EnterFuncs = new List>(); diff --git a/src/Umbraco.Infrastructure/Scoping/CallContext.cs b/src/Umbraco.Infrastructure/Scoping/CallContext.cs index 7b256434cd..6a5354fb04 100644 --- a/src/Umbraco.Infrastructure/Scoping/CallContext.cs +++ b/src/Umbraco.Infrastructure/Scoping/CallContext.cs @@ -8,34 +8,30 @@ namespace Umbraco.Core.Scoping /// Provides a way to set contextual data that flows with the call and /// async context of a test or invocation. /// - public static class CallContext + public static class CallContext { - private static readonly ConcurrentDictionary _state = new ConcurrentDictionary(); + static ConcurrentDictionary> _state = new ConcurrentDictionary>(); /// /// Stores a given object and associates it with the specified name. /// /// The name with which to associate the new item in the call context. /// The object to store in the call context. - public static void SetData(string name, Guid? data) - { - _state[name + Thread.CurrentThread.ManagedThreadId] = data; - } - + public static void SetData(string name, T data) => _state.GetOrAdd(name, _ => new AsyncLocal()).Value = data; /// - /// Retrieves an object with the specified name from the . + /// Retrieves an object with the specified name from the . /// + /// The type of the data being retrieved. Must match the type used when the was set via . /// The name of the item in the call context. - /// The object in the call context associated with the specified name, or if not found. - public static Guid? GetData(string name) - { - return _state.TryGetValue(name + Thread.CurrentThread.ManagedThreadId, out var data) ? data : null; - } + /// The object in the call context associated with the specified name, or a default value for if none is found. + public static T GetData(string name) => _state.TryGetValue(name, out var data) ? data.Value : default; - public static bool RemoveData(string name) - { - return _state.TryRemove(name+ Thread.CurrentThread.ManagedThreadId, out _); - } + /// + /// Clears the state from for the given name. + /// + /// + /// + public static bool RemoveData(string name) => _state.TryRemove(name, out _); } } diff --git a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs index ef3afb2dac..7ae8c63905 100644 --- a/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs +++ b/src/Umbraco.Infrastructure/Scoping/ScopeProvider.cs @@ -75,6 +75,9 @@ namespace Umbraco.Core.Scoping #region Context + // TODO: I don't think this whole thing is necessary anymore since we are using AsyncLocal which + // I don't believe has the same odd requirements as the old CallContext! Also see SafeCallContext + // objects that go into the logical call context better be serializable else they'll eventually // cause issues whenever some cross-AppDomain code executes - could be due to ReSharper running // tests, any other things (see https://msdn.microsoft.com/en-us/library/dn458353(v=vs.110).aspx), @@ -88,6 +91,7 @@ namespace Umbraco.Core.Scoping // and we can retrieve the actual objects from the table. // only issue: how are we supposed to clear the table? we can't, really. objects should take // care of de-registering themselves from context. + // see https://www.zpqrtbnk.net/posts/putting-things-in-contexts/ private static readonly object StaticCallContextObjectsLock = new object(); private static readonly Dictionary StaticCallContextObjects @@ -110,12 +114,12 @@ namespace Umbraco.Core.Scoping private static T GetCallContextObject(string key) where T : class { - var objectKey = CallContext.GetData(key); - if (objectKey is null) return null; + var objectKey = CallContext.GetData(key); + if (objectKey == Guid.Empty) return null; lock (StaticCallContextObjectsLock) { - if (StaticCallContextObjects.TryGetValue(objectKey.Value, out object callContextObject)) + if (StaticCallContextObjects.TryGetValue(objectKey, out object callContextObject)) { #if DEBUG_SCOPES Current.Logger.Debug("Got " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8)); @@ -125,7 +129,7 @@ namespace Umbraco.Core.Scoping } // hard to inject into a static method :( - Current.Logger.Warn("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.Value.ToString("N").Substring(0, 8)); + Current.Logger.Warn("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.ToString("N").Substring(0, 8)); #if DEBUG_SCOPES //Current.Logger.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); #endif @@ -157,16 +161,16 @@ namespace Umbraco.Core.Scoping #endif if (value == null) { - var objectKey = CallContext.GetData(key); - CallContext.RemoveData(key); - if (objectKey is null) return; + var objectKey = CallContext.GetData(key); + CallContext.RemoveData(key); + if (objectKey == Guid.Empty) return; lock (StaticCallContextObjectsLock) { #if DEBUG_SCOPES Current.Logger.Debug("Remove Object " + objectKey.ToString("N").Substring(0, 8)); //Current.Logger.Debug("At:\r\n" + Head(Environment.StackTrace, 24)); #endif - StaticCallContextObjects.Remove(objectKey.Value); + StaticCallContextObjects.Remove(objectKey); } } else @@ -183,7 +187,7 @@ namespace Umbraco.Core.Scoping #endif StaticCallContextObjects.Add(objectKey, value); } - CallContext.SetData(key, objectKey); + CallContext.SetData(key, objectKey); } } diff --git a/src/Umbraco.Tests/Scoping/ScopeTests.cs b/src/Umbraco.Tests/Scoping/ScopeTests.cs index d1f77d4ae0..0f35554472 100644 --- a/src/Umbraco.Tests/Scoping/ScopeTests.cs +++ b/src/Umbraco.Tests/Scoping/ScopeTests.cs @@ -8,10 +8,7 @@ using Umbraco.Core.Persistence; using Umbraco.Core.Scoping; using Umbraco.Tests.TestHelpers; using Umbraco.Tests.Testing; -using CallContext = Umbraco.Core.Scoping.CallContext; - -//using CallContext = Umbraco.Core.Scoping.CallContext; - +using CallContext = Umbraco.Core.Scoping.CallContext; namespace Umbraco.Tests.Scoping { @@ -126,7 +123,7 @@ namespace Umbraco.Tests.Scoping Assert.AreSame(scope, ((Scope) nested).ParentScope); // it's moved over to call context - var callContextKey = CallContext.GetData(ScopeProvider.ScopeItemKey).AsGuid(); + var callContextKey = CallContext.GetData(ScopeProvider.ScopeItemKey); Assert.AreNotEqual(Guid.Empty, callContextKey); // only if Core.DEBUG_SCOPES are defined