From a5adab6ef0fb62596e9fdc07f1e5fed51aa84fc6 Mon Sep 17 00:00:00 2001 From: Darrell Tunnell Date: Sun, 31 Jan 2021 23:15:56 +0000 Subject: [PATCH] #23 - Child container now uses a scope from the parent --- ...DependencyInjection.ChildContainers.csproj | 6 +- .../ServiceCollectionExtensions.cs | 100 +++++++---------- .../DependencyInjection.ReRouting.csproj | 4 + .../DisposableServiceProvider.cs | 103 ++++++++++++++++++ .../DisposeHelper.cs | 32 ++++++ .../ReRoutingServiceProvider.cs | 3 +- 6 files changed, 185 insertions(+), 63 deletions(-) create mode 100644 src/DependencyInjection.ReRouting/DisposableServiceProvider.cs create mode 100644 src/DependencyInjection.ReRouting/DisposeHelper.cs diff --git a/src/DependencyInjection.ChildContainers/DependencyInjection.ChildContainers.csproj b/src/DependencyInjection.ChildContainers/DependencyInjection.ChildContainers.csproj index 682c96d..51681a0 100644 --- a/src/DependencyInjection.ChildContainers/DependencyInjection.ChildContainers.csproj +++ b/src/DependencyInjection.ChildContainers/DependencyInjection.ChildContainers.csproj @@ -3,8 +3,12 @@ netstandard2.1;netstandard2.0;net50 Dazinator.Extensions.DependencyInjection.ChildContainers - + + + SUPPORTS_ASYNC_DISPOSE + + diff --git a/src/DependencyInjection.ChildContainers/ServiceCollectionExtensions.cs b/src/DependencyInjection.ChildContainers/ServiceCollectionExtensions.cs index 4f11ef4..250fda8 100644 --- a/src/DependencyInjection.ChildContainers/ServiceCollectionExtensions.cs +++ b/src/DependencyInjection.ChildContainers/ServiceCollectionExtensions.cs @@ -7,6 +7,7 @@ namespace Dazinator.Extensions.DependencyInjection using System.Text; using System.Threading.Tasks; using Dazinator.Extensions.DependencyInjection.ChildContainers; + using global::DependencyInjection.ReRouting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; @@ -56,10 +57,10 @@ public static async Task CreateChildServiceProviderAsync( return childContainer; } - public static IServiceProvider BuildChildServiceProvider( - - - this IChildServiceCollection childServiceCollection, IServiceProvider parentServiceProvider, Func buildSp, ParentSingletonOpenGenericRegistrationsBehaviour singletonOpenGenericBehaviour = ParentSingletonOpenGenericRegistrationsBehaviour.Delegate) + public static IServiceProvider BuildChildServiceProvider(this IChildServiceCollection childServiceCollection, + IServiceProvider parentServiceProvider, + Func buildSp, + ParentSingletonOpenGenericRegistrationsBehaviour singletonOpenGenericBehaviour = ParentSingletonOpenGenericRegistrationsBehaviour.Delegate) { // add all the same registrations that are in the parent to the child, // but rewrite them to resolve from the parent IServiceProvider. @@ -67,10 +68,11 @@ public static IServiceProvider BuildChildServiceProvider( var parentRegistrations = childServiceCollection.ParentDescriptors; var reWrittenServiceCollection = new ServiceCollection(); var unsupportedDescriptors = new List(); // we can't honor singleton open generic registrations (child container would get different instance) + var parentScope = parentServiceProvider.CreateScope(); // obtain a new scope from the parent that can be safely used by the child for the lifetime of the child. foreach (var item in parentRegistrations) { - var rewrittenDescriptor = CreateChildDescriptorForExternalService(item, parentServiceProvider, unsupportedDescriptors, singletonOpenGenericBehaviour); + var rewrittenDescriptor = CreateChildDescriptorForExternalService(item, parentScope.ServiceProvider, unsupportedDescriptors, singletonOpenGenericBehaviour); if (rewrittenDescriptor != null) { reWrittenServiceCollection.Add(rewrittenDescriptor); @@ -91,23 +93,50 @@ public static IServiceProvider BuildChildServiceProvider( reWrittenServiceCollection.Add(item); } + IServiceProvider innerSp = null; + IServiceProvider childSp = null; if (singletonOpenGenericBehaviour == ParentSingletonOpenGenericRegistrationsBehaviour.Delegate) { - var childSp = buildSp(reWrittenServiceCollection); + childSp = buildSp(reWrittenServiceCollection); var routingSp = new ReRoutingServiceProvider(childSp); - routingSp.ReRoute(parentServiceProvider, unsupportedDescriptors.Select(a => a.ServiceType)); - return routingSp; + routingSp.ReRoute(parentScope.ServiceProvider, unsupportedDescriptors.Select(a => a.ServiceType)); + innerSp = routingSp; } else { - var childSp = buildSp(reWrittenServiceCollection); - return childSp; + childSp = buildSp(reWrittenServiceCollection); + innerSp = childSp; } + // Make sure we dispose any parent sp scope that we have leased, when the IServiceProvider is disposed. + void onDispose() + { + // dispose of child sp first, then dispose parent scope that we leased to support the child sp. + DisposeHelper.DisposeIfImplemented(childSp); + parentScope.Dispose(); + }; + + +#if SUPPORTS_ASYNC_DISPOSE + async Task onDisposeAsync() + { + // dispose of child sp first, then dispose parent scope that we leased to support the child sp. + await DisposeHelper.DisposeAsyncIfImplemented(childSp); + await DisposeHelper.DisposeAsyncIfImplemented(parentScope); + } +#endif + var disposableSp = new DisposableServiceProvider(innerSp, onDispose +#if SUPPORTS_ASYNC_DISPOSE + , onDisposeAsync +#endif +); + return disposableSp; } + + private static void ThrowUnsupportedDescriptors(IEnumerable unsupportedDescriptors) { var typesMessageBuilder = new StringBuilder(); @@ -152,9 +181,6 @@ private static ServiceDescriptor CreateChildDescriptorForExternalService(Service return serviceDescriptor; } - // These incompatible services should have already been filtered out of the child service collection based on - // HideParentServices() - if (singletonOpenGenericBehaviour == ParentSingletonOpenGenericRegistrationsBehaviour.DuplicateSingletons) { // allow the open generic singleton registration to be added again to this child again resulting in additional singleton instance at child scope. @@ -170,54 +196,6 @@ private static ServiceDescriptor CreateChildDescriptorForExternalService(Service unsupportedDescriptors.Add(item); return null; - // oh flip - // e.g IOptions - // If so, when we resolve IOptions in the child container, we need it to resolve to an instance created in the parent container - // BUT we can't create the instance now, as we have to wait for the concrete type param to be provided.. - // This is a bit of a dilemma because if we register a function to run when the service is requsted, the child container will steal ownership. - // because it thinks its creating the singleton type. - // So we need to register an instance, - - - // Thoughts and Ideas.. - // Problem is how we ensure when an open generic is resolved from the child container that we resolve the same instance as resolved from the parent when the same type params are used. - - - // A) If the service is non generic, or is a closed generic type, we can create an instance from the parent container and register it in the child container so that - // the child container will re-use the same instance, AND won't take ownership of the object - - // B) If the service is an open generic, we can't create an instance ahead of time, because the container needs to create the instance based on the type params requested. - // In this case there is little we can do, but we can do this: - // - If the open generic type is not backed by an implementation type that implements IDisposable or IAsyncDisposable then it should - // be safe to allow the child container to "own" it - as technically that means very little. - // This means we can register it as a factory func, that returns the same instance from the parent? - // NOOOO THIS WONT WORK, because we can't register a factory func to satisfy an open generic. - - // NEXT IDEA - // A) We capture a list of all the open generic singleton registrations - // We derive from ServiceProvider and overide GetService (or wrap IServiceProvider and decorate the same) - // When a type is resolved thats assignable to an open generic type in our list, - // We forward the resolution to the parent service provider instead. (parent.GetRequiredService()). - // This might not work due to BuildServiceProvider capturing services to be injected at that point, therefore GetRequiredService might not be called on our decorated ServiceProvider to resolve the open generic type as the factory was already resolved during the BuildServiceProvider() method and the factory may just be called. - - // LAST IDEA - // We don't try to solve this in a general way, but detect specific open generic singleton registrations and try to make them work here specifically. - // bit yuck but not sure what else to do. - - - - //switch (singletonOpenGenericBehaviour) - //{ - // case ParentSingletonOpenGenericResolutionBehaviour.ThrowNotSupportedException: - // unsupportedDescriptors.Add(item); // we'll record this as an unsupported descriptor so an exception is thrown to include the detail. - // return null; - // case ParentSingletonOpenGenericResolutionBehaviour.RegisterAgainAsSeperateSingletonInstancesInChildContainer: - // return item; - // case ParentSingletonOpenGenericResolutionBehaviour.Omit: - // return null; - // default: - // return null; - //} } } } diff --git a/src/DependencyInjection.ReRouting/DependencyInjection.ReRouting.csproj b/src/DependencyInjection.ReRouting/DependencyInjection.ReRouting.csproj index aef51d9..7d5437a 100644 --- a/src/DependencyInjection.ReRouting/DependencyInjection.ReRouting.csproj +++ b/src/DependencyInjection.ReRouting/DependencyInjection.ReRouting.csproj @@ -5,6 +5,10 @@ Dazinator.Extensions.DependencyInjection.ReRouting + + SUPPORTS_ASYNC_DISPOSE + + diff --git a/src/DependencyInjection.ReRouting/DisposableServiceProvider.cs b/src/DependencyInjection.ReRouting/DisposableServiceProvider.cs new file mode 100644 index 0000000..5c7471c --- /dev/null +++ b/src/DependencyInjection.ReRouting/DisposableServiceProvider.cs @@ -0,0 +1,103 @@ +namespace Dazinator.Extensions.DependencyInjection +{ + using System; + using System.Threading.Tasks; + + /// + /// Decorates an inner service provider, adapting it to implement + /// and - so that additional provided callbacks can be fired on disposal. + /// + public class DisposableServiceProvider : IServiceProvider, IDisposable +#if SUPPORTS_ASYNC_DISPOSE + , IAsyncDisposable +#endif + { + private IServiceProvider _inner; + private Action _onDispose; + +#if SUPPORTS_ASYNC_DISPOSE + private Func _onAsyncDispose; +#endif + + public DisposableServiceProvider(IServiceProvider inner, + Action onDispose = null +#if SUPPORTS_ASYNC_DISPOSE + , Func onAsyncDispose = null +#endif + ) + { + _inner = inner; + _onDispose = onDispose; +#if SUPPORTS_ASYNC_DISPOSE + _onAsyncDispose = onAsyncDispose; +#endif + } + + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public object GetService(Type serviceType) => _inner.GetService(serviceType); + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Dispose of inner provider first. + if (_inner is IDisposable innerDisposable) + { + innerDisposable.Dispose(); + } + + _onDispose?.Invoke(); + } + _inner = null; + _onDispose = null; +#if SUPPORTS_ASYNC_DISPOSE + _onAsyncDispose = null; +#endif + } + +#if SUPPORTS_ASYNC_DISPOSE + public async ValueTask DisposeAsync() + { + await DisposeAsyncCore(); + + Dispose(disposing: false); + GC.SuppressFinalize(this); + } + + protected virtual async ValueTask DisposeAsyncCore() + { + + if (_inner is IAsyncDisposable innerDisposableAsync) + { + await innerDisposableAsync.DisposeAsync(); + } + else if (_inner is IDisposable innerDisposable) + { + innerDisposable.Dispose(); + } + + if (_onAsyncDispose != null) + { + await _onAsyncDispose().ConfigureAwait(false); + } + + //if (_onDispose != null) + //{ + // _onDispose?.Invoke(); + //} + + _onAsyncDispose = null; + _onDispose = null; + _inner = null; + } +#endif + + } + + +} diff --git a/src/DependencyInjection.ReRouting/DisposeHelper.cs b/src/DependencyInjection.ReRouting/DisposeHelper.cs new file mode 100644 index 0000000..84b1cf5 --- /dev/null +++ b/src/DependencyInjection.ReRouting/DisposeHelper.cs @@ -0,0 +1,32 @@ +namespace DependencyInjection.ReRouting +{ + using System; + using System.Threading.Tasks; + + public static class DisposeHelper + { + +#if SUPPORTS_ASYNC_DISPOSE + public static async Task DisposeAsyncIfImplemented(object objectToBeDisposed) + { + if (objectToBeDisposed is IAsyncDisposable asyncDisposable) + { + await asyncDisposable.DisposeAsync(); + } + else + { + DisposeIfImplemented(objectToBeDisposed); + } + } + +#endif + + public static void DisposeIfImplemented(object objectToBeDisposed) + { + if (objectToBeDisposed is IDisposable disposable) + { + disposable.Dispose(); + } + } + } +} diff --git a/src/DependencyInjection.ReRouting/ReRoutingServiceProvider.cs b/src/DependencyInjection.ReRouting/ReRoutingServiceProvider.cs index fbf8bbc..0f4d661 100644 --- a/src/DependencyInjection.ReRouting/ReRoutingServiceProvider.cs +++ b/src/DependencyInjection.ReRouting/ReRoutingServiceProvider.cs @@ -5,6 +5,8 @@ namespace Dazinator.Extensions.DependencyInjection using System.Collections.Immutable; using System.Linq; using System.Runtime.CompilerServices; + using System.Threading.Tasks; + /// /// Requests for specific service types can be re-routed to an alternative service provider. /// @@ -116,5 +118,4 @@ private IServiceProvider LookupServiceProvider(Type serviceType) } } - }