AB3791 - Moved scoping to infrastructure, implemented our own CallContext

This commit is contained in:
Bjarke Berg
2019-12-17 15:03:14 +01:00
parent 70adb70afd
commit f0d1c38aa8
17 changed files with 119 additions and 65 deletions

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Core.Scoping
/// <remarks>A scope context can enlist objects that will be attached to the scope, and available
/// for the duration of the scope. In addition, it can enlist actions, that will run when the
/// scope is exiting, and after the database transaction has been committed.</remarks>
public interface IScopeContext
public interface IScopeContext : IInstanceIdentifiable
{
/// <summary>
/// Enlists an action.
@@ -46,5 +46,7 @@ namespace Umbraco.Core.Scoping
/// <param name="key">The object unique identifier.</param>
/// <returns>The enlisted object, if any, else the default value.</returns>
T GetEnlisted<T>(string key);
void ScopeExit(bool completed);
}
}

View File

@@ -155,7 +155,6 @@
<Compile Include="Composing\Current.cs" />
<Compile Include="Composing\LightInject\LightInjectContainer.cs" />
<Compile Include="Composing\LightInject\MixedLightInjectScopeManagerProvider.cs" />
<Compile Include="Events\QueuingEventDispatcher.cs" />
<Compile Include="Logging\Viewer\LogTimePeriod.cs" />
<Compile Include="Manifest\ManifestParser.cs" />
<Compile Include="Manifest\ValueValidatorConverter.cs" />
@@ -215,12 +214,6 @@
<Compile Include="Persistence\Repositories\Implement\LanguageRepositoryExtensions.cs" />
<Compile Include="Migrations\Upgrade\V_8_0_0\RenameLabelAndRichTextPropertyEditorAliases.cs" />
<Compile Include="PublishedContentExtensions.cs" />
<Compile Include="Scoping\IScopeAccessor.cs" />
<Compile Include="Scoping\Scope.cs" />
<Compile Include="Scoping\ScopeContext.cs" />
<Compile Include="Scoping\ScopeContextualBase.cs" />
<Compile Include="Scoping\ScopeProvider.cs" />
<Compile Include="Scoping\ScopeReference.cs" />
<Compile Include="Security\PasswordSecurity.cs" />
<Compile Include="Services\Implement\PropertyValidationService.cs" />
<Compile Include="StringExtensions.cs" />

View File

@@ -1,4 +1,5 @@
using Umbraco.Core.Composing;
using Umbraco.Composing;
using Umbraco.Core.Composing;
using Umbraco.Core.IO;
namespace Umbraco.Core.Events
@@ -7,11 +8,14 @@ namespace Umbraco.Core.Events
/// An IEventDispatcher that queues events, and raise them when the scope
/// exits and has been completed.
/// </summary>
internal class QueuingEventDispatcher : QueuingEventDispatcherBase
public class QueuingEventDispatcher : QueuingEventDispatcherBase
{
public QueuingEventDispatcher()
private readonly IMediaFileSystem _mediaFileSystem;
public QueuingEventDispatcher(IMediaFileSystem mediaFileSystem)
: base(true)
{ }
{
_mediaFileSystem = mediaFileSystem;
}
protected override void ScopeExitCompleted()
{
@@ -28,13 +32,11 @@ namespace Umbraco.Core.Events
// but then where should it be (without making things too complicated)?
var delete = e.Args as IDeletingMediaFilesEventArgs;
if (delete != null && delete.MediaFilesToDelete.Count > 0)
MediaFileSystem.DeleteMediaFiles(delete.MediaFilesToDelete);
_mediaFileSystem.DeleteMediaFiles(delete.MediaFilesToDelete);
}
}
private IMediaFileSystem _mediaFileSystem;
// TODO: inject
private IMediaFileSystem MediaFileSystem => _mediaFileSystem ?? (_mediaFileSystem = Current.MediaFileSystem);
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Concurrent;
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.
/// </summary>
public static class CallContext
{
private static readonly ConcurrentDictionary<string, Guid?> _state = new ConcurrentDictionary<string, Guid?>();
/// <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;
}
/// <summary>
/// Retrieves an object with the specified name from the <see cref="CallContext"/>.
/// </summary>
/// <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;
}
public static bool RemoveData(string name)
{
return _state.TryRemove(name+ Thread.CurrentThread.ManagedThreadId, out _);
}
}
}

View File

@@ -2,6 +2,7 @@
using System.Data;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
@@ -16,6 +17,8 @@ namespace Umbraco.Core.Scoping
internal class Scope : IScope
{
private readonly ScopeProvider _scopeProvider;
private readonly ICoreDebug _coreDebug;
private readonly IMediaFileSystem _mediaFileSystem;
private readonly ILogger _logger;
private readonly ITypeFinder _typeFinder;
@@ -36,7 +39,9 @@ namespace Umbraco.Core.Scoping
// initializes a new scope
private Scope(ScopeProvider scopeProvider,
ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, Scope parent, ScopeContext scopeContext, bool detachable,
ICoreDebug coreDebug,
IMediaFileSystem mediaFileSystem,
ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, Scope parent, IScopeContext scopeContext, bool detachable,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
@@ -45,6 +50,8 @@ namespace Umbraco.Core.Scoping
bool autoComplete = false)
{
_scopeProvider = scopeProvider;
_coreDebug = coreDebug;
_mediaFileSystem = mediaFileSystem;
_logger = logger;
_typeFinder = typeFinder;
@@ -111,18 +118,22 @@ namespace Umbraco.Core.Scoping
// initializes a new scope
public Scope(ScopeProvider scopeProvider,
ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, bool detachable, ScopeContext scopeContext,
ICoreDebug coreDebug,
IMediaFileSystem mediaFileSystem,
ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, bool detachable, IScopeContext scopeContext,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, logger, typeFinder, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
: this(scopeProvider, coreDebug, mediaFileSystem, logger, typeFinder, fileSystems, null, scopeContext, detachable, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
// initializes a new scope in a nested scopes chain, with its parent
public Scope(ScopeProvider scopeProvider,
ICoreDebug coreDebug,
IMediaFileSystem mediaFileSystem,
ILogger logger, ITypeFinder typeFinder, FileSystems fileSystems, Scope parent,
IsolationLevel isolationLevel = IsolationLevel.Unspecified,
RepositoryCacheMode repositoryCacheMode = RepositoryCacheMode.Unspecified,
@@ -130,7 +141,7 @@ namespace Umbraco.Core.Scoping
bool? scopeFileSystems = null,
bool callContext = false,
bool autoComplete = false)
: this(scopeProvider, logger, typeFinder, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
: this(scopeProvider, coreDebug, mediaFileSystem, logger, typeFinder, fileSystems, parent, null, false, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete)
{ }
public Guid InstanceId { get; } = Guid.NewGuid();
@@ -194,10 +205,10 @@ namespace Umbraco.Core.Scoping
public Scope OrigScope { get; set; }
// the original context (when attaching a detachable scope)
public ScopeContext OrigContext { get; set; }
public IScopeContext OrigContext { get; set; }
// the context (for attaching & detaching only)
public ScopeContext Context { get; }
public IScopeContext Context { get; }
public IsolationLevel IsolationLevel
{
@@ -289,7 +300,7 @@ namespace Umbraco.Core.Scoping
{
EnsureNotDisposed();
if (ParentScope != null) return ParentScope.Events;
return _eventDispatcher ?? (_eventDispatcher = new QueuingEventDispatcher());
return _eventDispatcher ?? (_eventDispatcher = new QueuingEventDispatcher(_mediaFileSystem));
}
}
@@ -484,8 +495,8 @@ namespace Umbraco.Core.Scoping
// caching config
// true if Umbraco.CoreDebug.LogUncompletedScope appSetting is set to "true"
private static bool LogUncompletedScopes => (_logUncompletedScopes
?? (_logUncompletedScopes = Current.Configs.CoreDebug().LogUncompletedScopes)).Value;
private bool LogUncompletedScopes => (_logUncompletedScopes
?? (_logUncompletedScopes = _coreDebug.LogUncompletedScopes)).Value;
/// <inheritdoc />
public void ReadLock(params int[] lockIds) => Database.SqlContext.SqlSyntax.ReadLock(Database, lockIds);

View File

@@ -1,13 +1,15 @@
using System;
using System.Collections.Generic;
using System.Data;
using System.Runtime.Remoting.Messaging;
using Umbraco.Core.Cache;
using Umbraco.Core.Composing;
using Umbraco.Core.Configuration;
using Umbraco.Core.Events;
using Umbraco.Core.IO;
using Umbraco.Core.Logging;
using Umbraco.Core.Persistence;
using Current = Umbraco.Composing.Current;
#if DEBUG_SCOPES
using System.Linq;
using System.Text;
@@ -24,15 +26,18 @@ namespace Umbraco.Core.Scoping
private readonly ITypeFinder _typeFinder;
private readonly IRequestCache _requestCache;
private readonly FileSystems _fileSystems;
private readonly ICoreDebug _coreDebug;
private readonly IMediaFileSystem _mediaFileSystem;
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, ILogger logger, ITypeFinder typeFinder, IRequestCache requestCache)
public ScopeProvider(IUmbracoDatabaseFactory databaseFactory, FileSystems fileSystems, ICoreDebug coreDebug, IMediaFileSystem mediaFileSystem, ILogger logger, ITypeFinder typeFinder, IRequestCache requestCache)
{
DatabaseFactory = databaseFactory;
_fileSystems = fileSystems;
_coreDebug = coreDebug;
_mediaFileSystem = mediaFileSystem;
_logger = logger;
_typeFinder = typeFinder;
_requestCache = requestCache;
// take control of the FileSystems
_fileSystems.IsScoped = () => AmbientScope != null && AmbientScope.ScopedFileSystems;
@@ -105,12 +110,12 @@ namespace Umbraco.Core.Scoping
private static T GetCallContextObject<T>(string key)
where T : class
{
var objectKey = CallContext.LogicalGetData(key).AsGuid();
if (objectKey == Guid.Empty) return null;
var objectKey = CallContext.GetData(key);
if (objectKey is null) return null;
lock (StaticCallContextObjectsLock)
{
if (StaticCallContextObjects.TryGetValue(objectKey, out object callContextObject))
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));
@@ -120,7 +125,7 @@ namespace Umbraco.Core.Scoping
}
// hard to inject into a static method :(
Current.Logger.Warn<ScopeProvider>("Missed {TypeName} Object {ObjectKey}", typeof(T).Name, objectKey.ToString("N").Substring(0, 8));
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
@@ -136,7 +141,7 @@ namespace Umbraco.Core.Scoping
if (key == ScopeItemKey)
{
// first, null-register the existing value
var ambientKey = CallContext.LogicalGetData(ScopeItemKey).AsGuid();
var ambientKey = CallContext.GetData(ScopeItemKey).AsGuid();
object o = null;
lock (StaticCallContextObjectsLock)
{
@@ -152,16 +157,16 @@ namespace Umbraco.Core.Scoping
#endif
if (value == null)
{
var objectKey = CallContext.LogicalGetData(key).AsGuid();
CallContext.FreeNamedDataSlot(key);
if (objectKey == default) return;
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);
StaticCallContextObjects.Remove(objectKey.Value);
}
}
else
@@ -178,7 +183,7 @@ namespace Umbraco.Core.Scoping
#endif
StaticCallContextObjects.Add(objectKey, value);
}
CallContext.LogicalSetData(key, objectKey);
CallContext.SetData(key, objectKey);
}
}
@@ -228,13 +233,13 @@ namespace Umbraco.Core.Scoping
internal const string ContextItemKey = "Umbraco.Core.Scoping.ScopeContext";
public ScopeContext AmbientContext
public IScopeContext AmbientContext
{
get
{
// try http context, fallback onto call context
var value = GetHttpContextObject<ScopeContext>(ContextItemKey, false);
return value ?? GetCallContextObject<ScopeContext>(ContextItemKey);
var value = GetHttpContextObject<IScopeContext>(ContextItemKey, false);
return value ?? GetCallContextObject<IScopeContext>(ContextItemKey);
}
set
{
@@ -285,7 +290,7 @@ namespace Umbraco.Core.Scoping
#endregion
public void SetAmbient(Scope scope, ScopeContext context = null)
public void SetAmbient(Scope scope, IScopeContext context = null)
{
// clear all
SetHttpContextObject(ScopeItemKey, null, false);
@@ -319,7 +324,7 @@ namespace Umbraco.Core.Scoping
IEventDispatcher eventDispatcher = null,
bool? scopeFileSystems = null)
{
return new Scope(this, _logger, _typeFinder, _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
return new Scope(this, _coreDebug, _mediaFileSystem, _logger, _typeFinder, _fileSystems, true, null, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems);
}
/// <inheritdoc />
@@ -375,13 +380,13 @@ namespace Umbraco.Core.Scoping
{
var ambientContext = AmbientContext;
var newContext = ambientContext == null ? new ScopeContext() : null;
var scope = new Scope(this, _logger, _typeFinder, _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
var scope = new Scope(this, _coreDebug, _mediaFileSystem, _logger, _typeFinder, _fileSystems, false, newContext, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
// assign only if scope creation did not throw!
SetAmbient(scope, newContext ?? ambientContext);
return scope;
}
var nested = new Scope(this, _logger, _typeFinder, _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
var nested = new Scope(this, _coreDebug, _mediaFileSystem, _logger, _typeFinder, _fileSystems, ambientScope, isolationLevel, repositoryCacheMode, eventDispatcher, scopeFileSystems, callContext, autoComplete);
SetAmbient(nested, AmbientContext);
return nested;
}

View File

@@ -28,18 +28,7 @@
<ItemGroup>
<_UnmanagedRegistrationCache Remove="obj\Umbraco.Infrastructure.csproj.UnmanagedRegistration.cache" />
</ItemGroup>
<ItemGroup>
<Compile Remove="obj\**" />
<Compile Remove="Scoping\ScopeReference.cs" />
<Compile Remove="Scoping\ScopeProvider.cs" />
<Compile Remove="Scoping\ScopeContext.cs" />
<Compile Remove="Persistence\FaultHandling\SqlAzureTransientErrorDetectionStrategy.cs" />
<Compile Remove="Persistence\FaultHandling\NetworkConnectivityErrorDetectionStrategy.cs" />
<Compile Remove="Persistence\FaultHandling\Incremental.cs" />
<Compile Remove="Persistence\FaultHandling\FixedInterval.cs" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="obj\**" />
</ItemGroup>
@@ -65,8 +54,11 @@
</ItemGroup>
<ItemGroup>
<Folder Include="Events" />
<Folder Include="Models\Identity" />
</ItemGroup>
<ItemGroup>
<Compile Remove="obj\**" />
</ItemGroup>
</Project>

View File

@@ -38,7 +38,9 @@ namespace Umbraco.Tests.Components
var typeFinder = new TypeFinder(logger);
var f = new UmbracoDatabaseFactory(logger, new Lazy<IMapperCollection>(() => new MapperCollection(Enumerable.Empty<BaseMapper>())), TestHelper.GetConfigs(), TestHelper.DbProviderFactoryCreator, TestHelper.BulkSqlInsertProvider);
var fs = new FileSystems(mock.Object, logger, TestHelper.IOHelper, SettingsForTests.GenerateMockGlobalSettings());
var p = new ScopeProvider(f, fs, logger, typeFinder, NoAppCache.Instance);
var coreDebug = Mock.Of<ICoreDebug>();
var mediaFileSystem = Mock.Of<IMediaFileSystem>();
var p = new ScopeProvider(f, fs, coreDebug, mediaFileSystem, logger, typeFinder, NoAppCache.Instance);
mock.Setup(x => x.GetInstance(typeof (ILogger))).Returns(logger);
mock.Setup(x => x.GetInstance(typeof (IProfilingLogger))).Returns(new ProfilingLogger(Mock.Of<ILogger>(), Mock.Of<IProfiler>()));

View File

@@ -337,7 +337,7 @@ namespace Umbraco.Tests.Scoping
{
var counter = 0;
IScope ambientScope = null;
ScopeContext ambientContext = null;
IScopeContext ambientContext = null;
Guid value = Guid.Empty;
var scopeProvider = _testObjects.GetScopeProvider(Mock.Of<ILogger>()) as ScopeProvider;

View File

@@ -8,7 +8,10 @@ using Umbraco.Core.Persistence;
using Umbraco.Core.Scoping;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.Testing;
using ScopeProviderStatic = Umbraco.Core.Scoping.ScopeProvider;
using CallContext = Umbraco.Core.Scoping.CallContext;
//using CallContext = Umbraco.Core.Scoping.CallContext;
namespace Umbraco.Tests.Scoping
{
@@ -123,7 +126,7 @@ namespace Umbraco.Tests.Scoping
Assert.AreSame(scope, ((Scope) nested).ParentScope);
// it's moved over to call context
var callContextKey = CallContext.LogicalGetData(ScopeProviderStatic.ScopeItemKey).AsGuid();
var callContextKey = CallContext.GetData(ScopeProvider.ScopeItemKey).AsGuid();
Assert.AreNotEqual(Guid.Empty, callContextKey);
// only if Core.DEBUG_SCOPES are defined
@@ -485,7 +488,7 @@ namespace Umbraco.Tests.Scoping
bool? completed = null;
IScope ambientScope = null;
ScopeContext ambientContext = null;
IScopeContext ambientContext = null;
Assert.IsNull(scopeProvider.AmbientScope);
using (var scope = scopeProvider.CreateScope())

View File

@@ -248,7 +248,9 @@ namespace Umbraco.Tests.TestHelpers
typeFinder = typeFinder ?? new TypeFinder(logger);
fileSystems = fileSystems ?? new FileSystems(Current.Factory, logger, TestHelper.IOHelper, SettingsForTests.GenerateMockGlobalSettings());
var scopeProvider = new ScopeProvider(databaseFactory, fileSystems, logger, typeFinder, NoAppCache.Instance);
var coreDebug = Current.Configs.CoreDebug();
var mediaFileSystem = Mock.Of<IMediaFileSystem>();
var scopeProvider = new ScopeProvider(databaseFactory, fileSystems, coreDebug, mediaFileSystem, logger, typeFinder, NoAppCache.Instance);
return scopeProvider;
}

View File

@@ -23,7 +23,6 @@ using Umbraco.Web.Security;
using Umbraco.Web.Routing;
using File = System.IO.File;
using Umbraco.Core.Composing;
using Umbraco.Core.Hosting;
using Umbraco.Core.Persistence.Mappers;
using Umbraco.Core.Scoping;
using Umbraco.Tests.Testing;

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Threading;
using System.Xml.Linq;
using Examine;
using Moq;
@@ -64,6 +65,7 @@ namespace Umbraco.Tests.Testing
/// provides all the necessary environment, through DI. Yes, DI is bad in tests - unit tests.
/// But it is OK in integration tests.
/// </remarks>
[Apartment(ApartmentState.STA)]
public abstract class UmbracoTestBase
{
// this class