From 08ad3dce59ad6ea5801e0d5291381667f03fa8b0 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Fri, 4 Aug 2023 17:16:48 +0200 Subject: [PATCH 01/20] Refs: #646 Castle.Windsor.Extension.DepencencyInjection removed null check on scope cache (AsyncLocal can be null on Threads coming from Threadpool.UnsafeQueueUserWorkItem, having no null check was also the original behavior) --- .../ResolveFromThreadpoolUnsafe.cs | 56 +++++++++++++++++++ .../Scope/ExtensionContainerScopeCache.cs | 2 +- 2 files changed, 57 insertions(+), 1 deletion(-) create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs new file mode 100644 index 0000000000..754ea0381f --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -0,0 +1,56 @@ +using Castle.MicroKernel.Registration; +using Castle.Windsor.Extensions.DependencyInjection.Tests.Components; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Castle.Windsor.Extensions.DependencyInjection.Tests { + public class ResolveFromThreadpoolUnsafe { + [Fact] + public async Task Can_Resolve_From_ServiceProvider() { + + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + /* + ThreadPool.UnsafeQueueUserWorkItem(state => { + // resolving using castle (without scopes) works + var actualUserService = container.Resolve(nameof(UserService)); + Assert.NotNull(actualUserService); + }, null); + */ + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => { + try { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + } +} diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index d89f9dd1f2..1e094869c6 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -24,7 +24,7 @@ internal static class ExtensionContainerScopeCache /// Thrown when there is no scope available. internal static ExtensionContainerScopeBase Current { - get => current.Value ?? throw new InvalidOperationException("No scope available"); + get => current.Value; // ?? throw new InvalidOperationException("No scope available"); set => current.Value = value; } } From a8347ea39d932bf48e036075ef7c2518871960f4 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Fri, 4 Aug 2023 18:54:39 +0200 Subject: [PATCH 02/20] ExtensionContainerRootsScopeAccessor might return a null root scope in Threadpool.UnsafeQueueUserWorkItem. --- .../Scope/ExtensionContainerRootScopeAccessor.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index 25139efec9..d6c34e3099 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -12,22 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Castle.Windsor.Extensions.DependencyInjection.Scope -{ +namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using System; using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; - internal class ExtensionContainerRootScopeAccessor : IScopeAccessor - { - public ILifetimeScope GetScope(CreationContext context) - { + internal class ExtensionContainerRootScopeAccessor : IScopeAccessor { + public ILifetimeScope GetScope(CreationContext context) { + if (ExtensionContainerScopeCache.Current == null) { + // might be null in threads spawn from Threadpool.UnsafeQueueUserWorkItem + return null; + } return ExtensionContainerScopeCache.Current.RootScope ?? throw new InvalidOperationException("No root scope available"); } - public void Dispose() - { + public void Dispose() { } } } From ba03d1bd4ec5064660c1721dc338d1b063f9633e Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Mon, 7 Aug 2023 13:41:53 +0200 Subject: [PATCH 03/20] Added More failing test cases --- .../ResolveFromThreadpoolUnsafe.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index 754ea0381f..3aa8469fe1 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -1,4 +1,5 @@ using Castle.MicroKernel.Registration; +using Castle.Windsor.Extensions.DependencyInjection.Extensions; using Castle.Windsor.Extensions.DependencyInjection.Tests.Components; using Microsoft.Extensions.DependencyInjection; using System; @@ -52,5 +53,83 @@ public async Task Can_Resolve_From_ServiceProvider() { IUserService result = await task; Assert.NotNull(result); } + + [Fact] + public async Task Can_Resolve_From_CastleWindsor() { + + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + // Component.For().ImplementedBy().LifestyleNetTransient(), + Classes.FromThisAssembly().BasedOn().WithServiceAllInterfaces().LifestyleNetStatic() + ); + + IUserService actualUserService; + actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => { + IUserService actualUserService = null; + try { + // resolving (with the underlying Castle Windsor, not using Service Provider) with a lifecycle that has an + // accessor that uses something that is AsyncLocal might be troublesome. + // the custom lifecycle accessor will kicks in, but noone assigns the Current scope (which is uninitialized) + actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + [Fact] + public async Task Can_Resolve_From_ServiceProvider_cretaed_in_UnsafeQueueUserWorkItem() { + + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + // Component.For().ImplementedBy().LifestyleNetTransient(), + Classes.FromThisAssembly().BasedOn().WithServiceAllInterfaces().LifestyleNetStatic() + ); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => { + IUserService actualUserService = null; + try { + // creating a service provider here will be troublesome too + IServiceProvider sp = f.CreateServiceProvider(container); + + actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } } } From df9dd36e8a8202b06cccefcd76daefff9a825f40 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Wed, 9 Aug 2023 09:24:59 +0200 Subject: [PATCH 04/20] Added Threadpool.Unsafe... failing tests, added comments on critical files --- .editorconfig | 1 + .../ResolveFromThreadpoolUnsafe.cs | 445 ++++++++++++++++-- .../ExtensionContainerRootScopeAccessor.cs | 2 + .../Scope/ExtensionContainerScopeCache.cs | 2 +- 4 files changed, 412 insertions(+), 38 deletions(-) diff --git a/.editorconfig b/.editorconfig index 67ae502916..eaf2b54d43 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,4 @@ indent_size = 2 [*.cs] indent_style = tab indent_size = 4 +csharp_new_line_before_open_brace = all \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index 3aa8469fe1..a601c3b921 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -7,11 +7,55 @@ using System.Threading.Tasks; using Xunit; -namespace Castle.Windsor.Extensions.DependencyInjection.Tests { - public class ResolveFromThreadpoolUnsafe { +namespace Castle.Windsor.Extensions.DependencyInjection.Tests +{ + public class ResolveFromThreadpoolUnsafe + { + #region Singleton + [Fact] - public async Task Can_Resolve_From_ServiceProvider() { + public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + [Fact] + public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() + { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); var f = new WindsorServiceProviderFactory(container); @@ -26,22 +70,221 @@ public async Task Can_Resolve_From_ServiceProvider() { var actualUserService = sp.GetService(); Assert.NotNull(actualUserService); - /* - ThreadPool.UnsafeQueueUserWorkItem(state => { - // resolving using castle (without scopes) works - var actualUserService = container.Resolve(nameof(UserService)); - Assert.NotNull(actualUserService); + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(nameof(UserService)); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + [Fact] + public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.NetStatic() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + [Fact] + public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.NetStatic() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(nameof(UserService)); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + #endregion + + #region Scoped + + [Fact] + public async Task Can_Resolve_LifestyleScoped_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleScoped() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + [Fact] + public async Task Can_Resolve_LifestyleScoped_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleScoped() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(nameof(UserService)); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); }, null); - */ + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + [Fact] + public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvider() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.ScopedToNetServiceScope() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); TaskCompletionSource tcs = new TaskCompletionSource(); - ThreadPool.UnsafeQueueUserWorkItem(state => { - try { + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { var actualUserService = sp.GetService(); Assert.NotNull(actualUserService); } - catch (Exception ex) { + catch (Exception ex) + { tcs.SetException(ex); return; } @@ -55,34 +298,77 @@ public async Task Can_Resolve_From_ServiceProvider() { } [Fact] - public async Task Can_Resolve_From_CastleWindsor() { + public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.ScopedToNetServiceScope() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(nameof(UserService)); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + #endregion + + #region Transient + + [Fact] + public async Task Can_Resolve_LifestyleTransient_From_ServiceProvider() + { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( - // Component.For().ImplementedBy().LifestyleNetTransient(), - Classes.FromThisAssembly().BasedOn().WithServiceAllInterfaces().LifestyleNetStatic() - ); + Component.For().ImplementedBy().LifestyleTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); - IUserService actualUserService; - actualUserService = container.Resolve(); + var actualUserService = sp.GetService(); Assert.NotNull(actualUserService); TaskCompletionSource tcs = new TaskCompletionSource(); - ThreadPool.UnsafeQueueUserWorkItem(state => { - IUserService actualUserService = null; - try { - // resolving (with the underlying Castle Windsor, not using Service Provider) with a lifecycle that has an - // accessor that uses something that is AsyncLocal might be troublesome. - // the custom lifecycle accessor will kicks in, but noone assigns the Current scope (which is uninitialized) - actualUserService = container.Resolve(); + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); Assert.NotNull(actualUserService); } - catch (Exception ex) { + catch (Exception ex) + { tcs.SetException(ex); return; } @@ -96,30 +382,113 @@ public async Task Can_Resolve_From_CastleWindsor() { } [Fact] - public async Task Can_Resolve_From_ServiceProvider_cretaed_in_UnsafeQueueUserWorkItem() { + public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(nameof(UserService)); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } + + [Fact] + public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider() + { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( - // Component.For().ImplementedBy().LifestyleNetTransient(), - Classes.FromThisAssembly().BasedOn().WithServiceAllInterfaces().LifestyleNetStatic() - ); + Component.For().ImplementedBy().LifestyleNetTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); TaskCompletionSource tcs = new TaskCompletionSource(); - ThreadPool.UnsafeQueueUserWorkItem(state => { - IUserService actualUserService = null; - try { - // creating a service provider here will be troublesome too - IServiceProvider sp = f.CreateServiceProvider(container); + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + } - actualUserService = sp.GetService(); + [Fact] + public async Task Can_Resolve_LifestyleNetTransient_From_WindsorContainer() + { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifestyleNetTransient() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(nameof(UserService)); Assert.NotNull(actualUserService); } - catch (Exception ex) { + catch (Exception ex) + { tcs.SetException(ex); return; } @@ -131,5 +500,7 @@ public async Task Can_Resolve_From_ServiceProvider_cretaed_in_UnsafeQueueUserWor IUserService result = await task; Assert.NotNull(result); } + + #endregion } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index d6c34e3099..5059ea66a3 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -20,10 +20,12 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { internal class ExtensionContainerRootScopeAccessor : IScopeAccessor { public ILifetimeScope GetScope(CreationContext context) { + /* if (ExtensionContainerScopeCache.Current == null) { // might be null in threads spawn from Threadpool.UnsafeQueueUserWorkItem return null; } + */ return ExtensionContainerScopeCache.Current.RootScope ?? throw new InvalidOperationException("No root scope available"); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index 1e094869c6..9b7b6d8234 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -24,7 +24,7 @@ internal static class ExtensionContainerScopeCache /// Thrown when there is no scope available. internal static ExtensionContainerScopeBase Current { - get => current.Value; // ?? throw new InvalidOperationException("No scope available"); + get => current.Value ?? throw new InvalidOperationException("No scope available"); // originally there was no exception set => current.Value = value; } } From 64df1b0998c3aff76dac8f59ccf664a9726548af Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Wed, 9 Aug 2023 10:15:30 +0200 Subject: [PATCH 05/20] Improved tests and comments --- .../ResolveFromThreadpoolUnsafe.cs | 84 ++++++- .../Extensions/WindsorExtensions.cs | 4 + .../RegistrationAdapter.cs | 215 ++++++++---------- .../Scope/ExtensionContainerScopeAccessor.cs | 3 +- .../Scope/ExtensionContainerScopeCache.cs | 3 +- 5 files changed, 183 insertions(+), 126 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index a601c3b921..36a2d339cd 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -13,6 +13,11 @@ public class ResolveFromThreadpoolUnsafe { #region Singleton + /* + * Singleton tests should never fail, given you have a container instance you should always + * be able to resolve a singleton from it. + */ + [Fact] public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() { @@ -51,6 +56,9 @@ public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -76,7 +84,7 @@ public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() { try { - var actualUserService = container.Resolve(nameof(UserService)); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -91,6 +99,9 @@ public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -131,6 +142,9 @@ public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -156,7 +170,7 @@ public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() { try { - var actualUserService = container.Resolve(nameof(UserService)); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -171,12 +185,20 @@ public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } #endregion #region Scoped + /* + * Scoped tests might fail if for whatever reason you do not have a current scope + * (like when you run from Threadpool.UnsafeQueueUserWorkItem). + */ + [Fact] public async Task Can_Resolve_LifestyleScoped_From_ServiceProvider() { @@ -215,6 +237,9 @@ public async Task Can_Resolve_LifestyleScoped_From_ServiceProvider() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -240,7 +265,7 @@ public async Task Can_Resolve_LifestyleScoped_From_WindsorContainer() { try { - var actualUserService = container.Resolve(nameof(UserService)); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -255,8 +280,16 @@ public async Task Can_Resolve_LifestyleScoped_From_WindsorContainer() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } + /// + /// This test succeeds because WindsorScopedServiceProvider captured the root scope on creation + /// and forced it to be current before service resolution. + /// Scoped is tied to the rootscope = potential memory leak. + /// [Fact] public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvider() { @@ -295,6 +328,9 @@ public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvi var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -320,7 +356,7 @@ public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_WindsorConta { try { - var actualUserService = container.Resolve(nameof(UserService)); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -335,12 +371,26 @@ public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_WindsorConta var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } #endregion #region Transient + /* + * Transient tests failure is questionable: + * - if you have a container you should be able to resolve transient without a scope, + * but they might be tracked by the container itself (or the IServiceProvider) + * - when windsor container is disposed all transient services are disposed as well + * - when a IServiceProvider is disposed all transient services (created by it) are disposed as well + * - problem is: we have una instance of a windsor container passed on to multiple instances of IServiceProvider + * one solution will be to tie the Transients to a scope, and the scope is tied to service provider + * when both of them are disposed, the transient services are disposed as well + */ + [Fact] public async Task Can_Resolve_LifestyleTransient_From_ServiceProvider() { @@ -379,6 +429,9 @@ public async Task Can_Resolve_LifestyleTransient_From_ServiceProvider() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -404,7 +457,7 @@ public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer() { try { - var actualUserService = container.Resolve(nameof(UserService)); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -419,8 +472,16 @@ public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } + /// + /// This test succeeds because WindsorScopedServiceProvider captured the root scope on creation + /// and forced it to be current before service resolution. + /// Transient is tied to the rootscope = potential memory leak. + /// [Fact] public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider() { @@ -459,6 +520,9 @@ public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } [Fact] @@ -484,7 +548,7 @@ public async Task Can_Resolve_LifestyleNetTransient_From_WindsorContainer() { try { - var actualUserService = container.Resolve(nameof(UserService)); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -499,8 +563,16 @@ public async Task Can_Resolve_LifestyleNetTransient_From_WindsorContainer() var task = tcs.Task; IUserService result = await task; Assert.NotNull(result); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } #endregion + + /* + * Missing tests: we should also test what happens with injected IServiceProvider (what scope do they get?) + * Injected IServiceProvider might or might not have a scope (it depends on AsyncLocal value). + */ } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs index 38d46d34d7..6f38a7ed6f 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs @@ -49,6 +49,10 @@ public static ComponentRegistration LifestyleNetTransient(th /// public static ComponentRegistration NetStatic(this LifestyleGroup lifestyle) where TService : class { + //return lifestyle.Singleton; + // I don't think we need this lifestyle at all, usual Singleton should be good + // also we don't need the whole rootscope thing a normal scope set as current should be enough + // otherwise we should revert to static rootscope return lifestyle .Scoped(); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index 974c02ae88..b9cdf1045b 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -12,122 +12,101 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Castle.Windsor.Extensions.DependencyInjection -{ - using System; - - using Castle.MicroKernel.Registration; - using Castle.Windsor.Extensions.DependencyInjection.Extensions; - - using Microsoft.Extensions.DependencyInjection; - - internal class RegistrationAdapter - { - public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) - { - ComponentRegistration registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); - - if(service.ImplementationType != null) - { - registration = UsingImplementation(registration, service); - } - else - { - throw new System.ArgumentException("Unsupported ServiceDescriptor"); - } - - return ResolveLifestyle(registration, service) - .IsDefault(); - } - - public static IRegistration FromServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) - { - var registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); - - if (service.ImplementationFactory != null) - { - registration = UsingFactoryMethod(registration, service); - } - else if (service.ImplementationInstance != null) - { - registration = UsingInstance(registration, service); - } - else if (service.ImplementationType != null) - { - registration = UsingImplementation(registration, service); - } - - return ResolveLifestyle(registration, service) - .IsDefault(); - } - - public static string OriginalComponentName(string uniqueComponentName) - { - if(uniqueComponentName == null) - { - return null; - } - if(!uniqueComponentName.Contains("@")) - { - return uniqueComponentName; - } - return uniqueComponentName.Split('@')[0]; - } - - internal static string UniqueComponentName(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) - { - var result = ""; - if(service.ImplementationType != null) - { - result = service.ImplementationType.FullName; - } - else if(service.ImplementationInstance != null) - { - result = service.ImplementationInstance.GetType().FullName; - } - else - { - result = service.ImplementationFactory.GetType().FullName; - } - result = result + "@" + Guid.NewGuid().ToString(); - - return result; - } - - private static ComponentRegistration UsingFactoryMethod(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class - { - return registration.UsingFactoryMethod((kernel) => { - var serviceProvider = kernel.Resolve(); - return service.ImplementationFactory(serviceProvider) as TService; - }); - } - - private static ComponentRegistration UsingInstance(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class - { - return registration.Instance(service.ImplementationInstance as TService); - } - - private static ComponentRegistration UsingImplementation(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class - { - return registration.ImplementedBy(service.ImplementationType); - } - - private static ComponentRegistration ResolveLifestyle(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class - { - switch(service.Lifetime) - { - case ServiceLifetime.Singleton: - return registration.LifeStyle.NetStatic(); - case ServiceLifetime.Scoped: - return registration.LifeStyle.ScopedToNetServiceScope(); - case ServiceLifetime.Transient: - return registration.LifestyleNetTransient(); - - default: - throw new System.ArgumentException($"Invalid lifetime {service.Lifetime}"); - } - } - } +namespace Castle.Windsor.Extensions.DependencyInjection { + using System; + + using Castle.MicroKernel.Registration; + using Castle.Windsor.Extensions.DependencyInjection.Extensions; + + using Microsoft.Extensions.DependencyInjection; + + internal static class RegistrationAdapter { + public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { + ComponentRegistration registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); + + if (service.ImplementationType != null) { + registration = UsingImplementation(registration, service); + } + else { + throw new System.ArgumentException("Unsupported ServiceDescriptor"); + } + + return ResolveLifestyle(registration, service) + .IsDefault(); + } + + public static IRegistration FromServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { + var registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); + + if (service.ImplementationFactory != null) { + registration = UsingFactoryMethod(registration, service); + } + else if (service.ImplementationInstance != null) { + registration = UsingInstance(registration, service); + } + else if (service.ImplementationType != null) { + registration = UsingImplementation(registration, service); + } + + return ResolveLifestyle(registration, service) + .IsDefault(); + } + + public static string OriginalComponentName(string uniqueComponentName) { + if (uniqueComponentName == null) { + return null; + } + if (!uniqueComponentName.Contains("@")) { + return uniqueComponentName; + } + return uniqueComponentName.Split('@')[0]; + } + + internal static string UniqueComponentName(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { + var result = ""; + if (service.ImplementationType != null) { + result = service.ImplementationType.FullName; + } + else if (service.ImplementationInstance != null) { + result = service.ImplementationInstance.GetType().FullName; + } + else { + result = service.ImplementationFactory.GetType().FullName; + } + result = result + "@" + Guid.NewGuid().ToString(); + + return result; + } + + private static ComponentRegistration UsingFactoryMethod(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { + return registration.UsingFactoryMethod((kernel) => { + var serviceProvider = kernel.Resolve(); + return service.ImplementationFactory(serviceProvider) as TService; + }); + } + + private static ComponentRegistration UsingInstance(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { + return registration.Instance(service.ImplementationInstance as TService); + } + + private static ComponentRegistration UsingImplementation(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { + return registration.ImplementedBy(service.ImplementationType); + } + + private static ComponentRegistration ResolveLifestyle(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { + switch (service.Lifetime) { + case ServiceLifetime.Singleton: + return registration.LifeStyle.NetStatic(); + case ServiceLifetime.Scoped: + return registration.LifeStyle.ScopedToNetServiceScope(); + case ServiceLifetime.Transient: + return registration.LifestyleNetTransient(); + + default: + throw new System.ArgumentException($"Invalid lifetime {service.Lifetime}"); + } + } + } } \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs index 9042944d75..8eaaa9f1d2 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs @@ -16,12 +16,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; + using System; internal class ExtensionContainerScopeAccessor : IScopeAccessor { public ILifetimeScope GetScope(CreationContext context) { - return ExtensionContainerScopeCache.Current; + return ExtensionContainerScopeCache.Current ?? throw new InvalidOperationException("No scope available. Did you forget to call IServiceScopeFactory.CreateScope()?"); ; } public void Dispose() diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index 9b7b6d8234..cc48a80320 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -24,7 +24,8 @@ internal static class ExtensionContainerScopeCache /// Thrown when there is no scope available. internal static ExtensionContainerScopeBase Current { - get => current.Value ?? throw new InvalidOperationException("No scope available"); // originally there was no exception + // AysncLocal can be null in some cases (like Threadpool.UnsafeQueueUserWorkItem) + get => current.Value; // ?? throw new InvalidOperationException("No scope available. Did you forget to call IServiceScopeFactory.CreateScope()?"); set => current.Value = value; } } From 28ac185f27c61f23a4d07137eb23da17919d87be Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Wed, 9 Aug 2023 11:03:57 +0200 Subject: [PATCH 06/20] Option to map custom NetStatic to Castle Singleton lifestyle. this improve resolution of singleton object that might not need a root scope, the windsor container should be enough to resolve singletons --- .../ResolveFromThreadpoolUnsafe.cs | 39 ++++++++++++++++++- .../Extensions/WindsorExtensions.cs | 9 +++-- .../WindsorDependencyInjectionOptions.cs | 16 ++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index 36a2d339cd..7bba535e9b 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -9,8 +9,41 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Tests { - public class ResolveFromThreadpoolUnsafe + [CollectionDefinition(nameof(DoNotParallelize), DisableParallelization = true)] + public class DoNotParallelize { } + + /// + /// These is the original Castle Windsor Dependency Injection behavior. + /// + public class ResolveFromThreadpoolUnsafe_NetStatic: AbstractResolveFromThreadpoolUnsafe + { + public ResolveFromThreadpoolUnsafe_NetStatic() : base(false) + { + } + } + + /// + /// Mapping NetStatic to usual Singleton lifestyle. + /// + public class ResolveFromThreadpoolUnsafe_Singleton : AbstractResolveFromThreadpoolUnsafe + { + public ResolveFromThreadpoolUnsafe_Singleton() : base(true) + { + } + } + + /// + /// relying on static state (WindsorDependencyInjectionOptions) is not good for tests + /// that might run in parallel, can lead to false positives / negatives. + /// + [Collection(nameof(DoNotParallelize))] + public abstract class AbstractResolveFromThreadpoolUnsafe { + protected AbstractResolveFromThreadpoolUnsafe(bool mapNetStaticToSingleton) + { + WindsorDependencyInjectionOptions.MapNetStaticToSingleton = mapNetStaticToSingleton; + } + #region Singleton /* @@ -147,6 +180,10 @@ public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() container.Dispose(); } + /// + /// This test will Succeed is we use standard Castle Windsor Singleton lifestyle instead of the custom + /// NetStatic lifestyle. + /// [Fact] public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() { diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs index 6f38a7ed6f..5b7b95f9d4 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs @@ -49,10 +49,13 @@ public static ComponentRegistration LifestyleNetTransient(th /// public static ComponentRegistration NetStatic(this LifestyleGroup lifestyle) where TService : class { - //return lifestyle.Singleton; - // I don't think we need this lifestyle at all, usual Singleton should be good - // also we don't need the whole rootscope thing a normal scope set as current should be enough + // I don't think we need this lifestyle at all, usual Singleton should be good enough; + // also we maybe don't need the whole rootscope thing. A normal scope set as current should be enough // otherwise we should revert to static rootscope + if (WindsorDependencyInjectionOptions.MapNetStaticToSingleton) + { + return lifestyle.Singleton; + } return lifestyle .Scoped(); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs new file mode 100644 index 0000000000..ab1ec62ef7 --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorDependencyInjectionOptions.cs @@ -0,0 +1,16 @@ +namespace Castle.Windsor.Extensions.DependencyInjection +{ + /// + /// Global settins to change the dependency injection behavior. + /// These settings should be set before the container is created. + /// + public static class WindsorDependencyInjectionOptions + { + /// + /// Map NetStatic lifestyle to Castle Windsor Singleton lifestyle. + /// The whole RootScope handling is disabled. + /// (defaut: false) + /// + public static bool MapNetStaticToSingleton { get; set; } + } +} From a210c2a400aa540bfa9b9ad699a26863df16920f Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Wed, 9 Aug 2023 15:00:41 +0200 Subject: [PATCH 07/20] RootScope AsyncLocal cache can be null when AspNetCore tries to create scopes from Threadpool.UnsafeQueueUserWorkItem --- .../RegistrationAdapter.cs | 216 ++++++++++-------- .../Scope/ExtensionContainerScope.cs | 3 +- 2 files changed, 120 insertions(+), 99 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index b9cdf1045b..ad27dce4e0 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -12,101 +12,123 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Castle.Windsor.Extensions.DependencyInjection { - using System; - - using Castle.MicroKernel.Registration; - using Castle.Windsor.Extensions.DependencyInjection.Extensions; - - using Microsoft.Extensions.DependencyInjection; - - internal static class RegistrationAdapter { - public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { - ComponentRegistration registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); - - if (service.ImplementationType != null) { - registration = UsingImplementation(registration, service); - } - else { - throw new System.ArgumentException("Unsupported ServiceDescriptor"); - } - - return ResolveLifestyle(registration, service) - .IsDefault(); - } - - public static IRegistration FromServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { - var registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); - - if (service.ImplementationFactory != null) { - registration = UsingFactoryMethod(registration, service); - } - else if (service.ImplementationInstance != null) { - registration = UsingInstance(registration, service); - } - else if (service.ImplementationType != null) { - registration = UsingImplementation(registration, service); - } - - return ResolveLifestyle(registration, service) - .IsDefault(); - } - - public static string OriginalComponentName(string uniqueComponentName) { - if (uniqueComponentName == null) { - return null; - } - if (!uniqueComponentName.Contains("@")) { - return uniqueComponentName; - } - return uniqueComponentName.Split('@')[0]; - } - - internal static string UniqueComponentName(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { - var result = ""; - if (service.ImplementationType != null) { - result = service.ImplementationType.FullName; - } - else if (service.ImplementationInstance != null) { - result = service.ImplementationInstance.GetType().FullName; - } - else { - result = service.ImplementationFactory.GetType().FullName; - } - result = result + "@" + Guid.NewGuid().ToString(); - - return result; - } - - private static ComponentRegistration UsingFactoryMethod(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { - return registration.UsingFactoryMethod((kernel) => { - var serviceProvider = kernel.Resolve(); - return service.ImplementationFactory(serviceProvider) as TService; - }); - } - - private static ComponentRegistration UsingInstance(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { - return registration.Instance(service.ImplementationInstance as TService); - } - - private static ComponentRegistration UsingImplementation(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { - return registration.ImplementedBy(service.ImplementationType); - } - - private static ComponentRegistration ResolveLifestyle(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { - switch (service.Lifetime) { - case ServiceLifetime.Singleton: - return registration.LifeStyle.NetStatic(); - case ServiceLifetime.Scoped: - return registration.LifeStyle.ScopedToNetServiceScope(); - case ServiceLifetime.Transient: - return registration.LifestyleNetTransient(); - - default: - throw new System.ArgumentException($"Invalid lifetime {service.Lifetime}"); - } - } - } +namespace Castle.Windsor.Extensions.DependencyInjection +{ + using System; + + using Castle.MicroKernel.Registration; + using Castle.Windsor.Extensions.DependencyInjection.Extensions; + + using Microsoft.Extensions.DependencyInjection; + + internal static class RegistrationAdapter + { + public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + { + ComponentRegistration registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); + + if (service.ImplementationType != null) + { + registration = UsingImplementation(registration, service); + } + else + { + throw new System.ArgumentException("Unsupported ServiceDescriptor"); + } + + return ResolveLifestyle(registration, service) + .IsDefault(); + } + + public static IRegistration FromServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + { + var registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); + + if (service.ImplementationFactory != null) + { + registration = UsingFactoryMethod(registration, service); + } + else if (service.ImplementationInstance != null) + { + registration = UsingInstance(registration, service); + } + else if (service.ImplementationType != null) + { + registration = UsingImplementation(registration, service); + } + + return ResolveLifestyle(registration, service) + .IsDefault(); + } + + public static string OriginalComponentName(string uniqueComponentName) + { + if (uniqueComponentName == null) + { + return null; + } + if (!uniqueComponentName.Contains("@")) + { + return uniqueComponentName; + } + return uniqueComponentName.Split('@')[0]; + } + + internal static string UniqueComponentName(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + { + var result = ""; + if (service.ImplementationType != null) + { + result = service.ImplementationType.FullName; + } + else if (service.ImplementationInstance != null) + { + result = service.ImplementationInstance.GetType().FullName; + } + else + { + result = service.ImplementationFactory.GetType().FullName; + } + result = result + "@" + Guid.NewGuid().ToString(); + + return result; + } + + private static ComponentRegistration UsingFactoryMethod(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class + { + return registration.UsingFactoryMethod((kernel) => + { + var serviceProvider = kernel.Resolve(); + return service.ImplementationFactory(serviceProvider) as TService; + }); + } + + private static ComponentRegistration UsingInstance(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class + { + return registration.Instance(service.ImplementationInstance as TService); + } + + private static ComponentRegistration UsingImplementation(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class + { + return registration.ImplementedBy(service.ImplementationType); + } + + private static ComponentRegistration ResolveLifestyle(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class + { + switch (service.Lifetime) + { + case ServiceLifetime.Singleton: + return registration.LifeStyle.NetStatic(); + case ServiceLifetime.Scoped: + return registration.LifeStyle.ScopedToNetServiceScope(); + case ServiceLifetime.Transient: + return registration.LifestyleNetTransient(); + + default: + throw new System.ArgumentException($"Invalid lifetime {service.Lifetime}"); + } + } + } } \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs index 2ff5a5c497..9ad99907f7 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs @@ -25,10 +25,9 @@ protected ExtensionContainerScope() internal override ExtensionContainerScopeBase RootScope { get; set; } - internal static ExtensionContainerScopeBase BeginScope() { - var scope = new ExtensionContainerScope { RootScope = ExtensionContainerScopeCache.Current.RootScope }; + var scope = new ExtensionContainerScope { RootScope = ExtensionContainerScopeCache.Current?.RootScope }; ExtensionContainerScopeCache.Current = scope; return scope; } From b390405fc371b12396c8694edc3b89155fd333ea Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Thu, 10 Aug 2023 10:53:22 +0200 Subject: [PATCH 08/20] Improved Tests showing supported, unsupported and memory leaks scenarios --- .../ResolveFromThreadpoolUnsafe.cs | 318 +++++++++++++----- .../ExtensionContainerRootScopeAccessor.cs | 2 +- 2 files changed, 231 insertions(+), 89 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index 7bba535e9b..f8d2ce57a4 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -1,4 +1,5 @@ -using Castle.MicroKernel.Registration; +using Castle.MicroKernel.Lifestyle; +using Castle.MicroKernel.Registration; using Castle.Windsor.Extensions.DependencyInjection.Extensions; using Castle.Windsor.Extensions.DependencyInjection.Tests.Components; using Microsoft.Extensions.DependencyInjection; @@ -15,44 +16,90 @@ public class DoNotParallelize { } /// /// These is the original Castle Windsor Dependency Injection behavior. /// - public class ResolveFromThreadpoolUnsafe_NetStatic: AbstractResolveFromThreadpoolUnsafe + public class ResolveFromThreadpoolUnsafe_NetStatic : AbstractResolveFromThreadpoolUnsafe { public ResolveFromThreadpoolUnsafe_NetStatic() : base(false) { } - } - /// - /// Mapping NetStatic to usual Singleton lifestyle. - /// - public class ResolveFromThreadpoolUnsafe_Singleton : AbstractResolveFromThreadpoolUnsafe - { - public ResolveFromThreadpoolUnsafe_Singleton() : base(true) + #region "Singleton" + + /// + /// This test will Succeed is we use standard Castle Windsor Singleton lifestyle instead of the custom + /// NetStatic lifestyle. + /// + [Fact] + public async Task Cannot_Resolve_LifestyleNetStatic_From_WindsorContainer_NoRootScopeAvailable() { + var serviceProvider = new ServiceCollection(); + var container = new WindsorContainer(); + var f = new WindsorServiceProviderFactory(container); + f.CreateBuilder(serviceProvider); + + container.Register( + Component.For().ImplementedBy().LifeStyle.NetStatic() + ); + + IServiceProvider sp = f.CreateServiceProvider(container); + + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + + TaskCompletionSource tcs = new TaskCompletionSource(); + + ThreadPool.UnsafeQueueUserWorkItem(state => + { + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + // The test succeeds if we use standard Castle Windsor Singleton lifestyle instead of the custom NetStatic lifestyle. + Assert.NotNull(result); + }); + + // This test will fail if we use NetStatic lifestyle + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.Equal("No root scope available.", ex.Message); + + (sp as IDisposable)?.Dispose(); + container.Dispose(); } + + #endregion } /// - /// relying on static state (WindsorDependencyInjectionOptions) is not good for tests - /// that might run in parallel, can lead to false positives / negatives. + /// Mapping NetStatic to usual Singleton lifestyle. /// - [Collection(nameof(DoNotParallelize))] - public abstract class AbstractResolveFromThreadpoolUnsafe + public class ResolveFromThreadpoolUnsafe_Singleton : AbstractResolveFromThreadpoolUnsafe { - protected AbstractResolveFromThreadpoolUnsafe(bool mapNetStaticToSingleton) + public ResolveFromThreadpoolUnsafe_Singleton() : base(true) { - WindsorDependencyInjectionOptions.MapNetStaticToSingleton = mapNetStaticToSingleton; } - #region Singleton - - /* - * Singleton tests should never fail, given you have a container instance you should always - * be able to resolve a singleton from it. - */ + #region "Singleton" + /// + /// This test will Succeed is we use standard Castle Windsor Singleton lifestyle instead of the custom + /// NetStatic lifestyle. + /// [Fact] - public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() + public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -60,7 +107,7 @@ public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() f.CreateBuilder(serviceProvider); container.Register( - Component.For().ImplementedBy() + Component.For().ImplementedBy().LifeStyle.NetStatic() ); IServiceProvider sp = f.CreateServiceProvider(container); @@ -74,7 +121,7 @@ public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() { try { - var actualUserService = sp.GetService(); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -86,16 +133,42 @@ public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() }, null); // Wait for the work item to complete. - var task = tcs.Task; - IUserService result = await task; - Assert.NotNull(result); + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + // The test succeeds if we use standard Castle Windsor Singleton lifestyle instead of the custom NetStatic lifestyle. + Assert.NotNull(result); + }); (sp as IDisposable)?.Dispose(); container.Dispose(); } + #endregion + } + + /// + /// relying on static state (WindsorDependencyInjectionOptions) is not good for tests + /// that might run in parallel, can lead to false positives / negatives. + /// + [Collection(nameof(DoNotParallelize))] + public abstract class AbstractResolveFromThreadpoolUnsafe + { + protected AbstractResolveFromThreadpoolUnsafe(bool mapNetStaticToSingleton) + { + WindsorDependencyInjectionOptions.MapNetStaticToSingleton = mapNetStaticToSingleton; + } + + #region Singleton + + /* + * Singleton tests should never fail, given you have a container instance you should always + * be able to resolve a singleton from it. + */ + [Fact] - public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() + public async Task Can_Resolve_LifestyleSingleton_From_ServiceProvider() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -117,7 +190,7 @@ public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() { try { - var actualUserService = container.Resolve(); + var actualUserService = sp.GetService(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -138,7 +211,7 @@ public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() } [Fact] - public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() + public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -146,7 +219,7 @@ public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() f.CreateBuilder(serviceProvider); container.Register( - Component.For().ImplementedBy().LifeStyle.NetStatic() + Component.For().ImplementedBy() ); IServiceProvider sp = f.CreateServiceProvider(container); @@ -160,7 +233,7 @@ public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() { try { - var actualUserService = sp.GetService(); + var actualUserService = container.Resolve(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -180,12 +253,8 @@ public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() container.Dispose(); } - /// - /// This test will Succeed is we use standard Castle Windsor Singleton lifestyle instead of the custom - /// NetStatic lifestyle. - /// [Fact] - public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() + public async Task Can_Resolve_LifestyleNetStatic_From_ServiceProvider() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -207,7 +276,7 @@ public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() { try { - var actualUserService = container.Resolve(); + var actualUserService = sp.GetService(); Assert.NotNull(actualUserService); } catch (Exception ex) @@ -236,8 +305,12 @@ public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer() * (like when you run from Threadpool.UnsafeQueueUserWorkItem). */ + /// + /// This test will fail because the service provider adapter + /// does not create a standard Castle Windsor scope + /// [Fact] - public async Task Can_Resolve_LifestyleScoped_From_ServiceProvider() + public async Task Cannot_Resolve_LifestyleScoped_From_ServiceProvider() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -250,37 +323,52 @@ public async Task Can_Resolve_LifestyleScoped_From_ServiceProvider() IServiceProvider sp = f.CreateServiceProvider(container); - var actualUserService = sp.GetService(); - Assert.NotNull(actualUserService); + // must create a standard Castle Windsor scope (not managed by the adapter) + using (var s = container.BeginScope()) + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); - TaskCompletionSource tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new TaskCompletionSource(); - ThreadPool.UnsafeQueueUserWorkItem(state => - { - try + ThreadPool.UnsafeQueueUserWorkItem(state => { - var actualUserService = sp.GetService(); - Assert.NotNull(actualUserService); - } - catch (Exception ex) + try + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => { - tcs.SetException(ex); - return; - } - tcs.SetResult(actualUserService); - }, null); + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); - // Wait for the work item to complete. - var task = tcs.Task; - IUserService result = await task; - Assert.NotNull(result); + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("Scope was not available. Did you forget to call container.BeginScope()?", ex.Message); + } (sp as IDisposable)?.Dispose(); container.Dispose(); } + /// + /// This test will fail because the service provider adapter + /// does not create a standard Castle Windsor scope + /// [Fact] - public async Task Can_Resolve_LifestyleScoped_From_WindsorContainer() + public async Task Cannot_Resolve_LifestyleScoped_From_WindsorContainer() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -293,30 +381,41 @@ public async Task Can_Resolve_LifestyleScoped_From_WindsorContainer() IServiceProvider sp = f.CreateServiceProvider(container); - var actualUserService = sp.GetService(); - Assert.NotNull(actualUserService); + // must create a standard Castle Windsor scope (not managed by the adapter) + using (var s = container.BeginScope()) + { + var actualUserService = sp.GetService(); + Assert.NotNull(actualUserService); - TaskCompletionSource tcs = new TaskCompletionSource(); + TaskCompletionSource tcs = new TaskCompletionSource(); - ThreadPool.UnsafeQueueUserWorkItem(state => - { - try + ThreadPool.UnsafeQueueUserWorkItem(state => { - var actualUserService = container.Resolve(); - Assert.NotNull(actualUserService); - } - catch (Exception ex) + try + { + var actualUserService = container.Resolve(); + Assert.NotNull(actualUserService); + } + catch (Exception ex) + { + tcs.SetException(ex); + return; + } + tcs.SetResult(actualUserService); + }, null); + + // Wait for the work item to complete. + var ex = await Catches.ExceptionAsync(async () => { - tcs.SetException(ex); - return; - } - tcs.SetResult(actualUserService); - }, null); + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); - // Wait for the work item to complete. - var task = tcs.Task; - IUserService result = await task; - Assert.NotNull(result); + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("Scope was not available. Did you forget to call container.BeginScope()?", ex.Message); + } (sp as IDisposable)?.Dispose(); container.Dispose(); @@ -328,7 +427,7 @@ public async Task Can_Resolve_LifestyleScoped_From_WindsorContainer() /// Scoped is tied to the rootscope = potential memory leak. /// [Fact] - public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvider() + public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvider_MemoryLeak() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -371,7 +470,7 @@ public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvi } [Fact] - public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_WindsorContainer() + public async Task Cannot_Resolve_LifestyleScopedToNetServiceScope_From_WindsorContainer() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -405,9 +504,16 @@ public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_WindsorConta }, null); // Wait for the work item to complete. - var task = tcs.Task; - IUserService result = await task; - Assert.NotNull(result); + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); + + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("No scope available", ex.Message); (sp as IDisposable)?.Dispose(); container.Dispose(); @@ -520,7 +626,7 @@ public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer() /// Transient is tied to the rootscope = potential memory leak. /// [Fact] - public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider() + public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider_MemoryLeak() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -563,7 +669,7 @@ public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider() } [Fact] - public async Task Can_Resolve_LifestyleNetTransient_From_WindsorContainer() + public async Task Cannot_Resolve_LifestyleNetTransient_From_WindsorContainer_NoScopeAvailable() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); @@ -597,9 +703,16 @@ public async Task Can_Resolve_LifestyleNetTransient_From_WindsorContainer() }, null); // Wait for the work item to complete. - var task = tcs.Task; - IUserService result = await task; - Assert.NotNull(result); + var ex = await Catches.ExceptionAsync(async () => + { + var task = tcs.Task; + IUserService result = await task; + Assert.NotNull(result); + }); + + Assert.NotNull(ex); + Assert.IsType(ex); + Assert.StartsWith("No scope available", ex.Message); (sp as IDisposable)?.Dispose(); container.Dispose(); @@ -612,4 +725,33 @@ public async Task Can_Resolve_LifestyleNetTransient_From_WindsorContainer() * Injected IServiceProvider might or might not have a scope (it depends on AsyncLocal value). */ } + + public static class Catches + { + public static Exception Exception(Action action) + { + try + { + action(); + } + catch (Exception e) + { + return e; + } + return null; + } + + public async static Task ExceptionAsync(Func func) + { + try + { + await func(); + } + catch (Exception e) + { + return e; + } + return null; + } + } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index 5059ea66a3..62dc0d7638 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -26,7 +26,7 @@ public ILifetimeScope GetScope(CreationContext context) { return null; } */ - return ExtensionContainerScopeCache.Current.RootScope ?? throw new InvalidOperationException("No root scope available"); + return ExtensionContainerScopeCache.Current?.RootScope ?? throw new InvalidOperationException("No root scope available."); } public void Dispose() { From b997da02488af155cf7f29b0c904c82add359aac Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Thu, 10 Aug 2023 11:42:53 +0200 Subject: [PATCH 09/20] Added comments on WindsorScopedServiceProvider dispose --- .../WindsorScopedServiceProvider.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 4e61f2f765..006c475324 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -18,7 +18,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection using System; using System.Collections.Generic; using System.Reflection; - + using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; @@ -30,7 +30,7 @@ internal class WindsorScopedServiceProvider : IServiceProvider, ISupportRequired private bool disposing; private readonly IWindsorContainer container; - + public WindsorScopedServiceProvider(IWindsorContainer container) { this.container = container; @@ -39,27 +39,30 @@ public WindsorScopedServiceProvider(IWindsorContainer container) public object GetService(Type serviceType) { - using(_ = new ForcedScope(scope)) + using (_ = new ForcedScope(scope)) { - return ResolveInstanceOrNull(serviceType, true); + return ResolveInstanceOrNull(serviceType, true); } } public object GetRequiredService(Type serviceType) { - using(_ = new ForcedScope(scope)) + using (_ = new ForcedScope(scope)) { - return ResolveInstanceOrNull(serviceType, false); + return ResolveInstanceOrNull(serviceType, false); } } public void Dispose() { + // root scope should be tied to the root IserviceProvider, so + // it has to be disposed with the IserviceProvider to which is tied to if (!(scope is ExtensionContainerRootScope)) return; if (disposing) return; disposing = true; var disposableScope = scope as IDisposable; disposableScope?.Dispose(); + // disping the container here is questionable... what if I want to create another IServiceProvider form the factory? container.Dispose(); } private object ResolveInstanceOrNull(Type serviceType, bool isOptional) From 553253944d0c5ab43e9a2bacf17f2e56e43a5389 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Wed, 9 Aug 2023 14:59:34 +0200 Subject: [PATCH 10/20] Updated Castle.Windsor reference to nuget package to create private deploy packages --- .../Castle.Windsor.Extensions.DependencyInjection.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj index 24eaaa0ae0..b12a584303 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj @@ -31,6 +31,6 @@ - + From 518cc84d6054524ebe394047e04f94a6b71bb4c7 Mon Sep 17 00:00:00 2001 From: Alessandro Giorgetti Date: Thu, 10 Aug 2023 12:24:45 +0200 Subject: [PATCH 11/20] Added NuGet package build instructions --- .../PUBLISH_NUGET.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection/PUBLISH_NUGET.md diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/PUBLISH_NUGET.md b/src/Castle.Windsor.Extensions.DependencyInjection/PUBLISH_NUGET.md new file mode 100644 index 0000000000..cbb2173085 --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection/PUBLISH_NUGET.md @@ -0,0 +1,21 @@ +# Build NuGet package + +- Update Castle.Windsor to the desired version. + +- Open a terminal. + +- Set the APPVEYOR_BUILD_VERSION environment variable to the version you want to publish: + +```bash +set APPVEYOR_BUILD_VERSION=X.X.X-beta000X +``` + +- Launch the build script command: + +```bash +build.cmd +``` + +- The NuGet package is generated in the `build` folder. + +- Remove the environment variable, it can cause issues when building in Visual Studio. \ No newline at end of file From 81c6b05fe6693964cce722cc4b0cf929af86984d Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Fri, 9 Feb 2024 12:41:58 +0100 Subject: [PATCH 12/20] added gitversion --- .config/GitVersion.yml | 5 +++++ .config/dotnet-tools.json | 18 ++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 .config/GitVersion.yml create mode 100644 .config/dotnet-tools.json diff --git a/.config/GitVersion.yml b/.config/GitVersion.yml new file mode 100644 index 0000000000..0f1491231f --- /dev/null +++ b/.config/GitVersion.yml @@ -0,0 +1,5 @@ +branches: {} +ignore: + sha: [] +merge-message-formats: {} +mode: ContinuousDeployment \ No newline at end of file diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json new file mode 100644 index 0000000000..595804276c --- /dev/null +++ b/.config/dotnet-tools.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "nswag.consolecore": { + "version": "14.0.0", + "commands": [ + "nswag" + ] + }, + "gitversion.tool": { + "version": "5.2.4", + "commands": [ + "dotnet-gitversion" + ] + } + } +} \ No newline at end of file From 3707b52252deab2aa16ca610f2696f9ab43ffe40 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Fri, 9 Feb 2024 17:27:09 +0100 Subject: [PATCH 13/20] Update to work with .NET 8 dependency registration. Major fix was to support keyed registration scenario --- .vscode/tasks.json.old | 16 ++ buildscripts/common.props | 4 +- ...xtensions.DependencyInjection.Tests.csproj | 16 +- .../CustomAssumptionTests.cs | 103 +++++++++ ...leDependencyInjectionSpecificationTests.cs | 8 +- .../TestServiceCollection.cs | 13 ++ ...edDependencyInjectionSpecificationTests.cs | 124 +++++++++++ ...viceProviderCustomWindsorContainerTests.cs | 9 +- ...dsor.Extensions.DependencyInjection.csproj | 10 +- .../Definitions.cs | 7 + .../Extensions/ServiceDescriptorExtensions.cs | 8 +- .../KeyedRegistrationHelper.cs | 209 ++++++++++++++++++ .../KeyedServicesSubDependencyResolver.cs | 71 ++++++ .../RegistrationAdapter.cs | 166 ++++++++++++-- .../Scope/ExtensionContainerRootScope.cs | 1 - .../ExtensionContainerRootScopeAccessor.cs | 18 +- .../Scope/ExtensionContainerScopeBase.cs | 7 +- .../Scope/WindsorScopeFactory.cs | 10 +- .../WindsorScopedServiceProvider.cs | 177 ++++++++++++++- .../WindsorServiceProviderFactory.cs | 1 - .../WindsorServiceProviderFactoryBase.cs | 25 ++- .../Castle.Windsor.Extensions.Hosting.csproj | 2 +- .../DefaultComponentActivator.cs | 4 + .../Handlers/DefaultGenericHandler.cs | 2 +- .../GenericHandlerTypeMismatchException.cs | 4 +- 25 files changed, 950 insertions(+), 65 deletions(-) create mode 100644 .vscode/tasks.json.old create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection.Tests/TestServiceCollection.cs create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection/Definitions.cs create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs create mode 100644 src/Castle.Windsor.Extensions.DependencyInjection/KeyedServicesSubDependencyResolver.cs diff --git a/.vscode/tasks.json.old b/.vscode/tasks.json.old new file mode 100644 index 0000000000..e176868402 --- /dev/null +++ b/.vscode/tasks.json.old @@ -0,0 +1,16 @@ +{ + "version": "0.1.0", + "command": "dotnet", + "isShellCommand": true, + "args": [], + "tasks": [ + { + "taskName": "build", + "args": [ + "${workspaceRoot}/src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj" + ], + "isBuildCommand": true, + "problemMatcher": "$msCompile" + } + ] +} \ No newline at end of file diff --git a/buildscripts/common.props b/buildscripts/common.props index 6791e6c986..266e4e4ff4 100644 --- a/buildscripts/common.props +++ b/buildscripts/common.props @@ -4,7 +4,7 @@ $(NoWarn);CS1591;NU5048 git https://github.com/castleproject/Windsor - 0.0.0 + 6.0.0 $(APPVEYOR_BUILD_VERSION) $(BuildVersion.Split('.')[0]) $(BuildVersion.Split('-')[0]) @@ -18,7 +18,7 @@ Castle Windsor $(BuildVersionNoSuffix) $(BuildVersion) - $(BuildVersionMajor).0.0 + 6.0.0.0 Castle Windsor is best of breed, mature Inversion of Control container available for .NET Castle Project Contributors http://www.castleproject.org/projects/windsor/ diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj index 6ef705103a..aa6c272d3f 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj @@ -1,7 +1,7 @@  - netcoreapp3.1;net6.0 + netcoreapp3.1;net6.0;net8.0 false @@ -13,9 +13,9 @@ - - - + + + @@ -29,6 +29,14 @@ + + + + + + + + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs new file mode 100644 index 0000000000..baecd46632 --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -0,0 +1,103 @@ +#if NET8_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Linq; +using Xunit; + +namespace Castle.Windsor.Extensions.DependencyInjection.Tests +{ + public abstract class CustomAssumptionTests : IDisposable + { + private IServiceProvider _serviceProvider; + + [Fact] + public void Resolve_All() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddKeyedSingleton("one"); + serviceCollection.AddKeyedSingleton("one"); + serviceCollection.AddTransient(); + _serviceProvider = BuildServiceProvider(serviceCollection); + + // resolve all non-keyed services + var services = _serviceProvider.GetServices(); + Assert.Single(services); + Assert.IsType(services.First()); + + // passing "null" as the key should return all non-keyed services + var keyedServices = _serviceProvider.GetKeyedServices(null); + Assert.Single(keyedServices); + Assert.IsType(keyedServices.First()); + + // resolve all keyed services + keyedServices = _serviceProvider.GetKeyedServices("one"); + Assert.Equal(2, keyedServices.Count()); + Assert.IsType(keyedServices.First()); + Assert.IsType(keyedServices.Last()); + } + + protected abstract IServiceCollection GetServiceCollection(); + + protected abstract IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection); + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Dispose managed resources + if (_serviceProvider is IDisposable disposable) + { + disposable.Dispose(); + } + } + // Dispose unmanaged resources + } + } + + public class RealCustomAssumptionTests : CustomAssumptionTests + { + protected override IServiceCollection GetServiceCollection() + { + return new RealTestServiceCollection(); + } + + protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection) + { + return serviceCollection.BuildServiceProvider(); + } + } + + public class CastleWindsorCustomAssumptionTests : CustomAssumptionTests + { + protected override IServiceCollection GetServiceCollection() + { + return new TestServiceCollection(); + } + + protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection) + { + var factory = new WindsorServiceProviderFactory(); + var container = factory.CreateBuilder(serviceCollection); + return factory.CreateServiceProvider(container); + } + } + + internal class TestService : ITestService + { + } + + internal class AnotherTestService : ITestService + { + } + + internal interface ITestService + { + } +} +#endif \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/SkippableDependencyInjectionSpecificationTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/SkippableDependencyInjectionSpecificationTests.cs index 12537699c6..6a28ed98ce 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/SkippableDependencyInjectionSpecificationTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/SkippableDependencyInjectionSpecificationTests.cs @@ -23,14 +23,18 @@ using System.Diagnostics; using System.Linq; +#if NET8_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection.Extensions; +#endif + namespace Microsoft.Extensions.DependencyInjection.Specification { public abstract class SkippableDependencyInjectionSpecificationTests : DependencyInjectionSpecificationTests { - public string[] SkippedTests => new[] { "SingletonServiceCanBeResolvedFromScope" }; + public string[] SkippedTests => Array.Empty(); #if NET6_0_OR_GREATER - public override bool SupportsIServiceProviderIsService => false; + public override bool SupportsIServiceProviderIsService => true; #endif protected sealed override IServiceProvider CreateServiceProvider(IServiceCollection serviceCollection) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/TestServiceCollection.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/TestServiceCollection.cs new file mode 100644 index 0000000000..0b590e0ade --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/TestServiceCollection.cs @@ -0,0 +1,13 @@ +using Microsoft.Extensions.DependencyInjection; +using System.Collections.Generic; + +namespace Castle.Windsor.Extensions.DependencyInjection.Tests +{ + internal sealed class TestServiceCollection : List, IServiceCollection + { + } + + internal sealed class RealTestServiceCollection : ServiceCollection + { + } +} diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs new file mode 100644 index 0000000000..88da1a83fd --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs @@ -0,0 +1,124 @@ +#if NET8_0_OR_GREATER +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Specification; +using System; +using Xunit; + +namespace Castle.Windsor.Extensions.DependencyInjection.Tests +{ + public class WindsorKeyedDependencyInjectionSpecificationTests : KeyedDependencyInjectionSpecificationTests + { + protected override IServiceProvider CreateServiceProvider(IServiceCollection collection) + { + if (collection is TestServiceCollection) + { + var factory = new WindsorServiceProviderFactory(); + var container = factory.CreateBuilder(collection); + return factory.CreateServiceProvider(container); + } + + return collection.BuildServiceProvider(); + } + + //[Fact] + //public void ResolveKeyedServiceSingletonInstanceWithAnyKey2() + //{ + // var serviceCollection = new ServiceCollection(); + // serviceCollection.AddKeyedSingleton(KeyedService.AnyKey); + + // var provider = CreateServiceProvider(serviceCollection); + + // Assert.Null(provider.GetService()); + + // var serviceKey1 = "some-key"; + // var svc1 = provider.GetKeyedService(serviceKey1); + // Assert.NotNull(svc1); + // Assert.Equal(serviceKey1, svc1.ToString()); + + // var serviceKey2 = "some-other-key"; + // var svc2 = provider.GetKeyedService(serviceKey2); + // Assert.NotNull(svc2); + // Assert.Equal(serviceKey2, svc2.ToString()); + //} + + internal interface IService { } + + internal class Service : IService + { + private readonly string _id; + + public Service() => _id = Guid.NewGuid().ToString(); + + public Service([ServiceKey] string id) => _id = id; + + public override string? ToString() => _id; + } + + internal class OtherService + { + public OtherService( + [FromKeyedServices("service1")] IService service1, + [FromKeyedServices("service2")] IService service2) + { + Service1 = service1; + Service2 = service2; + } + + public IService Service1 { get; } + + public IService Service2 { get; } + } + + public class FakeService : IFakeEveryService, IDisposable + { + public PocoClass Value { get; set; } + + public bool Disposed { get; private set; } + + public void Dispose() + { + if (Disposed) + { + throw new ObjectDisposedException(nameof(FakeService)); + } + + Disposed = true; + } + } + + public interface IFakeEveryService : + IFakeService, + IFakeMultipleService, + IFakeScopedService + { + } + + public interface IFakeMultipleService : IFakeService + { + } + + public interface IFakeScopedService : IFakeService + { + } + + public interface IFakeService + { + } + + public class PocoClass + { + } + } + + //public class WindsorKeyedDependencyInjectionSpecificationExplicitContainerTests : KeyedDependencyInjectionSpecificationTests + //{ + // protected override IServiceProvider CreateServiceProvider(IServiceCollection collection) + // { + // var factory = new WindsorServiceProviderFactory(new WindsorContainer()); + // var container = factory.CreateBuilder(collection); + // return factory.CreateServiceProvider(container); + // } + //} +} + +#endif diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs index 00ece5738e..112ca051ac 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs @@ -15,9 +15,11 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Tests { using System; - + using System.Collections.Generic; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Specification; + using Microsoft.Extensions.DependencyInjection.Specification.Fakes; + using Xunit; public class WindsorScopedServiceProviderCustomWindsorContainerTests : SkippableDependencyInjectionSpecificationTests { @@ -27,5 +29,10 @@ protected override IServiceProvider CreateServiceProviderImpl(IServiceCollection var container = factory.CreateBuilder(serviceCollection); return factory.CreateServiceProvider(container); } + +#if NET6_0_OR_GREATER +#endif + } + } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj index b12a584303..f1012afd0a 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj @@ -1,8 +1,8 @@  - netstandard2.0;net6.0 - 9.0 + netstandard2.0;net6.0;net8.0 + latest @@ -30,6 +30,12 @@ + + + + + + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Definitions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Definitions.cs new file mode 100644 index 0000000000..d4dca6b8e3 --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Definitions.cs @@ -0,0 +1,7 @@ +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit { } +} diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/ServiceDescriptorExtensions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/ServiceDescriptorExtensions.cs index 4360b10a64..df957570e8 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/ServiceDescriptorExtensions.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/ServiceDescriptorExtensions.cs @@ -18,14 +18,16 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Extensions public static class ServiceDescriptorExtensions { - public static IRegistration CreateWindsorRegistration(this Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + public static IRegistration CreateWindsorRegistration( + this Microsoft.Extensions.DependencyInjection.ServiceDescriptor service, + IWindsorContainer windsorContainer) { if (service.ServiceType.ContainsGenericParameters) { - return RegistrationAdapter.FromOpenGenericServiceDescriptor(service); + return RegistrationAdapter.FromOpenGenericServiceDescriptor(service, windsorContainer); } - return RegistrationAdapter.FromServiceDescriptor(service); + return RegistrationAdapter.FromServiceDescriptor(service, windsorContainer); } } } \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs new file mode 100644 index 0000000000..4ae6cd451b --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs @@ -0,0 +1,209 @@ + +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Resources; + +namespace Castle.Windsor.Extensions.DependencyInjection +{ + /// + /// Microsoft Dependency Injection with keyed registeration uses keys + /// of type object, while castle has only string keys, we need to generate + /// a new key for each registration to fix this impedance mismatch + /// + internal class KeyedRegistrationHelper + { + /// + /// We need to keep tracks of all registration of a given key, we can have more than one service registered with a specific + /// key so when the caller want to resolve with a key we can know which service to resolve. Internally we generate a unique + /// string to register the object inside castle so we can easily resolve by name. + /// + /// The key used to register the service + /// Original Service Descriptor used to perform the registration + /// The name used inside castle to register the service. + /// If the constructor has one parameter with the framework attribute ServiceKey we are + /// saving into this property the name of the parameter + internal record KeyedRegistration( + object Key, + ServiceDescriptor ServiceDescriptor, + string CastleRegistrationKey, + string ServiceKeyParameterName) + { + /// + /// Resolve using the current key for castle. + /// + /// Container used to resolve + /// The current key used to resolve the service, this happens because if you use the generic + /// key we need to use the one used for the resolution. In can be null, in this scenario we will use the original + /// key used for the registration + /// + internal object Resolve(IWindsorContainer container, object currentKey = null) + { + //Support the parameter in constructor that want key to be injected + if (ServiceKeyParameterName != null) + { + var argumentParameters = new Dictionary() + { + [ServiceKeyParameterName] = currentKey ?? Key + }; + return container.Resolve(CastleRegistrationKey, ServiceDescriptor.ServiceType, argumentParameters); + } + return container.Resolve(CastleRegistrationKey, ServiceDescriptor.ServiceType); + } + } + + /// + /// For each key we can have more than one service registered, this allows us to resolve the correct service. Also you can + /// resolve all the interfaces registered with a specific key. + /// + private readonly ConcurrentDictionary> _keyToRegistrationMap = new(); + + private readonly ConcurrentDictionary> _typeToRegistrationMap = new(); + + private static readonly ConcurrentDictionary _keyedRegistrations = new(); + + internal static KeyedRegistrationHelper GetInstance(IWindsorContainer container) + { + if (!_keyedRegistrations.TryGetValue(container, out var helper)) + { + helper = new KeyedRegistrationHelper(); + _keyedRegistrations.TryAdd(container, helper); + } + + return helper; + } + + internal const string KeyedRegistrationPrefix = "__KEYEDSERVICE__"; + + /// + /// Framework has special key KeyedService.AnyKey that is used to register a service with any key. This means that + /// whenever we are resolving a service with a key that is not registered, this can be used as fallback. So we + /// need to explicitly know which services are registered with any key. + /// + private readonly ConcurrentDictionary _typeRegisteredWithAnyKey = new(); + +#if NET8_0_OR_GREATER + + public string GetOrCreateKey(object key, ServiceDescriptor serviceDescriptor) + { + ArgumentNullException.ThrowIfNull(key); + + var registrationKey = $"{KeyedRegistrationPrefix}+{Guid.NewGuid():X}"; + var registration = CreateRegistration(key, serviceDescriptor, registrationKey); + if (!_keyToRegistrationMap.TryGetValue(key, out var registrations)) + { + registrations = new List + { + registration + }; + _keyToRegistrationMap.AddOrUpdate(key, registrations, (_, v) => { v.Add(registration); return v; }); + } + else + { + //ok we already have the concurrent bag. + registrations.Add(registration); + } + + //Now create an inverse map, where we have all registration done for a specific type. + if (!_typeToRegistrationMap.TryGetValue(serviceDescriptor.ServiceType, out var keyList)) + { + keyList = new List + { + registration + }; + _typeToRegistrationMap.AddOrUpdate(serviceDescriptor.ServiceType, keyList, (_, v) => { v.Add(registration); return v; }); + } + else + { + keyList.Add(registration); + } + + if (key == KeyedService.AnyKey) + { + var registered = _typeRegisteredWithAnyKey.TryAdd(serviceDescriptor.ServiceType, registration); + if (!registered) + { + throw new NotSupportedException("Cannot register more than one instance of the same service with the anykey."); + } + } + + return registration.CastleRegistrationKey; + } + + /// + /// There is a special attribute called ServiceKeyAttribute in the framework that require DI system to resolve + /// the service and pass the key object to that specific parameter. It seems an unnecessary feature but it + /// is tested by the standard test suite. + /// + /// + /// + /// + /// + private static KeyedRegistration CreateRegistration(object key, ServiceDescriptor serviceDescriptor, string registrationKey) + { + string serviceKeyParameterName = null; + if (serviceDescriptor.KeyedImplementationType != null) + { + var constructors = serviceDescriptor.KeyedImplementationType.GetConstructors(); + foreach (var constructor in constructors) + { + var parameters = constructor.GetParameters(); + foreach (var parameter in parameters) + { + if (parameter.GetCustomAttributes(typeof(ServiceKeyAttribute), true).Length != 0) + { + serviceKeyParameterName = parameter.Name; + break; + } + } + } + } + + return new KeyedRegistration(key, serviceDescriptor, registrationKey, serviceKeyParameterName); + } + + public KeyedRegistration GetKey(object key, Type serviceType) + { + ArgumentNullException.ThrowIfNull(key); + ArgumentNullException.ThrowIfNull(serviceType); + + if (_keyToRegistrationMap.TryGetValue(key, out var registrations)) + { + //ok we have services for this key. + var registration = registrations.FirstOrDefault(r => r.ServiceDescriptor.ServiceType == serviceType); + return registration; + } + + //ok is it possible that this service was registered with any key? + if (_typeRegisteredWithAnyKey.TryGetValue(serviceType, out var registrationWithAnyKey)) + { + //ok we really + return registrationWithAnyKey; + } + + return null; + } + + public IEnumerable GetKeyedRegistrations(Type serviceType) + { + ArgumentNullException.ThrowIfNull(serviceType); + + if (_typeToRegistrationMap.TryGetValue(serviceType, out var registrations)) + { + return registrations; + } + + return Array.Empty(); + } + + public bool HasKey(object key) + { + ArgumentNullException.ThrowIfNull(key); + + return _keyToRegistrationMap.ContainsKey(key); + } +#endif + } +} \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/KeyedServicesSubDependencyResolver.cs b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedServicesSubDependencyResolver.cs new file mode 100644 index 0000000000..566d25d6a1 --- /dev/null +++ b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedServicesSubDependencyResolver.cs @@ -0,0 +1,71 @@ +#if NET8_0_OR_GREATER +using Castle.Core; +using Castle.MicroKernel; +using Castle.MicroKernel.Context; +using Microsoft.Extensions.DependencyInjection; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; + +namespace Castle.Windsor.Extensions.DependencyInjection +{ + internal class KeyedServicesSubDependencyResolver : ISubDependencyResolver + { + private readonly IWindsorContainer _container; + + public KeyedServicesSubDependencyResolver(IWindsorContainer container) + { + _container = container; + } + + /// + /// We cache in key all the factories that we have for specific constructor dependencies. + /// + private readonly Dictionary> _factoriesCache = new(); + + public bool CanResolve(CreationContext context, ISubDependencyResolver contextHandlerResolver, ComponentModel model, DependencyModel dependency) + { + if (dependency is ConstructorDependencyModel cdm) + { + var cacheKey = GetFactoryCacheKey(cdm); + Func factory; + if (!_factoriesCache.TryGetValue(cacheKey, out factory)) + { + //we are resolving a dependency of a constructor,where the parameter could be resolved by a keyed service + var constructorParameter = cdm.Constructor.Constructor.GetParameters().Single(p => p.Name == cdm.DependencyKey); + var attribute = constructorParameter.GetCustomAttribute(); + if (attribute != null) + { + var krh = KeyedRegistrationHelper.GetInstance(_container); + var registration = krh.GetKey(attribute.Key, cdm.TargetItemType); + //ok this parameter has an attribute, so we can cache + factory = () => registration.Resolve(_container); + } + _factoriesCache[cacheKey] = factory; + } + + if (factory != null) + { + return true; + } + } + return false; + } + + private static string GetFactoryCacheKey(ConstructorDependencyModel cdm) + { + return $"{cdm.TargetType.FullName}+{cdm.DependencyKey}"; + } + + public object Resolve(CreationContext context, ISubDependencyResolver contextHandlerResolver, ComponentModel model, DependencyModel dependency) + { + var cdm = (ConstructorDependencyModel)dependency; + + var cacheKey = GetFactoryCacheKey(cdm); + var factory = _factoriesCache[cacheKey]; + return factory(); + } + } +} +#endif \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index ad27dce4e0..b4096e5317 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -14,20 +14,49 @@ namespace Castle.Windsor.Extensions.DependencyInjection { - using System; - using Castle.MicroKernel.Registration; using Castle.Windsor.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.DependencyInjection; + using System; internal static class RegistrationAdapter { - public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + public static IRegistration FromOpenGenericServiceDescriptor( + Microsoft.Extensions.DependencyInjection.ServiceDescriptor service, + IWindsorContainer windsorContainer) { - ComponentRegistration registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); + ComponentRegistration registration; +#if NET8_0_OR_GREATER + if (service.IsKeyedService) + { + registration = Component.For(service.ServiceType) + .Named(KeyedRegistrationHelper.GetInstance(windsorContainer).GetOrCreateKey(service.ServiceKey, service)); + if (service.KeyedImplementationType != null) + { + registration = UsingImplementation(registration, service); + } + else + { + throw new System.ArgumentException("Unsupported ServiceDescriptor"); + } + } + else + { + registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); + if (service.ImplementationType != null) + { + registration = UsingImplementation(registration, service); + } + else + { + throw new System.ArgumentException("Unsupported ServiceDescriptor"); + } + } +#else + registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); if (service.ImplementationType != null) { registration = UsingImplementation(registration, service); @@ -36,16 +65,55 @@ public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extension { throw new System.ArgumentException("Unsupported ServiceDescriptor"); } - +#endif return ResolveLifestyle(registration, service) .IsDefault(); } - public static IRegistration FromServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + public static IRegistration FromServiceDescriptor( + Microsoft.Extensions.DependencyInjection.ServiceDescriptor service, + IWindsorContainer windsorContainer) { - var registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); + ComponentRegistration registration; +#if NET8_0_OR_GREATER + if (service.IsKeyedService) + { + registration = Component.For(service.ServiceType) + .Named(KeyedRegistrationHelper.GetInstance(windsorContainer).GetOrCreateKey(service.ServiceKey, service)); + if (service.KeyedImplementationFactory != null) + { + registration = UsingFactoryMethod(registration, service); + } + else if (service.KeyedImplementationInstance != null) + { + registration = UsingInstance(registration, service); + } + else if (service.KeyedImplementationType != null) + { + registration = UsingImplementation(registration, service); + } + } + else + { + registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); + if (service.ImplementationFactory != null) + { + registration = UsingFactoryMethod(registration, service); + } + else if (service.ImplementationInstance != null) + { + registration = UsingInstance(registration, service); + } + else if (service.ImplementationType != null) + { + registration = UsingImplementation(registration, service); + } + } +#else + registration = Component.For(service.ServiceType) + .NamedAutomatically(UniqueComponentName(service)); if (service.ImplementationFactory != null) { registration = UsingFactoryMethod(registration, service); @@ -58,7 +126,7 @@ public static IRegistration FromServiceDescriptor(Microsoft.Extensions.Dependenc { registration = UsingImplementation(registration, service); } - +#endif return ResolveLifestyle(registration, service) .IsDefault(); } @@ -79,7 +147,39 @@ public static string OriginalComponentName(string uniqueComponentName) internal static string UniqueComponentName(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) { var result = ""; - if (service.ImplementationType != null) +#if NET8_0_OR_GREATER + if (service.IsKeyedService) + { + if (service.KeyedImplementationType != null) + { + result = service.KeyedImplementationType.FullName; + } + else if (service.KeyedImplementationInstance != null) + { + result = service.KeyedImplementationInstance.GetType().FullName; + } + else + { + result = service.KeyedImplementationFactory.GetType().FullName; + } + } + else + { + if (service.ImplementationType != null) + { + result = service.ImplementationType.FullName; + } + else if (service.ImplementationInstance != null) + { + result = service.ImplementationInstance.GetType().FullName; + } + else + { + result = service.ImplementationFactory.GetType().FullName; + } + } +#else +if (service.ImplementationType != null) { result = service.ImplementationType.FullName; } @@ -91,28 +191,68 @@ internal static string UniqueComponentName(Microsoft.Extensions.DependencyInject { result = service.ImplementationFactory.GetType().FullName; } + + +#endif result = result + "@" + Guid.NewGuid().ToString(); return result; } - private static ComponentRegistration UsingFactoryMethod(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class + private static ComponentRegistration UsingFactoryMethod( + ComponentRegistration registration, + Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { return registration.UsingFactoryMethod((kernel) => { + //TODO: We should register a factory in castle that in turns call the implementation factory?? var serviceProvider = kernel.Resolve(); +#if NET8_0_OR_GREATER + if (service.IsKeyedService) + { + return service.KeyedImplementationFactory(serviceProvider, service.ServiceKey) as TService; + } + else + { + return service.ImplementationFactory(serviceProvider) as TService; + } +#else return service.ImplementationFactory(serviceProvider) as TService; + +#endif }); } private static ComponentRegistration UsingInstance(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { +#if NET8_0_OR_GREATER + if (service.IsKeyedService) + { + return registration.Instance(service.KeyedImplementationInstance as TService); + } + else + { + return registration.Instance(service.ImplementationInstance as TService); + } +#else return registration.Instance(service.ImplementationInstance as TService); +#endif } private static ComponentRegistration UsingImplementation(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class { +#if NET8_0_OR_GREATER + if (service.IsKeyedService) + { + return registration.ImplementedBy(service.KeyedImplementationType); + } + else + { + return registration.ImplementedBy(service.ImplementationType); + } +#else return registration.ImplementedBy(service.ImplementationType); +#endif } private static ComponentRegistration ResolveLifestyle(ComponentRegistration registration, Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) where TService : class diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScope.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScope.cs index 95f29a07d3..5df1bec2b8 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScope.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScope.cs @@ -16,7 +16,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { internal class ExtensionContainerRootScope : ExtensionContainerScopeBase { - public static ExtensionContainerRootScope BeginRootScope() { var scope = new ExtensionContainerRootScope(); diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index 62dc0d7638..401e19c4d4 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -12,24 +12,22 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Castle.Windsor.Extensions.DependencyInjection.Scope { +namespace Castle.Windsor.Extensions.DependencyInjection.Scope +{ using System; using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; - internal class ExtensionContainerRootScopeAccessor : IScopeAccessor { - public ILifetimeScope GetScope(CreationContext context) { - /* - if (ExtensionContainerScopeCache.Current == null) { - // might be null in threads spawn from Threadpool.UnsafeQueueUserWorkItem - return null; - } - */ + internal class ExtensionContainerRootScopeAccessor : IScopeAccessor + { + public ILifetimeScope GetScope(CreationContext context) + { return ExtensionContainerScopeCache.Current?.RootScope ?? throw new InvalidOperationException("No root scope available."); } - public void Dispose() { + public void Dispose() + { } } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs index edb784dfcb..ac94760a48 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs @@ -15,7 +15,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using System; - + using System.Threading; using Castle.Core; using Castle.MicroKernel; using Castle.MicroKernel.Lifestyle.Scoped; @@ -25,9 +25,14 @@ internal abstract class ExtensionContainerScopeBase : ILifetimeScope public static readonly string TransientMarker = "Transient"; private readonly IScopeCache scopeCache; + private static long _counter; + + public long Counter { get; private set; } + protected ExtensionContainerScopeBase() { scopeCache = new ScopeCache(); + Counter = Interlocked.Increment(ref _counter); } internal virtual ExtensionContainerScopeBase RootScope { get; set; } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs index 0686ea95a6..6a1e439dfd 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs @@ -15,16 +15,14 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { - using System; - using Castle.Windsor; - using Microsoft.Extensions.DependencyInjection; + using System; internal class WindsorScopeFactory : IServiceScopeFactory { private readonly IWindsorContainer scopeFactoryContainer; - + public WindsorScopeFactory(IWindsorContainer container) { scopeFactoryContainer = container; @@ -34,8 +32,8 @@ public IServiceScope CreateScope() { var scope = ExtensionContainerScope.BeginScope(); - //since WindsorServiceProvider is scoped, this gives us new instance - var provider = scopeFactoryContainer.Resolve(); + //Tests in .NET makes sure that we have a differente scoped service provider for each scope. + var provider = new WindsorScopedServiceProvider(scopeFactoryContainer); return new ServiceScope(scope, provider); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 006c475324..1ccb1935e5 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -12,19 +12,24 @@ // See the License for the specific language governing permissions and // limitations under the License. - namespace Castle.Windsor.Extensions.DependencyInjection { - using System; - using System.Collections.Generic; - using System.Reflection; - + using Castle.MicroKernel.Handlers; using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; - using Microsoft.Extensions.DependencyInjection; + using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; internal class WindsorScopedServiceProvider : IServiceProvider, ISupportRequiredService, IDisposable +#if NET6_0_OR_GREATER + , IServiceProviderIsService +#endif +#if NET8_0_OR_GREATER + , IKeyedServiceProvider, IServiceProviderIsKeyedService +#endif { private readonly ExtensionContainerScopeBase scope; private bool disposing; @@ -45,6 +50,26 @@ public object GetService(Type serviceType) } } +#if NET8_0_OR_GREATER + + public object GetKeyedService(Type serviceType, object serviceKey) + { + using (_ = new ForcedScope(scope)) + { + return ResolveInstanceOrNull(serviceType, serviceKey, true); + } + } + + public object GetRequiredKeyedService(Type serviceType, object serviceKey) + { + using (_ = new ForcedScope(scope)) + { + return ResolveInstanceOrNull(serviceType, serviceKey, false); + } + } + +#endif + public object GetRequiredService(Type serviceType) { using (_ = new ForcedScope(scope)) @@ -65,17 +90,139 @@ public void Dispose() // disping the container here is questionable... what if I want to create another IServiceProvider form the factory? container.Dispose(); } + private object ResolveInstanceOrNull(Type serviceType, bool isOptional) { if (container.Kernel.HasComponent(serviceType)) { +#if NET8_0_OR_GREATER + //this is complicated by the concept of keyed service, because if you are about to resolve WITHOUTH KEY you do not + //need to resolve keyed services. Now Keyed services are available only in version 8 but we register with an helper + //all registered services so we can know if a service was really registered with keyed service or not. + var componentRegistration = container.Kernel.GetHandler(serviceType); + if (componentRegistration.ComponentModel.Name.StartsWith(KeyedRegistrationHelper.KeyedRegistrationPrefix)) + { + //Component was registered as keyed component, so we really need to resolve with null because this is the old interface + //so no key is provided. + return null; + } +#endif return container.Resolve(serviceType); } if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { - var allObjects = container.ResolveAll(serviceType.GenericTypeArguments[0]); - return allObjects; + //ok we want to resolve all references, NON keyed services + var typeToResolve = serviceType.GenericTypeArguments[0]; + var allRegisteredTypes = container.Kernel.GetHandlers(typeToResolve); + var allNonKeyedService = allRegisteredTypes.Where(x => !x.ComponentModel.Name.StartsWith(KeyedRegistrationHelper.KeyedRegistrationPrefix)).ToList(); + //now we need to resolve one by one all these services + var listType = typeof(List<>).MakeGenericType(typeToResolve); + var objects = (System.Collections.IList)Activator.CreateInstance(listType); + + if (allNonKeyedService.Count == 0) + { + return objects; + } + else if (allNonKeyedService.Count == allRegisteredTypes.Length) + { + //simply resolve all + return container.ResolveAll(typeToResolve); + } + + //if we reach here some of the services are kyed and some are not, so we need to resolve one by one. + for (int i = 0; i < allNonKeyedService.Count; i++) + { + var service = allNonKeyedService[i]; + object obj; + //type is non generic, we can directly resolve. + try + { + obj = container.Resolve(allNonKeyedService[i].ComponentModel.Name, typeToResolve); + objects.Add(obj); + } + catch (GenericHandlerTypeMismatchException) + { + //ignore, this is the standard way we know that we cannot instantiate an open generic with a given type. + } + } + + return objects; + } + + if (isOptional) + { + return null; + } + + return container.Resolve(serviceType); + } + +#if NET6_0_OR_GREATER + + public bool IsService(Type serviceType) + { + if (serviceType.IsGenericTypeDefinition) + { + //Framework does not want the open definition to return true + return false; + } + + //IEnumerable always return true + if (serviceType.IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + var enumerableType = serviceType.GenericTypeArguments[0]; + if (container.Kernel.HasComponent(enumerableType)) + { + return true; + } + //Try to check if the real type is registered: framework test IEnumerableWithIsServiceAlwaysReturnsTrue1 + var interfaces = enumerableType.GetInterfaces(); + return interfaces.Any(container.Kernel.HasComponent); + } + return container.Kernel.HasComponent(serviceType); + } + +#endif + +#if NET8_0_OR_GREATER + + private object ResolveInstanceOrNull(Type serviceType, object serviceKey, bool isOptional) + { + if (serviceKey == null) + { + return ResolveInstanceOrNull(serviceType, isOptional); + } + + KeyedRegistrationHelper keyedRegistrationHelper = KeyedRegistrationHelper.GetInstance(container); + + if (container.Kernel.HasComponent(serviceType)) + { + var keyRegistrationHelper = keyedRegistrationHelper.GetKey(serviceKey, serviceType); + //this is a keyed service, actually we need to grab the name from the service key + if (keyRegistrationHelper != null) + { + return keyRegistrationHelper.Resolve(container, serviceKey); + } + } + + if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + var typeToResolve = serviceType.GenericTypeArguments[0]; + var registrations = keyedRegistrationHelper.GetKeyedRegistrations(typeToResolve); + var regisrationsWithKey = registrations.Where(x => x.Key == serviceKey).ToList(); + + if (regisrationsWithKey.Count > 0) + { + var listType = typeof(List<>).MakeGenericType(typeToResolve); + var objects = (System.Collections.IList)Activator.CreateInstance(listType); + foreach (var registration in regisrationsWithKey) + { + var obj = registration.Resolve(container, serviceKey); + objects.Add(obj); + } + return objects; + } } if (isOptional) @@ -85,5 +232,19 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) return container.Resolve(serviceType); } + + public bool IsKeyedService(Type serviceType, object serviceKey) + { + //we just need to know if the key is registered. + if (serviceKey == null) + { + //test NonKeyedServiceWithIsKeyedService shows that for real inversion of control when sercvice key is null + //it just mean that we need to know if the service is registered. + return IsService(serviceType); + } + return KeyedRegistrationHelper.GetInstance(container).HasKey(serviceKey); + } + +#endif } } \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactory.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactory.cs index b312ab974b..7527ccf865 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactory.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactory.cs @@ -18,7 +18,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection public sealed class WindsorServiceProviderFactory : WindsorServiceProviderFactoryBase { - public WindsorServiceProviderFactory() { CreateRootScope(); diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs index 21363c4fd9..1b8884486c 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs @@ -34,7 +34,7 @@ public abstract class WindsorServiceProviderFactoryBase : IServiceProviderFactor public virtual IWindsorContainer CreateBuilder(IServiceCollection services) { - return BuildContainer(services, rootContainer); + return BuildContainer(services); } public virtual IServiceProvider CreateServiceProvider(IWindsorContainer container) @@ -55,6 +55,9 @@ protected virtual void CreateRootContainer() protected virtual void SetRootContainer(IWindsorContainer container) { rootContainer = container; +#if NET8_0_OR_GREATER + rootContainer.Kernel.Resolver.AddSubResolver(new KeyedServicesSubDependencyResolver(rootContainer)); +#endif AddSubSystemToContainer(rootContainer); } @@ -66,7 +69,7 @@ protected virtual void AddSubSystemToContainer(IWindsorContainer container) ); } - protected virtual IWindsorContainer BuildContainer(IServiceCollection serviceCollection, IWindsorContainer windsorContainer) + protected virtual IWindsorContainer BuildContainer(IServiceCollection serviceCollection) { if (rootContainer == null) { @@ -85,7 +88,7 @@ protected virtual IWindsorContainer BuildContainer(IServiceCollection serviceCol AddSubResolvers(); - RegisterServiceCollection(serviceCollection, windsorContainer); + RegisterServiceCollection(serviceCollection); return rootContainer; } @@ -101,9 +104,17 @@ protected virtual void RegisterContainer(IWindsorContainer container) protected virtual void RegisterProviders(IWindsorContainer container) { container.Register(Component - .For() + .For() .ImplementedBy() - .LifeStyle.ScopedToNetServiceScope()); + .LifeStyle + .ScopedToNetServiceScope()); } protected virtual void RegisterFactories(IWindsorContainer container) @@ -119,11 +130,11 @@ protected virtual void RegisterFactories(IWindsorContainer container) .LifestyleSingleton()); } - protected virtual void RegisterServiceCollection(IServiceCollection serviceCollection,IWindsorContainer container) + protected virtual void RegisterServiceCollection(IServiceCollection serviceCollection) { foreach (var service in serviceCollection) { - rootContainer.Register(service.CreateWindsorRegistration()); + rootContainer.Register(service.CreateWindsorRegistration(rootContainer)); } } diff --git a/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj b/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj index 1aa19662d8..c830167ea1 100644 --- a/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj +++ b/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs b/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs index ba7bd50405..9f2f33c4df 100644 --- a/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs +++ b/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs @@ -138,6 +138,10 @@ protected virtual object CreateInstance(CreationContext context, ConstructorCand protected object CreateInstanceCore(ConstructorCandidate constructor, object[] arguments, Type implType) { object instance; + if (Model.Implementation.FullName.Contains("SiloConnectionFactory")) + { + + } try { #if FEATURE_REMOTING diff --git a/src/Castle.Windsor/MicroKernel/Handlers/DefaultGenericHandler.cs b/src/Castle.Windsor/MicroKernel/Handlers/DefaultGenericHandler.cs index f2f2995c48..2d2ab0acc1 100644 --- a/src/Castle.Windsor/MicroKernel/Handlers/DefaultGenericHandler.cs +++ b/src/Castle.Windsor/MicroKernel/Handlers/DefaultGenericHandler.cs @@ -361,7 +361,7 @@ private Type GetClosedImplementationType(CreationContext context, bool instanceR throw new HandlerException(message, ComponentModel.ComponentName, e); } // 3. at this point we should be 99% sure we have arguments that don't satisfy generic constraints of out service. - throw new GenericHandlerTypeMismatchException(genericArguments, ComponentModel, this); + throw new GenericHandlerTypeMismatchException(genericArguments, ComponentModel, this, e); } } diff --git a/src/Castle.Windsor/MicroKernel/Handlers/GenericHandlerTypeMismatchException.cs b/src/Castle.Windsor/MicroKernel/Handlers/GenericHandlerTypeMismatchException.cs index 1439df36f5..13ea6f6333 100644 --- a/src/Castle.Windsor/MicroKernel/Handlers/GenericHandlerTypeMismatchException.cs +++ b/src/Castle.Windsor/MicroKernel/Handlers/GenericHandlerTypeMismatchException.cs @@ -47,8 +47,8 @@ public GenericHandlerTypeMismatchException(string message, ComponentName name, E { } - public GenericHandlerTypeMismatchException(IEnumerable argumentsUsed, ComponentModel componentModel, DefaultGenericHandler handler) - : base(BuildMessage(argumentsUsed.Select(a => a.FullName).ToArray(), componentModel, handler), componentModel.ComponentName) + public GenericHandlerTypeMismatchException(IEnumerable argumentsUsed, ComponentModel componentModel, DefaultGenericHandler handler, Exception innerException) + : base(BuildMessage(argumentsUsed.Select(a => a.FullName).ToArray(), componentModel, handler), componentModel.ComponentName, innerException) { } From 1830955e338c7823270c80bf35bd6fc012048938 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Wed, 14 Feb 2024 16:54:30 +0100 Subject: [PATCH 14/20] Fixed bug in missing root scope with orleans/kestrel. The previous handling of root scope is wrong, the code set root scope in an AsyncLocal variable when WindsorServiceProviderFactoryBase was first creatd. The problem arise with kestrel or orleans in .NET 8, they use a Thread Pool that runs code outside AsyncLocal so it will break resolution. It was not possible to reproduce locally, but it was reproduced with production code. A repro for the bug still missing. Also we can support using a Global root scope only if we use only ONE CONTAINER in the .NET core DI, because basic structure does not allow to find the container that is resolving scoped component thus we cannot determin the right root context. Still work to do to support multiple container. --- .../CustomAssumptionTests.cs | 305 ++++++++++++++++-- .../ResolveFromThreadpoolUnsafe.cs | 11 +- .../Resolvers/LoggerDependencyResolver.cs | 2 +- .../ExtensionContainerRootScopeAccessor.cs | 21 +- .../Scope/ExtensionContainerScope.cs | 5 +- .../Scope/ExtensionContainerScopeAccessor.cs | 2 +- .../Scope/ExtensionContainerScopeCache.cs | 1 + .../WindsorServiceProviderFactoryBase.cs | 41 ++- 8 files changed, 351 insertions(+), 37 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs index baecd46632..4bba519bf3 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -1,7 +1,10 @@ #if NET8_0_OR_GREATER using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using System; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Xunit; namespace Castle.Windsor.Extensions.DependencyInjection.Tests @@ -36,28 +39,232 @@ public void Resolve_All() Assert.IsType(keyedServices.Last()); } + [Fact] + public void Scoped_keyed_service_resolved_by_thread_outside_scope() + { + Boolean stop = false; + Boolean shouldResolve = false; + ITestService resolvedInThread = null; + var thread = new Thread(_ => + { + while (!stop) + { + Thread.Sleep(100); + if (shouldResolve) + { + stop = true; + resolvedInThread = _serviceProvider.GetRequiredKeyedService("porcodio"); + } + } + }); + thread.Start(); + + var serviceCollection = GetServiceCollection(); + serviceCollection.AddKeyedScoped("porcodio"); + _serviceProvider = BuildServiceProvider(serviceCollection); + + //resolved outside scope + ITestService resolvedOutsideScope = _serviceProvider.GetRequiredKeyedService("porcodio"); + + // resolve in scope + ITestService resolvedInScope; + using (var scope = _serviceProvider.CreateScope()) + { + resolvedInScope = scope.ServiceProvider.GetRequiredKeyedService("porcodio"); + } + + shouldResolve = true; + //now wait for the original thread to finish + thread.Join(1000 * 10); + Assert.NotNull(resolvedInThread); + Assert.NotNull(resolvedOutsideScope); + Assert.NotNull(resolvedInScope); + + Assert.NotEqual(resolvedInScope, resolvedOutsideScope); + Assert.NotEqual(resolvedInScope, resolvedInThread); + Assert.Equal(resolvedOutsideScope, resolvedInThread); + } + + [Fact] + public void Scoped_service_resolved_outside_scope() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddScoped(); + _serviceProvider = BuildServiceProvider(serviceCollection); + + //resolved outside scope + ITestService resolvedOutsideScope = _serviceProvider.GetRequiredService(); + Assert.NotNull(resolvedOutsideScope); + + // resolve in scope + ITestService resolvedInScope; + using (var scope = _serviceProvider.CreateScope()) + { + resolvedInScope = scope.ServiceProvider.GetRequiredService(); + } + Assert.NotNull(resolvedInScope); + Assert.NotEqual(resolvedInScope, resolvedOutsideScope); + + ITestService resolvedAgainOutsideScope = _serviceProvider.GetRequiredService(); + Assert.NotNull(resolvedAgainOutsideScope); + Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope); + } + + [Fact] + public void Scoped_service_resolved_outside_scope_in_another_thread() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddScoped(); + _serviceProvider = BuildServiceProvider(serviceCollection); + + var task = Task.Run(() => + { + //resolved outside scope + ITestService resolvedOutsideScope = _serviceProvider.GetRequiredService(); + Assert.NotNull(resolvedOutsideScope); + + // resolve in scope + ITestService resolvedInScope; + using (var scope = _serviceProvider.CreateScope()) + { + resolvedInScope = scope.ServiceProvider.GetRequiredService(); + } + Assert.NotNull(resolvedInScope); + Assert.NotEqual(resolvedInScope, resolvedOutsideScope); + + ITestService resolvedAgainOutsideScope = _serviceProvider.GetRequiredService(); + Assert.NotNull(resolvedAgainOutsideScope); + Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope); + return true; + }); + + Assert.True(task.Result); + } + + [Fact] + public async void Scoped_service_resolved_outside_scope_in_another_unsafe_thread() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddScoped(); + _serviceProvider = BuildServiceProvider(serviceCollection); + + var tsc = new TaskCompletionSource(); + var worker = new QueueUserWorkItemWorker(_serviceProvider, tsc); + ThreadPool.UnsafeQueueUserWorkItem(worker, false); + await tsc.Task; + + Assert.Null(worker.ExecuteException); + Assert.NotNull(worker.ResolvedOutsideScope); + Assert.NotNull(worker.ResolvedInScope); + Assert.NotEqual(worker.ResolvedInScope, worker.ResolvedOutsideScope); + } + + [Fact] + public async void Simulate_async_timer_without_wait() + { + Boolean stop = false; + Boolean shouldResolve = false; + ITestService resolvedInThread = null; + async Task ExecuteAsync() + { + while (!stop) + { + await Task.Delay(100); + if (shouldResolve) + { + stop = true; + resolvedInThread = _serviceProvider.GetService(); + } + } + } + //fire and forget +#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + var task = ExecuteAsync(); +#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed + + await Task.Delay(500); + + var serviceCollection = GetServiceCollection(); + serviceCollection.AddScoped(); + _serviceProvider = BuildServiceProvider(serviceCollection); + + //resolved outside scope + ITestService resolvedOutsideScope = _serviceProvider.GetRequiredService(); + + // resolve in scope + ITestService resolvedInScope; + using (var scope = _serviceProvider.CreateScope()) + { + resolvedInScope = scope.ServiceProvider.GetRequiredService(); + } + + shouldResolve = true; + await task; + Assert.NotNull(resolvedInThread); + Assert.NotNull(resolvedOutsideScope); + Assert.NotNull(resolvedInScope); + + Assert.NotEqual(resolvedInScope, resolvedOutsideScope); + Assert.NotEqual(resolvedInScope, resolvedInThread); + Assert.Equal(resolvedOutsideScope, resolvedInThread); + } + + private class QueueUserWorkItemWorker : IThreadPoolWorkItem + { + private readonly IServiceProvider _provider; + private readonly TaskCompletionSource _taskCompletionSource; + + public QueueUserWorkItemWorker(IServiceProvider provider, TaskCompletionSource taskCompletionSource) + { + _provider = provider; + _taskCompletionSource = taskCompletionSource; + } + + public ITestService ResolvedOutsideScope { get; private set; } + public ITestService ResolvedInScope { get; private set; } + public Exception ExecuteException { get; private set; } + + public void Execute() + { + try + { + ResolvedOutsideScope = _provider.GetService(); + using (var scope = _provider.CreateScope()) + { + ResolvedInScope = scope.ServiceProvider.GetRequiredService(); + } + } + catch (Exception ex) + { + ExecuteException = ex; + } + + _taskCompletionSource.SetResult(); + } + } + protected abstract IServiceCollection GetServiceCollection(); protected abstract IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection); - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - // Dispose managed resources - if (_serviceProvider is IDisposable disposable) - { - disposable.Dispose(); - } - } - // Dispose unmanaged resources - } + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + // Dispose managed resources + if (_serviceProvider is IDisposable disposable) + { + disposable.Dispose(); + } + } + // Dispose unmanaged resources + } } public class RealCustomAssumptionTests : CustomAssumptionTests @@ -75,6 +282,8 @@ protected override IServiceProvider BuildServiceProvider(IServiceCollection serv public class CastleWindsorCustomAssumptionTests : CustomAssumptionTests { + private IWindsorContainer _container; + protected override IServiceCollection GetServiceCollection() { return new TestServiceCollection(); @@ -83,8 +292,64 @@ protected override IServiceCollection GetServiceCollection() protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection) { var factory = new WindsorServiceProviderFactory(); - var container = factory.CreateBuilder(serviceCollection); - return factory.CreateServiceProvider(container); + _container = factory.CreateBuilder(serviceCollection); + return factory.CreateServiceProvider(_container); + } + + [Fact] + public void Try_to_resolve_scoped_directly_with_castle_windsor_container() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddScoped(); + var provider = BuildServiceProvider(serviceCollection); + + //resolved outside scope + ITestService resolvedOutsideScope = _container.Resolve(); + Assert.NotNull(resolvedOutsideScope); + + // resolve in scope + ITestService resolvedInScope; + using (var scope = provider.CreateScope()) + { + resolvedInScope = _container.Resolve(); + } + Assert.NotNull(resolvedInScope); + Assert.NotEqual(resolvedInScope, resolvedOutsideScope); + + ITestService resolvedAgainOutsideScope = _container.Resolve(); + Assert.NotNull(resolvedAgainOutsideScope); + Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope); + } + + [Fact] + public void TryToResolveScopedInOtherThread() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddScoped(); + var provider = BuildServiceProvider(serviceCollection); + + var task = Task.Run(() => + { + //resolved outside scope + ITestService resolvedOutsideScope = _container.Resolve(); + Assert.NotNull(resolvedOutsideScope); + + // resolve in scope + ITestService resolvedInScope; + using (var scope = provider.CreateScope()) + { + resolvedInScope = _container.Resolve(); + } + Assert.NotNull(resolvedInScope); + Assert.NotEqual(resolvedInScope, resolvedOutsideScope); + + ITestService resolvedAgainOutsideScope = _container.Resolve(); + Assert.NotNull(resolvedAgainOutsideScope); + Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope); + return true; + }); + + Assert.True(task.Result); } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index f8d2ce57a4..68844e4bc3 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -1,4 +1,5 @@ -using Castle.MicroKernel.Lifestyle; +using Castle.MicroKernel; +using Castle.MicroKernel.Lifestyle; using Castle.MicroKernel.Registration; using Castle.Windsor.Extensions.DependencyInjection.Extensions; using Castle.Windsor.Extensions.DependencyInjection.Tests.Components; @@ -512,8 +513,8 @@ public async Task Cannot_Resolve_LifestyleScopedToNetServiceScope_From_WindsorCo }); Assert.NotNull(ex); - Assert.IsType(ex); - Assert.StartsWith("No scope available", ex.Message); + Assert.IsType(ex); + Assert.StartsWith("Could not obtain scope for component", ex.Message); (sp as IDisposable)?.Dispose(); container.Dispose(); @@ -711,8 +712,8 @@ public async Task Cannot_Resolve_LifestyleNetTransient_From_WindsorContainer_NoS }); Assert.NotNull(ex); - Assert.IsType(ex); - Assert.StartsWith("No scope available", ex.Message); + Assert.IsType(ex); + Assert.StartsWith("Could not obtain scope for component", ex.Message); (sp as IDisposable)?.Dispose(); container.Dispose(); diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Resolvers/LoggerDependencyResolver.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Resolvers/LoggerDependencyResolver.cs index d7813871cf..6a952b8492 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Resolvers/LoggerDependencyResolver.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Resolvers/LoggerDependencyResolver.cs @@ -18,7 +18,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Resolvers using Castle.Core; using Castle.MicroKernel; using Castle.MicroKernel.Context; - + using Microsoft.Extensions.Logging; public class LoggerDependencyResolver : ISubDependencyResolver diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index 401e19c4d4..e6ccdcff2c 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -15,7 +15,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using System; - + using Castle.Core.Logging; using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; @@ -23,7 +23,24 @@ internal class ExtensionContainerRootScopeAccessor : IScopeAccessor { public ILifetimeScope GetScope(CreationContext context) { - return ExtensionContainerScopeCache.Current?.RootScope ?? throw new InvalidOperationException("No root scope available."); + if (ExtensionContainerScopeCache.Current?.RootScope == null) + { + //TODO: if we have a way from context to retrieve the instance of container/kernel we could use it to get + //a reference of the correct root scope with a call to WindsorServiceProviderFactoryBase.GetSingleRootScope + //but since in this version we cannot determine the container from this context. + + //In this version we have the limit to have only one container registered in the dependency injection chain of + //.NET core, so we call a different method that gives us the single root scope. + var scope = WindsorServiceProviderFactoryBase.GetSingleRootScope(); + + if (scope == null) + { + throw new InvalidOperationException($"{nameof(ExtensionContainerRootScopeAccessor)}: We are trying to access a ROOT scope null for requested type {context.RequestedType}"); + } + + return scope; + } + return ExtensionContainerScopeCache.Current?.RootScope; } public void Dispose() diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs index 9ad99907f7..53cc991361 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +using System; + namespace Castle.Windsor.Extensions.DependencyInjection.Scope { internal class ExtensionContainerScope : ExtensionContainerScopeBase @@ -27,7 +29,8 @@ protected ExtensionContainerScope() internal static ExtensionContainerScopeBase BeginScope() { - var scope = new ExtensionContainerScope { RootScope = ExtensionContainerScopeCache.Current?.RootScope }; + var scope = new ExtensionContainerScope(); + scope.RootScope = ExtensionContainerScopeCache.Current?.RootScope; ExtensionContainerScopeCache.Current = scope; return scope; } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs index 8eaaa9f1d2..154e445611 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs @@ -22,7 +22,7 @@ internal class ExtensionContainerScopeAccessor : IScopeAccessor { public ILifetimeScope GetScope(CreationContext context) { - return ExtensionContainerScopeCache.Current ?? throw new InvalidOperationException("No scope available. Did you forget to call IServiceScopeFactory.CreateScope()?"); ; + return ExtensionContainerScopeCache.Current; } public void Dispose() diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index cc48a80320..17f26e58a2 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -20,6 +20,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope internal static class ExtensionContainerScopeCache { internal static readonly AsyncLocal current = new AsyncLocal(); + /// Current scope for the thread. Initial scope will be set when calling BeginRootScope from a ExtensionContainerRootScope instance. /// Thrown when there is no scope available. internal static ExtensionContainerScopeBase Current diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs index 1b8884486c..bba76fd149 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs @@ -12,22 +12,47 @@ // See the License for the specific language governing permissions and // limitations under the License. - namespace Castle.Windsor.Extensions.DependencyInjection { - using System; - using Castle.MicroKernel; using Castle.MicroKernel.Registration; using Castle.Windsor.Extensions.DependencyInjection.Extensions; using Castle.Windsor.Extensions.DependencyInjection.Resolvers; using Castle.Windsor.Extensions.DependencyInjection.Scope; - using Microsoft.Extensions.DependencyInjection; + using System; + using System.Collections.Generic; + using System.Linq; public abstract class WindsorServiceProviderFactoryBase : IServiceProviderFactory { - internal ExtensionContainerRootScope rootScope; + private static readonly Dictionary _factoryBaseMap = new(); + + internal static ExtensionContainerRootScope GetRootScopeForContainer(IWindsorContainer container) + { + if (_factoryBaseMap.TryGetValue(container, out var factory)) + { + return factory.RootScope; + } + throw new NotSupportedException("We are trying to access scopt for a container that was not associated with any WindsorServiceProviderFactoryBase. This is not supported."); + } + + internal static ExtensionContainerRootScope GetSingleRootScope() + { + if (_factoryBaseMap.Count == 0) + { + throw new NotSupportedException("No root scope created, did you forget to create an instance of IServiceProviderFActory?"); + } + else if (_factoryBaseMap.Count > 1) + { + throw new NotSupportedException("Multiple root scopes created, this is not supported because we cannot determine which is the right root scope bounded to actual container."); + } + + return _factoryBaseMap.Values.Single().RootScope; + } + + internal ExtensionContainerRootScope RootScope { get; private set; } + protected IWindsorContainer rootContainer; public virtual IWindsorContainer Container => rootContainer; @@ -44,7 +69,8 @@ public virtual IServiceProvider CreateServiceProvider(IWindsorContainer containe protected virtual void CreateRootScope() { - rootScope = ExtensionContainerRootScope.BeginRootScope(); + //first time we create the root scope we will initialize a root new scope + RootScope = ExtensionContainerRootScope.BeginRootScope(); } protected virtual void CreateRootContainer() @@ -55,6 +81,8 @@ protected virtual void CreateRootContainer() protected virtual void SetRootContainer(IWindsorContainer container) { rootContainer = container; + //Set the map associating this factoryh with the container. + _factoryBaseMap[rootContainer] = this; #if NET8_0_OR_GREATER rootContainer.Kernel.Resolver.AddSubResolver(new KeyedServicesSubDependencyResolver(rootContainer)); #endif @@ -122,7 +150,6 @@ protected virtual void RegisterFactories(IWindsorContainer container) container.Register(Component .For() .ImplementedBy() - .DependsOn(Dependency.OnValue(rootScope)) .LifestyleSingleton(), Component .For>() From 7437a7c69cfc29a9cc15c435db3ec61955242142 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Thu, 15 Feb 2024 13:19:07 +0100 Subject: [PATCH 15/20] Added logic to support multiple container in .NET 8 DI Needed to modify basic Castle.Winsor library to support the ability from IHandler interface to get the current kernel associated with the handler. This is needed to find the correct root scope associated with that kernel instance. --- buildscripts/common.props | 4 +- .../CustomAssumptionTests.cs | 17 ++- .../ResolveFromThreadpoolUnsafe.cs | 27 +++-- ...edDependencyInjectionSpecificationTests.cs | 108 +++--------------- ...viceProviderCustomWindsorContainerTests.cs | 31 ++++- .../WindsorScopedServiceProviderTests.cs | 31 ++++- ...dsor.Extensions.DependencyInjection.csproj | 6 +- .../ExtensionContainerRootScopeAccessor.cs | 13 +-- .../Scope/ExtensionContainerScopeBase.cs | 5 - .../WindsorServiceProviderFactoryBase.cs | 48 ++++---- .../Castle.Windsor.Extensions.Hosting.csproj | 2 +- .../DefaultComponentActivator.cs | 5 +- .../MicroKernel/DefaultKernel.cs | 7 +- .../MicroKernel/Handlers/AbstractHandler.cs | 5 + .../Handlers/ParentHandlerWrapper.cs | 5 + src/Castle.Windsor/MicroKernel/IHandler.cs | 6 + 16 files changed, 155 insertions(+), 165 deletions(-) diff --git a/buildscripts/common.props b/buildscripts/common.props index 266e4e4ff4..6791e6c986 100644 --- a/buildscripts/common.props +++ b/buildscripts/common.props @@ -4,7 +4,7 @@ $(NoWarn);CS1591;NU5048 git https://github.com/castleproject/Windsor - 6.0.0 + 0.0.0 $(APPVEYOR_BUILD_VERSION) $(BuildVersion.Split('.')[0]) $(BuildVersion.Split('-')[0]) @@ -18,7 +18,7 @@ Castle Windsor $(BuildVersionNoSuffix) $(BuildVersion) - 6.0.0.0 + $(BuildVersionMajor).0.0 Castle Windsor is best of breed, mature Inversion of Control container available for .NET Castle Project Contributors http://www.castleproject.org/projects/windsor/ diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs index 4bba519bf3..ac83c0f0ab 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -282,6 +282,7 @@ protected override IServiceProvider BuildServiceProvider(IServiceCollection serv public class CastleWindsorCustomAssumptionTests : CustomAssumptionTests { + private WindsorServiceProviderFactory _factory; private IWindsorContainer _container; protected override IServiceCollection GetServiceCollection() @@ -291,9 +292,9 @@ protected override IServiceCollection GetServiceCollection() protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection) { - var factory = new WindsorServiceProviderFactory(); - _container = factory.CreateBuilder(serviceCollection); - return factory.CreateServiceProvider(_container); + _factory = new WindsorServiceProviderFactory(); + _container = _factory.CreateBuilder(serviceCollection); + return _factory.CreateServiceProvider(_container); } [Fact] @@ -351,6 +352,16 @@ public void TryToResolveScopedInOtherThread() Assert.True(task.Result); } + + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + if (disposing) + { + _factory.Dispose(); + } + } } internal class TestService : ITestService diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs index 68844e4bc3..1f91093574 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs @@ -30,11 +30,11 @@ public ResolveFromThreadpoolUnsafe_NetStatic() : base(false) /// NetStatic lifestyle. /// [Fact] - public async Task Cannot_Resolve_LifestyleNetStatic_From_WindsorContainer_NoRootScopeAvailable() + public async Task Can_Resolve_LifestyleNetStatic_From_WindsorContainer_NoRootScopeAvailable() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -68,14 +68,13 @@ public async Task Cannot_Resolve_LifestyleNetStatic_From_WindsorContainer_NoRoot { var task = tcs.Task; IUserService result = await task; - // The test succeeds if we use standard Castle Windsor Singleton lifestyle instead of the custom NetStatic lifestyle. + //with the fix we can now use correctly a fallback for the root scope so we can access root scope even + //if we are outside of scope Assert.NotNull(result); }); // This test will fail if we use NetStatic lifestyle - Assert.NotNull(ex); - Assert.IsType(ex); - Assert.Equal("No root scope available.", ex.Message); + Assert.Null(ex); (sp as IDisposable)?.Dispose(); container.Dispose(); @@ -315,7 +314,7 @@ public async Task Cannot_Resolve_LifestyleScoped_From_ServiceProvider() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -373,7 +372,7 @@ public async Task Cannot_Resolve_LifestyleScoped_From_WindsorContainer() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -432,7 +431,7 @@ public async Task Can_Resolve_LifestyleScopedToNetServiceScope_From_ServiceProvi { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -475,7 +474,7 @@ public async Task Cannot_Resolve_LifestyleScopedToNetServiceScope_From_WindsorCo { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -540,7 +539,7 @@ public async Task Can_Resolve_LifestyleTransient_From_ServiceProvider() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -583,7 +582,7 @@ public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer() { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -631,7 +630,7 @@ public async Task Can_Resolve_LifestyleNetTransient_From_ServiceProvider_MemoryL { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( @@ -674,7 +673,7 @@ public async Task Cannot_Resolve_LifestyleNetTransient_From_WindsorContainer_NoS { var serviceProvider = new ServiceCollection(); var container = new WindsorContainer(); - var f = new WindsorServiceProviderFactory(container); + using var f = new WindsorServiceProviderFactory(container); f.CreateBuilder(serviceProvider); container.Register( diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs index 88da1a83fd..6fc401577f 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs @@ -6,119 +6,43 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Tests { - public class WindsorKeyedDependencyInjectionSpecificationTests : KeyedDependencyInjectionSpecificationTests + public class WindsorKeyedDependencyInjectionSpecificationTests : KeyedDependencyInjectionSpecificationTests, IDisposable { + private bool _disposedValue; + private WindsorServiceProviderFactory _factory; + protected override IServiceProvider CreateServiceProvider(IServiceCollection collection) { if (collection is TestServiceCollection) { - var factory = new WindsorServiceProviderFactory(); - var container = factory.CreateBuilder(collection); - return factory.CreateServiceProvider(container); + _factory = new WindsorServiceProviderFactory(); + var container = _factory.CreateBuilder(collection); + return _factory.CreateServiceProvider(container); } return collection.BuildServiceProvider(); } - //[Fact] - //public void ResolveKeyedServiceSingletonInstanceWithAnyKey2() - //{ - // var serviceCollection = new ServiceCollection(); - // serviceCollection.AddKeyedSingleton(KeyedService.AnyKey); - - // var provider = CreateServiceProvider(serviceCollection); - - // Assert.Null(provider.GetService()); - - // var serviceKey1 = "some-key"; - // var svc1 = provider.GetKeyedService(serviceKey1); - // Assert.NotNull(svc1); - // Assert.Equal(serviceKey1, svc1.ToString()); - - // var serviceKey2 = "some-other-key"; - // var svc2 = provider.GetKeyedService(serviceKey2); - // Assert.NotNull(svc2); - // Assert.Equal(serviceKey2, svc2.ToString()); - //} - - internal interface IService { } - - internal class Service : IService - { - private readonly string _id; - - public Service() => _id = Guid.NewGuid().ToString(); - - public Service([ServiceKey] string id) => _id = id; - - public override string? ToString() => _id; - } - - internal class OtherService + protected virtual void Dispose(bool disposing) { - public OtherService( - [FromKeyedServices("service1")] IService service1, - [FromKeyedServices("service2")] IService service2) + if (!_disposedValue) { - Service1 = service1; - Service2 = service2; - } - - public IService Service1 { get; } - - public IService Service2 { get; } - } - - public class FakeService : IFakeEveryService, IDisposable - { - public PocoClass Value { get; set; } - - public bool Disposed { get; private set; } - - public void Dispose() - { - if (Disposed) + if (disposing) { - throw new ObjectDisposedException(nameof(FakeService)); + _factory?.Dispose(); } - Disposed = true; + _disposedValue = true; } } - public interface IFakeEveryService : - IFakeService, - IFakeMultipleService, - IFakeScopedService - { - } - - public interface IFakeMultipleService : IFakeService - { - } - - public interface IFakeScopedService : IFakeService - { - } - - public interface IFakeService - { - } - - public class PocoClass + public void Dispose() { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } } - - //public class WindsorKeyedDependencyInjectionSpecificationExplicitContainerTests : KeyedDependencyInjectionSpecificationTests - //{ - // protected override IServiceProvider CreateServiceProvider(IServiceCollection collection) - // { - // var factory = new WindsorServiceProviderFactory(new WindsorContainer()); - // var container = factory.CreateBuilder(collection); - // return factory.CreateServiceProvider(container); - // } - //} } #endif diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs index 112ca051ac..54f909b975 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs @@ -21,13 +21,36 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Tests using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; - public class WindsorScopedServiceProviderCustomWindsorContainerTests : SkippableDependencyInjectionSpecificationTests + public class WindsorScopedServiceProviderCustomWindsorContainerTests : SkippableDependencyInjectionSpecificationTests, IDisposable { + private bool _disposedValue; + private WindsorServiceProviderFactory _factory; + protected override IServiceProvider CreateServiceProviderImpl(IServiceCollection serviceCollection) { - var factory = new WindsorServiceProviderFactory(new WindsorContainer()); - var container = factory.CreateBuilder(serviceCollection); - return factory.CreateServiceProvider(container); + _factory = new WindsorServiceProviderFactory(new WindsorContainer()); + var container = _factory.CreateBuilder(serviceCollection); + return _factory.CreateServiceProvider(container); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _factory?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } #if NET6_0_OR_GREATER diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderTests.cs index c335dd163b..c567ba7c1f 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderTests.cs @@ -19,13 +19,36 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Tests using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Specification; - public class WindsorScopedServiceProviderTests : SkippableDependencyInjectionSpecificationTests + public class WindsorScopedServiceProviderTests : SkippableDependencyInjectionSpecificationTests, IDisposable { + private bool _disposedValue; + private WindsorServiceProviderFactory _factory; + protected override IServiceProvider CreateServiceProviderImpl(IServiceCollection serviceCollection) { - var factory = new WindsorServiceProviderFactory(); - var container = factory.CreateBuilder(serviceCollection); - return factory.CreateServiceProvider(container); + _factory = new WindsorServiceProviderFactory(); + var container = _factory.CreateBuilder(serviceCollection); + return _factory.CreateServiceProvider(container); + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _factory?.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); } } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj index f1012afd0a..9884c8b7a3 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj @@ -36,7 +36,7 @@ - - - + + + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs index e6ccdcff2c..88f4f3d383 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -14,10 +14,9 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { - using System; - using Castle.Core.Logging; using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; + using System; internal class ExtensionContainerRootScopeAccessor : IScopeAccessor { @@ -25,17 +24,11 @@ public ILifetimeScope GetScope(CreationContext context) { if (ExtensionContainerScopeCache.Current?.RootScope == null) { - //TODO: if we have a way from context to retrieve the instance of container/kernel we could use it to get - //a reference of the correct root scope with a call to WindsorServiceProviderFactoryBase.GetSingleRootScope - //but since in this version we cannot determine the container from this context. - - //In this version we have the limit to have only one container registered in the dependency injection chain of - //.NET core, so we call a different method that gives us the single root scope. - var scope = WindsorServiceProviderFactoryBase.GetSingleRootScope(); + var scope = WindsorServiceProviderFactoryBase.GetRootScopeForKernel(context.Handler.GetKernel()); if (scope == null) { - throw new InvalidOperationException($"{nameof(ExtensionContainerRootScopeAccessor)}: We are trying to access a ROOT scope null for requested type {context.RequestedType}"); + throw new InvalidOperationException($"{nameof(ExtensionContainerRootScopeAccessor)}: We are trying to access a ROOT scope null for requested type {context.RequestedType} current kernel is not associated with any root scope"); } return scope; diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs index ac94760a48..c372ef5ecc 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs @@ -25,14 +25,9 @@ internal abstract class ExtensionContainerScopeBase : ILifetimeScope public static readonly string TransientMarker = "Transient"; private readonly IScopeCache scopeCache; - private static long _counter; - - public long Counter { get; private set; } - protected ExtensionContainerScopeBase() { scopeCache = new ScopeCache(); - Counter = Interlocked.Increment(ref _counter); } internal virtual ExtensionContainerScopeBase RootScope { get; set; } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs index bba76fd149..e507916f39 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs @@ -21,39 +21,26 @@ namespace Castle.Windsor.Extensions.DependencyInjection using Castle.Windsor.Extensions.DependencyInjection.Scope; using Microsoft.Extensions.DependencyInjection; using System; + using System.Collections.Concurrent; using System.Collections.Generic; - using System.Linq; - public abstract class WindsorServiceProviderFactoryBase : IServiceProviderFactory + public abstract class WindsorServiceProviderFactoryBase : IServiceProviderFactory, IDisposable { - private static readonly Dictionary _factoryBaseMap = new(); + private static readonly ConcurrentDictionary _factoryBaseMap = new(); - internal static ExtensionContainerRootScope GetRootScopeForContainer(IWindsorContainer container) + internal static ExtensionContainerRootScope GetRootScopeForKernel(IKernel kernel) { - if (_factoryBaseMap.TryGetValue(container, out var factory)) + if (_factoryBaseMap.TryGetValue(kernel, out var factory)) { return factory.RootScope; } throw new NotSupportedException("We are trying to access scopt for a container that was not associated with any WindsorServiceProviderFactoryBase. This is not supported."); } - internal static ExtensionContainerRootScope GetSingleRootScope() - { - if (_factoryBaseMap.Count == 0) - { - throw new NotSupportedException("No root scope created, did you forget to create an instance of IServiceProviderFActory?"); - } - else if (_factoryBaseMap.Count > 1) - { - throw new NotSupportedException("Multiple root scopes created, this is not supported because we cannot determine which is the right root scope bounded to actual container."); - } - - return _factoryBaseMap.Values.Single().RootScope; - } - internal ExtensionContainerRootScope RootScope { get; private set; } protected IWindsorContainer rootContainer; + private bool _disposedValue; public virtual IWindsorContainer Container => rootContainer; @@ -82,7 +69,7 @@ protected virtual void SetRootContainer(IWindsorContainer container) { rootContainer = container; //Set the map associating this factoryh with the container. - _factoryBaseMap[rootContainer] = this; + _factoryBaseMap[rootContainer.Kernel] = this; #if NET8_0_OR_GREATER rootContainer.Kernel.Resolver.AddSubResolver(new KeyedServicesSubDependencyResolver(rootContainer)); #endif @@ -171,5 +158,26 @@ protected virtual void AddSubResolvers() rootContainer.Kernel.Resolver.AddSubResolver(new OptionsSubResolver(rootContainer.Kernel)); rootContainer.Kernel.Resolver.AddSubResolver(new LoggerDependencyResolver(rootContainer.Kernel)); } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + //when this is disposed just remove the kernel from the map. + _factoryBaseMap.TryRemove(this.Container.Kernel, out var _); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } } } diff --git a/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj b/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj index c830167ea1..1aa19662d8 100644 --- a/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj +++ b/src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj @@ -23,7 +23,7 @@ - + diff --git a/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs b/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs index 9f2f33c4df..089ee7a6e0 100644 --- a/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs +++ b/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs @@ -138,10 +138,7 @@ protected virtual object CreateInstance(CreationContext context, ConstructorCand protected object CreateInstanceCore(ConstructorCandidate constructor, object[] arguments, Type implType) { object instance; - if (Model.Implementation.FullName.Contains("SiloConnectionFactory")) - { - - } + try { #if FEATURE_REMOTING diff --git a/src/Castle.Windsor/MicroKernel/DefaultKernel.cs b/src/Castle.Windsor/MicroKernel/DefaultKernel.cs index 2c94c47d31..46f8c9bd4c 100644 --- a/src/Castle.Windsor/MicroKernel/DefaultKernel.cs +++ b/src/Castle.Windsor/MicroKernel/DefaultKernel.cs @@ -694,9 +694,10 @@ protected CreationContext CreateCreationContext(IHandler handler, Type requested return new CreationContext(handler, policy, requestedType, additionalArguments, ConversionSubSystem, parent); } - /// - /// It is the responsibility of the kernel to ensure that handler is only ever disposed once. - /// + /// + /// It is the responsibility of the kernel to ensure that handler is only ever disposed once. + /// + /// protected void DisposeHandler(IHandler handler) { var disposable = handler as IDisposable; diff --git a/src/Castle.Windsor/MicroKernel/Handlers/AbstractHandler.cs b/src/Castle.Windsor/MicroKernel/Handlers/AbstractHandler.cs index 819b98a641..15c1bf6efd 100644 --- a/src/Castle.Windsor/MicroKernel/Handlers/AbstractHandler.cs +++ b/src/Castle.Windsor/MicroKernel/Handlers/AbstractHandler.cs @@ -126,6 +126,11 @@ private bool HasCustomParameter(object key) return model.CustomDependencies.Contains(key); } + IKernel IHandler.GetKernel() + { + return kernel; + } + /// /// Saves the kernel instance, subscribes to event, creates the lifestyle manager instance and computes the handler state. /// diff --git a/src/Castle.Windsor/MicroKernel/Handlers/ParentHandlerWrapper.cs b/src/Castle.Windsor/MicroKernel/Handlers/ParentHandlerWrapper.cs index 935fe552a0..c20af37787 100644 --- a/src/Castle.Windsor/MicroKernel/Handlers/ParentHandlerWrapper.cs +++ b/src/Castle.Windsor/MicroKernel/Handlers/ParentHandlerWrapper.cs @@ -62,6 +62,11 @@ public void Dispose() Dispose(true); } + IKernel IHandler.GetKernel() + { + return parentHandler.GetKernel(); + } + public virtual void Init(IKernelInternal kernel) { } diff --git a/src/Castle.Windsor/MicroKernel/IHandler.cs b/src/Castle.Windsor/MicroKernel/IHandler.cs index 4590bd4806..8f603b549e 100644 --- a/src/Castle.Windsor/MicroKernel/IHandler.cs +++ b/src/Castle.Windsor/MicroKernel/IHandler.cs @@ -82,5 +82,11 @@ public interface IHandler : ISubDependencyResolver /// /// object TryResolve(CreationContext context); + + /// + /// Needed to support root scope for multiple container when integrating with .NET 8 + /// DI engine. + /// + IKernel GetKernel(); } } \ No newline at end of file From 36bbbf120216c7706d6ac73de065c063f9210a81 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Mon, 11 Mar 2024 10:17:16 +0100 Subject: [PATCH 16/20] Changed constants in keyedregistration helper. --- .../KeyedRegistrationHelper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs index 4ae6cd451b..680429ade2 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs @@ -90,7 +90,7 @@ public string GetOrCreateKey(object key, ServiceDescriptor serviceDescriptor) { ArgumentNullException.ThrowIfNull(key); - var registrationKey = $"{KeyedRegistrationPrefix}+{Guid.NewGuid():X}"; + var registrationKey = $"{KeyedRegistrationPrefix}+{Guid.NewGuid():N}"; var registration = CreateRegistration(key, serviceDescriptor, registrationKey); if (!_keyToRegistrationMap.TryGetValue(key, out var registrations)) { From 17d1010fe1eee8bcae77b7ce692be966024ec731 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Tue, 21 May 2024 17:59:35 +0200 Subject: [PATCH 17/20] Updated nuget test adapter fixed a resolution bug The bug happened when you register a service with one NON keyed component then again with KEYED components. The adapter incorrectly checked only the first returned service for KEYED and returns null. Identified after update to Orleans 8.1.0 --- .../Castle.Facilities.AspNet.Mvc.Tests.csproj | 4 +- ...e.Facilities.AspNet.SystemWeb.Tests.csproj | 4 +- ...stle.Facilities.AspNet.WebApi.Tests.csproj | 4 +- .../Castle.Facilities.AspNetCore.Tests.csproj | 4 +- ...tle.Facilities.WcfIntegration.Tests.csproj | 4 +- ...xtensions.DependencyInjection.Tests.csproj | 2 +- .../CustomAssumptionTests.cs | 38 ++++++++------ ...edDependencyInjectionSpecificationTests.cs | 1 - ...viceProviderCustomWindsorContainerTests.cs | 23 +++++++++ .../WindsorScopedServiceProvider.cs | 49 +++++++++++++++++-- .../Castle.Windsor.Tests.csproj | 2 +- 11 files changed, 102 insertions(+), 33 deletions(-) diff --git a/src/Castle.Facilities.AspNet.Mvc.Tests/Castle.Facilities.AspNet.Mvc.Tests.csproj b/src/Castle.Facilities.AspNet.Mvc.Tests/Castle.Facilities.AspNet.Mvc.Tests.csproj index ccb0686472..11cd2e8905 100644 --- a/src/Castle.Facilities.AspNet.Mvc.Tests/Castle.Facilities.AspNet.Mvc.Tests.csproj +++ b/src/Castle.Facilities.AspNet.Mvc.Tests/Castle.Facilities.AspNet.Mvc.Tests.csproj @@ -15,11 +15,11 @@ - + - + diff --git a/src/Castle.Facilities.AspNet.SystemWeb.Tests/Castle.Facilities.AspNet.SystemWeb.Tests.csproj b/src/Castle.Facilities.AspNet.SystemWeb.Tests/Castle.Facilities.AspNet.SystemWeb.Tests.csproj index 76f9f3266d..2667d9d4ae 100644 --- a/src/Castle.Facilities.AspNet.SystemWeb.Tests/Castle.Facilities.AspNet.SystemWeb.Tests.csproj +++ b/src/Castle.Facilities.AspNet.SystemWeb.Tests/Castle.Facilities.AspNet.SystemWeb.Tests.csproj @@ -19,10 +19,10 @@ - + - + diff --git a/src/Castle.Facilities.AspNet.WebApi.Tests/Castle.Facilities.AspNet.WebApi.Tests.csproj b/src/Castle.Facilities.AspNet.WebApi.Tests/Castle.Facilities.AspNet.WebApi.Tests.csproj index af37968903..dbd7491c2a 100644 --- a/src/Castle.Facilities.AspNet.WebApi.Tests/Castle.Facilities.AspNet.WebApi.Tests.csproj +++ b/src/Castle.Facilities.AspNet.WebApi.Tests/Castle.Facilities.AspNet.WebApi.Tests.csproj @@ -15,11 +15,11 @@ - + - + diff --git a/src/Castle.Facilities.AspNetCore.Tests/Castle.Facilities.AspNetCore.Tests.csproj b/src/Castle.Facilities.AspNetCore.Tests/Castle.Facilities.AspNetCore.Tests.csproj index 4efbaf31da..195b1176eb 100644 --- a/src/Castle.Facilities.AspNetCore.Tests/Castle.Facilities.AspNetCore.Tests.csproj +++ b/src/Castle.Facilities.AspNetCore.Tests/Castle.Facilities.AspNetCore.Tests.csproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/Castle.Facilities.WcfIntegration.Tests/Castle.Facilities.WcfIntegration.Tests.csproj b/src/Castle.Facilities.WcfIntegration.Tests/Castle.Facilities.WcfIntegration.Tests.csproj index 11b5401dc0..82b2bed240 100644 --- a/src/Castle.Facilities.WcfIntegration.Tests/Castle.Facilities.WcfIntegration.Tests.csproj +++ b/src/Castle.Facilities.WcfIntegration.Tests/Castle.Facilities.WcfIntegration.Tests.csproj @@ -12,9 +12,9 @@ - + - + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj index aa6c272d3f..327125b31c 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj @@ -11,7 +11,7 @@ - + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs index ac83c0f0ab..df34569513 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -1,6 +1,6 @@ #if NET8_0_OR_GREATER +using Castle.MicroKernel; using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using System; using System.Linq; using System.Threading; @@ -110,6 +110,21 @@ public void Scoped_service_resolved_outside_scope() Assert.Equal(resolvedOutsideScope, resolvedAgainOutsideScope); } + [Fact] + public void Mix_of_keyed_and_not_keyed() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddKeyedSingleton("bla"); + + _serviceProvider = BuildServiceProvider(serviceCollection); + + //can resolve the non-keyed + var nonKeyed = _serviceProvider.GetRequiredService(); + Assert.NotNull(nonKeyed); + Assert.IsType(nonKeyed); + } + [Fact] public void Scoped_service_resolved_outside_scope_in_another_thread() { @@ -167,7 +182,8 @@ public async void Simulate_async_timer_without_wait() ITestService resolvedInThread = null; async Task ExecuteAsync() { - while (!stop) + DateTime start = DateTime.UtcNow; + while (!stop && DateTime.UtcNow.Subtract(start).TotalSeconds < 10) { await Task.Delay(100); if (shouldResolve) @@ -178,10 +194,7 @@ async Task ExecuteAsync() } } //fire and forget -#pragma warning disable CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed var task = ExecuteAsync(); -#pragma warning restore CS4014 // Because this call is not awaited, execution of the current method continues before the call is completed - await Task.Delay(500); var serviceCollection = GetServiceCollection(); @@ -353,7 +366,6 @@ public void TryToResolveScopedInOtherThread() Assert.True(task.Result); } - protected override void Dispose(bool disposing) { base.Dispose(disposing); @@ -364,16 +376,12 @@ protected override void Dispose(bool disposing) } } - internal class TestService : ITestService - { - } + internal class TestService : ITestService; - internal class AnotherTestService : ITestService - { - } + internal class AnotherTestService : ITestService; - internal interface ITestService - { - } + internal class ThirdTestService : ITestService; + + internal interface ITestService; } #endif \ No newline at end of file diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs index 6fc401577f..c4bcdb1da8 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs @@ -2,7 +2,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Specification; using System; -using Xunit; namespace Castle.Windsor.Extensions.DependencyInjection.Tests { diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs index 54f909b975..34bf9b424b 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs @@ -53,6 +53,29 @@ public void Dispose() GC.SuppressFinalize(this); } + ///// + ///// To verify when a single test failed, open the corresponding test in dotnet/runtime repository, + ///// then copy the test here, change name and execute with debugging etc etc. + ///// This helps because source link support seems to be not to easy to use from the test runner + ///// and this tricks makes everything really simpler. + ///// + //[Fact] + //public void ClosedServicesPreferredOverOpenGenericServices_custom() + //{ + // // Arrange + // var collection = new TestServiceCollection(); + // collection.AddTransient(typeof(IFakeOpenGenericService), typeof(FakeService)); + // collection.AddTransient(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>)); + // collection.AddSingleton(); + // var provider = CreateServiceProvider(collection); + + // // Act + // var service = provider.GetService>(); + + // // Assert + // Assert.IsType(service); + //} + #if NET6_0_OR_GREATER #endif diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 1ccb1935e5..7e2c1a6966 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -14,6 +14,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection { + using Castle.MicroKernel; using Castle.MicroKernel.Handlers; using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; @@ -99,15 +100,53 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) //this is complicated by the concept of keyed service, because if you are about to resolve WITHOUTH KEY you do not //need to resolve keyed services. Now Keyed services are available only in version 8 but we register with an helper //all registered services so we can know if a service was really registered with keyed service or not. - var componentRegistration = container.Kernel.GetHandler(serviceType); - if (componentRegistration.ComponentModel.Name.StartsWith(KeyedRegistrationHelper.KeyedRegistrationPrefix)) + var componentRegistrations = container.Kernel.GetHandlers(serviceType); + + //now since the caller requested a NON Keyed component, we need to skip all keyed components. + var realRegistrations = componentRegistrations.Where(x => !x.ComponentModel.Name.StartsWith(KeyedRegistrationHelper.KeyedRegistrationPrefix)).ToList(); + string registrationName = null; + if (realRegistrations.Count == 1) + { + registrationName = realRegistrations[0].ComponentModel.Name; + } + else if (realRegistrations.Count == 0) + { + //No component is registered for the interface without key, resolution cannot be done. + registrationName = null; + } + else if (realRegistrations.Count > 1) + { + //more than one component is registered for the interface without key, we have some ambiguity that is resolved, based on test + //found in framework with this rule. + //1. Last component win. + //2. closed service are preferred over open generic. + + //take first non generic + for (int i = realRegistrations.Count - 1; i >= 0; i--) + { + if (!realRegistrations[i].ComponentModel.Implementation.IsGenericTypeDefinition) + { + registrationName = realRegistrations[i].ComponentModel.Name; + break; + } + } + + //if we did not find any non generic, take the last one. + if (registrationName == null) + { + registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name; + } + } + + if (registrationName == null) { - //Component was registered as keyed component, so we really need to resolve with null because this is the old interface - //so no key is provided. return null; } -#endif + return container.Resolve(registrationName, serviceType); +#else + //no keyed component in previous framework, just resolve. return container.Resolve(serviceType); +#endif } if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) diff --git a/src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj b/src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj index 474f70187f..1a30263e02 100644 --- a/src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj +++ b/src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj @@ -47,7 +47,7 @@ - + From 9499d3a73c86bb74bbc02b913101c02c2097d025 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Mon, 24 Jun 2024 17:20:21 +0200 Subject: [PATCH 18/20] Fixed IsDefault() usage in DependencyInjectionAdapter If multiple concrete classes are registered in castle, when you resolve castle resolves the first one. With Microsoft DI is the oposite, you want the last registered. The resolution is now fixed to honor IsDefault() because previous code registered every component with IsDefault() if it is registered from the adapter. --- .vscode/tasks.json | 15 ++-- build_without_wcf_tests.cmd | 18 ++++ buildscripts/build_without_wcf_tests.cmd | 83 +++++++++++++++++++ .../CustomAssumptionTests.cs | 47 +++++++++++ .../RegistrationAdapter.cs | 6 +- .../WindsorScopedServiceProvider.cs | 72 +++++++++++----- 6 files changed, 211 insertions(+), 30 deletions(-) create mode 100644 build_without_wcf_tests.cmd create mode 100644 buildscripts/build_without_wcf_tests.cmd diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e176868402..36904a4e05 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,16 +1,21 @@ { - "version": "0.1.0", + "version": "2.0.0", "command": "dotnet", - "isShellCommand": true, "args": [], "tasks": [ { - "taskName": "build", + "label": "build", + "type": "shell", + "command": "dotnet", "args": [ + "build", "${workspaceRoot}/src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj" ], - "isBuildCommand": true, - "problemMatcher": "$msCompile" + "problemMatcher": "$msCompile", + "group": { + "_id": "build", + "isDefault": false + } } ] } \ No newline at end of file diff --git a/build_without_wcf_tests.cmd b/build_without_wcf_tests.cmd new file mode 100644 index 0000000000..15406a6966 --- /dev/null +++ b/build_without_wcf_tests.cmd @@ -0,0 +1,18 @@ +@ECHO OFF +REM **************************************************************************** +REM Copyright 2004-2013 Castle Project - http://www.castleproject.org/ +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM **************************************************************************** + +@call buildscripts\build_without_wcf_tests.cmd %* + diff --git a/buildscripts/build_without_wcf_tests.cmd b/buildscripts/build_without_wcf_tests.cmd new file mode 100644 index 0000000000..ce3776e44d --- /dev/null +++ b/buildscripts/build_without_wcf_tests.cmd @@ -0,0 +1,83 @@ +@ECHO OFF +REM **************************************************************************** +REM Copyright 2004-2013 Castle Project - http://www.castleproject.org/ +REM Licensed under the Apache License, Version 2.0 (the "License"); +REM you may not use this file except in compliance with the License. +REM You may obtain a copy of the License at +REM +REM http://www.apache.org/licenses/LICENSE-2.0 +REM +REM Unless required by applicable law or agreed to in writing, software +REM distributed under the License is distributed on an "AS IS" BASIS, +REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +REM See the License for the specific language governing permissions and +REM limitations under the License. +REM **************************************************************************** + +if "%1" == "" goto no_config +if "%1" NEQ "" goto set_config + +:set_config +SET Configuration=%1 +GOTO restore_packages + +:no_config +SET Configuration=Release +GOTO restore_packages + +:restore_packages +dotnet restore ./tools/Explicit.NuGet.Versions/Explicit.NuGet.Versions.csproj +dotnet restore ./buildscripts/BuildScripts.csproj +dotnet restore ./src/Castle.Windsor/Castle.Windsor.csproj +dotnet restore ./src/Castle.Facilities.Logging/Castle.Facilities.Logging.csproj +dotnet restore ./src/Castle.Facilities.AspNet.SystemWeb/Castle.Facilities.AspNet.SystemWeb.csproj +dotnet restore ./src/Castle.Facilities.AspNet.SystemWeb.Tests/Castle.Facilities.AspNet.SystemWeb.Tests.csproj +dotnet restore ./src/Castle.Facilities.AspNet.Mvc/Castle.Facilities.AspNet.Mvc.csproj +dotnet restore ./src/Castle.Facilities.AspNet.Mvc.Tests/Castle.Facilities.AspNet.Mvc.Tests.csproj +dotnet restore ./src/Castle.Facilities.AspNet.WebApi/Castle.Facilities.AspNet.WebApi.csproj +dotnet restore ./src/Castle.Facilities.AspNet.WebApi.Tests/Castle.Facilities.AspNet.WebApi.Tests.csproj +dotnet restore ./src/Castle.Facilities.AspNetCore/Castle.Facilities.AspNetCore.csproj +dotnet restore ./src/Castle.Facilities.AspNetCore.Tests/Castle.Facilities.AspNetCore.Tests.csproj +dotnet restore ./src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj +dotnet restore ./src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +dotnet restore ./src/Castle.Windsor.Extensions.Hosting/Castle.Windsor.Extensions.Hosting.csproj +dotnet restore ./src/Castle.Facilities.WcfIntegration/Castle.Facilities.WcfIntegration.csproj +dotnet restore ./src/Castle.Facilities.WcfIntegration.Demo/Castle.Facilities.WcfIntegration.Demo.csproj +dotnet restore ./src/Castle.Facilities.WcfIntegration.Tests/Castle.Facilities.WcfIntegration.Tests.csproj +dotnet restore ./src/Castle.Windsor.Tests/Castle.Windsor.Tests.csproj + + +GOTO build + +:build +dotnet build ./tools/Explicit.NuGet.Versions/Explicit.NuGet.Versions.sln +dotnet build Castle.Windsor.sln -c %Configuration% +GOTO test + +:test + +echo ------------- +echo Running Tests +echo ------------- + +dotnet test src\Castle.Windsor.Tests || exit /b 1 +dotnet test src\Castle.Windsor.Extensions.DependencyInjection.Tests || exit /b 1 +dotnet test src\Castle.Facilities.AspNetCore.Tests || exit /b 1 +dotnet test src\Castle.Facilities.AspNet.SystemWeb.Tests || exit /b 1 +dotnet test src\Castle.Facilities.AspNet.Mvc.Tests || exit /b 1 +dotnet test src\Castle.Facilities.AspNet.WebApi.Tests || exit /b 1 +rem dotnet test src\Castle.Facilities.WcfIntegration.Tests || exit /b 1 + +GOTO nuget_explicit_versions + +:nuget_explicit_versions + +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.windsor" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.loggingfacility" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.windsor.extensions.dependencyinjection" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.windsor.extensions.hosting" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.facilities.aspnetcore" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.facilities.aspnet.mvc" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.facilities.aspnet.webapi" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.facilities.aspnet.systemweb" +.\tools\Explicit.NuGet.Versions\build\nev.exe ".\build" "castle.wcfintegrationfacility" diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs index df34569513..5b2d6458d7 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -1,5 +1,6 @@ #if NET8_0_OR_GREATER using Castle.MicroKernel; +using Castle.MicroKernel.Registration; using Microsoft.Extensions.DependencyInjection; using System; using System.Linq; @@ -366,6 +367,52 @@ public void TryToResolveScopedInOtherThread() Assert.True(task.Result); } + [Fact] + public void Resolve_order_in_castle() + { + var serviceCollection = GetServiceCollection(); + _factory = new WindsorServiceProviderFactory(); + _container = _factory.CreateBuilder(serviceCollection); + + _container.Register( + Component.For().ImplementedBy() + , Component.For().ImplementedBy()); + + var provider = _factory.CreateServiceProvider(_container); + + var resolvedWithCastle = _container.Resolve(); + var resolvedWithProvider = provider.GetRequiredService(); + + //SUper important: Assumption for resolve multiple services registerd with the same + //interface is different: castle resolves the first, Microsoft DI require you to + //resolve the latest. + Assert.IsType(resolvedWithCastle); + Assert.IsType(resolvedWithProvider); + } + + [Fact] + public void Resolve_order_in_castle_with_is_default() + { + var serviceCollection = GetServiceCollection(); + _factory = new WindsorServiceProviderFactory(); + _container = _factory.CreateBuilder(serviceCollection); + + _container.Register( + Component.For().ImplementedBy().IsDefault() + , Component.For().ImplementedBy()); + + var provider = _factory.CreateServiceProvider(_container); + + var resolvedWithCastle = _container.Resolve(); + var resolvedWithProvider = provider.GetRequiredService(); + + //SUper important: Assumption for resolve multiple services registerd with the same + //interface is different: castle resolves the first, Microsoft DI require you to + //resolve the latest. + Assert.IsType(resolvedWithCastle); + Assert.IsType(resolvedWithProvider); + } + protected override void Dispose(bool disposing) { base.Dispose(disposing); diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index b4096e5317..0781d1b514 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -66,8 +66,7 @@ public static IRegistration FromOpenGenericServiceDescriptor( throw new System.ArgumentException("Unsupported ServiceDescriptor"); } #endif - return ResolveLifestyle(registration, service) - .IsDefault(); + return ResolveLifestyle(registration, service); } public static IRegistration FromServiceDescriptor( @@ -127,8 +126,7 @@ public static IRegistration FromServiceDescriptor( registration = UsingImplementation(registration, service); } #endif - return ResolveLifestyle(registration, service) - .IsDefault(); + return ResolveLifestyle(registration, service); } public static string OriginalComponentName(string uniqueComponentName) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 7e2c1a6966..1b8b846439 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -14,7 +14,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection { - using Castle.MicroKernel; using Castle.MicroKernel.Handlers; using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; @@ -96,7 +95,6 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) { if (container.Kernel.HasComponent(serviceType)) { -#if NET8_0_OR_GREATER //this is complicated by the concept of keyed service, because if you are about to resolve WITHOUTH KEY you do not //need to resolve keyed services. Now Keyed services are available only in version 8 but we register with an helper //all registered services so we can know if a service was really registered with keyed service or not. @@ -105,7 +103,7 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) //now since the caller requested a NON Keyed component, we need to skip all keyed components. var realRegistrations = componentRegistrations.Where(x => !x.ComponentModel.Name.StartsWith(KeyedRegistrationHelper.KeyedRegistrationPrefix)).ToList(); string registrationName = null; - if (realRegistrations.Count == 1) + if (realRegistrations.Count == 1) { registrationName = realRegistrations[0].ComponentModel.Name; } @@ -116,25 +114,39 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) } else if (realRegistrations.Count > 1) { - //more than one component is registered for the interface without key, we have some ambiguity that is resolved, based on test - //found in framework with this rule. - //1. Last component win. - //2. closed service are preferred over open generic. + //Need to honor IsDefault for castle registrations. + var isDefaultRegistration = realRegistrations + .FirstOrDefault(dh => dh.ComponentModel.ExtendedProperties.Any(ComponentIsDefault)); - //take first non generic - for (int i = realRegistrations.Count - 1; i >= 0; i--) + //Remember that castle has a specific order of resolution, if someone registered something in castle with + //IsDefault() it Must be honored. + if (isDefaultRegistration != null) + { + registrationName = isDefaultRegistration.ComponentModel.Name; + } + else { - if (!realRegistrations[i].ComponentModel.Implementation.IsGenericTypeDefinition) + //more than one component is registered for the interface without key, we have some ambiguity that is resolved, based on test + //found in framework with this rule. In this situation we do not use the same rule of Castle where the first service win but + //we use the framework rule that: + //1. Last component win. + //2. closed service are preferred over open generic. + + //take first non generic + for (int i = realRegistrations.Count - 1; i >= 0; i--) { - registrationName = realRegistrations[i].ComponentModel.Name; - break; + if (!realRegistrations[i].ComponentModel.Implementation.IsGenericTypeDefinition) + { + registrationName = realRegistrations[i].ComponentModel.Name; + break; + } } - } - //if we did not find any non generic, take the last one. - if (registrationName == null) - { - registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name; + //if we did not find any non generic, take the last one. + if (registrationName == null) + { + registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name; + } } } @@ -143,10 +155,6 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) return null; } return container.Resolve(registrationName, serviceType); -#else - //no keyed component in previous framework, just resolve. - return container.Resolve(serviceType); -#endif } if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) @@ -197,6 +205,28 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) return container.Resolve(serviceType); } + private static bool ComponentIsDefault(KeyValuePair property) + { + if (!Core.Internal.Constants.DefaultComponentForServiceFilter.Equals(property.Key)) + { + //not the property we are looking for + return false; + } + + if (property.Value is bool boolValue) + { + return boolValue; + } + + if (property.Value is Predicate predicate) + { + //this is a method info that we can invoke to get the value. + return predicate(null); + } + + return false; + } + #if NET6_0_OR_GREATER public bool IsService(Type serviceType) From b71829cb7f642766c52749b4b9dd49afb4ca6ee2 Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Tue, 25 Jun 2024 13:20:15 +0200 Subject: [PATCH 19/20] Fixed (again) resolution rules during resolve. We added a new concept, an extendede property that allows the code to understand if the dependency was registered through the adapter (ServiceCollection) or directly through the Container. This allows us to change the resolution rule in case of multiple services registered with the same name. --- README.md | 29 +++++++++++ ...xtensions.DependencyInjection.Tests.csproj | 1 + .../CustomAssumptionTests.cs | 51 ++++++++++++++++++- ...dsor.Extensions.DependencyInjection.csproj | 1 + .../RegistrationAdapter.cs | 17 ++++++- .../Scope/WindsorScopeFactory.cs | 1 - .../WindsorScopedServiceProvider.cs | 46 +++++++++++++---- 7 files changed, 132 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 9739ab5c48..aced57fdba 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,35 @@ Castle Windsor is a best of breed, mature Inversion of Control container availab See the [documentation](docs/README.md). +## Considerations + +Castle.Windsor.Extensions.DependencyInjection try to make Microsoft Dependency Injection works with Castle.Windsor. We have some +really different rules in the two world, one is the order of resolution exposed by the test Resolve_order_in_castle that shows +how the two have two different strategies. + +1. Microsof DI want to resolve the last registered service +2. Castle.Windsor want to resolve the first registered service. + +This is one of the point where the integration become painful, because it can happen that the very same service got resolved +in two distinct way, depending on who is resolving the service. + + The preferred solution is to understand who is registering the service and resolve everything accordingly. + +## I want to try everything locally. + +If you want to easily try a local compiled version on your project you can use the following trick. + +1. Add the GenerateAssemblyInfo to false on the project file +1. Add an assemblyinfo.cs in Properties folder and add the [assembly: AssemblyVersion("6.0.0")] attribute to force the correct version +1. Compile the project +1. Copy into the local nuget cache, from the output folder of this project run + +``` +copy * %Uer Profile%\.nuget\packages\castle.windsor.extensions.dependencyinjection\6.0.x\lib\net8.0 +``` + +This usually works. + ## Releases See the [releases](https://github.com/castleproject/Windsor/releases). diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj index 327125b31c..71767ccad3 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs index 5b2d6458d7..aac2482909 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -369,11 +369,38 @@ public void TryToResolveScopedInOtherThread() [Fact] public void Resolve_order_in_castle() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + var provider = BuildServiceProvider(serviceCollection); + + + var castleContainer = new WindsorContainer(); + castleContainer.Register( + Component.For().ImplementedBy() + , Component.For().ImplementedBy()); + + var resolvedWithCastle = castleContainer.Resolve(); + var resolvedWithProvider = provider.GetRequiredService(); + + //SUper important: Assumption for resolve multiple services registerd with the same + //interface is different: castle resolves the first, Microsoft DI require you to + //resolve the latest. + Assert.IsType(resolvedWithCastle); + Assert.IsType(resolvedWithProvider); + } + + [Fact] + public void If_we_register_through_container_resolution_is_castle() { var serviceCollection = GetServiceCollection(); _factory = new WindsorServiceProviderFactory(); _container = _factory.CreateBuilder(serviceCollection); + //We are recording component with castle, it is not important that we resolve + //with castle or with the adapter, we use castle rules because who registered + //the components wants probably castle semantic. _container.Register( Component.For().ImplementedBy() , Component.For().ImplementedBy()); @@ -387,6 +414,26 @@ public void Resolve_order_in_castle() //interface is different: castle resolves the first, Microsoft DI require you to //resolve the latest. Assert.IsType(resolvedWithCastle); + Assert.IsType(resolvedWithProvider); + } + + [Fact] + public void If_we_register_through_adapter_resolution_is_microsoft() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + _factory = new WindsorServiceProviderFactory(); + _container = _factory.CreateBuilder(serviceCollection); + var provider = _factory.CreateServiceProvider(_container); + + var resolvedWithCastle = _container.Resolve(); + var resolvedWithProvider = provider.GetRequiredService(); + + //SUper important: Assumption for resolve multiple services registerd with the same + //interface is different: castle resolves the first, Microsoft DI require you to + //resolve the latest. + Assert.IsType(resolvedWithCastle); Assert.IsType(resolvedWithProvider); } @@ -398,7 +445,9 @@ public void Resolve_order_in_castle_with_is_default() _container = _factory.CreateBuilder(serviceCollection); _container.Register( - Component.For().ImplementedBy().IsDefault() + Component.For().ImplementedBy() + .IsDefault() + .ExtendedProperties(new Property("porcodio", "porcamadonna")) , Component.For().ImplementedBy()); var provider = _factory.CreateServiceProvider(_container); diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj index 9884c8b7a3..2ede9a6be1 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj @@ -16,6 +16,7 @@ true Castle.Windsor.Extensions.DependencyInjection Castle.Windsor.Extensions.DependencyInjection + Castle.Windsor.Extensions.DependencyInjection diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index 0781d1b514..556dd837b0 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -21,6 +21,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection internal static class RegistrationAdapter { + /// + /// This is a constants that is used as key in the extended properties of a component + /// when it is registered through RegistrationAdapter. This allows to understand which + /// is the best semantic to use when resolving the component. + /// + internal static string RegistrationKeyExtendedPropertyKey = "microsoft-di-registered"; + public static IRegistration FromOpenGenericServiceDescriptor( Microsoft.Extensions.DependencyInjection.ServiceDescriptor service, IWindsorContainer windsorContainer) @@ -66,7 +73,11 @@ public static IRegistration FromOpenGenericServiceDescriptor( throw new System.ArgumentException("Unsupported ServiceDescriptor"); } #endif - return ResolveLifestyle(registration, service); + //Extended properties allows to understand when the service was registered through the adapter + //and IsDefault is needed to change the semantic of the resolution, LAST registered service win. + return ResolveLifestyle(registration, service) + .ExtendedProperties(RegistrationKeyExtendedPropertyKey) + .IsDefault(); } public static IRegistration FromServiceDescriptor( @@ -126,7 +137,9 @@ public static IRegistration FromServiceDescriptor( registration = UsingImplementation(registration, service); } #endif - return ResolveLifestyle(registration, service); + return ResolveLifestyle(registration, service) + .ExtendedProperties(RegistrationKeyExtendedPropertyKey) + .IsDefault(); } public static string OriginalComponentName(string uniqueComponentName) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs index 6a1e439dfd..83ab718842 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs @@ -17,7 +17,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using Castle.Windsor; using Microsoft.Extensions.DependencyInjection; - using System; internal class WindsorScopeFactory : IServiceScopeFactory { diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 1b8b846439..3189f2e65c 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -14,6 +14,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection { + using Castle.Core.Logging; using Castle.MicroKernel.Handlers; using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; @@ -28,18 +29,24 @@ internal class WindsorScopedServiceProvider : IServiceProvider, ISupportRequired , IServiceProviderIsService #endif #if NET8_0_OR_GREATER - , IKeyedServiceProvider, IServiceProviderIsKeyedService + , IKeyedServiceProvider, IServiceProviderIsKeyedService #endif { private readonly ExtensionContainerScopeBase scope; private bool disposing; - + private ILogger _logger = NullLogger.Instance; private readonly IWindsorContainer container; public WindsorScopedServiceProvider(IWindsorContainer container) { this.container = container; scope = ExtensionContainerScopeCache.Current; + + if (container.Kernel.HasComponent(typeof(ILoggerFactory))) + { + var loggerFactory = container.Resolve(); + _logger = loggerFactory.Create(typeof(WindsorScopedServiceProvider)); + } } public object GetService(Type serviceType) @@ -69,7 +76,6 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey) } #endif - public object GetRequiredService(Type serviceType) { using (_ = new ForcedScope(scope)) @@ -114,18 +120,32 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) } else if (realRegistrations.Count > 1) { - //Need to honor IsDefault for castle registrations. - var isDefaultRegistration = realRegistrations - .FirstOrDefault(dh => dh.ComponentModel.ExtendedProperties.Any(ComponentIsDefault)); + //ok we have a big problem, we have multiple registration and different semantic, because + //Microsoft.DI wants the latest registered service to win + //Caste instead wants the first registered service to win. + + //how can we live with this to have a MINIMUM (never zero) impact on everything that registers things? + //we need to determine who registered the components. + var registeredByMicrosoftDi = realRegistrations.Any(r => r.ComponentModel.ExtendedProperties.Any(ep => RegistrationAdapter.RegistrationKeyExtendedPropertyKey.Equals(ep.Key))); - //Remember that castle has a specific order of resolution, if someone registered something in castle with - //IsDefault() it Must be honored. - if (isDefaultRegistration != null) + if (!registeredByMicrosoftDi) { - registrationName = isDefaultRegistration.ComponentModel.Name; + if (_logger.IsDebugEnabled) + { + _logger.Debug($@"Multiple components registered for service {serviceType.FullName} All services {string.Join(",", realRegistrations.Select(r => r.ComponentModel.Implementation.Name))}"); + } + + //ok we are in a situation where no component was registered through the adapter, this is the situatino of a component + //registered purely in castle (this should mean that the user want to use castle semantic). + //let the standard castle rules apply. + return container.Resolve(serviceType); } else { + //If we are here at least one of the component was registered throuh Microsoft.DI, this means that the code that regiestered + //the component want to use the semantic of Microsoft.DI. This means that we need to use different set of rules. + + //RULES: //more than one component is registered for the interface without key, we have some ambiguity that is resolved, based on test //found in framework with this rule. In this situation we do not use the same rule of Castle where the first service win but //we use the framework rule that: @@ -148,6 +168,12 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name; } } + + if (_logger.IsDebugEnabled) + { + _logger.Debug($@"Multiple components registered for service {serviceType.FullName}. Selected component {registrationName} +all services {string.Join(",", realRegistrations.Select(r => r.ComponentModel.Implementation.Name))}"); + } } if (registrationName == null) From 495fc21ca2f302a4ee25946834708457ef7e1cbe Mon Sep 17 00:00:00 2001 From: Gian Maria Ricci Date: Thu, 11 Jul 2024 15:06:23 +0200 Subject: [PATCH 20/20] Cleanup from PR Requests --- ...viceProviderCustomWindsorContainerTests.cs | 36 +++++-------------- ...dsor.Extensions.DependencyInjection.csproj | 1 - .../RegistrationAdapter.cs | 2 -- .../Scope/ExtensionContainerScope.cs | 2 -- .../Scope/ExtensionContainerScopeCache.cs | 2 +- 5 files changed, 9 insertions(+), 34 deletions(-) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs index 34bf9b424b..3e1d432167 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs @@ -21,6 +21,14 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Tests using Microsoft.Extensions.DependencyInjection.Specification.Fakes; using Xunit; + /// + /// This test inherits from a test class of the framework that contains a set of base tests to verify various assumption + /// that must be satisfied by DependencyInjection. + /// To debug a single test, open the corresponding test in dotnet/runtime repository, + /// then copy the test here, change name and execute with debugging etc etc. + /// This helps because source link support seems to be not to easy to use from the test runner + /// and this tricks makes everything really simpler. + /// public class WindsorScopedServiceProviderCustomWindsorContainerTests : SkippableDependencyInjectionSpecificationTests, IDisposable { private bool _disposedValue; @@ -52,33 +60,5 @@ public void Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } - - ///// - ///// To verify when a single test failed, open the corresponding test in dotnet/runtime repository, - ///// then copy the test here, change name and execute with debugging etc etc. - ///// This helps because source link support seems to be not to easy to use from the test runner - ///// and this tricks makes everything really simpler. - ///// - //[Fact] - //public void ClosedServicesPreferredOverOpenGenericServices_custom() - //{ - // // Arrange - // var collection = new TestServiceCollection(); - // collection.AddTransient(typeof(IFakeOpenGenericService), typeof(FakeService)); - // collection.AddTransient(typeof(IFakeOpenGenericService<>), typeof(FakeOpenGenericService<>)); - // collection.AddSingleton(); - // var provider = CreateServiceProvider(collection); - - // // Act - // var service = provider.GetService>(); - - // // Assert - // Assert.IsType(service); - //} - -#if NET6_0_OR_GREATER -#endif - } - } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj index 2ede9a6be1..9884c8b7a3 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj @@ -16,7 +16,6 @@ true Castle.Windsor.Extensions.DependencyInjection Castle.Windsor.Extensions.DependencyInjection - Castle.Windsor.Extensions.DependencyInjection diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index 556dd837b0..d6076d8752 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -216,7 +216,6 @@ private static ComponentRegistration UsingFactoryMethod( { return registration.UsingFactoryMethod((kernel) => { - //TODO: We should register a factory in castle that in turns call the implementation factory?? var serviceProvider = kernel.Resolve(); #if NET8_0_OR_GREATER if (service.IsKeyedService) @@ -229,7 +228,6 @@ private static ComponentRegistration UsingFactoryMethod( } #else return service.ImplementationFactory(serviceProvider) as TService; - #endif }); } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs index 53cc991361..c5519abec1 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; - namespace Castle.Windsor.Extensions.DependencyInjection.Scope { internal class ExtensionContainerScope : ExtensionContainerScopeBase diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index 17f26e58a2..5c56c8a52a 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -26,7 +26,7 @@ internal static class ExtensionContainerScopeCache internal static ExtensionContainerScopeBase Current { // AysncLocal can be null in some cases (like Threadpool.UnsafeQueueUserWorkItem) - get => current.Value; // ?? throw new InvalidOperationException("No scope available. Did you forget to call IServiceScopeFactory.CreateScope()?"); + get => current.Value; set => current.Value = value; } }