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
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/.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/.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/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/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.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 6ef705103a..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
@@ -1,7 +1,7 @@
- netcoreapp3.1;net6.0
+ netcoreapp3.1;net6.0;net8.0
false
@@ -11,11 +11,11 @@
-
+
-
-
-
+
+
+
@@ -29,6 +29,15 @@
+
+
+
+
+
+
+
+
+
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..aac2482909
--- /dev/null
+++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs
@@ -0,0 +1,483 @@
+#if NET8_0_OR_GREATER
+using Castle.MicroKernel;
+using Castle.MicroKernel.Registration;
+using Microsoft.Extensions.DependencyInjection;
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+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());
+ }
+
+ [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 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()
+ {
+ 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()
+ {
+ DateTime start = DateTime.UtcNow;
+ while (!stop && DateTime.UtcNow.Subtract(start).TotalSeconds < 10)
+ {
+ await Task.Delay(100);
+ if (shouldResolve)
+ {
+ stop = true;
+ resolvedInThread = _serviceProvider.GetService();
+ }
+ }
+ }
+ //fire and forget
+ var task = ExecuteAsync();
+ 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 class RealCustomAssumptionTests : CustomAssumptionTests
+ {
+ protected override IServiceCollection GetServiceCollection()
+ {
+ return new RealTestServiceCollection();
+ }
+
+ protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection)
+ {
+ return serviceCollection.BuildServiceProvider();
+ }
+ }
+
+ public class CastleWindsorCustomAssumptionTests : CustomAssumptionTests
+ {
+ private WindsorServiceProviderFactory _factory;
+ private IWindsorContainer _container;
+
+ protected override IServiceCollection GetServiceCollection()
+ {
+ return new TestServiceCollection();
+ }
+
+ protected override IServiceProvider BuildServiceProvider(IServiceCollection serviceCollection)
+ {
+ _factory = new WindsorServiceProviderFactory();
+ _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);
+ }
+
+ [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());
+
+ 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 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);
+ }
+
+ [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()
+ .ExtendedProperties(new Property("porcodio", "porcamadonna"))
+ , 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);
+ if (disposing)
+ {
+ _factory.Dispose();
+ }
+ }
+ }
+
+ internal class TestService : ITestService;
+
+ internal class AnotherTestService : ITestService;
+
+ internal class ThirdTestService : ITestService;
+
+ internal interface ITestService;
+}
+#endif
\ 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
new file mode 100644
index 0000000000..1f91093574
--- /dev/null
+++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/ResolveFromThreadpoolUnsafe.cs
@@ -0,0 +1,757 @@
+using Castle.MicroKernel;
+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;
+using System;
+using System.Threading;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace Castle.Windsor.Extensions.DependencyInjection.Tests
+{
+ [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)
+ {
+ }
+
+ #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_LifestyleNetStatic_From_WindsorContainer_NoRootScopeAvailable()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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;
+ //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.Null(ex);
+
+ (sp as IDisposable)?.Dispose();
+ container.Dispose();
+ }
+
+ #endregion
+ }
+
+ ///
+ /// 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 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();
+ 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);
+ });
+
+ (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_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);
+
+ (sp as IDisposable)?.Dispose();
+ container.Dispose();
+ }
+
+ [Fact]
+ public async Task Can_Resolve_LifestyleSingleton_From_WindsorContainer()
+ {
+ 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 = 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);
+
+ (sp as IDisposable)?.Dispose();
+ container.Dispose();
+ }
+
+ [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);
+
+ (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).
+ */
+
+ ///
+ /// This test will fail because the service provider adapter
+ /// does not create a standard Castle Windsor scope
+ ///
+ [Fact]
+ public async Task Cannot_Resolve_LifestyleScoped_From_ServiceProvider()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using var f = new WindsorServiceProviderFactory(container);
+ f.CreateBuilder(serviceProvider);
+
+ container.Register(
+ Component.For().ImplementedBy().LifestyleScoped()
+ );
+
+ IServiceProvider sp = f.CreateServiceProvider(container);
+
+ // 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();
+
+ 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 ex = await Catches.ExceptionAsync(async () =>
+ {
+ 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 Cannot_Resolve_LifestyleScoped_From_WindsorContainer()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using var f = new WindsorServiceProviderFactory(container);
+ f.CreateBuilder(serviceProvider);
+
+ container.Register(
+ Component.For().ImplementedBy().LifestyleScoped()
+ );
+
+ IServiceProvider sp = f.CreateServiceProvider(container);
+
+ // 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();
+
+ 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;
+ 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 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_MemoryLeak()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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 = 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);
+
+ (sp as IDisposable)?.Dispose();
+ container.Dispose();
+ }
+
+ [Fact]
+ public async Task Cannot_Resolve_LifestyleScopedToNetServiceScope_From_WindsorContainer()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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();
+ 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;
+ Assert.NotNull(result);
+ });
+
+ Assert.NotNull(ex);
+ Assert.IsType(ex);
+ Assert.StartsWith("Could not obtain scope for component", ex.Message);
+
+ (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()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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 = 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);
+
+ (sp as IDisposable)?.Dispose();
+ container.Dispose();
+ }
+
+ [Fact]
+ public async Task Can_Resolve_LifestyleTransient_From_WindsorContainer()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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();
+ 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);
+
+ (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_MemoryLeak()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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 = 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);
+
+ (sp as IDisposable)?.Dispose();
+ container.Dispose();
+ }
+
+ [Fact]
+ public async Task Cannot_Resolve_LifestyleNetTransient_From_WindsorContainer_NoScopeAvailable()
+ {
+ var serviceProvider = new ServiceCollection();
+ var container = new WindsorContainer();
+ using 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();
+ 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;
+ Assert.NotNull(result);
+ });
+
+ Assert.NotNull(ex);
+ Assert.IsType(ex);
+ Assert.StartsWith("Could not obtain scope for component", ex.Message);
+
+ (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).
+ */
+ }
+
+ 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.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..c4bcdb1da8
--- /dev/null
+++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorKeyedDependencyInjectionSpecificationTests.cs
@@ -0,0 +1,47 @@
+#if NET8_0_OR_GREATER
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Specification;
+using System;
+
+namespace Castle.Windsor.Extensions.DependencyInjection.Tests
+{
+ public class WindsorKeyedDependencyInjectionSpecificationTests : KeyedDependencyInjectionSpecificationTests, IDisposable
+ {
+ private bool _disposedValue;
+ private WindsorServiceProviderFactory _factory;
+
+ protected override IServiceProvider CreateServiceProvider(IServiceCollection collection)
+ {
+ if (collection is TestServiceCollection)
+ {
+ _factory = new WindsorServiceProviderFactory();
+ var container = _factory.CreateBuilder(collection);
+ return _factory.CreateServiceProvider(container);
+ }
+
+ return collection.BuildServiceProvider();
+ }
+
+ 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);
+ }
+ }
+}
+
+#endif
diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs
index 00ece5738e..3e1d432167 100644
--- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs
+++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/WindsorScopedServiceProviderCustomWindsorContainerTests.cs
@@ -15,17 +15,50 @@
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
+ ///
+ /// 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;
+ 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);
}
}
}
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 24eaaa0ae0..9884c8b7a3 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,7 +30,13 @@
-
-
-
+
+
+
+
+
+
+
+
+
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/Extensions/WindsorExtensions.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs
index 38d46d34d7..5b7b95f9d4 100644
--- a/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs
+++ b/src/Castle.Windsor.Extensions.DependencyInjection/Extensions/WindsorExtensions.cs
@@ -49,6 +49,13 @@ public static ComponentRegistration LifestyleNetTransient(th
///
public static ComponentRegistration NetStatic(this LifestyleGroup lifestyle) where TService : class
{
+ // 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/KeyedRegistrationHelper.cs b/src/Castle.Windsor.Extensions.DependencyInjection/KeyedRegistrationHelper.cs
new file mode 100644
index 0000000000..680429ade2
--- /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
diff --git a/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs b/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs
index ba7bd50405..089ee7a6e0 100644
--- a/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs
+++ b/src/Castle.Windsor/MicroKernel/ComponentActivator/DefaultComponentActivator.cs
@@ -138,6 +138,7 @@ protected virtual object CreateInstance(CreationContext context, ConstructorCand
protected object CreateInstanceCore(ConstructorCandidate constructor, object[] arguments, Type implType)
{
object instance;
+
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/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)
{
}
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