Skip to content

Commit 3cd489d

Browse files
authored
Merge pull request #714 from stakx/bugfix/method-invocation-target
Fix `ArgumentNullException` thrown by `invocation.MethodInvocationTarget` for default interface methods
2 parents 7a06b94 + ae8f1c2 commit 3cd489d

File tree

7 files changed

+110
-7
lines changed

7 files changed

+110
-7
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Enhancements:
1010

1111
Bugfixes:
1212
- `InvalidProgramException` when proxying `MemoryStream` with .NET 7 (@stakx, #651)
13+
- `invocation.MethodInvocationTarget` throws `ArgumentNullException` for default interface method (@stakx, #684)
1314

1415
Deprecations:
1516
- .NET Core 2.1, .NET Core 3.1, .NET 6, and mono tests

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

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,28 @@ public void Can_proceed_to_method_default_implementation_in_proxied_interface()
100100
Assert.AreEqual(expected, actual);
101101
}
102102

103+
[Test]
104+
public void Invocation_InvocationTarget_is_proxy_for_intercepted_method_with_default_implementation()
105+
{
106+
var interceptor = new KeepDataInterceptor();
107+
var proxy = generator.CreateInterfaceProxyWithoutTarget<IHaveMethodWithDefaultImplementation>(interceptor);
108+
proxy.MethodWithDefaultImplementation();
109+
Assert.AreSame(
110+
expected: proxy,
111+
actual: interceptor.Invocation.InvocationTarget);
112+
}
113+
114+
[Test]
115+
public void Invocation_MethodInvocationTarget_is_intercepted_method_with_default_implementation()
116+
{
117+
var interceptor = new KeepDataInterceptor();
118+
var proxy = generator.CreateInterfaceProxyWithoutTarget<IHaveMethodWithDefaultImplementation>(interceptor);
119+
proxy.MethodWithDefaultImplementation();
120+
Assert.AreSame(
121+
expected: typeof(IHaveMethodWithDefaultImplementation).GetMethod(nameof(IHaveMethodWithDefaultImplementation.MethodWithDefaultImplementation)),
122+
actual: interceptor.Invocation.MethodInvocationTarget);
123+
}
124+
103125
#endregion
104126

105127
#region Generic methods with default implementation
@@ -296,6 +318,28 @@ public void Can_proceed_to_method_default_implementation_from_additional_interfa
296318
Assert.AreEqual(expected, actual);
297319
}
298320

321+
[Test]
322+
public void Invocation_InvocationTarget_is_proxy_for_intercepted_method_with_default_implementation_from_additional_interface()
323+
{
324+
var interceptor = new KeepDataInterceptor();
325+
var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IEmpty), new[] { typeof(IHaveMethodWithDefaultImplementation) }, interceptor);
326+
((IHaveMethodWithDefaultImplementation)proxy).MethodWithDefaultImplementation();
327+
Assert.AreSame(
328+
expected: proxy,
329+
actual: interceptor.Invocation.InvocationTarget);
330+
}
331+
332+
[Test]
333+
public void Invocation_MethodInvocationTarget_is_intercepted_method_with_default_implementation_from_additional_interface()
334+
{
335+
var interceptor = new KeepDataInterceptor();
336+
var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IEmpty), new[] { typeof(IHaveMethodWithDefaultImplementation) }, interceptor);
337+
((IHaveMethodWithDefaultImplementation)proxy).MethodWithDefaultImplementation();
338+
Assert.AreSame(
339+
expected: typeof(IHaveMethodWithDefaultImplementation).GetMethod(nameof(IHaveMethodWithDefaultImplementation.MethodWithDefaultImplementation)),
340+
actual: interceptor.Invocation.MethodInvocationTarget);
341+
}
342+
299343
#endregion
300344

301345
#region Non-public methods with default implementation
@@ -457,6 +501,34 @@ public void Can_proceed_to_method_default_implementation_from_mixin_in_proxied_i
457501
var actual = ((IHaveMethodWithDefaultImplementation)proxy).MethodWithDefaultImplementation();
458502
Assert.AreEqual(expected, actual);
459503
}
504+
505+
[Test]
506+
public void Invocation_InvocationTarget_is_mixin_for_intercepted_method_with_default_implementation_from_mixin()
507+
{
508+
var options = new ProxyGenerationOptions();
509+
var mixin = new InheritsMethodWithDefaultImplementation();
510+
options.AddMixinInstance(mixin);
511+
var interceptor = new KeepDataInterceptor();
512+
var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IEmpty), options, interceptor);
513+
((IHaveMethodWithDefaultImplementation)proxy).MethodWithDefaultImplementation();
514+
Assert.AreSame(
515+
expected: mixin,
516+
actual: interceptor.Invocation.InvocationTarget);
517+
}
518+
519+
[Test]
520+
public void Invocation_MethodInvocationTarget_is_intercepted_method_with_default_implementation_from_mixin()
521+
{
522+
var options = new ProxyGenerationOptions();
523+
options.AddMixinInstance(new InheritsMethodWithDefaultImplementation());
524+
var interceptor = new KeepDataInterceptor();
525+
var proxy = generator.CreateInterfaceProxyWithoutTarget(typeof(IEmpty), options, interceptor);
526+
((IHaveMethodWithDefaultImplementation)proxy).MethodWithDefaultImplementation();
527+
Assert.AreSame(
528+
expected: typeof(IHaveMethodWithDefaultImplementation).GetMethod(nameof(IHaveMethodWithDefaultImplementation.MethodWithDefaultImplementation)),
529+
actual: interceptor.Invocation.MethodInvocationTarget);
530+
}
531+
460532
#endregion
461533

462534
public interface IHaveGenericMethodWithDefaultImplementation

src/Castle.Core/DynamicProxy/Contributors/ClassProxyTargetContributor.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,16 @@ protected override MethodGenerator GetMethodGenerator(MetaMethod method, ClassEm
7676
return ExplicitlyImplementedInterfaceMethodGenerator(method, @class, overrideMethod);
7777
}
7878

79+
// since this contributor is used for class proxies without target,
80+
// the invocation type will be derived from `InheritanceInvocation`:
7981
var invocation = GetInvocationType(method, @class);
8082

8183
GetTargetExpressionDelegate getTargetTypeExpression = (c, m) => new TypeTokenExpression(targetType);
8284

85+
// `MethodWithInvocationGenerator` uses its `getTargetExpression` argument
86+
// to determine the first argument to be passed to the received invocation type;
87+
// and since `InheritanceInvocation`s' first ctor param is `targetType`,
88+
// we pass `getTargetTypeExpression` here:
8389
return new MethodWithInvocationGenerator(method,
8490
@class.GetField("__interceptors"),
8591
invocation,

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,11 @@ private IEnumerable<Type> GetTypeImplementerMapping(out IEnumerable<ITypeContrib
153153
// 3. then additional interfaces
154154
if (interfaces.Length > 0)
155155
{
156-
var additionalInterfacesContributor = new InterfaceProxyWithoutTargetContributor(namingScope, (c, m) => NullExpression.Instance) { Logger = Logger };
156+
// this is explained in `InterfaceProxyWithoutTargetGenerator.GetProxyTargetContributor`:
157+
GetTargetExpressionDelegate getTargetType =
158+
(c, m) => m.IsAbstract ? NullExpression.Instance
159+
: new TypeTokenExpression(m.DeclaringType);
160+
var additionalInterfacesContributor = new InterfaceProxyWithoutTargetContributor(namingScope, getTargetType) { Logger = Logger };
157161
contributorsList.Add(additionalInterfacesContributor);
158162

159163
foreach (var @interface in interfaces)

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,11 @@ protected override Type GenerateType(string typeName, INamingScope namingScope)
145145
protected virtual InterfaceProxyWithoutTargetContributor GetContributorForAdditionalInterfaces(
146146
INamingScope namingScope)
147147
{
148-
return new InterfaceProxyWithoutTargetContributor(namingScope, (c, m) => NullExpression.Instance) { Logger = Logger };
148+
// this is explained in `InterfaceProxyWithoutTargetGenerator.GetProxyTargetContributor`:
149+
GetTargetExpressionDelegate getTargetType =
150+
(c, m) => m.IsAbstract ? NullExpression.Instance
151+
: new TypeTokenExpression(m.DeclaringType);
152+
return new InterfaceProxyWithoutTargetContributor(namingScope, getTargetType) { Logger = Logger };
149153
}
150154

151155
protected virtual IEnumerable<Type> GetTypeImplementerMapping(Type proxyTargetType,

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,16 @@ public InterfaceProxyWithoutTargetGenerator(ModuleScope scope, Type targetType,
3535

3636
protected override CompositeTypeContributor GetProxyTargetContributor(Type proxyTargetType, INamingScope namingScope)
3737
{
38-
return new InterfaceProxyWithoutTargetContributor(namingScope, (c, m) => NullExpression.Instance) { Logger = Logger };
38+
// The type of contributor instantiated below will use an inheritance-based invocation type.
39+
// Those expect a target type, not a target instance (see first ctor param of `InheritanceInvocation`).
40+
// For interface proxies without target, there typically isn't a target at all...
41+
// except when an interface method has a default implementation (i.e. isn't abstract).
42+
// Then the target type is the method's declaring interface itself.
43+
// (Similar scenario: class proxies w/o target inherit method impls from the proxied class.)
44+
GetTargetExpressionDelegate getTargetType =
45+
(c, m) => m.IsAbstract ? NullExpression.Instance
46+
: new TypeTokenExpression(m.DeclaringType);
47+
return new InterfaceProxyWithoutTargetContributor(namingScope, getTargetType) { Logger = Logger };
3948
}
4049

4150
protected override ProxyTargetAccessorContributor GetProxyTargetAccessorContributor()

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,17 @@ private static MethodInfo ObtainMethod(MethodInfo proxiedMethod, Type type)
6565
MethodInfo methodOnTarget = null;
6666
if (declaringType.IsInterface)
6767
{
68-
var mapping = type.GetInterfaceMap(declaringType);
69-
var index = Array.IndexOf(mapping.InterfaceMethods, proxiedMethod);
70-
Debug.Assert(index != -1);
71-
methodOnTarget = mapping.TargetMethods[index];
68+
if (proxiedMethod.IsAbstract)
69+
{
70+
var mapping = type.GetInterfaceMap(declaringType);
71+
var index = Array.IndexOf(mapping.InterfaceMethods, proxiedMethod);
72+
Debug.Assert(index != -1);
73+
methodOnTarget = mapping.TargetMethods[index];
74+
}
75+
else
76+
{
77+
methodOnTarget = proxiedMethod;
78+
}
7279
}
7380
else
7481
{

0 commit comments

Comments
 (0)