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> _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():N}"; + 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/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 diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index 974c02ae88..d6076d8752 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -14,21 +14,57 @@ namespace Castle.Windsor.Extensions.DependencyInjection { - using System; - using Castle.MicroKernel.Registration; using Castle.Windsor.Extensions.DependencyInjection.Extensions; - using Microsoft.Extensions.DependencyInjection; + using System; - internal class RegistrationAdapter + internal static class RegistrationAdapter { - public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extensions.DependencyInjection.ServiceDescriptor service) + /// + /// 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) { - ComponentRegistration registration = Component.For(service.ServiceType) - .NamedAutomatically(UniqueComponentName(service)); + ComponentRegistration registration; - if(service.ImplementationType != null) +#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 +72,58 @@ public static IRegistration FromOpenGenericServiceDescriptor(Microsoft.Extension { throw new System.ArgumentException("Unsupported ServiceDescriptor"); } - +#endif + //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(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,18 +136,19 @@ public static IRegistration FromServiceDescriptor(Microsoft.Extensions.Dependenc { registration = UsingImplementation(registration, service); } - +#endif return ResolveLifestyle(registration, service) + .ExtendedProperties(RegistrationKeyExtendedPropertyKey) .IsDefault(); } public static string OriginalComponentName(string uniqueComponentName) { - if(uniqueComponentName == null) + if (uniqueComponentName == null) { return null; } - if(!uniqueComponentName.Contains("@")) + if (!uniqueComponentName.Contains("@")) { return uniqueComponentName; } @@ -79,11 +158,43 @@ 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; } - else if(service.ImplementationInstance != null) + else if (service.ImplementationInstance != null) { result = service.ImplementationInstance.GetType().FullName; } @@ -91,32 +202,71 @@ 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) => { + return registration.UsingFactoryMethod((kernel) => + { 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 { - switch(service.Lifetime) + switch (service.Lifetime) { case ServiceLifetime.Singleton: return registration.LifeStyle.NetStatic(); @@ -124,7 +274,7 @@ private static ComponentRegistration ResolveLifestyle(Compon return registration.LifeStyle.ScopedToNetServiceScope(); case ServiceLifetime.Transient: return registration.LifestyleNetTransient(); - + default: throw new System.ArgumentException($"Invalid lifetime {service.Lifetime}"); } 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/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 25139efec9..88f4f3d383 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerRootScopeAccessor.cs @@ -14,16 +14,26 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { - using System; - using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; + using System; 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) + { + 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} current kernel is not associated with any root scope"); + } + + 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 2ff5a5c497..c5519abec1 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScope.cs @@ -25,10 +25,10 @@ protected ExtensionContainerScope() internal override ExtensionContainerScopeBase RootScope { get; set; } - 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 9042944d75..154e445611 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeAccessor.cs @@ -16,6 +16,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using Castle.MicroKernel.Context; using Castle.MicroKernel.Lifestyle.Scoped; + using System; internal class ExtensionContainerScopeAccessor : IScopeAccessor { diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeBase.cs index edb784dfcb..c372ef5ecc 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; diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs index d89f9dd1f2..5c56c8a52a 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/ExtensionContainerScopeCache.cs @@ -20,11 +20,13 @@ 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 { - get => current.Value ?? throw new InvalidOperationException("No scope available"); + // AysncLocal can be null in some cases (like Threadpool.UnsafeQueueUserWorkItem) + get => current.Value; set => current.Value = value; } } diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs index 0686ea95a6..83ab718842 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs @@ -15,16 +15,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { - using System; - using Castle.Windsor; - using Microsoft.Extensions.DependencyInjection; internal class WindsorScopeFactory : IServiceScopeFactory { private readonly IWindsorContainer scopeFactoryContainer; - + public WindsorScopeFactory(IWindsorContainer container) { scopeFactoryContainer = container; @@ -34,8 +31,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/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; } + } +} diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 4e61f2f765..3189f2e65c 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -12,67 +12,312 @@ // 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.Core.Logging; + 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; - + 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) { - using(_ = new ForcedScope(scope)) + using (_ = new ForcedScope(scope)) + { + return ResolveInstanceOrNull(serviceType, true); + } + } + +#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, true); + return ResolveInstanceOrNull(serviceType, serviceKey, false); } } +#endif 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) { if (container.Kernel.HasComponent(serviceType)) { - return container.Resolve(serviceType); + //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 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) + { + //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))); + + if (!registeredByMicrosoftDi) + { + 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: + //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 (_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) + { + return null; + } + return container.Resolve(registrationName, serviceType); + } + + if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + //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); + } + + 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) + { + 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 allObjects = container.ResolveAll(serviceType.GenericTypeArguments[0]); - return allObjects; + 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) @@ -82,5 +327,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..e507916f39 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorServiceProviderFactoryBase.cs @@ -12,29 +12,41 @@ // 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.Concurrent; + using System.Collections.Generic; - public abstract class WindsorServiceProviderFactoryBase : IServiceProviderFactory + public abstract class WindsorServiceProviderFactoryBase : IServiceProviderFactory, IDisposable { - internal ExtensionContainerRootScope rootScope; + private static readonly ConcurrentDictionary _factoryBaseMap = new(); + + internal static ExtensionContainerRootScope GetRootScopeForKernel(IKernel kernel) + { + 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 ExtensionContainerRootScope RootScope { get; private set; } + protected IWindsorContainer rootContainer; + private bool _disposedValue; public virtual IWindsorContainer Container => rootContainer; public virtual IWindsorContainer CreateBuilder(IServiceCollection services) { - return BuildContainer(services, rootContainer); + return BuildContainer(services); } public virtual IServiceProvider CreateServiceProvider(IWindsorContainer container) @@ -44,7 +56,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 +68,11 @@ protected virtual void CreateRootContainer() protected virtual void SetRootContainer(IWindsorContainer container) { rootContainer = container; + //Set the map associating this factoryh with the container. + _factoryBaseMap[rootContainer.Kernel] = this; +#if NET8_0_OR_GREATER + rootContainer.Kernel.Resolver.AddSubResolver(new KeyedServicesSubDependencyResolver(rootContainer)); +#endif AddSubSystemToContainer(rootContainer); } @@ -66,7 +84,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 +103,7 @@ protected virtual IWindsorContainer BuildContainer(IServiceCollection serviceCol AddSubResolvers(); - RegisterServiceCollection(serviceCollection, windsorContainer); + RegisterServiceCollection(serviceCollection); return rootContainer; } @@ -101,9 +119,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) @@ -111,7 +137,6 @@ protected virtual void RegisterFactories(IWindsorContainer container) container.Register(Component .For() .ImplementedBy() - .DependsOn(Dependency.OnValue(rootScope)) .LifestyleSingleton(), Component .For>() @@ -119,11 +144,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)); } } @@ -133,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.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 @@ - + 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