Skip to content

Commit

Permalink
Fixed (again) resolution rules during resolve.
Browse files Browse the repository at this point in the history
We added a new concept, an extendede property that
allows the code to understand if the dependency
was registered through the adapter (ServiceCollection)
or directly through the Container.

This allows us to change the resolution rule in case
of multiple services registered with the same name.
  • Loading branch information
alkampfergit committed Jun 25, 2024
1 parent 9499d3a commit b71829c
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 14 deletions.
29 changes: 29 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Specification.Tests" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -369,11 +369,38 @@ public void TryToResolveScopedInOtherThread()

[Fact]
public void Resolve_order_in_castle()
{
var serviceCollection = GetServiceCollection();
serviceCollection.AddSingleton<ITestService, TestService>();
serviceCollection.AddSingleton<ITestService, AnotherTestService>();
var provider = BuildServiceProvider(serviceCollection);


var castleContainer = new WindsorContainer();
castleContainer.Register(
Component.For<ITestService>().ImplementedBy<TestService>()
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());

var resolvedWithCastle = castleContainer.Resolve<ITestService>();
var resolvedWithProvider = provider.GetRequiredService<ITestService>();

//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<TestService>(resolvedWithCastle);
Assert.IsType<AnotherTestService>(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<ITestService>().ImplementedBy<TestService>()
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());
Expand All @@ -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<TestService>(resolvedWithCastle);
Assert.IsType<TestService>(resolvedWithProvider);
}

[Fact]
public void If_we_register_through_adapter_resolution_is_microsoft()
{
var serviceCollection = GetServiceCollection();
serviceCollection.AddSingleton<ITestService, TestService>();
serviceCollection.AddSingleton<ITestService, AnotherTestService>();
_factory = new WindsorServiceProviderFactory();
_container = _factory.CreateBuilder(serviceCollection);
var provider = _factory.CreateServiceProvider(_container);

var resolvedWithCastle = _container.Resolve<ITestService>();
var resolvedWithProvider = provider.GetRequiredService<ITestService>();

//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<AnotherTestService>(resolvedWithCastle);
Assert.IsType<AnotherTestService>(resolvedWithProvider);
}

Expand All @@ -398,7 +445,9 @@ public void Resolve_order_in_castle_with_is_default()
_container = _factory.CreateBuilder(serviceCollection);

_container.Register(
Component.For<ITestService>().ImplementedBy<TestService>().IsDefault()
Component.For<ITestService>().ImplementedBy<TestService>()
.IsDefault()
.ExtendedProperties(new Property("porcodio", "porcamadonna"))
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());

var provider = _factory.CreateServiceProvider(_container);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<AssemblyName>Castle.Windsor.Extensions.DependencyInjection</AssemblyName>
<RootNamespace>Castle.Windsor.Extensions.DependencyInjection</RootNamespace>
<RootNamespace>Castle.Windsor.Extensions.DependencyInjection</RootNamespace>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection

internal static class RegistrationAdapter
{
/// <summary>
/// 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.
/// </summary>
internal static string RegistrationKeyExtendedPropertyKey = "microsoft-di-registered";

public static IRegistration FromOpenGenericServiceDescriptor(
Microsoft.Extensions.DependencyInjection.ServiceDescriptor service,
IWindsorContainer windsorContainer)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope
{
using Castle.Windsor;
using Microsoft.Extensions.DependencyInjection;
using System;

internal class WindsorScopeFactory : IServiceScopeFactory
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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<ILoggerFactory>();
_logger = loggerFactory.Create(typeof(WindsorScopedServiceProvider));
}
}

public object GetService(Type serviceType)
Expand Down Expand Up @@ -69,7 +76,6 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey)
}

#endif

public object GetRequiredService(Type serviceType)
{
using (_ = new ForcedScope(scope))
Expand Down Expand Up @@ -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:
Expand All @@ -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)
Expand Down

0 comments on commit b71829c

Please sign in to comment.