scope - cleanup

This commit is contained in:
Stephan
2017-04-28 09:48:25 +02:00
parent d24b0aa9eb
commit f89ce1ff26
10 changed files with 116 additions and 53 deletions

View File

@@ -5,8 +5,10 @@ using System.Linq;
namespace Umbraco.Core.Events namespace Umbraco.Core.Events
{ {
/// <summary> /// <summary>
/// This event manager supports event cancellation and will raise the events as soon as they are tracked, it does not store tracked events /// An IEventDispatcher that immediately raise all events.
/// </summary> /// </summary>
/// <remarks>This means that events will be raised during the scope transaction,
/// whatever happens, and the transaction could roll back in the end.</remarks>
internal class PassThroughEventDispatcher : IEventDispatcher internal class PassThroughEventDispatcher : IEventDispatcher
{ {
public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null) public bool DispatchCancelable(EventHandler eventHandler, object sender, CancellableEventArgs args, string eventName = null)

View File

@@ -3,14 +3,12 @@ using Umbraco.Core.IO;
namespace Umbraco.Core.Events namespace Umbraco.Core.Events
{ {
/// <summary> /// <summary>
/// This event manager is created for each scope and is aware of if it is nested in an outer scope /// An IEventDispatcher that queues events, and raise them when the scope
/// exits and has been completed.
/// </summary> /// </summary>
/// <remarks> internal class QueuingEventDispatcher : QueuingEventDispatcherBase
/// The outer scope is the only scope that can raise events, the inner scope's will defer to the outer scope
/// </remarks>
internal class ScopeEventDispatcher : ScopeEventDispatcherBase
{ {
public ScopeEventDispatcher() public QueuingEventDispatcher()
: base(true) : base(true)
{ } { }

View File

@@ -4,12 +4,21 @@ using System.Linq;
namespace Umbraco.Core.Events namespace Umbraco.Core.Events
{ {
public abstract class ScopeEventDispatcherBase : IEventDispatcher /// <summary>
/// An IEventDispatcher that queues events.
/// </summary>
/// <remarks>
/// <para>Can raise, or ignore, cancelable events, depending on option.</para>
/// <para>Implementations must override ScopeExitCompleted to define what
/// to do with the events when the scope exits and has been completed.</para>
/// <para>If the scope exits without being completed, events are ignored.</para>
/// </remarks>
public abstract class QueuingEventDispatcherBase : IEventDispatcher
{ {
private List<IEventDefinition> _events; private List<IEventDefinition> _events;
private readonly bool _raiseCancelable; private readonly bool _raiseCancelable;
protected ScopeEventDispatcherBase(bool raiseCancelable) protected QueuingEventDispatcherBase(bool raiseCancelable)
{ {
_raiseCancelable = raiseCancelable; _raiseCancelable = raiseCancelable;
} }

View File

@@ -8,5 +8,6 @@ namespace Umbraco.Core.Persistence.UnitOfWork
IScope Scope { get; } IScope Scope { get; }
EventMessages Messages { get; } EventMessages Messages { get; }
IEventDispatcher Events { get; } IEventDispatcher Events { get; }
void Flush();
} }
} }

View File

@@ -160,7 +160,33 @@ namespace Umbraco.Core.Persistence.UnitOfWork
_key = Guid.NewGuid(); _key = Guid.NewGuid();
} }
public object Key public void Flush()
{
if (_readOnly)
throw new NotSupportedException("This unit of work is read-only.");
while (_operations.Count > 0)
{
var operation = _operations.Dequeue();
switch (operation.Type)
{
case TransactionType.Insert:
operation.Repository.PersistNewItem(operation.Entity);
break;
case TransactionType.Delete:
operation.Repository.PersistDeletedItem(operation.Entity);
break;
case TransactionType.Update:
operation.Repository.PersistUpdatedItem(operation.Entity);
break;
}
}
_operations.Clear();
_key = Guid.NewGuid();
}
public object Key
{ {
get { return _key; } get { return _key; }
} }

View File

@@ -321,7 +321,7 @@ namespace Umbraco.Core.Scoping
{ {
EnsureNotDisposed(); EnsureNotDisposed();
if (ParentScope != null) return ParentScope.Events; if (ParentScope != null) return ParentScope.Events;
return _eventDispatcher ?? (_eventDispatcher = new ScopeEventDispatcher()); return _eventDispatcher ?? (_eventDispatcher = new QueuingEventDispatcher());
} }
} }
@@ -465,6 +465,7 @@ namespace Umbraco.Core.Scoping
} }
finally finally
{ {
// removes the ambient context (ambient scope already gone)
_scopeProvider.SetAmbient(null); _scopeProvider.SetAmbient(null);
} }
} }

View File

@@ -1,16 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Umbraco.Core.Scoping namespace Umbraco.Core.Scoping
{ {
public class ScopeContext : IInstanceIdentifiable public class ScopeContext : IInstanceIdentifiable
{ {
private Dictionary<string, IEnlistedObject> _enlisted; private Dictionary<string, IEnlistedObject> _enlisted;
private bool _exiting;
public void ScopeExit(bool completed) public void ScopeExit(bool completed)
{ {
if (_enlisted == null)
return;
_exiting = true;
List<Exception> exceptions = null; List<Exception> exceptions = null;
foreach (var enlisted in Enlisted.Values) foreach (var enlisted in _enlisted.Values.OrderBy(x => x.Priority))
{ {
try try
{ {
@@ -23,6 +30,7 @@ namespace Umbraco.Core.Scoping
exceptions.Add(e); exceptions.Add(e);
} }
} }
if (exceptions != null) if (exceptions != null)
throw new AggregateException("Exceptions were thrown by listed actions.", exceptions); throw new AggregateException("Exceptions were thrown by listed actions.", exceptions);
} }
@@ -42,73 +50,53 @@ namespace Umbraco.Core.Scoping
private interface IEnlistedObject private interface IEnlistedObject
{ {
void Execute(bool completed); void Execute(bool completed);
int Priority { get; }
} }
private class EnlistedObject<T> : IEnlistedObject private class EnlistedObject<T> : IEnlistedObject
{ {
private readonly Action<bool, T> _action; private readonly Action<bool, T> _action;
public EnlistedObject(T item) public EnlistedObject(T item, Action<bool, T> action, int priority)
{
Item = item;
}
public EnlistedObject(T item, Action<bool, T> action)
{ {
Item = item; Item = item;
Priority = priority;
_action = action; _action = action;
} }
public T Item { get; private set; } public T Item { get; private set; }
public int Priority { get; private set; }
public void Execute(bool completed) public void Execute(bool completed)
{ {
_action(completed, Item); _action(completed, Item);
} }
} }
/// <inheritdoc /> public void Enlist(string key, Action<bool> action, int priority = 100)
public T Enlist<T>(string key, Func<T> creator)
{ {
IEnlistedObject enlisted; Enlist<object>(key, null, (completed, item) => action(completed), priority);
if (Enlisted.TryGetValue(key, out enlisted))
{
var enlistedAs = enlisted as EnlistedObject<T>;
if (enlistedAs == null) throw new Exception("An item with a different type has already been enlisted with the same key.");
return enlistedAs.Item;
}
var enlistedOfT = new EnlistedObject<T>(creator());
Enlisted[key] = enlistedOfT;
return enlistedOfT.Item;
} }
/// <inheritdoc /> public T Enlist<T>(string key, Func<T> creator = null, Action<bool, T> action = null, int priority = 100)
public T Enlist<T>(string key, Func<T> creator, Action<bool, T> action)
{ {
if (_exiting)
throw new InvalidOperationException("Cannot enlist now, context is exiting.");
var enlistedObjects = _enlisted ?? (_enlisted = new Dictionary<string, IEnlistedObject>());
IEnlistedObject enlisted; IEnlistedObject enlisted;
if (Enlisted.TryGetValue(key, out enlisted)) if (enlistedObjects.TryGetValue(key, out enlisted))
{ {
var enlistedAs = enlisted as EnlistedObject<T>; var enlistedAs = enlisted as EnlistedObject<T>;
if (enlistedAs == null) throw new Exception("An item with a different type has already been enlisted with the same key."); if (enlistedAs == null) throw new InvalidOperationException("An item with the key already exists, but with a different type.");
if (enlistedAs.Priority != priority) throw new InvalidOperationException("An item with the key already exits, but with a different priority.");
return enlistedAs.Item; return enlistedAs.Item;
} }
var enlistedOfT = new EnlistedObject<T>(creator(), action); var enlistedOfT = new EnlistedObject<T>(creator == null ? default(T) : creator(), action, priority);
Enlisted[key] = enlistedOfT; Enlisted[key] = enlistedOfT;
return enlistedOfT.Item; return enlistedOfT.Item;
} }
/// <inheritdoc />
public void Enlist(string key, Action<bool> action)
{
IEnlistedObject enlisted;
if (Enlisted.TryGetValue(key, out enlisted))
{
var enlistedAs = enlisted as EnlistedObject<object>;
if (enlistedAs == null) throw new Exception("An item with a different type has already been enlisted with the same key.");
return;
}
var enlistedOfT = new EnlistedObject<object>(null, (completed, item) => action(completed));
Enlisted[key] = enlistedOfT;
}
} }
} }

View File

@@ -337,7 +337,7 @@
<Compile Include="Deploy\IFileTypeCollection.cs" /> <Compile Include="Deploy\IFileTypeCollection.cs" />
<Compile Include="Events\EventDefinitionFilter.cs" /> <Compile Include="Events\EventDefinitionFilter.cs" />
<Compile Include="Events\IDeletingMediaFilesEventArgs.cs" /> <Compile Include="Events\IDeletingMediaFilesEventArgs.cs" />
<Compile Include="Events\ScopeEventDispatcherBase.cs" /> <Compile Include="Events\QueuingEventDispatcherBase.cs" />
<Compile Include="Events\ScopeLifespanMessagesFactory.cs" /> <Compile Include="Events\ScopeLifespanMessagesFactory.cs" />
<Compile Include="Exceptions\ConnectionException.cs" /> <Compile Include="Exceptions\ConnectionException.cs" />
<Compile Include="IHttpContextAccessor.cs" /> <Compile Include="IHttpContextAccessor.cs" />
@@ -377,7 +377,7 @@
<Compile Include="Events\EventNameExtractorResult.cs" /> <Compile Include="Events\EventNameExtractorResult.cs" />
<Compile Include="Events\IEventDefinition.cs" /> <Compile Include="Events\IEventDefinition.cs" />
<Compile Include="Events\PassThroughEventDispatcher.cs" /> <Compile Include="Events\PassThroughEventDispatcher.cs" />
<Compile Include="Events\ScopeEventDispatcher.cs" /> <Compile Include="Events\QueuingEventDispatcher.cs" />
<Compile Include="Events\EventMessage.cs" /> <Compile Include="Events\EventMessage.cs" />
<Compile Include="Events\EventMessages.cs" /> <Compile Include="Events\EventMessages.cs" />
<Compile Include="Events\ExportEventArgs.cs" /> <Compile Include="Events\ExportEventArgs.cs" />

View File

@@ -183,7 +183,7 @@ namespace Umbraco.Tests.Scoping
public static event TypedEventHandler<ScopeEventDispatcherTests, SaveEventArgs<decimal>> DoThing3; public static event TypedEventHandler<ScopeEventDispatcherTests, SaveEventArgs<decimal>> DoThing3;
public class PassiveEventDispatcher : ScopeEventDispatcherBase public class PassiveEventDispatcher : QueuingEventDispatcherBase
{ {
public PassiveEventDispatcher() public PassiveEventDispatcher()
: base(false) : base(false)

View File

@@ -591,7 +591,7 @@ namespace Umbraco.Tests.Scoping
Assert.IsNull(scopeProvider.AmbientContext); Assert.IsNull(scopeProvider.AmbientContext);
// back to original thread // back to original thread
ScopeProvider.HttpContextItemsGetter = () => httpContextItems; ScopeProvider.HttpContextItemsGetter = () => httpContextItems;
} }
Assert.IsNotNull(scopeProvider.AmbientScope); Assert.IsNotNull(scopeProvider.AmbientScope);
Assert.AreSame(scope, scopeProvider.AmbientScope); Assert.AreSame(scope, scopeProvider.AmbientScope);
@@ -656,6 +656,44 @@ namespace Umbraco.Tests.Scoping
Assert.IsNotNull(ambientContext); // the context is still there Assert.IsNotNull(ambientContext); // the context is still there
} }
[TestCase(true)]
[TestCase(false)]
public void ScopeContextEnlistAgain(bool complete)
{
var scopeProvider = DatabaseContext.ScopeProvider;
bool? completed = null;
Exception exception = null;
Assert.IsNull(scopeProvider.AmbientScope);
using (var scope = scopeProvider.CreateScope())
{
scopeProvider.Context.Enlist("name", c =>
{
completed = c;
// at that point the scope is gone, but the context is still there
var ambientContext = scopeProvider.AmbientContext;
try
{
ambientContext.Enlist("another", c2 => { });
}
catch (Exception e)
{
exception = e;
}
});
if (complete)
scope.Complete();
}
Assert.IsNull(scopeProvider.AmbientScope);
Assert.IsNull(scopeProvider.AmbientContext);
Assert.IsNotNull(completed);
Assert.IsNotNull(exception);
Assert.IsInstanceOf<InvalidOperationException>(exception);
}
[Test] [Test]
public void ScopeContextException() public void ScopeContextException()
{ {