diff --git a/src/Autofac/Autofac.csproj b/src/Autofac/Autofac.csproj index ddb8cc2bb..f79bd457d 100644 --- a/src/Autofac/Autofac.csproj +++ b/src/Autofac/Autofac.csproj @@ -3,7 +3,7 @@ Autofac is an IoC container for Microsoft .NET. It manages the dependencies between classes so that applications stay easy to change as they grow in size and complexity. 5.0.0 - netstandard2.0;netstandard1.1;net45 + netstandard2.1;netstandard2.0;net461 latest $(NoWarn);CS1591;IDE0008 true @@ -50,8 +50,12 @@ - - + + + + + + @@ -150,10 +154,15 @@ True ResolveOperationResources.resx - + + DisposerResources.resx True True + + ServiceResources.resx + True + True True @@ -334,6 +343,10 @@ ResXFileCodeGenerator ResolveOperationResources.Designer.cs + + DisposerResources.Designer.cs + ResXFileCodeGenerator + ResXFileCodeGenerator ServiceResources.Designer.cs diff --git a/src/Autofac/Core/Container.cs b/src/Autofac/Core/Container.cs index 3512832ef..1285faa7c 100644 --- a/src/Autofac/Core/Container.cs +++ b/src/Autofac/Core/Container.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using Autofac.Core.Activators.Delegate; using Autofac.Core.Lifetime; using Autofac.Core.Registration; @@ -178,6 +179,20 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + await _rootLifetimeScope.DisposeAsync(); + + // Registries are not likely to have async tasks to dispose of, + // so we will leave it as a straight dispose. + ComponentRegistry.Dispose(); + } + + // Do not call the base, otherwise the standard Dispose will fire. + } + /// /// Gets the service object of the specified type. /// diff --git a/src/Autofac/Core/DependencyResolutionException.cs b/src/Autofac/Core/DependencyResolutionException.cs index 80eb19238..b53407479 100644 --- a/src/Autofac/Core/DependencyResolutionException.cs +++ b/src/Autofac/Core/DependencyResolutionException.cs @@ -24,9 +24,7 @@ // OTHER DEALINGS IN THE SOFTWARE. using System; -#if !NETSTANDARD1_1 using System.Runtime.Serialization; -#endif namespace Autofac.Core { @@ -36,17 +34,13 @@ namespace Autofac.Core /// been made during the operation. For example, 'on activated' handlers may have already been /// fired, or 'single instance' components partially constructed. /// -#if !NETSTANDARD1_1 [Serializable] -#endif public class DependencyResolutionException : Exception { -#if !NETSTANDARD1_1 protected DependencyResolutionException(SerializationInfo info, StreamingContext context) : base(info, context) { } -#endif /// /// Initializes a new instance of the class. diff --git a/src/Autofac/Core/Disposer.cs b/src/Autofac/Core/Disposer.cs index 1567556cf..03da9621b 100644 --- a/src/Autofac/Core/Disposer.cs +++ b/src/Autofac/Core/Disposer.cs @@ -25,6 +25,8 @@ using System; using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Autofac.Util; namespace Autofac.Core @@ -36,11 +38,13 @@ namespace Autofac.Core internal class Disposer : Disposable, IDisposer { /// - /// Contents all implement IDisposable. + /// Contents all implement IDisposable or IAsyncDisposable. /// - private Stack _items = new Stack(); + private Stack _items = new Stack(); - private readonly object _synchRoot = new object(); + // Need to use a semaphore instead of a simple object to lock on, because + // we need to synchronise an awaitable block. + private SemaphoreSlim _synchRoot = new SemaphoreSlim(1, 1); /// /// Releases unmanaged and - optionally - managed resources. @@ -50,32 +54,128 @@ protected override void Dispose(bool disposing) { if (disposing) { - lock (_synchRoot) + _synchRoot.Wait(); + try { while (_items.Count > 0) { var item = _items.Pop(); - item.Dispose(); + + // If we are in synchronous dispose, and an object implements IDisposable, + // then use it. + if (item is IDisposable disposable) + { + disposable.Dispose(); + } + else + { + // Type only implements IAsyncDisposable, which is not valid if there + // is a synchronous dispose being done. + throw new InvalidOperationException(string.Format( + DisposerResources.Culture, + DisposerResources.TypeOnlyImplementsIAsyncDisposable, + item.GetType().FullName)); + } } _items = null; } + finally + { + _synchRoot.Release(); + + // We don't need the semaphore any more. + _synchRoot.Dispose(); + } } base.Dispose(disposing); } + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + // Acquire our semaphore. + await _synchRoot.WaitAsync().ConfigureAwait(false); + try + { + while (_items.Count > 0) + { + var item = _items.Pop(); + + // If the item implements IAsyncDisposable we will call its DisposeAsync Method. + if (item is IAsyncDisposable asyncDisposable) + { + var vt = asyncDisposable.DisposeAsync(); + + // Don't await if it's already completed (this is a slight gain in performance of using ValueTask). + if (!vt.IsCompletedSuccessfully) + { + await vt.ConfigureAwait(false); + } + } + else if (item is IDisposable disposable) + { + // Call the standard Dispose. + disposable.Dispose(); + } + } + + _items = null; + } + finally + { + _synchRoot.Release(); + + // We don't need the semaphore any more. + _synchRoot.Dispose(); + } + } + } + + /// + /// Adds an object to the disposer, where that object only implements IAsyncDisposable. When the disposer is + /// disposed, so will the object be. + /// This is not typically recommended, and you should implement IDisposable as well. + /// + /// The instance. + /// + /// If this Disposer is disposed of using a synchronous Dispose call, that call will throw an exception. + /// + public void AddInstanceForAsyncDisposal(IAsyncDisposable instance) + { + AddInternal(instance); + } + /// /// Adds an object to the disposer. When the disposer is /// disposed, so will the object be. /// /// The instance. public void AddInstanceForDisposal(IDisposable instance) + { + AddInternal(instance); + } + + private void AddInternal(object instance) { if (instance == null) throw new ArgumentNullException(nameof(instance)); - lock (_synchRoot) + if (IsDisposed) + { + throw new ObjectDisposedException(nameof(Disposer), DisposerResources.CannotAddToDisposedDisposer); + } + + _synchRoot.Wait(); + try + { _items.Push(instance); + } + finally + { + _synchRoot.Release(); + } } } } diff --git a/src/Autofac/Core/DisposerResources.Designer.cs b/src/Autofac/Core/DisposerResources.Designer.cs new file mode 100644 index 000000000..c6ed5fb32 --- /dev/null +++ b/src/Autofac/Core/DisposerResources.Designer.cs @@ -0,0 +1,82 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace Autofac.Core { + using System; + using System.Reflection; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class DisposerResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal DisposerResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("Autofac.Core.DisposerResources", typeof(DisposerResources).GetTypeInfo().Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to The Disposer object has already been Disposed, so no items can be added to it.. + /// + internal static string CannotAddToDisposedDisposer { + get { + return ResourceManager.GetString("CannotAddToDisposedDisposer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A synchronous Dispose has been attempted, but the tracked object of type '{0}' only implements IAsyncDisposable.. + /// + internal static string TypeOnlyImplementsIAsyncDisposable { + get { + return ResourceManager.GetString("TypeOnlyImplementsIAsyncDisposable", resourceCulture); + } + } + } +} diff --git a/src/Autofac/Core/DisposerResources.resx b/src/Autofac/Core/DisposerResources.resx new file mode 100644 index 000000000..2ec5f5fbc --- /dev/null +++ b/src/Autofac/Core/DisposerResources.resx @@ -0,0 +1,126 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The Disposer object has already been Disposed, so no items can be added to it. + + + A synchronous Dispose has been attempted, but the tracked object of type '{0}' only implements IAsyncDisposable. + + \ No newline at end of file diff --git a/src/Autofac/Core/IDisposer.cs b/src/Autofac/Core/IDisposer.cs index f37ce7939..9e21c99f7 100644 --- a/src/Autofac/Core/IDisposer.cs +++ b/src/Autofac/Core/IDisposer.cs @@ -31,7 +31,7 @@ namespace Autofac.Core /// Provided on an object that will dispose of other objects when it is /// itself disposed. /// - public interface IDisposer : IDisposable + public interface IDisposer : IDisposable, IAsyncDisposable { /// /// Adds an object to the disposer. When the disposer is @@ -39,5 +39,17 @@ public interface IDisposer : IDisposable /// /// The instance. void AddInstanceForDisposal(IDisposable instance); + + /// + /// Adds an object to the disposer, where that object implements IAsyncDisposable. When the disposer is + /// disposed, so will the object be. + /// You should most likely implement IDisposable as well, and call instead of this method. + /// + /// The instance. + /// + /// If the provided object only implements IAsyncDisposable, and the is disposed of using a synchronous Dispose call, + /// that call will throw an exception when it attempts to dispose of the provided instance. + /// + void AddInstanceForAsyncDisposal(IAsyncDisposable instance); } } diff --git a/src/Autofac/Core/Lifetime/LifetimeScope.cs b/src/Autofac/Core/Lifetime/LifetimeScope.cs index 8541a7bd4..901021bce 100644 --- a/src/Autofac/Core/Lifetime/LifetimeScope.cs +++ b/src/Autofac/Core/Lifetime/LifetimeScope.cs @@ -29,6 +29,7 @@ using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Runtime.CompilerServices; +using System.Threading.Tasks; using Autofac.Builder; using Autofac.Core.Registration; using Autofac.Core.Resolving; @@ -357,6 +358,28 @@ protected override void Dispose(bool disposing) base.Dispose(disposing); } + protected override async ValueTask DisposeAsync(bool disposing) + { + if (disposing) + { + var handler = CurrentScopeEnding; + + try + { + handler?.Invoke(this, new LifetimeScopeEndingEventArgs(this)); + } + finally + { + await Disposer.DisposeAsync(); + } + + // ReSharper disable once InconsistentlySynchronizedField + _sharedInstances.Clear(); + } + + // Don't call the base (which would just call the normal Dispose). + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void CheckNotDisposed() { diff --git a/src/Autofac/Core/Resolving/InstanceLookup.cs b/src/Autofac/Core/Resolving/InstanceLookup.cs index 040abb376..3650e8e25 100644 --- a/src/Autofac/Core/Resolving/InstanceLookup.cs +++ b/src/Autofac/Core/Resolving/InstanceLookup.cs @@ -144,7 +144,13 @@ private object Activate(IEnumerable parameters, out object decoratorT // instance once the instance has been activated - assuming that it will be // done during the lifetime scope's Disposer executing. if (decoratorTarget is IDisposable instanceAsDisposable) + { _activationScope.Disposer.AddInstanceForDisposal(instanceAsDisposable); + } + else if (decoratorTarget is IAsyncDisposable asyncDisposableInstance) + { + _activationScope.Disposer.AddInstanceForAsyncDisposal(asyncDisposableInstance); + } } ComponentRegistration.RaiseActivating(this, resolveParameters, ref _newInstance); diff --git a/src/Autofac/Core/ServiceResources.Designer.cs b/src/Autofac/Core/ServiceResources.Designer.cs index cecee5589..62362a2dc 100644 --- a/src/Autofac/Core/ServiceResources.Designer.cs +++ b/src/Autofac/Core/ServiceResources.Designer.cs @@ -11,8 +11,8 @@ namespace Autofac.Core { using System; using System.Reflection; - - + + /// /// A strongly-typed resource class, for looking up localized strings, etc. /// diff --git a/src/Autofac/Features/LazyDependencies/LazyWithMetadataRegistrationSource.cs b/src/Autofac/Features/LazyDependencies/LazyWithMetadataRegistrationSource.cs index 8f2703188..79e566a33 100644 --- a/src/Autofac/Features/LazyDependencies/LazyWithMetadataRegistrationSource.cs +++ b/src/Autofac/Features/LazyDependencies/LazyWithMetadataRegistrationSource.cs @@ -53,15 +53,10 @@ public IEnumerable RegistrationsFor(Service service, Fun throw new ArgumentNullException(nameof(registrationAccessor)); var swt = service as IServiceWithType; -#if NET45 - var lazyType = GetLazyType(swt); - if (swt == null || lazyType == null || !swt.ServiceType.IsGenericTypeDefinedBy(lazyType)) - return Enumerable.Empty(); -#else + var lazyType = typeof(Lazy<,>); if (swt == null || !swt.ServiceType.IsGenericTypeDefinedBy(lazyType)) return Enumerable.Empty(); -#endif var genericTypeArguments = swt.ServiceType.GetTypeInfo().GenericTypeArguments.ToArray(); var valueType = genericTypeArguments[0]; @@ -104,16 +99,5 @@ private IComponentRegistration CreateLazyRegistration(Service provided return rb.CreateRegistration(); } - -#if NET45 - private static Type GetLazyType(IServiceWithType serviceWithType) - { - return serviceWithType != null - && serviceWithType.ServiceType.GetTypeInfo().IsGenericType - && serviceWithType.ServiceType.GetGenericTypeDefinition().FullName == "System.Lazy`2" - ? serviceWithType.ServiceType.GetGenericTypeDefinition() - : null; - } -#endif } } diff --git a/src/Autofac/ILifetimeScope.cs b/src/Autofac/ILifetimeScope.cs index 434be5a53..a1d5d6607 100644 --- a/src/Autofac/ILifetimeScope.cs +++ b/src/Autofac/ILifetimeScope.cs @@ -69,7 +69,7 @@ namespace Autofac /// /// /// - public interface ILifetimeScope : IComponentContext, IDisposable + public interface ILifetimeScope : IComponentContext, IDisposable, IAsyncDisposable { /// /// Begin a new nested scope. Component instances created via the new scope diff --git a/src/Autofac/Util/Disposable.cs b/src/Autofac/Util/Disposable.cs index d5a406009..1f38c0350 100644 --- a/src/Autofac/Util/Disposable.cs +++ b/src/Autofac/Util/Disposable.cs @@ -26,13 +26,14 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Threading; +using System.Threading.Tasks; namespace Autofac.Util { /// /// Base class for disposable objects. /// - public class Disposable : IDisposable + public class Disposable : IDisposable, IAsyncDisposable { private const int DisposedFlag = 1; private int _isDisposed; @@ -70,5 +71,33 @@ protected bool IsDisposed return _isDisposed == DisposedFlag; } } + + [SuppressMessage( + "Usage", + "CA1816:Dispose methods should call SuppressFinalize", + Justification = "DisposeAsync should also call SuppressFinalize (see various .NET internal implementations).")] + public ValueTask DisposeAsync() + { + // Still need to check if we've already disposed; can't do both. + var wasDisposed = Interlocked.Exchange(ref _isDisposed, DisposedFlag); + if (wasDisposed != DisposedFlag) + { + GC.SuppressFinalize(this); + + // Always true, but means we get the similar syntax as Dispose, + // and separates the two overloads. + return DisposeAsync(true); + } + + return default(ValueTask); + } + + protected virtual ValueTask DisposeAsync(bool disposing) + { + // Default implementation does a synchronous dispose. + Dispose(disposing); + + return default; + } } } diff --git a/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj b/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj index f3ee91d5d..629e23ce6 100644 --- a/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj +++ b/test/Autofac.Specification.Test/Autofac.Specification.Test.csproj @@ -1,11 +1,12 @@  - netcoreapp3.0;net46 + netcoreapp3.0;net461 $(NoWarn);CS1591;SA1602;SA1611 true ../../build/Analyzers.ruleset false + latest @@ -13,12 +14,16 @@ + + + + - + diff --git a/test/Autofac.Specification.Test/Features/DecoratorTests.cs b/test/Autofac.Specification.Test/Features/DecoratorTests.cs index ff856586b..5b6cf28e4 100644 --- a/test/Autofac.Specification.Test/Features/DecoratorTests.cs +++ b/test/Autofac.Specification.Test/Features/DecoratorTests.cs @@ -152,7 +152,6 @@ public void CanResolveDecoratorWithLazy() Assert.IsType(decoratedService.Decorated); } -#if NETCOREAPP2_1 [Fact] public void CanResolveDecoratorWithLazyWithMetadata() { @@ -168,7 +167,6 @@ public void CanResolveDecoratorWithLazyWithMetadata() Assert.IsType(decoratedService.Decorated); Assert.Equal(123, meta.Metadata.A); } -#endif [Fact] public void CanResolveDecoratorWithOwned() diff --git a/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj b/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj index 98e68cd28..e63da0e20 100644 --- a/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj +++ b/test/Autofac.Test.Scenarios.ScannedAssembly/Autofac.Test.Scenarios.ScannedAssembly.csproj @@ -1,7 +1,7 @@  - net45;netstandard1.1;netstandard2.0 + netstandard2.0 $(NoWarn);CS1591 true Autofac.Test.Scenarios.ScannedAssembly @@ -9,7 +9,6 @@ true true Autofac.Test.Scenarios.ScannedAssembly - 1.6.0 false ../../build/Analyzers.ruleset diff --git a/test/Autofac.Test/Autofac.Test.csproj b/test/Autofac.Test/Autofac.Test.csproj index 4fa726766..dd876a864 100644 --- a/test/Autofac.Test/Autofac.Test.csproj +++ b/test/Autofac.Test/Autofac.Test.csproj @@ -1,7 +1,7 @@  - netcoreapp3.0;net46 + netcoreapp3.0;net461 $(NoWarn);CS1591;SA1602;SA1611 true ../../Autofac.snk @@ -10,6 +10,7 @@ true ../../build/Analyzers.ruleset false + latest @@ -35,11 +36,11 @@ - + $(DefineConstants);PARTIAL_TRUST - - + + diff --git a/test/Autofac.Test/Core/ContainerTests.cs b/test/Autofac.Test/Core/ContainerTests.cs index 3abdef000..56954c2d4 100644 --- a/test/Autofac.Test/Core/ContainerTests.cs +++ b/test/Autofac.Test/Core/ContainerTests.cs @@ -1,6 +1,8 @@ using System; +using System.Threading.Tasks; using Autofac.Core; using Autofac.Test.Scenarios.Parameterisation; +using Autofac.Test.Util; using Xunit; namespace Autofac.Test.Core @@ -177,6 +179,26 @@ public void ReplaceInstance_ModuleActivatingHandlerProvidesResultToRelease() } } + [Fact] + public async ValueTask AsyncContainerDisposeTriggersAsyncServiceDispose() + { + var builder = new ContainerBuilder(); + builder.Register(c => new AsyncDisposeTracker()).SingleInstance(); + + AsyncDisposeTracker tracker; + + await using (var container = builder.Build()) + { + tracker = container.Resolve(); + + Assert.False(tracker.IsSyncDisposed); + Assert.False(tracker.IsAsyncDisposed); + } + + Assert.False(tracker.IsSyncDisposed); + Assert.True(tracker.IsAsyncDisposed); + } + private class ReplaceInstanceModule : Module { protected override void AttachToComponentRegistration(IComponentRegistry componentRegistry, IComponentRegistration registration) diff --git a/test/Autofac.Test/Core/DisposerTests.cs b/test/Autofac.Test/Core/DisposerTests.cs index 0bd09ace7..668d1a058 100644 --- a/test/Autofac.Test/Core/DisposerTests.cs +++ b/test/Autofac.Test/Core/DisposerTests.cs @@ -1,5 +1,6 @@ using System; -using System.Linq; +using System.Collections.Generic; +using System.Threading.Tasks; using Autofac.Core; using Autofac.Test.Util; using Xunit; @@ -38,5 +39,135 @@ public void OnDispose_DisposerDisposesContainedInstances() disposer.Dispose(); Assert.True(instance.IsDisposed); } + + [Fact] + public void CannotAddObjectsToDisposerAfterSyncDispose() + { + var instance = new DisposeTracker(); + + var disposer = new Disposer(); + disposer.AddInstanceForDisposal(instance); + Assert.False(instance.IsDisposed); + Assert.False(instance.IsDisposed); + disposer.Dispose(); + Assert.True(instance.IsDisposed); + + Assert.Throws(() => + { + disposer.AddInstanceForDisposal(instance); + }); + } + + [Fact] + public async ValueTask DisposerDisposesOfObjectsAsyncIfIAsyncDisposableDeclared() + { + var instance = new AsyncDisposeTracker(); + + var disposer = new Disposer(); + disposer.AddInstanceForDisposal(instance); + Assert.False(instance.IsSyncDisposed); + Assert.False(instance.IsAsyncDisposed); + var result = disposer.DisposeAsync(); + Assert.False(instance.IsSyncDisposed); + + // Dispose is happening async, so this won't be true yet. + Assert.False(instance.IsAsyncDisposed); + + // Now we wait. + await result; + + Assert.False(instance.IsSyncDisposed); + Assert.True(instance.IsAsyncDisposed); + } + + [Fact] + public async ValueTask DisposerDisposesOfObjectsSyncIfIDisposableOnly() + { + var instance = new DisposeTracker(); + + var disposer = new Disposer(); + disposer.AddInstanceForDisposal(instance); + Assert.False(instance.IsDisposed); + await disposer.DisposeAsync(); + Assert.True(instance.IsDisposed); + } + + [Fact] + public void DisposerDisposesOfObjectsSyncIfIAsyncDisposableDeclaredButSyncDisposeCalled() + { + var instance = new AsyncDisposeTracker(); + + var disposer = new Disposer(); + disposer.AddInstanceForDisposal(instance); + Assert.False(instance.IsSyncDisposed); + Assert.False(instance.IsAsyncDisposed); + disposer.Dispose(); + Assert.True(instance.IsSyncDisposed); + Assert.False(instance.IsAsyncDisposed); + } + + [Fact] + public async ValueTask CannotAddObjectsToDisposerAfterAsyncDispose() + { + var instance = new AsyncDisposeTracker(); + + var disposer = new Disposer(); + disposer.AddInstanceForDisposal(instance); + Assert.False(instance.IsSyncDisposed); + Assert.False(instance.IsAsyncDisposed); + await disposer.DisposeAsync(); + Assert.False(instance.IsSyncDisposed); + Assert.True(instance.IsAsyncDisposed); + + Assert.Throws(() => + { + disposer.AddInstanceForDisposal(instance); + }); + } + + [Fact] + public void SyncDisposalOnObjectWithNoIDisposableThrows() + { + var instance = new AsyncOnlyDisposeTracker(); + + var disposer = new Disposer(); + disposer.AddInstanceForAsyncDisposal(instance); + + Assert.Throws(() => + { + disposer.Dispose(); + }); + } + + [Fact] + public async ValueTask DisposerAsyncDisposesContainedInstances_InReverseOfOrderAdded() + { + var disposeOrder = new List(); + + var asyncInstance1 = new AsyncDisposeTracker(); + asyncInstance1.Disposing += (s, e) => disposeOrder.Add(asyncInstance1); + var asyncOnlyInstance2 = new AsyncOnlyDisposeTracker(); + asyncOnlyInstance2.Disposing += (s, e) => disposeOrder.Add(asyncOnlyInstance2); + var syncInstance3 = new DisposeTracker(); + syncInstance3.Disposing += (s, e) => disposeOrder.Add(syncInstance3); + var syncInstance4 = new DisposeTracker(); + syncInstance4.Disposing += (s, e) => disposeOrder.Add(syncInstance4); + + var disposer = new Disposer(); + + disposer.AddInstanceForDisposal(asyncInstance1); + disposer.AddInstanceForDisposal(syncInstance3); + disposer.AddInstanceForDisposal(syncInstance4); + disposer.AddInstanceForAsyncDisposal(asyncOnlyInstance2); + + await disposer.DisposeAsync(); + + Assert.Collection( + disposeOrder, + o1 => Assert.Same(asyncOnlyInstance2, o1), + o2 => Assert.Same(syncInstance4, o2), + o3 => Assert.Same(syncInstance3, o3), + o4 => Assert.Same(asyncInstance1, o4)); + } } } diff --git a/test/Autofac.Test/Core/Lifetime/LifetimeScopeTests.cs b/test/Autofac.Test/Core/Lifetime/LifetimeScopeTests.cs index 8d4a580f4..6a6b5d12b 100644 --- a/test/Autofac.Test/Core/Lifetime/LifetimeScopeTests.cs +++ b/test/Autofac.Test/Core/Lifetime/LifetimeScopeTests.cs @@ -1,8 +1,10 @@ using System; using System.Linq; +using System.Threading.Tasks; using Autofac.Core; using Autofac.Core.Registration; using Autofac.Test.Scenarios.RegistrationSources; +using Autofac.Test.Util; using Xunit; namespace Autofac.Test.Core.Lifetime @@ -100,6 +102,67 @@ public void NestedLifetimeScopesMaintainServiceLimitTypes() } } + [Fact] + public async ValueTask AsyncDisposeLifetimeScopeDisposesRegistrationsAsync() + { + var cb = new ContainerBuilder(); + + cb.RegisterType().InstancePerLifetimeScope().AsSelf(); + cb.RegisterType().InstancePerLifetimeScope().AsSelf(); + cb.RegisterType().InstancePerLifetimeScope().AsSelf(); + + var container = cb.Build(); + + DisposeTracker tracker; + AsyncDisposeTracker asyncTracker; + AsyncOnlyDisposeTracker asyncOnlyTracker; + + await using (var scope = container.BeginLifetimeScope()) + { + tracker = scope.Resolve(); + asyncTracker = scope.Resolve(); + asyncOnlyTracker = scope.Resolve(); + + Assert.False(tracker.IsDisposed); + Assert.False(asyncTracker.IsSyncDisposed); + Assert.False(asyncTracker.IsAsyncDisposed); + Assert.False(asyncOnlyTracker.IsAsyncDisposed); + } + + Assert.True(tracker.IsDisposed); + Assert.True(asyncTracker.IsAsyncDisposed); + Assert.True(asyncOnlyTracker.IsAsyncDisposed); + Assert.False(asyncTracker.IsSyncDisposed); + } + + [Fact] + public void DisposeLifetimeScopeDisposesRegistrationsThatAreAsyncAndSyncDispose() + { + var cb = new ContainerBuilder(); + + cb.RegisterType().InstancePerLifetimeScope().AsSelf(); + cb.RegisterType().InstancePerLifetimeScope().AsSelf(); + + var container = cb.Build(); + + DisposeTracker tracker; + AsyncDisposeTracker asyncTracker; + + using (var scope = container.BeginLifetimeScope()) + { + tracker = scope.Resolve(); + asyncTracker = scope.Resolve(); + + Assert.False(tracker.IsDisposed); + Assert.False(asyncTracker.IsSyncDisposed); + Assert.False(asyncTracker.IsAsyncDisposed); + } + + Assert.True(tracker.IsDisposed); + Assert.False(asyncTracker.IsAsyncDisposed); + Assert.True(asyncTracker.IsSyncDisposed); + } + internal class DependsOnRegisteredInstance { public DependsOnRegisteredInstance(object instance) diff --git a/test/Autofac.Test/Util/AsyncDisposeTracker.cs b/test/Autofac.Test/Util/AsyncDisposeTracker.cs new file mode 100644 index 000000000..652020c75 --- /dev/null +++ b/test/Autofac.Test/Util/AsyncDisposeTracker.cs @@ -0,0 +1,36 @@ +using System; +using System.Threading.Tasks; + +namespace Autofac.Test.Util +{ + public class AsyncDisposeTracker : IDisposable, IAsyncDisposable + { + public event EventHandler Disposing; + + public bool IsSyncDisposed { get; set; } + + public bool IsAsyncDisposed { get; set; } + + public void Dispose() + { + this.IsSyncDisposed = true; + + if (this.Disposing != null) + { + this.Disposing(this, EventArgs.Empty); + } + } + + public async ValueTask DisposeAsync() + { + await Task.Delay(1); + + IsAsyncDisposed = true; + + if (this.Disposing != null) + { + this.Disposing(this, EventArgs.Empty); + } + } + } +} diff --git a/test/Autofac.Test/Util/AsyncOnlyDisposeTracker.cs b/test/Autofac.Test/Util/AsyncOnlyDisposeTracker.cs new file mode 100644 index 000000000..052a3edc1 --- /dev/null +++ b/test/Autofac.Test/Util/AsyncOnlyDisposeTracker.cs @@ -0,0 +1,24 @@ +using System; +using System.Threading.Tasks; + +namespace Autofac.Test.Util +{ + public class AsyncOnlyDisposeTracker : IAsyncDisposable + { + public event EventHandler Disposing; + + public bool IsAsyncDisposed { get; set; } + + public async ValueTask DisposeAsync() + { + await Task.Delay(1); + + IsAsyncDisposed = true; + + if (this.Disposing != null) + { + this.Disposing(this, EventArgs.Empty); + } + } + } +}