From 451b01658f459648aa10935a8bd9063f6c487576 Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Wed, 22 Jul 2020 08:47:34 +0100 Subject: [PATCH 1/4] Add support for Func handlers for Core Autofac Events --- src/Autofac/Builder/IRegistrationBuilder.cs | 23 +++++ ...imit,TActivatorData,TRegistrationStyle}.cs | 34 +++++++ .../Core/Resolving/ResolveEventType.cs | 4 +- .../RegistrationExtensions.EventHandler.cs | 40 +++++++- src/Autofac/Util/AsyncReleaseAction.cs | 64 ++++++++++++ .../Lifetime/LifetimeEventTests.cs | 99 +++++++++++++++++++ 6 files changed, 261 insertions(+), 3 deletions(-) create mode 100644 src/Autofac/Util/AsyncReleaseAction.cs diff --git a/src/Autofac/Builder/IRegistrationBuilder.cs b/src/Autofac/Builder/IRegistrationBuilder.cs index b60ecebb7..e014ebf87 100644 --- a/src/Autofac/Builder/IRegistrationBuilder.cs +++ b/src/Autofac/Builder/IRegistrationBuilder.cs @@ -26,6 +26,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Threading.Tasks; using Autofac.Core; using Autofac.Core.Resolving.Pipeline; @@ -257,6 +258,14 @@ public interface IRegistrationBuilderA registration builder allowing further configuration of the component. IRegistrationBuilder OnPreparing(Action handler); + /// + /// Add an async handler for the Preparing event. This event allows manipulating of the parameters + /// that will be provided to the component. + /// + /// An event handler; the resolve process will not continue until the returned task completes. + /// A registration builder allowing further configuration of the component. + IRegistrationBuilder OnPreparing(Func handler); + /// /// Add a handler for the Activating event. /// @@ -264,6 +273,13 @@ public interface IRegistrationBuilderA registration builder allowing further configuration of the component. IRegistrationBuilder OnActivating(Action> handler); + /// + /// Add an async handler for the Activating event. + /// + /// An event handler; the resolve process will not continue until the returned task completes. + /// A registration builder allowing further configuration of the component. + IRegistrationBuilder OnActivating(Func, Task> handler); + /// /// Add a handler for the Activated event. /// @@ -271,6 +287,13 @@ public interface IRegistrationBuilderA registration builder allowing further configuration of the component. IRegistrationBuilder OnActivated(Action> handler); + /// + /// Add a handler for the Activated event. + /// + /// An event handler; the resolve process will not continue until the returned task completes. + /// A registration builder allowing further configuration of the component. + IRegistrationBuilder OnActivated(Func, Task> handler); + /// /// Configure the component so that any properties whose types are registered in the /// container and follow specific criteria will be wired to instances of the appropriate service. diff --git a/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs b/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs index 22686e275..63b78c4b3 100644 --- a/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs +++ b/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs @@ -27,6 +27,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Threading.Tasks; using Autofac.Core; using Autofac.Core.Activators.Reflection; using Autofac.Core.Lifetime; @@ -418,6 +419,17 @@ public RegistrationBuilder(Service defaultService, TActivatorData activatorData, return this; } + /// + public IRegistrationBuilder OnPreparing(Func handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return OnPreparing(args => handler(args).ConfigureAwait(false).GetAwaiter().GetResult()); + } + /// /// Add a handler for the Activating event. /// @@ -444,6 +456,17 @@ public RegistrationBuilder(Service defaultService, TActivatorData activatorData, return this; } + /// + public IRegistrationBuilder OnActivating(Func, Task> handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return OnActivating(args => handler(args).ConfigureAwait(false).GetAwaiter().GetResult()); + } + /// /// Add a handler for the Activated event. /// @@ -484,6 +507,17 @@ public RegistrationBuilder(Service defaultService, TActivatorData activatorData, return this; } + /// + public IRegistrationBuilder OnActivated(Func, Task> handler) + { + if (handler == null) + { + throw new ArgumentNullException(nameof(handler)); + } + + return OnActivated(args => handler(args).ConfigureAwait(false).GetAwaiter().GetResult()); + } + /// /// Configure the component so that any properties whose types are registered in the /// container and follow specific criteria will be wired to instances of the appropriate service. diff --git a/src/Autofac/Core/Resolving/ResolveEventType.cs b/src/Autofac/Core/Resolving/ResolveEventType.cs index 3960fc47f..01e9fe37e 100644 --- a/src/Autofac/Core/Resolving/ResolveEventType.cs +++ b/src/Autofac/Core/Resolving/ResolveEventType.cs @@ -8,7 +8,7 @@ namespace Autofac.Core.Resolving.Middleware public enum ResolveEventType { /// - /// Event type for the OnPreparing event registered by . + /// Event type for the OnPreparing event registered by . /// OnPreparing, @@ -23,7 +23,7 @@ public enum ResolveEventType OnActivated, /// - /// Event type for the OnRelease event registered by + /// Event type for the OnRelease event registered by /// OnRelease } diff --git a/src/Autofac/RegistrationExtensions.EventHandler.cs b/src/Autofac/RegistrationExtensions.EventHandler.cs index e212e1ea4..bce1b1455 100644 --- a/src/Autofac/RegistrationExtensions.EventHandler.cs +++ b/src/Autofac/RegistrationExtensions.EventHandler.cs @@ -25,6 +25,7 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Threading.Tasks; using Autofac.Builder; using Autofac.Core; using Autofac.Core.Resolving.Middleware; @@ -94,7 +95,6 @@ public static partial class RegistrationExtensions /// Registration to set release action for. /// An action to perform instead of disposing the instance. /// Registration builder allowing the registration to be configured. - /// Only one release action can be configured per registration. public static IRegistrationBuilder OnRelease( this IRegistrationBuilder registration, @@ -124,5 +124,43 @@ public static partial class RegistrationExtensions return registration; } + + /// + /// Run a supplied async action instead of disposing instances when they're no + /// longer required. + /// + /// Registration limit type. + /// Activator data type. + /// Registration style. + /// Registration to set release action for. + /// + /// An action to perform instead of disposing the instance. + /// The release/disposal process will not continue until the returned task completes. + /// + /// Registration builder allowing the registration to be configured. + public static IRegistrationBuilder + OnRelease( + this IRegistrationBuilder registration, + Func releaseAction) + { + if (registration == null) throw new ArgumentNullException(nameof(registration)); + if (releaseAction == null) throw new ArgumentNullException(nameof(releaseAction)); + + registration.ExternallyOwned(); + + var middleware = new CoreEventMiddleware(ResolveEventType.OnRelease, PipelinePhase.Activation, (ctxt, next) => + { + // Continue down the pipeline. + next(ctxt); + + // Use an async release action that invokes the release callback in a proper async/await flow if someone + // is using actual async disposal. + ctxt.ActivationScope.Disposer.AddInstanceForAsyncDisposal(new AsyncReleaseAction(releaseAction, () => (TLimit)ctxt.Instance!)); + }); + + registration.ResolvePipeline.Use(middleware, MiddlewareInsertionMode.StartOfPhase); + + return registration; + } } } diff --git a/src/Autofac/Util/AsyncReleaseAction.cs b/src/Autofac/Util/AsyncReleaseAction.cs new file mode 100644 index 000000000..894e8c1fe --- /dev/null +++ b/src/Autofac/Util/AsyncReleaseAction.cs @@ -0,0 +1,64 @@ +// Copyright (c) Autofac Project. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Autofac.Util +{ + /// + /// Adapts an async action to the interface. + /// + internal class AsyncReleaseAction : Disposable + { + private readonly Func _action; + private readonly Func _factory; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The async action to execute on disposal. + /// + /// + /// A factory that retrieves the value on which the + /// should be executed. + /// + public AsyncReleaseAction(Func action, Func factory) + { + if (action == null) throw new ArgumentNullException(nameof(action)); + if (factory == null) throw new ArgumentNullException(nameof(factory)); + + _action = action; + _factory = factory; + } + + /// + protected override async ValueTask DisposeAsync(bool disposing) + { + // Value retrieval for the disposal is deferred until + // disposal runs to ensure any calls to, say, .ReplaceInstance() + // during .OnActivating() will be accounted for. + if (disposing) + { + await _action(_factory()).ConfigureAwait(false); + } + + base.Dispose(disposing); + } + + /// + protected override void Dispose(bool disposing) + { + // Value retrieval for the disposal is deferred until + // disposal runs to ensure any calls to, say, .ReplaceInstance() + // during .OnActivating() will be accounted for. + if (disposing) + { + _action(_factory()).ConfigureAwait(false).GetAwaiter().GetResult(); + } + + base.Dispose(disposing); + } + } +} diff --git a/test/Autofac.Specification.Test/Lifetime/LifetimeEventTests.cs b/test/Autofac.Specification.Test/Lifetime/LifetimeEventTests.cs index 4850280d8..ced47ef61 100644 --- a/test/Autofac.Specification.Test/Lifetime/LifetimeEventTests.cs +++ b/test/Autofac.Specification.Test/Lifetime/LifetimeEventTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Autofac.Core; using Autofac.Features.Metadata; using Autofac.Specification.Test.Util; @@ -24,6 +26,25 @@ public void ActivatedAllowsMethodInjection() Assert.Equal(pval, invokee.Param); } + [Fact] + public void ActivatedAllowsTaskReturningHandler() + { + var pval = 12; + var builder = new ContainerBuilder(); + builder.RegisterType() + .InstancePerLifetimeScope() + .OnActivated(async e => + { + await Task.Delay(1); + e.Instance.Method(pval); + }); + + var container = builder.Build(); + var scope = container.BeginLifetimeScope(); + var invokee = scope.Resolve(); + Assert.Equal(pval, invokee.Param); + } + [Fact] public void ActivatedCanReceiveParameters() { @@ -130,6 +151,24 @@ public void PreparingCanProvideParametersToActivator() Assert.False(parameters.Except(actual).Any()); } + [Fact] + public void AsyncPreparingCanProvideParametersToActivator() + { + IEnumerable parameters = new Parameter[] { new NamedParameter("n", 1) }; + IEnumerable actual = null; + var cb = new ContainerBuilder(); + cb.RegisterType() + .OnPreparing(async e => + { + await Task.Delay(1); + e.Parameters = parameters; + }) + .OnActivating(e => actual = e.Parameters); + var container = cb.Build(); + container.Resolve(); + Assert.False(parameters.Except(actual).Any()); + } + [Fact] public void PreparingRaisedForEachResolveInstancePerDependency() { @@ -247,6 +286,22 @@ public void ActivatingRaisedOnceSingleInstance() } } + [Fact] + public void AsyncActivatingSupported() + { + var activatingRaised = 0; + var cb = new ContainerBuilder(); + cb.RegisterType() + .OnActivating(async e => + { + await Task.Delay(1); + activatingRaised++; + }); + var container = cb.Build(); + container.Resolve(); + Assert.Equal(1, activatingRaised); + } + [Fact] public void ActivatingRaisedForFirstResolveInEachLifetimeScope() { @@ -430,6 +485,50 @@ public void ReleaseStopsAutomaticDisposal() Assert.False(dt.IsDisposed); } + [Fact] + public void AsyncReleaseHandlersRunUnderNormalDisposal() + { + var builder = new ContainerBuilder(); + object instance = null; + builder.RegisterType() + .OnRelease(async i => + { + await Task.Delay(1); + instance = i; + }); + + var container = builder.Build(); + var dt = container.Resolve(); + container.Dispose(); + Assert.Same(dt, instance); + } + + [Fact] + public async Task AsyncReleaseHandlersRunUnderAsyncDisposal() + { + var asyncLocal = new AsyncLocal(); + asyncLocal.Value = 5; + + var builder = new ContainerBuilder(); + object instance = null; + builder.RegisterType() + .OnRelease(async i => + { + await Task.Delay(1); + + // Assert that the async local is preserved. + Assert.Equal(5, asyncLocal.Value); + instance = i; + }); + + var container = builder.Build(); + var dt = container.Resolve(); + + await container.DisposeAsync(); + + Assert.Same(dt, instance); + } + [Fact] public void EventRaisedFromComponentRegistrationCanGetServiceBeingResolved() { From 58bc8db214628f706126392ce9006e3bdddfa6e3 Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Wed, 22 Jul 2020 09:14:27 +0100 Subject: [PATCH 2/4] Use value task instead of task for the allocation saving. --- src/Autofac/Builder/IRegistrationBuilder.cs | 6 ++-- ...imit,TActivatorData,TRegistrationStyle}.cs | 36 +++++++++++++++---- .../RegistrationExtensions.EventHandler.cs | 2 +- src/Autofac/Util/AsyncReleaseAction.cs | 11 ++++-- 4 files changed, 42 insertions(+), 13 deletions(-) diff --git a/src/Autofac/Builder/IRegistrationBuilder.cs b/src/Autofac/Builder/IRegistrationBuilder.cs index e014ebf87..1e04d8121 100644 --- a/src/Autofac/Builder/IRegistrationBuilder.cs +++ b/src/Autofac/Builder/IRegistrationBuilder.cs @@ -264,7 +264,7 @@ public interface IRegistrationBuilder /// An event handler; the resolve process will not continue until the returned task completes. /// A registration builder allowing further configuration of the component. - IRegistrationBuilder OnPreparing(Func handler); + IRegistrationBuilder OnPreparing(Func handler); /// /// Add a handler for the Activating event. @@ -278,7 +278,7 @@ public interface IRegistrationBuilder /// An event handler; the resolve process will not continue until the returned task completes. /// A registration builder allowing further configuration of the component. - IRegistrationBuilder OnActivating(Func, Task> handler); + IRegistrationBuilder OnActivating(Func, ValueTask> handler); /// /// Add a handler for the Activated event. @@ -292,7 +292,7 @@ public interface IRegistrationBuilder /// An event handler; the resolve process will not continue until the returned task completes. /// A registration builder allowing further configuration of the component. - IRegistrationBuilder OnActivated(Func, Task> handler); + IRegistrationBuilder OnActivated(Func, ValueTask> handler); /// /// Configure the component so that any properties whose types are registered in the diff --git a/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs b/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs index 63b78c4b3..face65270 100644 --- a/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs +++ b/src/Autofac/Builder/RegistrationBuilder{TLimit,TActivatorData,TRegistrationStyle}.cs @@ -420,14 +420,22 @@ public RegistrationBuilder(Service defaultService, TActivatorData activatorData, } /// - public IRegistrationBuilder OnPreparing(Func handler) + public IRegistrationBuilder OnPreparing(Func handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } - return OnPreparing(args => handler(args).ConfigureAwait(false).GetAwaiter().GetResult()); + return OnPreparing(args => + { + var vt = handler(args); + + if (!vt.IsCompletedSuccessfully) + { + vt.ConfigureAwait(false).GetAwaiter().GetResult(); + } + }); } /// @@ -457,14 +465,22 @@ public RegistrationBuilder(Service defaultService, TActivatorData activatorData, } /// - public IRegistrationBuilder OnActivating(Func, Task> handler) + public IRegistrationBuilder OnActivating(Func, ValueTask> handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } - return OnActivating(args => handler(args).ConfigureAwait(false).GetAwaiter().GetResult()); + return OnActivating(args => + { + var vt = handler(args); + + if (!vt.IsCompletedSuccessfully) + { + vt.ConfigureAwait(false).GetAwaiter().GetResult(); + } + }); } /// @@ -508,14 +524,22 @@ public RegistrationBuilder(Service defaultService, TActivatorData activatorData, } /// - public IRegistrationBuilder OnActivated(Func, Task> handler) + public IRegistrationBuilder OnActivated(Func, ValueTask> handler) { if (handler == null) { throw new ArgumentNullException(nameof(handler)); } - return OnActivated(args => handler(args).ConfigureAwait(false).GetAwaiter().GetResult()); + return OnActivated(args => + { + var vt = handler(args); + + if (!vt.IsCompletedSuccessfully) + { + vt.ConfigureAwait(false).GetAwaiter().GetResult(); + } + }); } /// diff --git a/src/Autofac/RegistrationExtensions.EventHandler.cs b/src/Autofac/RegistrationExtensions.EventHandler.cs index bce1b1455..f5f14c2db 100644 --- a/src/Autofac/RegistrationExtensions.EventHandler.cs +++ b/src/Autofac/RegistrationExtensions.EventHandler.cs @@ -141,7 +141,7 @@ public static partial class RegistrationExtensions public static IRegistrationBuilder OnRelease( this IRegistrationBuilder registration, - Func releaseAction) + Func releaseAction) { if (registration == null) throw new ArgumentNullException(nameof(registration)); if (releaseAction == null) throw new ArgumentNullException(nameof(releaseAction)); diff --git a/src/Autofac/Util/AsyncReleaseAction.cs b/src/Autofac/Util/AsyncReleaseAction.cs index 894e8c1fe..3a3787cf9 100644 --- a/src/Autofac/Util/AsyncReleaseAction.cs +++ b/src/Autofac/Util/AsyncReleaseAction.cs @@ -11,7 +11,7 @@ namespace Autofac.Util /// internal class AsyncReleaseAction : Disposable { - private readonly Func _action; + private readonly Func _action; private readonly Func _factory; /// @@ -24,7 +24,7 @@ internal class AsyncReleaseAction : Disposable /// A factory that retrieves the value on which the /// should be executed. /// - public AsyncReleaseAction(Func action, Func factory) + public AsyncReleaseAction(Func action, Func factory) { if (action == null) throw new ArgumentNullException(nameof(action)); if (factory == null) throw new ArgumentNullException(nameof(factory)); @@ -55,7 +55,12 @@ protected override void Dispose(bool disposing) // during .OnActivating() will be accounted for. if (disposing) { - _action(_factory()).ConfigureAwait(false).GetAwaiter().GetResult(); + var vt = _action(_factory()); + + if (!vt.IsCompletedSuccessfully) + { + vt.ConfigureAwait(false).GetAwaiter().GetResult(); + } } base.Dispose(disposing); From 07ffa81280ecbcbf018c001990d7432f1602ee1e Mon Sep 17 00:00:00 2001 From: Alistair Evans Date: Wed, 22 Jul 2020 10:48:23 +0100 Subject: [PATCH 3/4] Rerun build From c9f5cf85f8c87e87a7aa7c56fde5d939b12be227 Mon Sep 17 00:00:00 2001 From: alistairjevans Date: Wed, 22 Jul 2020 18:00:19 +0100 Subject: [PATCH 4/4] Upgrade analyser packages to latest beta --- src/Autofac/Autofac.csproj | 4 ++-- src/Autofac/Core/Registration/ServiceRegistrationInfo.cs | 2 +- src/Autofac/Core/Resolving/ResolveOperationBase.cs | 9 +++++++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Autofac/Autofac.csproj b/src/Autofac/Autofac.csproj index 088ef57cf..5ddae3097 100644 --- a/src/Autofac/Autofac.csproj +++ b/src/Autofac/Autofac.csproj @@ -44,10 +44,10 @@ - + All - + All diff --git a/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs b/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs index 6533fe2dd..015ecbc7f 100644 --- a/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs +++ b/src/Autofac/Core/Registration/ServiceRegistrationInfo.cs @@ -45,7 +45,7 @@ internal class ServiceRegistrationInfo : IResolvePipelineBuilder [SuppressMessage("Microsoft.Performance", "CA1823:AvoidUnusedPrivateFields", Justification = "The _service field is useful in debugging and diagnostics.")] private readonly Service _service; - private IComponentRegistration? _fixedRegistration = null; + private IComponentRegistration? _fixedRegistration; /// /// List of implicit default service implementations. Overriding default implementations are appended to the end, diff --git a/src/Autofac/Core/Resolving/ResolveOperationBase.cs b/src/Autofac/Core/Resolving/ResolveOperationBase.cs index 11bb946f7..5e62809f4 100644 --- a/src/Autofac/Core/Resolving/ResolveOperationBase.cs +++ b/src/Autofac/Core/Resolving/ResolveOperationBase.cs @@ -41,8 +41,8 @@ public abstract class ResolveOperationBase : IResolveOperation private const int SuccessListInitialCapacity = 32; private bool _ended; - private List _successfulRequests = new List(SuccessListInitialCapacity); - private int _nextCompleteSuccessfulRequestStartPos = 0; + private readonly List _successfulRequests = new List(SuccessListInitialCapacity); + private int _nextCompleteSuccessfulRequestStartPos; /// /// Initializes a new instance of the class. @@ -179,6 +179,11 @@ protected object ExecuteOperation(ResolveRequest request) /// public object GetOrCreateInstance(ISharingLifetimeScope currentOperationScope, ResolveRequest request) { + if (request is null) + { + throw new ArgumentNullException(nameof(request)); + } + if (_ended) throw new ObjectDisposedException(ResolveOperationResources.TemporaryContextDisposed, innerException: null); // Create a new request context.