Skip to content

Commit bf0297a

Browse files
authored
Merge pull request #721 from stakx/bugfix/proxy-generation-hook-record-classes
Fix `Equals` and `GetHashCode` override check for proxy generation hooks having multiple `Equals` methods
2 parents d592cc4 + 5f32f34 commit bf0297a

3 files changed

Lines changed: 78 additions & 2 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ Bugfixes:
1414
- `InvalidProgramException` when proxying `MemoryStream` with .NET 7 (@stakx, #651)
1515
- `invocation.MethodInvocationTarget` throws `ArgumentNullException` for default interface method (@stakx, #684)
1616
- `DynamicProxyException` ("duplicate element") when type to proxy contains members whose names differ only in case (@stakx, #691)
17+
- `AmbiguousMatchException` when using a proxy generation hook that is implemented as a `record class` (@stakx, #720)
1718

1819
## 5.2.1 (2025-03-09)
1920

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

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
namespace Castle.DynamicProxy.Tests
1616
{
1717
using System;
18+
using System.Reflection;
1819

1920
using Castle.DynamicProxy.Tests.Interceptors;
2021
using Castle.DynamicProxy.Tests.Interfaces;
@@ -29,6 +30,47 @@ public class ProxyTypeCachingWithDifferentHooksTestCase : BasePEVerifyTestCase
2930
#endif
3031
public class CustomHook : AllMethodsHook { }
3132

33+
#if FEATURE_SERIALIZATION
34+
[Serializable]
35+
#endif
36+
public class EquatableHook : IProxyGenerationHook, IEquatable<EquatableHook>
37+
{
38+
public override bool Equals(object obj) => Equals(obj as EquatableHook);
39+
public override int GetHashCode() => GetType().GetHashCode();
40+
41+
public bool Equals(EquatableHook other) => other is EquatableHook;
42+
43+
public void MethodsInspected() { }
44+
public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo) { }
45+
public bool ShouldInterceptMethod(Type type, MethodInfo methodInfo) => false;
46+
}
47+
48+
#if FEATURE_SERIALIZATION
49+
[Serializable]
50+
#endif
51+
public record class RecordClassHook : IProxyGenerationHook
52+
{
53+
public RecordClassHook(string id)
54+
{
55+
Id = id;
56+
}
57+
58+
public string Id { get; }
59+
60+
public void MethodsInspected() { }
61+
public void NonProxyableMemberNotification(Type type, MemberInfo memberInfo) { }
62+
public bool ShouldInterceptMethod(Type type, MethodInfo methodInfo) => false;
63+
}
64+
65+
// `IEquatable<>` can lead to there being more than one `Equals` method.
66+
// This is relevant for hook classes because DynamicProxy checks whether they
67+
// override the inherited `Equals` method for proper equality semantics.
68+
[Test]
69+
public void Can_use_hook_that_implements_IEquatable()
70+
{
71+
_ = CreateProxyWithHook<EquatableHook>();
72+
}
73+
3274
[Test]
3375
public void Proxies_with_same_hook_will_use_cached_proxy_type()
3476
{
@@ -45,9 +87,42 @@ public void Proxies_with_different_hooks_will_use_different_proxy_types()
4587
Assert.AreNotEqual(first.GetType(), second.GetType());
4688
}
4789

90+
[Test]
91+
public void Proxies_with_different_but_equal_record_class_hooks_will_use_cached_proxy_type()
92+
{
93+
var firstHook = new RecordClassHook("1");
94+
var secondHook = new RecordClassHook("1");
95+
Assume.That(firstHook, Is.Not.SameAs(secondHook));
96+
Assume.That(firstHook, Is.EqualTo(secondHook));
97+
98+
var first = CreateProxyWithHook(firstHook);
99+
var second = CreateProxyWithHook(secondHook);
100+
101+
Assert.AreEqual(first.GetType(), second.GetType());
102+
}
103+
104+
[Test]
105+
public void Proxies_with_different_and_unequal_record_class_hooks_will_use_different_proxy_types()
106+
{
107+
var firstHook = new RecordClassHook("1");
108+
var secondHook = new RecordClassHook("2");
109+
Assume.That(firstHook, Is.Not.SameAs(secondHook));
110+
Assume.That(firstHook, Is.Not.EqualTo(secondHook));
111+
112+
var first = CreateProxyWithHook(firstHook);
113+
var second = CreateProxyWithHook(secondHook);
114+
115+
Assert.AreNotEqual(first.GetType(), second.GetType());
116+
}
117+
48118
private object CreateProxyWithHook<THook>() where THook : IProxyGenerationHook, new()
49119
{
50-
return generator.CreateInterfaceProxyWithoutTarget(typeof(IEmpty), new ProxyGenerationOptions(new THook()), new DoNothingInterceptor());
120+
return CreateProxyWithHook(new THook());
121+
}
122+
123+
private object CreateProxyWithHook(IProxyGenerationHook hook)
124+
{
125+
return generator.CreateInterfaceProxyWithoutTarget(typeof(IEmpty), new ProxyGenerationOptions(hook), new DoNothingInterceptor());
51126
}
52127
}
53128
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -393,7 +393,7 @@ protected void InitializeStaticFields(Type builtType)
393393

394394
private bool OverridesEqualsAndGetHashCode(Type type)
395395
{
396-
var equalsMethod = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance);
396+
var equalsMethod = type.GetMethod("Equals", BindingFlags.Public | BindingFlags.Instance, null, [ typeof(object) ], null);
397397
if (equalsMethod == null || equalsMethod.DeclaringType == typeof(object) || equalsMethod.IsAbstract)
398398
{
399399
return false;

0 commit comments

Comments
 (0)