Skip to content

Commit 1252d3d

Browse files
authored
Merge pull request castleproject#692 from JelleKerkstra/master
Fix `DynamicProxyException` ("duplicate element") due to DynamicProxy not accounting for outer type names of nested types
2 parents cc3d3a8 + d7e3f80 commit 1252d3d

7 files changed

Lines changed: 206 additions & 52 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Bugfixes:
1616
- `InvalidProgramException` when proxying `MemoryStream` with .NET 7 (@stakx, #651)
1717
- `invocation.MethodInvocationTarget` throws `ArgumentNullException` for default interface method (@stakx, #684)
1818
- `DynamicProxyException` ("duplicate element") when type to proxy contains members whose names differ only in case (@stakx, #691)
19+
- `DynamicProxyException` ("duplicate element") due to DynamicProxy not accounting for outer type names of nested types (@JelleKerkstra, #692)
1920
- `AmbiguousMatchException` when using a proxy generation hook that is implemented as a `record class` (@stakx, #720)
2021

2122
## 5.2.1 (2025-03-09)

src/Castle.Core.Tests/DynamicProxy.Tests/BasicInterfaceProxyTestCase.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// 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
231231
Assert.AreEqual("SomeMethod", boolMethod.Name);
232232

233233
var intMethod = type.GetInterfaceMap(intInterfaceType).TargetMethods[0];
234-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[Int32].SomeMethod", intMethod.Name);
234+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[System.Int32].SomeMethod", intMethod.Name);
235235

236236
var nestedGenericBoolMethod = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[0];
237-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[IGenericWithNonGenericMethod`1[Boolean]].SomeMethod", nestedGenericBoolMethod.Name);
237+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[System.Boolean]].SomeMethod", nestedGenericBoolMethod.Name);
238238
}
239239

240240
[Test]
@@ -257,13 +257,13 @@ public void Should_choose_noncolliding_property_accessor_names_when_implementing
257257

258258
var intGetter = type.GetInterfaceMap(intInterfaceType).TargetMethods[0];
259259
var intSetter = type.GetInterfaceMap(intInterfaceType).TargetMethods[1];
260-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Int32].get_SomeProperty", intGetter.Name);
261-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Int32].set_SomeProperty", intSetter.Name);
260+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Int32].get_SomeProperty", intGetter.Name);
261+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Int32].set_SomeProperty", intSetter.Name);
262262

263263
var nestedGenericBoolGetter = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[0];
264264
var nestedGenericBoolSetter = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[1];
265-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[IGenericWithProperty`1[Boolean]].get_SomeProperty", nestedGenericBoolGetter.Name);
266-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[IGenericWithProperty`1[Boolean]].set_SomeProperty", nestedGenericBoolSetter.Name);
265+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Boolean]].get_SomeProperty", nestedGenericBoolGetter.Name);
266+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithProperty`1[System.Boolean]].set_SomeProperty", nestedGenericBoolSetter.Name);
267267
}
268268

269269
[Test]
@@ -286,13 +286,13 @@ public void Should_choose_noncolliding_event_accessor_names_when_implementing_sa
286286

287287
var intAdder = type.GetInterfaceMap(intInterfaceType).TargetMethods[0];
288288
var intRemover = type.GetInterfaceMap(intInterfaceType).TargetMethods[1];
289-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Int32].add_SomeEvent", intAdder.Name);
290-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Int32].remove_SomeEvent", intRemover.Name);
289+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Int32].add_SomeEvent", intAdder.Name);
290+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Int32].remove_SomeEvent", intRemover.Name);
291291

292292
var nestedGenericBoolAdder = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[0];
293293
var nestedGenericBoolRemover = type.GetInterfaceMap(nestedGenericBoolInterfaceType).TargetMethods[1];
294-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[IGenericWithEvent`1[Boolean]].add_SomeEvent", nestedGenericBoolAdder.Name);
295-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[IGenericWithEvent`1[Boolean]].remove_SomeEvent", nestedGenericBoolRemover.Name);
294+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Boolean]].add_SomeEvent", nestedGenericBoolAdder.Name);
295+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[Castle.DynamicProxy.Tests.Interfaces.IGenericWithEvent`1[System.Boolean]].remove_SomeEvent", nestedGenericBoolRemover.Name);
296296
}
297297

298298
[Test]
@@ -312,10 +312,10 @@ public void Should_choose_noncolliding_member_names_when_implementing_same_gener
312312
Assert.AreEqual("SomeMethod", boolIntMethod.Name);
313313

314314
var intBoolMethod = type.GetInterfaceMap(intBoolInterfaceType).TargetMethods[0];
315-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[Int32,Boolean].SomeMethod", intBoolMethod.Name);
315+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[System.Int32,System.Boolean].SomeMethod", intBoolMethod.Name);
316316

317317
var intNestedGenericBoolMethod = type.GetInterfaceMap(intNestedGenericBoolInterfaceType).TargetMethods[0];
318-
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[Int32,IGenericWithNonGenericMethod`1[Boolean]].SomeMethod", intNestedGenericBoolMethod.Name);
318+
Assert.AreEqual("Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`2[System.Int32,Castle.DynamicProxy.Tests.Interfaces.IGenericWithNonGenericMethod`1[System.Boolean]].SomeMethod", intNestedGenericBoolMethod.Name);
319319
}
320320

321321
[Test]
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Castle.DynamicProxy.Tests
16+
{
17+
using System;
18+
using System.Reflection;
19+
20+
using NUnit.Framework;
21+
22+
using INestedSharedNameFromA = Interfaces.OuterWrapper.InnerWrapperA.ISharedName;
23+
using INestedSharedNameFromB = Interfaces.OuterWrapper.InnerWrapperB.ISharedName;
24+
using INestedSharedNameFromC = Interfaces.OuterWrapper.InnerWrapperC.ISharedName;
25+
26+
[TestFixture]
27+
public class ExplicitlyImplementedNestedMethodNamesTestCase
28+
{
29+
[Test]
30+
public void DynamicProxy_includes_namespace_and_declaring_type_and_type_name_in_names_of_explicitly_implemented_methods()
31+
{
32+
var a = typeof(INestedSharedNameFromA);
33+
var b = typeof(INestedSharedNameFromB);
34+
var c = typeof(INestedSharedNameFromC);
35+
36+
var proxy = new ProxyGenerator().CreateInterfaceProxyWithoutTarget(
37+
interfaceToProxy: a,
38+
additionalInterfacesToProxy: new[] { b, c },
39+
interceptors: new StandardInterceptor());
40+
41+
var implementingType = proxy.GetType();
42+
43+
AssertNamingSchemeOfExplicitlyImplementedMethods(b, c, implementingType);
44+
}
45+
46+
private void AssertNamingSchemeOfExplicitlyImplementedMethods(Type b, Type c, Type implementingType)
47+
{
48+
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
49+
50+
// The assertions at the end of this method only make sense if certain preconditions
51+
// are met. We verify those using NUnit assumptions:
52+
53+
// We require two interface types that have the same name and a method named `M` each:
54+
Assume.That(b.IsInterface);
55+
Assume.That(c.IsInterface);
56+
Assume.That(b.Name == c.Name);
57+
Assume.That(b.GetMethod("M") != null);
58+
Assume.That(c.GetMethod("M") != null);
59+
60+
// We also need a type that implements the above interfaces:
61+
Assume.That(b.IsAssignableFrom(implementingType));
62+
Assume.That(c.IsAssignableFrom(implementingType));
63+
64+
// If all of the above conditions are met, we expect the methods from the interfaces
65+
// to be implemented explicitly. For our purposes, this means that they follow the
66+
// naming scheme `<namespace>.<parent types>.<type>.M`:
67+
Assert.NotNull(implementingType.GetMethod($"{b.Namespace}.{b.DeclaringType.DeclaringType.Name}+{b.DeclaringType.Name}+{b.Name}.M", bindingFlags));
68+
Assert.NotNull(implementingType.GetMethod($"{c.Namespace}.{b.DeclaringType.DeclaringType.Name}+{b.DeclaringType.Name}+{c.Name}.M", bindingFlags));
69+
}
70+
}
71+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
namespace Castle.DynamicProxy.Tests.Interfaces
16+
{
17+
public static class OuterWrapper
18+
{
19+
public static class InnerWrapperA
20+
{
21+
public interface ISharedName
22+
{
23+
void M();
24+
}
25+
}
26+
27+
public static class InnerWrapperB
28+
{
29+
public interface ISharedName
30+
{
31+
void M();
32+
}
33+
}
34+
35+
public static class InnerWrapperC
36+
{
37+
public interface ISharedName
38+
{
39+
void M();
40+
}
41+
}
42+
}
43+
}

src/Castle.Core.Tests/DynamicProxy.Tests/TypeUtilTestCase.cs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -14,8 +14,11 @@
1414

1515
namespace Castle.DynamicProxy.Tests
1616
{
17+
using System;
18+
using System.Collections.Generic;
1719
using System.Linq;
1820
using System.Reflection;
21+
using System.Text;
1922

2023
using Castle.DynamicProxy.Internal;
2124

@@ -24,6 +27,22 @@ namespace Castle.DynamicProxy.Tests
2427
[TestFixture]
2528
public class TypeUtilTestCase
2629
{
30+
[TestCase(typeof(object), "System.Object")]
31+
[TestCase(typeof(List<>), "System.Collections.Generic.List`1")]
32+
[TestCase(typeof(List<object>), "System.Collections.Generic.List`1[System.Object]")]
33+
[TestCase(typeof(List<object>.Enumerator), "System.Collections.Generic.List`1+Enumerator[System.Object]")]
34+
[TestCase(typeof(Dictionary<,>), "System.Collections.Generic.Dictionary`2")]
35+
[TestCase(typeof(Dictionary<object, bool>), "System.Collections.Generic.Dictionary`2[System.Object,System.Boolean]")]
36+
public void AppendNamespaceQualifiedNameOf(Type type, string expected)
37+
{
38+
var builder = new StringBuilder();
39+
40+
builder.AppendNamespaceQualifiedNameOf(type);
41+
42+
var actual = builder.ToString();
43+
Assert.AreEqual(expected, actual);
44+
}
45+
2746
[Test]
2847
public void GetAllInstanceMethods_GetsPublicAndNonPublicMethods()
2948
{

src/Castle.Core/DynamicProxy/Generators/MetaTypeElement.cs

Lines changed: 15 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2021 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -19,6 +19,8 @@ namespace Castle.DynamicProxy.Generators
1919
using System.Reflection;
2020
using System.Text;
2121

22+
using Castle.DynamicProxy.Internal;
23+
2224
internal abstract class MetaTypeElement
2325
{
2426
private readonly MemberInfo member;
@@ -50,48 +52,24 @@ protected MemberInfo Member
5052
protected void SwitchToExplicitImplementationName()
5153
{
5254
var name = member.Name;
53-
var sourceType = member.DeclaringType;
54-
var ns = sourceType.Namespace;
55-
Debug.Assert(ns == null || ns != "");
55+
var declaringType = member.DeclaringType;
5656

57-
if (sourceType.IsGenericType)
58-
{
59-
var nameBuilder = new StringBuilder();
60-
if (ns != null)
61-
{
62-
nameBuilder.Append(ns);
63-
nameBuilder.Append('.');
64-
}
65-
AppendTypeName(nameBuilder, sourceType);
66-
nameBuilder.Append('.');
67-
nameBuilder.Append(name);
68-
this.name = nameBuilder.ToString();
69-
}
70-
else if (ns != null)
57+
if (declaringType.IsGenericType || declaringType.IsNested)
7158
{
72-
this.name = string.Concat(ns, ".", sourceType.Name, ".", name);
59+
var builder = new StringBuilder();
60+
builder.AppendNamespaceQualifiedNameOf(declaringType).Append('.').Append(name);
61+
this.name = builder.ToString();
7362
}
7463
else
7564
{
76-
this.name = string.Concat(sourceType.Name, ".", name);
77-
}
78-
79-
static void AppendTypeName(StringBuilder nameBuilder, Type type)
80-
{
81-
nameBuilder.Append(type.Name);
82-
if (type.IsGenericType)
65+
var ns = declaringType.Namespace;
66+
if (string.IsNullOrEmpty(ns))
67+
{
68+
this.name = string.Concat(declaringType.Name, ".", name);
69+
}
70+
else
8371
{
84-
nameBuilder.Append('[');
85-
var genericTypeArguments = type.GetGenericArguments();
86-
for (int i = 0, n = genericTypeArguments.Length; i < n; ++i)
87-
{
88-
if (i > 0)
89-
{
90-
nameBuilder.Append(',');
91-
}
92-
AppendTypeName(nameBuilder, genericTypeArguments[i]);
93-
}
94-
nameBuilder.Append(']');
72+
this.name = string.Concat(ns, ".", declaringType.Name, ".", name);
9573
}
9674
}
9775
}

src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright 2004-2025 Castle Project - http://www.castleproject.org/
1+
// Copyright 2004-2026 Castle Project - http://www.castleproject.org/
22
//
33
// Licensed under the Apache License, Version 2.0 (the "License");
44
// you may not use this file except in compliance with the License.
@@ -22,6 +22,7 @@ namespace Castle.DynamicProxy.Internal
2222
using System.Linq;
2323
using System.Reflection;
2424
using System.Reflection.Emit;
25+
using System.Text;
2526

2627
using Castle.DynamicProxy.Generators;
2728
using Castle.DynamicProxy.Generators.Emitters;
@@ -31,6 +32,47 @@ public static class TypeUtil
3132
private static readonly Dictionary<Type, MethodInfo[]> instanceMethodsCache = new Dictionary<Type, MethodInfo[]>();
3233
private static readonly Dictionary<Type, bool> hasAnyOverridableDefaultImplementationsCache = new Dictionary<Type, bool>();
3334

35+
internal static StringBuilder AppendNamespaceQualifiedNameOf(this StringBuilder builder, Type type)
36+
{
37+
if (type.IsGenericParameter == false)
38+
{
39+
if (type.IsNested)
40+
{
41+
builder.AppendNamespaceQualifiedNameOf(type.DeclaringType!).Append('+');
42+
}
43+
else
44+
{
45+
var ns = type.Namespace;
46+
if (string.IsNullOrEmpty(ns) == false)
47+
{
48+
builder.Append(ns).Append('.');
49+
}
50+
}
51+
}
52+
53+
builder.Append(type.Name);
54+
55+
if (type.IsConstructedGenericType)
56+
{
57+
builder.Append('[');
58+
59+
var typeArgs = type.GetGenericArguments();
60+
for (int i = 0, n = typeArgs.Length; i < n; ++i)
61+
{
62+
if (i > 0)
63+
{
64+
builder.Append(',');
65+
}
66+
67+
builder.AppendNamespaceQualifiedNameOf(typeArgs[i]);
68+
}
69+
70+
builder.Append(']');
71+
}
72+
73+
return builder;
74+
}
75+
3476
internal static bool IsNullableType(this Type type)
3577
{
3678
return type.IsGenericType &&

0 commit comments

Comments
 (0)