Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
Expand Down Expand Up @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand All @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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 `<namespace>.<parent types>.<type>.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));
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
}
21 changes: 20 additions & 1 deletion src/Castle.Core.Tests/DynamicProxy.Tests/TypeUtilTestCase.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;

Expand All @@ -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<object>), "System.Collections.Generic.List`1[System.Object]")]
[TestCase(typeof(List<object>.Enumerator), "System.Collections.Generic.List`1+Enumerator[System.Object]")]
[TestCase(typeof(Dictionary<,>), "System.Collections.Generic.Dictionary`2")]
[TestCase(typeof(Dictionary<object, bool>), "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()
{
Expand Down
52 changes: 15 additions & 37 deletions src/Castle.Core/DynamicProxy/Generators/MetaTypeElement.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
}
}
Expand Down
44 changes: 43 additions & 1 deletion src/Castle.Core/DynamicProxy/Internal/TypeUtil.cs
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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;
Expand All @@ -31,6 +32,47 @@ public static class TypeUtil
private static readonly Dictionary<Type, MethodInfo[]> instanceMethodsCache = new Dictionary<Type, MethodInfo[]>();
private static readonly Dictionary<Type, bool> hasAnyOverridableDefaultImplementationsCache = new Dictionary<Type, bool>();

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 &&
Expand Down