Skip to content

Commit b71829c

Browse files
committed
Fixed (again) resolution rules during resolve.
We added a new concept, an extendede property that allows the code to understand if the dependency was registered through the adapter (ServiceCollection) or directly through the Container. This allows us to change the resolution rule in case of multiple services registered with the same name.
1 parent 9499d3a commit b71829c

File tree

7 files changed

+132
-14
lines changed

7 files changed

+132
-14
lines changed

README.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,35 @@ Castle Windsor is a best of breed, mature Inversion of Control container availab
66

77
See the [documentation](docs/README.md).
88

9+
## Considerations
10+
11+
Castle.Windsor.Extensions.DependencyInjection try to make Microsoft Dependency Injection works with Castle.Windsor. We have some
12+
really different rules in the two world, one is the order of resolution exposed by the test Resolve_order_in_castle that shows
13+
how the two have two different strategies.
14+
15+
1. Microsof DI want to resolve the last registered service
16+
2. Castle.Windsor want to resolve the first registered service.
17+
18+
This is one of the point where the integration become painful, because it can happen that the very same service got resolved
19+
in two distinct way, depending on who is resolving the service.
20+
21+
The preferred solution is to understand who is registering the service and resolve everything accordingly.
22+
23+
## I want to try everything locally.
24+
25+
If you want to easily try a local compiled version on your project you can use the following trick.
26+
27+
1. Add the GenerateAssemblyInfo to false on the project file
28+
1. Add an assemblyinfo.cs in Properties folder and add the [assembly: AssemblyVersion("6.0.0")] attribute to force the correct version
29+
1. Compile the project
30+
1. Copy into the local nuget cache, from the output folder of this project run
31+
32+
```
33+
copy * %Uer Profile%\.nuget\packages\castle.windsor.extensions.dependencyinjection\6.0.x\lib\net8.0
34+
```
35+
36+
This usually works.
37+
938
## Releases
1039

1140
See the [releases](https://github.com/castleproject/Windsor/releases).

src/Castle.Windsor.Extensions.DependencyInjection.Tests/Castle.Windsor.Extensions.DependencyInjection.Tests.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="8.0.0" />
3434
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
3535
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="8.0.0" />
36+
<PackageReference Include="Microsoft.Extensions.Logging" Version="8.0.0" />
3637
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Specification.Tests" Version="8.0.0" />
3738
<PackageReference Include="Microsoft.Extensions.Options" Version="8.0.0" />
3839
</ItemGroup>

src/Castle.Windsor.Extensions.DependencyInjection.Tests/CustomAssumptionTests.cs

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -369,11 +369,38 @@ public void TryToResolveScopedInOtherThread()
369369

370370
[Fact]
371371
public void Resolve_order_in_castle()
372+
{
373+
var serviceCollection = GetServiceCollection();
374+
serviceCollection.AddSingleton<ITestService, TestService>();
375+
serviceCollection.AddSingleton<ITestService, AnotherTestService>();
376+
var provider = BuildServiceProvider(serviceCollection);
377+
378+
379+
var castleContainer = new WindsorContainer();
380+
castleContainer.Register(
381+
Component.For<ITestService>().ImplementedBy<TestService>()
382+
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());
383+
384+
var resolvedWithCastle = castleContainer.Resolve<ITestService>();
385+
var resolvedWithProvider = provider.GetRequiredService<ITestService>();
386+
387+
//SUper important: Assumption for resolve multiple services registerd with the same
388+
//interface is different: castle resolves the first, Microsoft DI require you to
389+
//resolve the latest.
390+
Assert.IsType<TestService>(resolvedWithCastle);
391+
Assert.IsType<AnotherTestService>(resolvedWithProvider);
392+
}
393+
394+
[Fact]
395+
public void If_we_register_through_container_resolution_is_castle()
372396
{
373397
var serviceCollection = GetServiceCollection();
374398
_factory = new WindsorServiceProviderFactory();
375399
_container = _factory.CreateBuilder(serviceCollection);
376400

401+
//We are recording component with castle, it is not important that we resolve
402+
//with castle or with the adapter, we use castle rules because who registered
403+
//the components wants probably castle semantic.
377404
_container.Register(
378405
Component.For<ITestService>().ImplementedBy<TestService>()
379406
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());
@@ -387,6 +414,26 @@ public void Resolve_order_in_castle()
387414
//interface is different: castle resolves the first, Microsoft DI require you to
388415
//resolve the latest.
389416
Assert.IsType<TestService>(resolvedWithCastle);
417+
Assert.IsType<TestService>(resolvedWithProvider);
418+
}
419+
420+
[Fact]
421+
public void If_we_register_through_adapter_resolution_is_microsoft()
422+
{
423+
var serviceCollection = GetServiceCollection();
424+
serviceCollection.AddSingleton<ITestService, TestService>();
425+
serviceCollection.AddSingleton<ITestService, AnotherTestService>();
426+
_factory = new WindsorServiceProviderFactory();
427+
_container = _factory.CreateBuilder(serviceCollection);
428+
var provider = _factory.CreateServiceProvider(_container);
429+
430+
var resolvedWithCastle = _container.Resolve<ITestService>();
431+
var resolvedWithProvider = provider.GetRequiredService<ITestService>();
432+
433+
//SUper important: Assumption for resolve multiple services registerd with the same
434+
//interface is different: castle resolves the first, Microsoft DI require you to
435+
//resolve the latest.
436+
Assert.IsType<AnotherTestService>(resolvedWithCastle);
390437
Assert.IsType<AnotherTestService>(resolvedWithProvider);
391438
}
392439

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

400447
_container.Register(
401-
Component.For<ITestService>().ImplementedBy<TestService>().IsDefault()
448+
Component.For<ITestService>().ImplementedBy<TestService>()
449+
.IsDefault()
450+
.ExtendedProperties(new Property("porcodio", "porcamadonna"))
402451
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());
403452

404453
var provider = _factory.CreateServiceProvider(_container);

src/Castle.Windsor.Extensions.DependencyInjection/Castle.Windsor.Extensions.DependencyInjection.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
1717
<AssemblyName>Castle.Windsor.Extensions.DependencyInjection</AssemblyName>
1818
<RootNamespace>Castle.Windsor.Extensions.DependencyInjection</RootNamespace>
19+
<RootNamespace>Castle.Windsor.Extensions.DependencyInjection</RootNamespace>
1920
</PropertyGroup>
2021

2122
<ItemGroup Condition="'$(TargetFramework)'=='netstandard2.0'">

src/Castle.Windsor.Extensions.DependencyInjection/RegistrationAdapter.cs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,13 @@ namespace Castle.Windsor.Extensions.DependencyInjection
2121

2222
internal static class RegistrationAdapter
2323
{
24+
/// <summary>
25+
/// This is a constants that is used as key in the extended properties of a component
26+
/// when it is registered through RegistrationAdapter. This allows to understand which
27+
/// is the best semantic to use when resolving the component.
28+
/// </summary>
29+
internal static string RegistrationKeyExtendedPropertyKey = "microsoft-di-registered";
30+
2431
public static IRegistration FromOpenGenericServiceDescriptor(
2532
Microsoft.Extensions.DependencyInjection.ServiceDescriptor service,
2633
IWindsorContainer windsorContainer)
@@ -66,7 +73,11 @@ public static IRegistration FromOpenGenericServiceDescriptor(
6673
throw new System.ArgumentException("Unsupported ServiceDescriptor");
6774
}
6875
#endif
69-
return ResolveLifestyle(registration, service);
76+
//Extended properties allows to understand when the service was registered through the adapter
77+
//and IsDefault is needed to change the semantic of the resolution, LAST registered service win.
78+
return ResolveLifestyle(registration, service)
79+
.ExtendedProperties(RegistrationKeyExtendedPropertyKey)
80+
.IsDefault();
7081
}
7182

7283
public static IRegistration FromServiceDescriptor(
@@ -126,7 +137,9 @@ public static IRegistration FromServiceDescriptor(
126137
registration = UsingImplementation(registration, service);
127138
}
128139
#endif
129-
return ResolveLifestyle(registration, service);
140+
return ResolveLifestyle(registration, service)
141+
.ExtendedProperties(RegistrationKeyExtendedPropertyKey)
142+
.IsDefault();
130143
}
131144

132145
public static string OriginalComponentName(string uniqueComponentName)

src/Castle.Windsor.Extensions.DependencyInjection/Scope/WindsorScopeFactory.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@ namespace Castle.Windsor.Extensions.DependencyInjection.Scope
1717
{
1818
using Castle.Windsor;
1919
using Microsoft.Extensions.DependencyInjection;
20-
using System;
2120

2221
internal class WindsorScopeFactory : IServiceScopeFactory
2322
{

src/Castle.Windsor.Extensions.DependencyInjection/WindsorScopedServiceProvider.cs

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
namespace Castle.Windsor.Extensions.DependencyInjection
1616
{
17+
using Castle.Core.Logging;
1718
using Castle.MicroKernel.Handlers;
1819
using Castle.Windsor;
1920
using Castle.Windsor.Extensions.DependencyInjection.Scope;
@@ -28,18 +29,24 @@ internal class WindsorScopedServiceProvider : IServiceProvider, ISupportRequired
2829
, IServiceProviderIsService
2930
#endif
3031
#if NET8_0_OR_GREATER
31-
, IKeyedServiceProvider, IServiceProviderIsKeyedService
32+
, IKeyedServiceProvider, IServiceProviderIsKeyedService
3233
#endif
3334
{
3435
private readonly ExtensionContainerScopeBase scope;
3536
private bool disposing;
36-
37+
private ILogger _logger = NullLogger.Instance;
3738
private readonly IWindsorContainer container;
3839

3940
public WindsorScopedServiceProvider(IWindsorContainer container)
4041
{
4142
this.container = container;
4243
scope = ExtensionContainerScopeCache.Current;
44+
45+
if (container.Kernel.HasComponent(typeof(ILoggerFactory)))
46+
{
47+
var loggerFactory = container.Resolve<ILoggerFactory>();
48+
_logger = loggerFactory.Create(typeof(WindsorScopedServiceProvider));
49+
}
4350
}
4451

4552
public object GetService(Type serviceType)
@@ -69,7 +76,6 @@ public object GetRequiredKeyedService(Type serviceType, object serviceKey)
6976
}
7077

7178
#endif
72-
7379
public object GetRequiredService(Type serviceType)
7480
{
7581
using (_ = new ForcedScope(scope))
@@ -114,18 +120,32 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
114120
}
115121
else if (realRegistrations.Count > 1)
116122
{
117-
//Need to honor IsDefault for castle registrations.
118-
var isDefaultRegistration = realRegistrations
119-
.FirstOrDefault(dh => dh.ComponentModel.ExtendedProperties.Any(ComponentIsDefault));
123+
//ok we have a big problem, we have multiple registration and different semantic, because
124+
//Microsoft.DI wants the latest registered service to win
125+
//Caste instead wants the first registered service to win.
126+
127+
//how can we live with this to have a MINIMUM (never zero) impact on everything that registers things?
128+
//we need to determine who registered the components.
129+
var registeredByMicrosoftDi = realRegistrations.Any(r => r.ComponentModel.ExtendedProperties.Any(ep => RegistrationAdapter.RegistrationKeyExtendedPropertyKey.Equals(ep.Key)));
120130

121-
//Remember that castle has a specific order of resolution, if someone registered something in castle with
122-
//IsDefault() it Must be honored.
123-
if (isDefaultRegistration != null)
131+
if (!registeredByMicrosoftDi)
124132
{
125-
registrationName = isDefaultRegistration.ComponentModel.Name;
133+
if (_logger.IsDebugEnabled)
134+
{
135+
_logger.Debug($@"Multiple components registered for service {serviceType.FullName} All services {string.Join(",", realRegistrations.Select(r => r.ComponentModel.Implementation.Name))}");
136+
}
137+
138+
//ok we are in a situation where no component was registered through the adapter, this is the situatino of a component
139+
//registered purely in castle (this should mean that the user want to use castle semantic).
140+
//let the standard castle rules apply.
141+
return container.Resolve(serviceType);
126142
}
127143
else
128144
{
145+
//If we are here at least one of the component was registered throuh Microsoft.DI, this means that the code that regiestered
146+
//the component want to use the semantic of Microsoft.DI. This means that we need to use different set of rules.
147+
148+
//RULES:
129149
//more than one component is registered for the interface without key, we have some ambiguity that is resolved, based on test
130150
//found in framework with this rule. In this situation we do not use the same rule of Castle where the first service win but
131151
//we use the framework rule that:
@@ -148,6 +168,12 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
148168
registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name;
149169
}
150170
}
171+
172+
if (_logger.IsDebugEnabled)
173+
{
174+
_logger.Debug($@"Multiple components registered for service {serviceType.FullName}. Selected component {registrationName}
175+
all services {string.Join(",", realRegistrations.Select(r => r.ComponentModel.Implementation.Name))}");
176+
}
151177
}
152178

153179
if (registrationName == null)

0 commit comments

Comments
 (0)