diff --git a/CHANGELOG.md b/CHANGELOG.md index 203000c05a..4050126bdc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Bugfixes: - `InvalidProgramException` when proxying `MemoryStream` with .NET 7 (@stakx, #651) - `invocation.MethodInvocationTarget` throws `ArgumentNullException` for default interface method (@stakx, #684) - `DynamicProxyException` ("duplicate element") when type to proxy contains members whose names differ only in case (@stakx, #691) +- `DynamicProxyException` ("duplicate element") due to DynamicProxy not accounting for outer type names of nested types (@JelleKerkstra, #692) - `AmbiguousMatchException` when using a proxy generation hook that is implemented as a `record class` (@stakx, #720) ## 5.2.1 (2025-03-09) diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/BasicInterfaceProxyTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/BasicInterfaceProxyTestCase.cs index 66a3e67952..aff755eefa 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/BasicInterfaceProxyTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/BasicInterfaceProxyTestCase.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -231,10 +231,10 @@ public void Should_choose_noncolliding_method_names_when_implementing_same_gener Assert.AreEqual("SomeMethod", boolMethod.Name); var intMethod = type.GetInterfaceMap(intInterfaceType).TargetMethods[0]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[Int32].SomeMethod", intMethod.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[System.Int32].SomeMethod", intMethod.Name); var nestedGenericBoolMethod = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[0]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[IGenericWithNonGenericMethod`1[Boolean]].SomeMethod", nestedGenericBoolMethod.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[System.Boolean]].SomeMethod", nestedGenericBoolMethod.Name); } [Test] @@ -257,13 +257,13 @@ public void Should_choose_noncolliding_property_accessor_names_when_implementing var intGetter = type.GetInterfaceMap(intInterfaceType).TargetMethods[0]; var intSetter = type.GetInterfaceMap(intInterfaceType).TargetMethods[1]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Int32].get_SomeProperty", intGetter.Name); - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Int32].set_SomeProperty", intSetter.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Int32].get_SomeProperty", intGetter.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Int32].set_SomeProperty", intSetter.Name); var nestedGenericBoolGetter = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[0]; var nestedGenericBoolSetter = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[1]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[IGenericWithProperty`1[Boolean]].get_SomeProperty", nestedGenericBoolGetter.Name); - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[IGenericWithProperty`1[Boolean]].set_SomeProperty", nestedGenericBoolSetter.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Boolean]].get_SomeProperty", nestedGenericBoolGetter.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Boolean]].set_SomeProperty", nestedGenericBoolSetter.Name); } [Test] @@ -286,13 +286,13 @@ public void Should_choose_noncolliding_event_accessor_names_when_implementing_sa var intAdder = type.GetInterfaceMap(intInterfaceType).TargetMethods[0]; var intRemover = type.GetInterfaceMap(intInterfaceType).TargetMethods[1]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Int32].add_SomeEvent", intAdder.Name); - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Int32].remove_SomeEvent", intRemover.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Int32].add_SomeEvent", intAdder.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Int32].remove_SomeEvent", intRemover.Name); var nestedGenericBoolAdder = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[0]; var nestedGenericBoolRemover = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[1]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[IGenericWithEvent`1[Boolean]].add_SomeEvent", nestedGenericBoolAdder.Name); - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[IGenericWithEvent`1[Boolean]].remove_SomeEvent", nestedGenericBoolRemover.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Boolean]].add_SomeEvent", nestedGenericBoolAdder.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Boolean]].remove_SomeEvent", nestedGenericBoolRemover.Name); } [Test] @@ -312,10 +312,10 @@ public void Should_choose_noncolliding_member_names_when_implementing_same_gener Assert.AreEqual("SomeMethod", boolIntMethod.Name); var intBoolMethod = type.GetInterfaceMap(intBoolInterfaceType).TargetMethods[0]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[Int32,Boolean].SomeMethod", intBoolMethod.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[System.Int32,System.Boolean].SomeMethod", intBoolMethod.Name); var intNestedGenericBoolMethod = type.GetInterfaceMap(intNestedGenericBoolInterfaceType).TargetMethods[0]; - Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[Int32,IGenericWithNonGenericMethod`1[Boolean]].SomeMethod", intNestedGenericBoolMethod.Name); + Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[System.Int32,Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[System.Boolean]].SomeMethod", intNestedGenericBoolMethod.Name); } [Test] diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/ExplicitlyImplementedNestedMethodNamesTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/ExplicitlyImplementedNestedMethodNamesTestCase.cs new file mode 100644 index 0000000000..8cbfc1e4cc --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/ExplicitlyImplementedNestedMethodNamesTestCase.cs @@ -0,0 +1,71 @@ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests +{ + using System; + using System.Reflection; + + using NUnit.Framework; + + using INestedSharedNameFromA = Interfaces.OuterWrapper.InnerWrapperA.ISharedName; + using INestedSharedNameFromB = Interfaces.OuterWrapper.InnerWrapperB.ISharedName; + using INestedSharedNameFromC = Interfaces.OuterWrapper.InnerWrapperC.ISharedName; + + [TestFixture] + public class ExplicitlyImplementedNestedMethodNamesTestCase + { + [Test] + public void DynamicProxy_includes_namespace_and_declaring_type_and_type_name_in_names_of_explicitly_implemented_methods() + { + var a = typeof(INestedSharedNameFromA); + var b = typeof(INestedSharedNameFromB); + var c = typeof(INestedSharedNameFromC); + + var proxy = new ProxyGenerator().CreateInterfaceProxyWithoutTarget( + interfaceToProxy: a, + additionalInterfacesToProxy: new[] { b, c }, + interceptors: new StandardInterceptor()); + + var implementingType = proxy.GetType(); + + AssertNamingSchemeOfExplicitlyImplementedMethods(b, c, implementingType); + } + + private void AssertNamingSchemeOfExplicitlyImplementedMethods(Type b, Type c, Type implementingType) + { + const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; + + // The assertions at the end of this method only make sense if certain preconditions + // are met. We verify those using NUnit assumptions: + + // We require two interface types that have the same name and a method named `M` each: + Assume.That(b.IsInterface); + Assume.That(c.IsInterface); + Assume.That(b.Name == c.Name); + Assume.That(b.GetMethod("M") != null); + Assume.That(c.GetMethod("M") != null); + + // We also need a type that implements the above interfaces: + Assume.That(b.IsAssignableFrom(implementingType)); + Assume.That(c.IsAssignableFrom(implementingType)); + + // If all of the above conditions are met, we expect the methods from the interfaces + // to be implemented explicitly. For our purposes, this means that they follow the + // naming scheme `...M`: + Assert.NotNull(implementingType.GetMethod($"{b.Namespace}.{b.DeclaringType.DeclaringType.Name}+{b.DeclaringType.Name}+{b.Name}.M", bindingFlags)); + Assert.NotNull(implementingType.GetMethod($"{c.Namespace}.{b.DeclaringType.DeclaringType.Name}+{b.DeclaringType.Name}+{c.Name}.M", bindingFlags)); + } + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/INestedSharedName.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/INestedSharedName.cs new file mode 100644 index 0000000000..989ce1cb2c --- /dev/null +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/Interfaces/INestedSharedName.cs @@ -0,0 +1,43 @@ +// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Castle.DynamicProxy.Tests.Interfaces +{ + public static class OuterWrapper + { + public static class InnerWrapperA + { + public interface ISharedName + { + void M(); + } + } + + public static class InnerWrapperB + { + public interface ISharedName + { + void M(); + } + } + + public static class InnerWrapperC + { + public interface ISharedName + { + void M(); + } + } + } +} diff --git a/src/Castle.Core.Tests/DynamicProxy.Tests/TypeUtilTestCase.cs b/src/Castle.Core.Tests/DynamicProxy.Tests/TypeUtilTestCase.cs index 154a112cf7..1c02333c23 100644 --- a/src/Castle.Core.Tests/DynamicProxy.Tests/TypeUtilTestCase.cs +++ b/src/Castle.Core.Tests/DynamicProxy.Tests/TypeUtilTestCase.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,8 +14,11 @@ namespace Castle.DynamicProxy.Tests { + using System; + using System.Collections.Generic; using System.Linq; using System.Reflection; + using System.Text; using Castle.DynamicProxy.Internal; @@ -24,6 +27,22 @@ namespace Castle.DynamicProxy.Tests [TestFixture] public class TypeUtilTestCase { + [TestCase(typeof(object), "System.Object")] + [TestCase(typeof(List<>), "System.Collections.Generic.List`1")] + [TestCase(typeof(List), "System.Collections.Generic.List`1[System.Object]")] + [TestCase(typeof(List.Enumerator), "System.Collections.Generic.List`1+Enumerator[System.Object]")] + [TestCase(typeof(Dictionary<,>), "System.Collections.Generic.Dictionary`2")] + [TestCase(typeof(Dictionary), "System.Collections.Generic.Dictionary`2[System.Object,System.Boolean]")] + public void AppendNamespaceQualifiedNameOf(Type type, string expected) + { + var builder = new StringBuilder(); + + builder.AppendNamespaceQualifiedNameOf(type); + + var actual = builder.ToString(); + Assert.AreEqual(expected, actual); + } + [Test] public void GetAllInstanceMethods_GetsPublicAndNonPublicMethods() { diff --git a/src/Castle.Core/DynamicProxy/Generators/MetaTypeElement.cs b/src/Castle.Core/DynamicProxy/Generators/MetaTypeElement.cs index 12088d8ca7..e08a41becd 100644 --- a/src/Castle.Core/DynamicProxy/Generators/MetaTypeElement.cs +++ b/src/Castle.Core/DynamicProxy/Generators/MetaTypeElement.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2021 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ namespace Castle.DynamicProxy.Generators using System.Reflection; using System.Text; + using Castle.DynamicProxy.Internal; + internal abstract class MetaTypeElement { private readonly MemberInfo member; @@ -50,48 +52,24 @@ protected MemberInfo Member protected void SwitchToExplicitImplementationName() { var name = member.Name; - var sourceType = member.DeclaringType; - var ns = sourceType.Namespace; - Debug.Assert(ns == null || ns != ""); + var declaringType = member.DeclaringType; - if (sourceType.IsGenericType) - { - var nameBuilder = new StringBuilder(); - if (ns != null) - { - nameBuilder.Append(ns); - nameBuilder.Append('.'); - } - AppendTypeName(nameBuilder, sourceType); - nameBuilder.Append('.'); - nameBuilder.Append(name); - this.name = nameBuilder.ToString(); - } - else if (ns != null) + if (declaringType.IsGenericType || declaringType.IsNested) { - this.name = string.Concat(ns, ".", sourceType.Name, ".", name); + var builder = new StringBuilder(); + builder.AppendNamespaceQualifiedNameOf(declaringType).Append('.').Append(name); + this.name = builder.ToString(); } else { - this.name = string.Concat(sourceType.Name, ".", name); - } - - static void AppendTypeName(StringBuilder nameBuilder, Type type) - { - nameBuilder.Append(type.Name); - if (type.IsGenericType) + var ns = declaringType.Namespace; + if (string.IsNullOrEmpty(ns)) + { + this.name = string.Concat(declaringType.Name, ".", name); + } + else { - nameBuilder.Append('['); - var genericTypeArguments = type.GetGenericArguments(); - for (int i = 0, n = genericTypeArguments.Length; i < n; ++i) - { - if (i > 0) - { - nameBuilder.Append(','); - } - AppendTypeName(nameBuilder, genericTypeArguments[i]); - } - nameBuilder.Append(']'); + this.name = string.Concat(ns, ".", declaringType.Name, ".", name); } } } diff --git a/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs b/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs index 6e11e36fa8..230aeae51e 100644 --- a/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs +++ b/src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs @@ -1,4 +1,4 @@ -// Copyright 2004-2025 Castle Project - http://www.castleproject.org/ +// Copyright 2004-2026 Castle Project - http://www.castleproject.org/ // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ namespace Castle.DynamicProxy.Internal using System.Linq; using System.Reflection; using System.Reflection.Emit; + using System.Text; using Castle.DynamicProxy.Generators; using Castle.DynamicProxy.Generators.Emitters; @@ -31,6 +32,47 @@ public static class TypeUtil private static readonly Dictionary instanceMethodsCache = new Dictionary(); private static readonly Dictionary hasAnyOverridableDefaultImplementationsCache = new Dictionary(); + internal static StringBuilder AppendNamespaceQualifiedNameOf(this StringBuilder builder, Type type) + { + if (type.IsGenericParameter == false) + { + if (type.IsNested) + { + builder.AppendNamespaceQualifiedNameOf(type.DeclaringType!).Append('+'); + } + else + { + var ns = type.Namespace; + if (string.IsNullOrEmpty(ns) == false) + { + builder.Append(ns).Append('.'); + } + } + } + + builder.Append(type.Name); + + if (type.IsConstructedGenericType) + { + builder.Append('['); + + var typeArgs = type.GetGenericArguments(); + for (int i = 0, n = typeArgs.Length; i < n; ++i) + { + if (i > 0) + { + builder.Append(','); + } + + builder.AppendNamespaceQualifiedNameOf(typeArgs[i]); + } + + builder.Append(']'); + } + + return builder; + } + internal static bool IsNullableType(this Type type) { return type.IsGenericType &&