diff --git a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs index 14f00998f9..a5794af42e 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/ChildUnitOfWork.cs @@ -2,11 +2,16 @@ using System; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Volo.ExtensionMethods; namespace Volo.Abp.Uow { internal class ChildUnitOfWork : IUnitOfWork { + public event EventHandler Completed; + public event EventHandler Failed; + public event EventHandler Disposed; + public IServiceProvider ServiceProvider => _parent.ServiceProvider; private readonly IUnitOfWork _parent; @@ -16,6 +21,10 @@ namespace Volo.Abp.Uow Check.NotNull(parent, nameof(parent)); _parent = parent; + + _parent.Completed += (sender, args) => { Completed.InvokeSafely(sender, args); }; + _parent.Failed += (sender, args) => { Failed.InvokeSafely(sender, args); }; + _parent.Disposed += (sender, args) => { Disposed.InvokeSafely(sender, args); }; } public void SaveChanges() @@ -30,7 +39,7 @@ namespace Volo.Abp.Uow public void Complete() { - _parent.Complete(); + } public Task CompleteAsync(CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/Volo.Abp/Volo/Abp/Uow/IAmbientUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/IAmbientUnitOfWork.cs index 894576ab71..2293eecc44 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IAmbientUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IAmbientUnitOfWork.cs @@ -1,9 +1,6 @@ -using JetBrains.Annotations; - -namespace Volo.Abp.Uow +namespace Volo.Abp.Uow { public interface IAmbientUnitOfWork : IUnitOfWorkAccessor { - void SetUnitOfWork([CanBeNull] IUnitOfWork unitOfWork); } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs index ed13406e6c..ed75210680 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IBasicUnitOfWork.cs @@ -7,6 +7,12 @@ namespace Volo.Abp.Uow //Find a better naming :( public interface IBasicUnitOfWork : IDisposable { + event EventHandler Completed; + + event EventHandler Failed; + + event EventHandler Disposed; + void SaveChanges(); Task SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken)); diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs index 26dab4c8ee..01d6baeb2f 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWork.cs @@ -2,5 +2,6 @@ { public interface IUnitOfWork : IBasicUnitOfWork, IDatabaseApiContainer { + } } diff --git a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkAccessor.cs b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkAccessor.cs index c94fc74027..861d4e05fe 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkAccessor.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/IUnitOfWorkAccessor.cs @@ -6,5 +6,7 @@ namespace Volo.Abp.Uow { [CanBeNull] IUnitOfWork UnitOfWork { get; } + + void SetUnitOfWork([CanBeNull] IUnitOfWork unitOfWork); } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs index 208adcfa68..f128c7d470 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWork.cs @@ -3,22 +3,27 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Volo.DependencyInjection; +using Volo.ExtensionMethods; using Volo.ExtensionMethods.Collections.Generic; namespace Volo.Abp.Uow { public class UnitOfWork : IUnitOfWork, ITransientDependency { + public event EventHandler Completed; + public event EventHandler Failed; + public event EventHandler Disposed; + public IServiceProvider ServiceProvider { get; } private readonly Dictionary _databaseApis; - private readonly IAmbientUnitOfWork _ambientUnitOfWork; + private Exception _exception; + private bool _isCompleted; private bool _isDisposed; - public UnitOfWork(IServiceProvider serviceProvider, IAmbientUnitOfWork ambientUnitOfWork) + public UnitOfWork(IServiceProvider serviceProvider) { - _ambientUnitOfWork = ambientUnitOfWork; ServiceProvider = serviceProvider; _databaseApis = new Dictionary(); @@ -32,7 +37,13 @@ namespace Volo.Abp.Uow } _isDisposed = true; - _ambientUnitOfWork.SetUnitOfWork(null); + + if (!_isCompleted || _exception != null) + { + OnFailed(_exception); + } + + OnDisposed(); } public void SaveChanges() @@ -56,12 +67,33 @@ namespace Volo.Abp.Uow public void Complete() { - SaveChanges(); + PreventMultipleComplete(); + try + { + SaveChanges(); + OnCompleted(); + } + catch (Exception ex) + { + _exception = ex; + throw; + } } public async Task CompleteAsync(CancellationToken cancellationToken = default(CancellationToken)) { - await SaveChangesAsync(cancellationToken); + PreventMultipleComplete(); + + try + { + await SaveChangesAsync(cancellationToken); + OnCompleted(); + } + catch (Exception ex) + { + _exception = ex; + throw; + } } public IDatabaseApi FindDatabaseApi(string id) @@ -76,5 +108,30 @@ namespace Volo.Abp.Uow return _databaseApis.GetOrAdd(id, factory); } + + protected virtual void OnCompleted() + { + Completed.InvokeSafely(this); + } + + protected virtual void OnFailed(Exception exception) + { + Failed.InvokeSafely(this, new UnitOfWorkFailedEventArgs(exception)); + } + + protected virtual void OnDisposed() + { + Disposed.InvokeSafely(this); + } + + private void PreventMultipleComplete() + { + if (_isCompleted) + { + throw new AbpException("Complete is called before!"); + } + + _isCompleted = true; + } } } \ No newline at end of file diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs new file mode 100644 index 0000000000..870f125f4f --- /dev/null +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkFailedEventArgs.cs @@ -0,0 +1,28 @@ +using System; +using JetBrains.Annotations; + +namespace Volo.Abp.Uow +{ + /// + /// Used as event arguments on event. + /// + public class UnitOfWorkFailedEventArgs : EventArgs + { + /// + /// Exception that caused failure. This is set only if an error occured during . + /// Can be null if there is no exception, but is not called. + /// Can be null if another exception occurred during the UOW. + /// + [CanBeNull] + public Exception Exception { get; private set; } + + /// + /// Creates a new object. + /// + /// Exception that caused failure + public UnitOfWorkFailedEventArgs([CanBeNull] Exception exception) + { + Exception = exception; + } + } +} diff --git a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs index cb147809a1..97e4819631 100644 --- a/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs +++ b/src/Volo.Abp/Volo/Abp/Uow/UnitOfWorkManager.cs @@ -25,10 +25,13 @@ namespace Volo.Abp.Uow return new ChildUnitOfWork(_ambientUnitOfWork.UnitOfWork); } + var parentUow = _ambientUnitOfWork.UnitOfWork; + var scope = _serviceProvider.CreateScope(); + IUnitOfWork unitOfWork; try { - _ambientUnitOfWork.SetUnitOfWork(scope.ServiceProvider.GetRequiredService()); + unitOfWork = scope.ServiceProvider.GetRequiredService(); } catch { @@ -36,7 +39,28 @@ namespace Volo.Abp.Uow throw; } - Debug.Assert(_ambientUnitOfWork.UnitOfWork != null, "_ambientUnitOfWork.UnitOfWork can not be null since it's set by _ambientUnitOfWork.SetUnitOfWork method!"); + _ambientUnitOfWork.SetUnitOfWork(unitOfWork); + + Debug.Assert( + _ambientUnitOfWork.UnitOfWork != null, + "_ambientUnitOfWork.UnitOfWork can not be null since it's set by _ambientUnitOfWork.SetUnitOfWork method!" + ); + + unitOfWork.Completed += (sender, args) => + { + _ambientUnitOfWork.SetUnitOfWork(parentUow); + }; + + unitOfWork.Failed += (sender, args) => + { + _ambientUnitOfWork.SetUnitOfWork(parentUow); + }; + + unitOfWork.Disposed += (sender, args) => + { + scope.Dispose(); + }; + return _ambientUnitOfWork.UnitOfWork; } } diff --git a/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs new file mode 100644 index 0000000000..6df7d6d403 --- /dev/null +++ b/test/Volo.Abp.Tests/Volo/Abp/Uow/UnitOfWork_Events_Tests.cs @@ -0,0 +1,113 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using Shouldly; +using Volo.Abp.Modularity; +using Volo.Abp.TestBase; +using Xunit; + +namespace Volo.Abp.Uow +{ + public class UnitOfWork_Events_Tests : AbpIntegratedTest + { + private readonly IUnitOfWorkManager _unitOfWorkManager; + + public UnitOfWork_Events_Tests() + { + _unitOfWorkManager = ServiceProvider.GetRequiredService(); + } + + [Fact] + public void Should_Trigger_Complete_On_Success() + { + var completed = false; + var disposed = false; + + using (var uow = _unitOfWorkManager.Begin()) + { + uow.Completed += (sender, args) => completed = true; + uow.Disposed += (sender, args) => disposed = true; + + uow.Complete(); + + completed.ShouldBeTrue(); + } + + disposed.ShouldBeTrue(); + } + + [Fact] + public void Should_Trigger_Complete_On_Success_In_Child_Uow() + { + var completed = false; + var disposed = false; + + using (var uow = _unitOfWorkManager.Begin()) + { + using (var childUow = _unitOfWorkManager.Begin()) + { + childUow.Completed += (sender, args) => completed = true; + uow.Disposed += (sender, args) => disposed = true; + + childUow.Complete(); + + completed.ShouldBeFalse(); //Parent has not been completed yet! + disposed.ShouldBeFalse(); + } + + completed.ShouldBeFalse(); //Parent has not been completed yet! + disposed.ShouldBeFalse(); + + uow.Complete(); + + completed.ShouldBeTrue(); //It's completed now! + disposed.ShouldBeFalse(); //But not disposed yet! + } + + disposed.ShouldBeTrue(); + } + + + [Fact] + public void Should_Not_Trigger_Complete_If_Uow_Is_Not_Completed() + { + var completed = false; + var failed = false; + var disposed = false; + + using (var uow = _unitOfWorkManager.Begin()) + { + uow.Completed += (sender, args) => completed = true; + uow.Failed += (sender, args) => failed = true; + uow.Disposed += (sender, args) => disposed = true; + } + + completed.ShouldBeFalse(); + failed.ShouldBeTrue(); + disposed.ShouldBeTrue(); + } + + [Fact] + public void Should_Trigger_Failed_If_Uow_Throws_Exception() + { + var completed = false; + var failed = false; + var disposed = false; + + Assert.Throws(new Action(() => + { + using (var uow = _unitOfWorkManager.Begin()) + { + uow.Completed += (sender, args) => completed = true; + uow.Failed += (sender, args) => failed = true; + uow.Disposed += (sender, args) => disposed = true; + + throw new Exception("test exception"); + } + })).Message.ShouldBe("test exception"); + + completed.ShouldBeFalse(); + failed.ShouldBeTrue(); + disposed.ShouldBeTrue(); + } + } +}