Merge remote-tracking branch 'origin/netcore/dev' into netcore/feature/AB4227-move-files-to-infrastructure
This commit is contained in:
@@ -4,6 +4,14 @@ using System.Collections.Generic;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public class SafeCallContext : IDisposable
|
||||
{
|
||||
private static readonly List<Func<object>> EnterFuncs = new List<Func<object>>();
|
||||
|
||||
@@ -5,37 +5,35 @@ using System.Threading;
|
||||
namespace Umbraco.Core.Scoping
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides a way to set contextual data that flows with the call and
|
||||
/// async context of a test or invocation.
|
||||
/// Represents ambient data that is local to a given asynchronous control flow, such as an asynchronous method.
|
||||
/// </summary>
|
||||
public static class CallContext
|
||||
/// <remarks>
|
||||
/// This is just a simple wrapper around <seealso cref="AsyncLocal{T}"/>
|
||||
/// </remarks>
|
||||
public static class CallContext<T>
|
||||
{
|
||||
private static readonly ConcurrentDictionary<string, Guid?> _state = new ConcurrentDictionary<string, Guid?>();
|
||||
private static ConcurrentDictionary<string, AsyncLocal<T>> _state = new ConcurrentDictionary<string, AsyncLocal<T>>();
|
||||
|
||||
/// <summary>
|
||||
/// Stores a given object and associates it with the specified name.
|
||||
/// </summary>
|
||||
/// <param name="name">The name with which to associate the new item in the call context.</param>
|
||||
/// <param name="data">The object to store in the call context.</param>
|
||||
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<T>()).Value = data;
|
||||
|
||||
/// <summary>
|
||||
/// Retrieves an object with the specified name from the <see cref="CallContext"/>.
|
||||
/// Retrieves an object with the specified name from the <see cref="CallContext{T}"/>.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the data being retrieved. Must match the type used when the <paramref name="name"/> was set via <see cref="SetData{T}(string, T)"/>.</typeparam>
|
||||
/// <param name="name">The name of the item in the call context.</param>
|
||||
/// <returns>The object in the call context associated with the specified name, or <see langword="null"/> if not found.</returns>
|
||||
public static Guid? GetData(string name)
|
||||
{
|
||||
return _state.TryGetValue(name + Thread.CurrentThread.ManagedThreadId, out var data) ? data : null;
|
||||
}
|
||||
/// <returns>The object in the call context associated with the specified name, or a default value for <typeparamref name="T"/> if none is found.</returns>
|
||||
public static T GetData(string name) => _state.TryGetValue(name, out var data) ? data.Value : default;
|
||||
|
||||
// NOTE: If you have used the old CallContext in the past you might be thinking you need to clean this up but that is not the case.
|
||||
// With CallContext you had to call FreeNamedDataSlot to prevent leaks but with AsyncLocal this is not the case, there is no way to clean this up.
|
||||
// The above dictionary is sort of a trick because sure, there is always going to be a string key that will exist in the collection but the values
|
||||
// themselves are managed per ExecutionContext so they don't build up.
|
||||
// There's an SO article relating to this here https://stackoverflow.com/questions/36511243/safety-of-asynclocal-in-asp-net-core
|
||||
|
||||
public static bool RemoveData(string name)
|
||||
{
|
||||
return _state.TryRemove(name+ Thread.CurrentThread.ManagedThreadId, out _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,7 +88,7 @@ namespace Umbraco.Core.Scoping
|
||||
ISqlContext SqlContext { get; }
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
Dictionary<Guid, object> CallContextObjects { get; }
|
||||
|
||||
IEnumerable<ScopeInfo> ScopeInfos { get; }
|
||||
ScopeInfo GetScopeInfo(IScope scope);
|
||||
#endif
|
||||
|
||||
@@ -49,23 +49,23 @@ namespace Umbraco.Core.Scoping
|
||||
SafeCallContext.Register(
|
||||
() =>
|
||||
{
|
||||
var scope = GetCallContextObject<Scope>(ScopeItemKey);
|
||||
var context = GetCallContextObject<ScopeContext>(ContextItemKey);
|
||||
SetCallContextObject(ScopeItemKey, null);
|
||||
SetCallContextObject(ContextItemKey, null);
|
||||
var scope = GetCallContextObject<IScope>(ScopeItemKey);
|
||||
var context = GetCallContextObject<IScopeContext>(ContextItemKey);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, null);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, null);
|
||||
return Tuple.Create(scope, context);
|
||||
},
|
||||
o =>
|
||||
{
|
||||
// cannot re-attached over leaked scope/context
|
||||
if (GetCallContextObject<Scope>(ScopeItemKey) != null)
|
||||
if (GetCallContextObject<IScope>(ScopeItemKey) != null)
|
||||
throw new Exception("Found leaked scope when restoring call context.");
|
||||
if (GetCallContextObject<ScopeContext>(ContextItemKey) != null)
|
||||
if (GetCallContextObject<IScopeContext>(ContextItemKey) != null)
|
||||
throw new Exception("Found leaked context when restoring call context.");
|
||||
|
||||
var t = (Tuple<Scope, ScopeContext>) o;
|
||||
SetCallContextObject(ScopeItemKey, t.Item1);
|
||||
SetCallContextObject(ContextItemKey, t.Item2);
|
||||
var t = (Tuple<IScope, IScopeContext>) o;
|
||||
SetCallContextObject<IScope>(ScopeItemKey, t.Item1);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, t.Item2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -75,65 +75,16 @@ namespace Umbraco.Core.Scoping
|
||||
|
||||
#region Context
|
||||
|
||||
// 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),
|
||||
// but we don't want to make all of our objects serializable since they are *not* meant to be
|
||||
// used in cross-AppDomain scenario anyways.
|
||||
// in addition, whatever goes into the logical call context is serialized back and forth any
|
||||
// time cross-AppDomain code executes, so if we put an "object" there, we'll can *another*
|
||||
// "object" instance - and so we cannot use a random object as a key.
|
||||
// so what we do is: we register a guid in the call context, and we keep a table mapping those
|
||||
// guids to the actual objects. the guid serializes back and forth without causing any issue,
|
||||
// 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.
|
||||
|
||||
private static readonly object StaticCallContextObjectsLock = new object();
|
||||
private static readonly Dictionary<Guid, object> StaticCallContextObjects
|
||||
= new Dictionary<Guid, object>();
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
public Dictionary<Guid, object> CallContextObjects
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (StaticCallContextObjectsLock)
|
||||
{
|
||||
// capture in a dictionary
|
||||
return StaticCallContextObjects.ToDictionary(x => x.Key, x => x.Value);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private static T GetCallContextObject<T>(string key)
|
||||
where T : class
|
||||
where T : class, IInstanceIdentifiable
|
||||
{
|
||||
var objectKey = CallContext.GetData(key);
|
||||
if (objectKey is null) return null;
|
||||
|
||||
lock (StaticCallContextObjectsLock)
|
||||
{
|
||||
if (StaticCallContextObjects.TryGetValue(objectKey.Value, out object callContextObject))
|
||||
{
|
||||
#if DEBUG_SCOPES
|
||||
Current.Logger.Debug<ScopeProvider>("Got " + typeof(T).Name + " Object " + objectKey.ToString("N").Substring(0, 8));
|
||||
//_logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
|
||||
#endif
|
||||
return (T)callContextObject;
|
||||
}
|
||||
|
||||
// hard to inject into a static method :(
|
||||
Current.Logger.Warn<ScopeProvider>("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.Value.ToString("N").Substring(0, 8));
|
||||
#if DEBUG_SCOPES
|
||||
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
|
||||
#endif
|
||||
return null;
|
||||
}
|
||||
var obj = CallContext<T>.GetData(key);
|
||||
if (obj == default(T)) return null;
|
||||
return obj;
|
||||
}
|
||||
|
||||
private static void SetCallContextObject(string key, IInstanceIdentifiable value)
|
||||
private static void SetCallContextObject<T>(string key, T value)
|
||||
where T: class, IInstanceIdentifiable
|
||||
{
|
||||
#if DEBUG_SCOPES
|
||||
// manage the 'context' that contains the scope (null, "http" or "call")
|
||||
@@ -141,14 +92,8 @@ namespace Umbraco.Core.Scoping
|
||||
if (key == ScopeItemKey)
|
||||
{
|
||||
// first, null-register the existing value
|
||||
var ambientKey = CallContext.GetData(ScopeItemKey).AsGuid();
|
||||
object o = null;
|
||||
lock (StaticCallContextObjectsLock)
|
||||
{
|
||||
if (ambientKey != default(Guid))
|
||||
StaticCallContextObjects.TryGetValue(ambientKey, out o);
|
||||
}
|
||||
var ambientScope = o as IScope;
|
||||
var ambientScope = CallContext<IScope>.GetData(ScopeItemKey);
|
||||
|
||||
if (ambientScope != null) RegisterContext(ambientScope, null);
|
||||
// then register the new value
|
||||
var scope = value as IScope;
|
||||
@@ -157,33 +102,18 @@ namespace Umbraco.Core.Scoping
|
||||
#endif
|
||||
if (value == null)
|
||||
{
|
||||
var objectKey = CallContext.GetData(key);
|
||||
CallContext.RemoveData(key);
|
||||
if (objectKey is null) return;
|
||||
lock (StaticCallContextObjectsLock)
|
||||
{
|
||||
#if DEBUG_SCOPES
|
||||
Current.Logger.Debug<ScopeProvider>("Remove Object " + objectKey.ToString("N").Substring(0, 8));
|
||||
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
|
||||
#endif
|
||||
StaticCallContextObjects.Remove(objectKey.Value);
|
||||
}
|
||||
var obj = CallContext<T>.GetData(key);
|
||||
CallContext<T>.SetData(key, default); // aka remove
|
||||
if (obj == null) return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// note - we are *not* detecting an already-existing value
|
||||
// because our code in this class *always* sets to null before
|
||||
// setting to a real value
|
||||
var objectKey = value.InstanceId;
|
||||
lock (StaticCallContextObjectsLock)
|
||||
{
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
Current.Logger.Debug<ScopeProvider>("AddObject " + objectKey.ToString("N").Substring(0, 8));
|
||||
//Current.Logger.Debug<ScopeProvider>("At:\r\n" + Head(Environment.StackTrace, 24));
|
||||
Current.Logger.Debug<ScopeProvider>("AddObject " + value.InstanceId.ToString("N").Substring(0, 8));
|
||||
#endif
|
||||
StaticCallContextObjects.Add(objectKey, value);
|
||||
}
|
||||
CallContext.SetData(key, objectKey);
|
||||
|
||||
CallContext<T>.SetData(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,12 +175,12 @@ namespace Umbraco.Core.Scoping
|
||||
{
|
||||
// clear both
|
||||
SetHttpContextObject(ContextItemKey, null, false);
|
||||
SetCallContextObject(ContextItemKey, null);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, null);
|
||||
if (value == null) return;
|
||||
|
||||
// set http/call context
|
||||
if (SetHttpContextObject(ContextItemKey, value, false) == false)
|
||||
SetCallContextObject(ContextItemKey, value);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,21 +200,22 @@ namespace Umbraco.Core.Scoping
|
||||
public Scope AmbientScope
|
||||
{
|
||||
// try http context, fallback onto call context
|
||||
get => GetHttpContextObject<Scope>(ScopeItemKey, false)
|
||||
?? GetCallContextObject<Scope>(ScopeItemKey);
|
||||
// we are casting here because we know its a concrete type
|
||||
get => (Scope)GetHttpContextObject<IScope>(ScopeItemKey, false)
|
||||
?? (Scope)GetCallContextObject<IScope>(ScopeItemKey);
|
||||
set
|
||||
{
|
||||
// clear both
|
||||
SetHttpContextObject(ScopeItemKey, null, false);
|
||||
SetHttpContextObject(ScopeRefItemKey, null, false);
|
||||
SetCallContextObject(ScopeItemKey, null);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, null);
|
||||
if (value == null) return;
|
||||
|
||||
// set http/call context
|
||||
if (value.CallContext == false && SetHttpContextObject(ScopeItemKey, value, false))
|
||||
SetHttpContextObject(ScopeRefItemKey, _scopeReference);
|
||||
else
|
||||
SetCallContextObject(ScopeItemKey, value);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,9 +226,9 @@ namespace Umbraco.Core.Scoping
|
||||
// clear all
|
||||
SetHttpContextObject(ScopeItemKey, null, false);
|
||||
SetHttpContextObject(ScopeRefItemKey, null, false);
|
||||
SetCallContextObject(ScopeItemKey, null);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, null);
|
||||
SetHttpContextObject(ContextItemKey, null, false);
|
||||
SetCallContextObject(ContextItemKey, null);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, null);
|
||||
if (scope == null)
|
||||
{
|
||||
if (context != null)
|
||||
@@ -312,8 +243,8 @@ namespace Umbraco.Core.Scoping
|
||||
}
|
||||
else
|
||||
{
|
||||
SetCallContextObject(ScopeItemKey, scope);
|
||||
SetCallContextObject(ContextItemKey, context);
|
||||
SetCallContextObject<IScope>(ScopeItemKey, scope);
|
||||
SetCallContextObject<IScopeContext>(ContextItemKey, context);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Runtime.Remoting.Messaging;
|
||||
using NUnit.Framework;
|
||||
using NUnit.Framework;
|
||||
using System;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Scoping;
|
||||
|
||||
namespace Umbraco.Tests.CoreThings
|
||||
{
|
||||
@@ -13,10 +14,10 @@ namespace Umbraco.Tests.CoreThings
|
||||
{
|
||||
SafeCallContext.Register(() =>
|
||||
{
|
||||
CallContext.FreeNamedDataSlot("test1");
|
||||
CallContext.FreeNamedDataSlot("test2");
|
||||
CallContext<string>.SetData("test1", null);
|
||||
CallContext<string>.SetData("test2", null);
|
||||
return null;
|
||||
}, o => {});
|
||||
}, o => { });
|
||||
}
|
||||
|
||||
[OneTimeSetUp]
|
||||
@@ -44,10 +45,10 @@ namespace Umbraco.Tests.CoreThings
|
||||
[Test]
|
||||
public void Test1()
|
||||
{
|
||||
CallContext.LogicalSetData("test1", "test1");
|
||||
Assert.IsNull(CallContext.LogicalGetData("test2"));
|
||||
CallContext<string>.SetData("test1", "test1");
|
||||
Assert.IsNull(CallContext<string>.GetData("test2"));
|
||||
|
||||
CallContext.LogicalSetData("test3b", "test3b");
|
||||
CallContext<string>.SetData("test3b", "test3b");
|
||||
|
||||
if (_first)
|
||||
{
|
||||
@@ -55,21 +56,21 @@ namespace Umbraco.Tests.CoreThings
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotNull(CallContext.LogicalGetData("test3a")); // leak!
|
||||
Assert.IsNotNull(CallContext<string>.GetData("test3a")); // leak!
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test2()
|
||||
{
|
||||
CallContext.LogicalSetData("test2", "test2");
|
||||
Assert.IsNull(CallContext.LogicalGetData("test1"));
|
||||
CallContext<string>.SetData("test2", "test2");
|
||||
Assert.IsNull(CallContext<string>.GetData("test1"));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Test3()
|
||||
{
|
||||
CallContext.LogicalSetData("test3a", "test3a");
|
||||
CallContext<string>.SetData("test3a", "test3a");
|
||||
|
||||
if (_first)
|
||||
{
|
||||
@@ -77,7 +78,7 @@ namespace Umbraco.Tests.CoreThings
|
||||
}
|
||||
else
|
||||
{
|
||||
Assert.IsNotNull(CallContext.LogicalGetData("test3b")); // leak!
|
||||
Assert.IsNotNull(CallContext<string>.GetData("test3b")); // leak!
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
@@ -45,8 +46,18 @@ namespace Umbraco.Tests.Migrations
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
|
||||
|
||||
|
||||
public IScopeContext Context { get; set; }
|
||||
public ISqlContext SqlContext { get; set; }
|
||||
|
||||
#if DEBUG_SCOPES
|
||||
public ScopeInfo GetScopeInfo(IScope scope)
|
||||
{
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
public IEnumerable<ScopeInfo> ScopeInfos => throw new NotImplementedException();
|
||||
#endif
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
@@ -13,7 +13,7 @@ namespace Umbraco.Tests.Persistence
|
||||
{
|
||||
[TestFixture]
|
||||
[Timeout(60000)]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Serilog)]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest, Logger = UmbracoTestOptions.Logger.Console)]
|
||||
public class LocksTests : TestWithDatabaseBase
|
||||
{
|
||||
protected override void Initialize()
|
||||
|
||||
@@ -1,17 +1,10 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Runtime.Remoting.Messaging;
|
||||
using System.Threading;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Core;
|
||||
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;
|
||||
|
||||
|
||||
namespace Umbraco.Tests.Scoping
|
||||
{
|
||||
@@ -126,8 +119,8 @@ namespace Umbraco.Tests.Scoping
|
||||
Assert.AreSame(scope, ((Scope) nested).ParentScope);
|
||||
|
||||
// it's moved over to call context
|
||||
var callContextKey = CallContext.GetData(ScopeProvider.ScopeItemKey).AsGuid();
|
||||
Assert.AreNotEqual(Guid.Empty, callContextKey);
|
||||
var callContextScope = CallContext<IScope>.GetData(ScopeProvider.ScopeItemKey);
|
||||
Assert.IsNotNull(callContextScope);
|
||||
|
||||
// only if Core.DEBUG_SCOPES are defined
|
||||
//var ccnested = scopeProvider.CallContextObjects[callContextKey];
|
||||
|
||||
Reference in New Issue
Block a user