Skip to content

Commit

Permalink
Fixed IsDefault() usage in DependencyInjectionAdapter
Browse files Browse the repository at this point in the history
If multiple concrete classes are registered in castle, when
you resolve castle resolves the first one. With Microsoft DI
is the oposite, you want the last registered. The resolution
is now fixed to honor IsDefault() because previous code registered
every component with IsDefault() if it is registered from
the adapter.
  • Loading branch information
alkampfergit committed Jun 24, 2024
1 parent 17d1010 commit 9499d3a
Show file tree
Hide file tree
Showing 6 changed files with 211 additions and 30 deletions.
15 changes: 10 additions & 5 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
@@ -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
}
}
]
}
18 changes: 18 additions & 0 deletions build_without_wcf_tests.cmd
Original file line number Diff line number Diff line change
@@ -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 %*

83 changes: 83 additions & 0 deletions buildscripts/build_without_wcf_tests.cmd
Original file line number Diff line number Diff line change
@@ -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"
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#if NET8_0_OR_GREATER
using Castle.MicroKernel;
using Castle.MicroKernel.Registration;
using Microsoft.Extensions.DependencyInjection;
using System;
using System.Linq;
Expand Down Expand Up @@ -366,6 +367,52 @@ public void TryToResolveScopedInOtherThread()
Assert.True(task.Result);
}

[Fact]
public void Resolve_order_in_castle()
{
var serviceCollection = GetServiceCollection();
_factory = new WindsorServiceProviderFactory();
_container = _factory.CreateBuilder(serviceCollection);

_container.Register(
Component.For<ITestService>().ImplementedBy<TestService>()
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());

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<TestService>(resolvedWithCastle);
Assert.IsType<AnotherTestService>(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<ITestService>().ImplementedBy<TestService>().IsDefault()
, Component.For<ITestService>().ImplementedBy<AnotherTestService>());

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

protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,7 @@ public static IRegistration FromOpenGenericServiceDescriptor(
throw new System.ArgumentException("Unsupported ServiceDescriptor");
}
#endif
return ResolveLifestyle(registration, service)
.IsDefault();
return ResolveLifestyle(registration, service);
}

public static IRegistration FromServiceDescriptor(
Expand Down Expand Up @@ -127,8 +126,7 @@ public static IRegistration FromServiceDescriptor(
registration = UsingImplementation(registration, service);
}
#endif
return ResolveLifestyle(registration, service)
.IsDefault();
return ResolveLifestyle(registration, service);
}

public static string OriginalComponentName(string uniqueComponentName)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

namespace Castle.Windsor.Extensions.DependencyInjection
{
using Castle.MicroKernel;
using Castle.MicroKernel.Handlers;
using Castle.Windsor;
using Castle.Windsor.Extensions.DependencyInjection.Scope;
Expand Down Expand Up @@ -96,7 +95,6 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
{
if (container.Kernel.HasComponent(serviceType))
{
#if NET8_0_OR_GREATER
//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.
Expand All @@ -105,7 +103,7 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
//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)
if (realRegistrations.Count == 1)
{
registrationName = realRegistrations[0].ComponentModel.Name;
}
Expand All @@ -116,25 +114,39 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
}
else if (realRegistrations.Count > 1)
{
//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.
//1. Last component win.
//2. closed service are preferred over open generic.
//Need to honor IsDefault for castle registrations.
var isDefaultRegistration = realRegistrations
.FirstOrDefault(dh => dh.ComponentModel.ExtendedProperties.Any(ComponentIsDefault));

//take first non generic
for (int i = realRegistrations.Count - 1; i >= 0; i--)
//Remember that castle has a specific order of resolution, if someone registered something in castle with
//IsDefault() it Must be honored.
if (isDefaultRegistration != null)
{
registrationName = isDefaultRegistration.ComponentModel.Name;
}
else
{
if (!realRegistrations[i].ComponentModel.Implementation.IsGenericTypeDefinition)
//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--)
{
registrationName = realRegistrations[i].ComponentModel.Name;
break;
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 we did not find any non generic, take the last one.
if (registrationName == null)
{
registrationName = realRegistrations[realRegistrations.Count - 1].ComponentModel.Name;
}
}
}

Expand All @@ -143,10 +155,6 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
return null;
}
return container.Resolve(registrationName, serviceType);
#else
//no keyed component in previous framework, just resolve.
return container.Resolve(serviceType);
#endif
}

if (serviceType.GetTypeInfo().IsGenericType && serviceType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
Expand Down Expand Up @@ -197,6 +205,28 @@ private object ResolveInstanceOrNull(Type serviceType, bool isOptional)
return container.Resolve(serviceType);
}

private static bool ComponentIsDefault(KeyValuePair<object, object> 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<Type> 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)
Expand Down

0 comments on commit 9499d3a

Please sign in to comment.