diff --git a/README.md b/README.md index 9739ab5c4..aced57fdb 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,35 @@ Castle Windsor is a best of breed, mature Inversion of Control container availab See the [documentation](docs/README.md). +## Considerations + +Castle.Windsor.Extensions.DependencyInjection try to make Microsoft Dependency Injection works with Castle.Windsor. We have some +really different rules in the two world, one is the order of resolution exposed by the test Resolve_order_in_castle that shows +how the two have two different strategies. + +1. Microsof DI want to resolve the last registered service +2. Castle.Windsor want to resolve the first registered service. + +This is one of the point where the integration become painful, because it can happen that the very same service got resolved +in two distinct way, depending on who is resolving the service. + + The preferred solution is to understand who is registering the service and resolve everything accordingly. + +## I want to try everything locally. + +If you want to easily try a local compiled version on your project you can use the following trick. + +1. Add the GenerateAssemblyInfo to false on the project file +1. Add an assemblyinfo.cs in Properties folder and add the [assembly: AssemblyVersion("6.0.0")] attribute to force the correct version +1. Compile the project +1. Copy into the local nuget cache, from the output folder of this project run + +``` +copy * %Uer Profile%\.nuget\packages\castle.windsor.extensions.dependencyinjection\6.0.x\lib\net8.0 +``` + +This usually works. + ## Releases See the [releases](https://github.com/castleproject/Windsor/releases). diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj index 327125b31..71767ccad 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj @@ -33,6 +33,7 @@ + diff --git a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs index 5b2d6458d..aac248290 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs @@ -369,11 +369,38 @@ public void TryToResolveScopedInOtherThread() [Fact] public void Resolve_order_in_castle() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + var provider = BuildServiceProvider(serviceCollection); + + + var castleContainer = new WindsorContainer(); + castleContainer.Register( + Component.For().ImplementedBy() + , Component.For().ImplementedBy()); + + var resolvedWithCastle = castleContainer.Resolve(); + var resolvedWithProvider = provider.GetRequiredService(); + + //SUper important: Assumption for resolve multiple services registerd with the same + //interface is different: castle resolves the first, Microsoft DI require you to + //resolve the latest. + Assert.IsType(resolvedWithCastle); + Assert.IsType(resolvedWithProvider); + } + + [Fact] + public void If_we_register_through_container_resolution_is_castle() { var serviceCollection = GetServiceCollection(); _factory = new WindsorServiceProviderFactory(); _container = _factory.CreateBuilder(serviceCollection); + //We are recording component with castle, it is not important that we resolve + //with castle or with the adapter, we use castle rules because who registered + //the components wants probably castle semantic. _container.Register( Component.For().ImplementedBy() , Component.For().ImplementedBy()); @@ -387,6 +414,26 @@ public void Resolve_order_in_castle() //interface is different: castle resolves the first, Microsoft DI require you to //resolve the latest. Assert.IsType(resolvedWithCastle); + Assert.IsType(resolvedWithProvider); + } + + [Fact] + public void If_we_register_through_adapter_resolution_is_microsoft() + { + var serviceCollection = GetServiceCollection(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + _factory = new WindsorServiceProviderFactory(); + _container = _factory.CreateBuilder(serviceCollection); + var provider = _factory.CreateServiceProvider(_container); + + var resolvedWithCastle = _container.Resolve(); + var resolvedWithProvider = provider.GetRequiredService(); + + //SUper important: Assumption for resolve multiple services registerd with the same + //interface is different: castle resolves the first, Microsoft DI require you to + //resolve the latest. + Assert.IsType(resolvedWithCastle); Assert.IsType(resolvedWithProvider); } @@ -398,7 +445,9 @@ public void Resolve_order_in_castle_with_is_default() _container = _factory.CreateBuilder(serviceCollection); _container.Register( - Component.For().ImplementedBy().IsDefault() + Component.For().ImplementedBy() + .IsDefault() + .ExtendedProperties(new Property("porcodio", "porcamadonna")) , Component.For().ImplementedBy()); var provider = _factory.CreateServiceProvider(_container); diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj index 9884c8b7a..2ede9a6be 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj @@ -16,6 +16,7 @@ true Castle.Windsor.Extensions.DependencyInjection Castle.Windsor.Extensions.DependencyInjection + Castle.Windsor.Extensions.DependencyInjection diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs index 0781d1b51..556dd837b 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs @@ -21,6 +21,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection internal static class RegistrationAdapter { + /// + /// This is a constants that is used as key in the extended properties of a component + /// when it is registered through RegistrationAdapter. This allows to understand which + /// is the best semantic to use when resolving the component. + /// + internal static string RegistrationKeyExtendedPropertyKey = "microsoft-di-registered"; + public static IRegistration FromOpenGenericServiceDescriptor( Microsoft.Extensions.DependencyInjection.ServiceDescriptor service, IWindsorContainer windsorContainer) @@ -66,7 +73,11 @@ public static IRegistration FromOpenGenericServiceDescriptor( throw new System.ArgumentException("Unsupported ServiceDescriptor"); } #endif - return ResolveLifestyle(registration, service); + //Extended properties allows to understand when the service was registered through the adapter + //and IsDefault is needed to change the semantic of the resolution, LAST registered service win. + return ResolveLifestyle(registration, service) + .ExtendedProperties(RegistrationKeyExtendedPropertyKey) + .IsDefault(); } public static IRegistration FromServiceDescriptor( @@ -126,7 +137,9 @@ public static IRegistration FromServiceDescriptor( registration = UsingImplementation(registration, service); } #endif - return ResolveLifestyle(registration, service); + return ResolveLifestyle(registration, service) + .ExtendedProperties(RegistrationKeyExtendedPropertyKey) + .IsDefault(); } public static string OriginalComponentName(string uniqueComponentName) diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs index 6a1e439df..83ab71884 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs @@ -17,7 +17,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope { using Castle.Windsor; using Microsoft.Extensions.DependencyInjection; - using System; internal class WindsorScopeFactory : IServiceScopeFactory { diff --git a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs index 1b8b84643..3189f2e65 100644 --- a/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs +++ b/src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs @@ -14,6 +14,7 @@ namespace Castle.Windsor.Extensions.DependencyInjection { + using Castle.Core.Logging; using Castle.MicroKernel.Handlers; using Castle.Windsor; using Castle.Windsor.Extensions.DependencyInjection.Scope; @@ -28,18 +29,24 @@ internal class WindsorScopedServiceProvider : IServiceProvider, ISupportRequired , IServiceProviderIsService #endif #if NET8_0_OR_GREATER - , IKeyedServiceProvider, IServiceProviderIsKeyedService + , IKeyedServiceProvider, IServiceProviderIsKeyedService #endif { private readonly ExtensionContainerScopeBase scope; private bool disposing; - + private ILogger _logger = NullLogger.Instance; private readonly IWindsorContainer container; public WindsorScopedServiceProvider(IWindsorContainer container) { this.container = container; scope = ExtensionContainerScopeCache.Current; + + if (container.Kernel.HasComponent(typeof(ILoggerFactory))) + { + var loggerFactory = container.Resolve(); + _logger = loggerFactory.Create(typeof(WindsorScopedServiceProvider)); + } } public object GetService(Type serviceType) @@ -69,7 +76,6 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey) } #endif - public object GetRequiredService(Type serviceType) { using (_ = new ForcedScope(scope)) @@ -114,18 +120,32 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) } else if (realRegistrations.Count > 1) { - //Need to honor IsDefault for castle registrations. - var isDefaultRegistration = realRegistrations - .FirstOrDefault(dh => dh.ComponentModel.ExtendedProperties.Any(ComponentIsDefault)); + //ok we have a big problem, we have multiple registration and different semantic, because + //Microsoft.DI wants the latest registered service to win + //Caste instead wants the first registered service to win. + + //how can we live with this to have a MINIMUM (never zero) impact on everything that registers things? + //we need to determine who registered the components. + var registeredByMicrosoftDi = realRegistrations.Any(r => r.ComponentModel.ExtendedProperties.Any(ep => RegistrationAdapter.RegistrationKeyExtendedPropertyKey.Equals(ep.Key))); - //Remember that castle has a specific order of resolution, if someone registered something in castle with - //IsDefault() it Must be honored. - if (isDefaultRegistration != null) + if (!registeredByMicrosoftDi) { - registrationName = isDefaultRegistration.ComponentModel.Name; + if (_logger.IsDebugEnabled) + { + _logger.Debug($@"Multiple components registered for service {serviceType.FullName} All services {string.Join(",", realRegistrations.Select(r => r.ComponentModel.Implementation.Name))}"); + } + + //ok we are in a situation where no component was registered through the adapter, this is the situatino of a component + //registered purely in castle (this should mean that the user want to use castle semantic). + //let the standard castle rules apply. + return container.Resolve(serviceType); } else { + //If we are here at least one of the component was registered throuh Microsoft.DI, this means that the code that regiestered + //the component want to use the semantic of Microsoft.DI. This means that we need to use different set of rules. + + //RULES: //more than one component is registered for the interface without key, we have some ambiguity that is resolved, based on test //found in framework with this rule. In this situation we do not use the same rule of Castle where the first service win but //we use the framework rule that: @@ -148,6 +168,12 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional) registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name; } } + + if (_logger.IsDebugEnabled) + { + _logger.Debug($@"Multiple components registered for service {serviceType.FullName}. Selected component {registrationName} +all services {string.Join(",", realRegistrations.Select(r => r.ComponentModel.Implementation.Name))}"); + } } if (registrationName == null)