Skip to content

Commit 4d258a2

Browse files
Feature: Enable call forwarding and substitution for non virtual methods or sealed classes implementing an interface. (#700)
How to use: var substitute = Substitute.ForTypeForwardingTo <ISomeInterface,SomeImplementation>(argsList); In this case, it doesn't matter if methods are virtual or not; it will intercept all calls since we will be working with an interface all the time. For Limitations: Overriding virtual methods effectively replaces its implementation both for internal and external calls. With this implementation NSubstitute will only intercept calls made by client classes using the interface. Calls made from inside the object itself to its own method, will hit the actual implementation.
1 parent b8b0184 commit 4d258a2

File tree

10 files changed

+242
-18
lines changed

10 files changed

+242
-18
lines changed

src/NSubstitute/Core/IProxyFactory.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ namespace NSubstitute.Core;
22

33
public interface IProxyFactory
44
{
5-
object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments);
5+
object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments);
66
}

src/NSubstitute/Core/SubstituteFactory.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public class SubstituteFactory(ISubstituteStateFactory substituteStateFactory, I
1414
/// <returns></returns>
1515
public object Create(Type[] typesToProxy, object?[] constructorArguments)
1616
{
17-
return Create(typesToProxy, constructorArguments, callBaseByDefault: false);
17+
return Create(typesToProxy, constructorArguments, callBaseByDefault: false, isPartial: false);
1818
}
1919

2020
/// <summary>
@@ -33,10 +33,10 @@ public object CreatePartial(Type[] typesToProxy, object?[] constructorArguments)
3333
throw new CanNotPartiallySubForInterfaceOrDelegateException(primaryProxyType);
3434
}
3535

36-
return Create(typesToProxy, constructorArguments, callBaseByDefault: true);
36+
return Create(typesToProxy, constructorArguments, callBaseByDefault: true, isPartial: true);
3737
}
3838

39-
private object Create(Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault)
39+
private object Create(Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault, bool isPartial)
4040
{
4141
var substituteState = substituteStateFactory.Create(this);
4242
substituteState.CallBaseConfiguration.CallBaseByDefault = callBaseByDefault;
@@ -46,7 +46,7 @@ private object Create(Type[] typesToProxy, object?[] constructorArguments, bool
4646

4747
var callRouter = callRouterFactory.Create(substituteState, canConfigureBaseCalls);
4848
var additionalTypes = typesToProxy.Where(x => x != primaryProxyType).ToArray();
49-
var proxy = proxyFactory.GenerateProxy(callRouter, primaryProxyType, additionalTypes, constructorArguments);
49+
var proxy = proxyFactory.GenerateProxy(callRouter, primaryProxyType, additionalTypes, isPartial, constructorArguments);
5050
return proxy;
5151
}
5252

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
namespace NSubstitute.Exceptions;
2+
3+
public abstract class TypeForwardingException(string message) : SubstituteException(message)
4+
{
5+
}
6+
7+
public sealed class CanNotForwardCallsToClassNotImplementingInterfaceException(Type type) : TypeForwardingException(DescribeProblem(type))
8+
{
9+
private static string DescribeProblem(Type type)
10+
{
11+
return string.Format("The provided class '{0}' doesn't implement all requested interfaces. ", type.Name);
12+
}
13+
}
14+
15+
public sealed class CanNotForwardCallsToAbstractClassException(Type type) : TypeForwardingException(DescribeProblem(type))
16+
{
17+
private static string DescribeProblem(Type type)
18+
{
19+
return string.Format("The provided class '{0}' is abstract. ", type.Name);
20+
}
21+
}

src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs

Lines changed: 56 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,14 @@ public class CastleDynamicProxyFactory(ICallFactory callFactory, IArgumentSpecif
1010
private readonly ProxyGenerator _proxyGenerator = new ProxyGenerator();
1111
private readonly AllMethodsExceptCallRouterCallsHook _allMethodsExceptCallRouterCallsHook = new AllMethodsExceptCallRouterCallsHook();
1212

13-
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
13+
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
1414
{
1515
return typeToProxy.IsDelegate()
1616
? GenerateDelegateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments)
17-
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
17+
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
1818
}
1919

20-
private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
20+
private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
2121
{
2222
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);
2323

@@ -31,7 +31,8 @@ private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[
3131
additionalInterfaces,
3232
constructorArguments,
3333
[proxyIdInterceptor, forwardingInterceptor],
34-
proxyGenerationOptions);
34+
proxyGenerationOptions,
35+
isPartial);
3536

3637
forwardingInterceptor.SwitchToFullDispatchMode();
3738
return proxy;
@@ -54,7 +55,8 @@ private object GenerateDelegateProxy(ICallRouter callRouter, Type delegateType,
5455
additionalInterfaces: null,
5556
constructorArguments: null,
5657
interceptors: [proxyIdInterceptor, forwardingInterceptor],
57-
proxyGenerationOptions);
58+
proxyGenerationOptions,
59+
isPartial: false);
5860

5961
forwardingInterceptor.SwitchToFullDispatchMode();
6062

@@ -75,8 +77,13 @@ private CastleForwardingInterceptor CreateForwardingInterceptor(ICallRouter call
7577
private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? additionalInterfaces,
7678
object?[]? constructorArguments,
7779
IInterceptor[] interceptors,
78-
ProxyGenerationOptions proxyGenerationOptions)
80+
ProxyGenerationOptions proxyGenerationOptions,
81+
bool isPartial)
7982
{
83+
if (isPartial)
84+
return CreatePartialProxy(typeToProxy, additionalInterfaces, constructorArguments, interceptors, proxyGenerationOptions, isPartial);
85+
86+
8087
if (typeToProxy.GetTypeInfo().IsInterface)
8188
{
8289
VerifyNoConstructorArgumentsGivenForInterface(constructorArguments);
@@ -96,13 +103,40 @@ private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? ad
96103
additionalInterfaces = interfaces;
97104
}
98105

106+
99107
return _proxyGenerator.CreateClassProxy(typeToProxy,
100108
additionalInterfaces,
101109
proxyGenerationOptions,
102110
constructorArguments,
103111
interceptors);
104112
}
105113

114+
private object CreatePartialProxy(Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, bool isPartial)
115+
{
116+
if (typeToProxy.GetTypeInfo().IsClass &&
117+
additionalInterfaces != null &&
118+
additionalInterfaces.Any())
119+
{
120+
VerifyClassIsNotAbstract(typeToProxy);
121+
VerifyClassImplementsAllInterfaces(typeToProxy, additionalInterfaces);
122+
123+
var targetObject = Activator.CreateInstance(typeToProxy, constructorArguments);
124+
typeToProxy = additionalInterfaces.First();
125+
126+
return _proxyGenerator.CreateInterfaceProxyWithTarget(typeToProxy,
127+
additionalInterfaces,
128+
target: targetObject,
129+
options: proxyGenerationOptions,
130+
interceptors: interceptors);
131+
}
132+
133+
return _proxyGenerator.CreateClassProxy(typeToProxy,
134+
additionalInterfaces,
135+
proxyGenerationOptions,
136+
constructorArguments,
137+
interceptors);
138+
}
139+
106140
private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter callRouter)
107141
{
108142
var options = new ProxyGenerationOptions(_allMethodsExceptCallRouterCallsHook);
@@ -116,6 +150,22 @@ private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter c
116150
return options;
117151
}
118152

153+
private static void VerifyClassImplementsAllInterfaces(Type classType, IEnumerable<Type> additionalInterfaces)
154+
{
155+
if (!additionalInterfaces.All(x => x.GetTypeInfo().IsAssignableFrom(classType.GetTypeInfo())))
156+
{
157+
throw new CanNotForwardCallsToClassNotImplementingInterfaceException(classType);
158+
}
159+
}
160+
161+
private static void VerifyClassIsNotAbstract(Type classType)
162+
{
163+
if (classType.GetTypeInfo().IsAbstract)
164+
{
165+
throw new CanNotForwardCallsToAbstractClassException(classType);
166+
}
167+
}
168+
119169
private static void VerifyNoConstructorArgumentsGivenForInterface(object?[]? constructorArguments)
120170
{
121171
if (HasItems(constructorArguments))

src/NSubstitute/Proxies/CastleDynamicProxy/CastleInvocationMapper.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ public virtual ICall Map(IInvocation castleInvocation)
1010
Func<object>? baseMethod = null;
1111
if (castleInvocation.InvocationTarget != null &&
1212
castleInvocation.MethodInvocationTarget.IsVirtual &&
13-
!castleInvocation.MethodInvocationTarget.IsAbstract &&
14-
!castleInvocation.MethodInvocationTarget.IsFinal)
13+
!castleInvocation.MethodInvocationTarget.IsAbstract)
1514
{
1615
baseMethod = CreateBaseResultInvocation(castleInvocation);
1716
}

src/NSubstitute/Proxies/DelegateProxy/DelegateProxyFactory.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ public class DelegateProxyFactory(CastleDynamicProxyFactory objectProxyFactory)
88
{
99
private readonly CastleDynamicProxyFactory _castleObjectProxyFactory = objectProxyFactory ?? throw new ArgumentNullException(nameof(objectProxyFactory));
1010

11-
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
11+
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
1212
{
1313
// Castle factory can now resolve delegate proxies as well.
14-
return _castleObjectProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
14+
return _castleObjectProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
1515
}
1616
}

src/NSubstitute/Proxies/ProxyFactory.cs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ namespace NSubstitute.Proxies;
55
[Obsolete("This class is deprecated and will be removed in future versions of the product.")]
66
public class ProxyFactory(IProxyFactory delegateFactory, IProxyFactory dynamicProxyFactory) : IProxyFactory
77
{
8-
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments)
8+
public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
99
{
1010
var isDelegate = typeToProxy.IsDelegate();
1111
return isDelegate
12-
? delegateFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments)
13-
: dynamicProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, constructorArguments);
12+
? delegateFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments)
13+
: dynamicProxyFactory.GenerateProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
1414
}
1515
}

src/NSubstitute/Substitute.cs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,4 +89,24 @@ public static T ForPartsOf<T>(params object[] constructorArguments)
8989
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
9090
return (T)substituteFactory.CreatePartial([typeof(T)], constructorArguments);
9191
}
92+
93+
/// <summary>
94+
/// Creates a proxy for a class that implements an interface, forwarding methods and properties to an instance of the class, effectively mimicking a real instance.
95+
/// Both the interface and the class must be provided as parameters.
96+
/// The proxy will log calls made to the interface members and delegate them to an instance of the class. Specific members can be substituted
97+
/// by using <see cref="WhenCalled{T}.DoNotCallBase()">When(() => call).DoNotCallBase()</see> or by
98+
/// <see cref="SubstituteExtensions.Returns{T}(T,T,T[])">setting a value to return value</see> for that member.
99+
/// This extension supports sealed classes and non-virtual members, with some limitations. Since the substituted method is non-virtual, internal calls within the object will invoke the original implementation and will not be logged.
100+
/// </summary>
101+
/// <typeparam name="TInterface">The interface the substitute will implement.</typeparam>
102+
/// <typeparam name="TClass">The class type implementing the interface. Must be a class; not a delegate or interface. </typeparam>
103+
/// <param name="constructorArguments"></param>
104+
/// <returns>An object implementing the selected interface. Calls will be forwarded to the actuall methods, but allows parts to be selectively
105+
/// overridden via `Returns` and `When..DoNotCallBase`.</returns>
106+
public static TInterface ForTypeForwardingTo<TInterface, TClass>(params object[] constructorArguments)
107+
where TInterface : class
108+
{
109+
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
110+
return (TInterface)substituteFactory.CreatePartial([typeof(TInterface), typeof(TClass)], constructorArguments);
111+
}
92112
}

tests/NSubstitute.Acceptance.Specs/SubbingForConcreteTypesAndMultipleInterfaces.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using NUnit.Framework;
2+
using NUnit.Framework.Legacy;
23

34
namespace NSubstitute.Acceptance.Specs;
45

@@ -31,6 +32,30 @@ public void Can_sub_for_concrete_type_and_implement_other_interfaces()
3132
subAsIFirst.Received().First();
3233
}
3334

35+
[Test]
36+
public void Can_sub_for_abstract_type_and_implement_other_two_interfaces()
37+
{
38+
// test from docs
39+
var substitute = Substitute.For([typeof(IFirst), typeof(ISecond), typeof(ClassWithCtorArgs)],
40+
["hello world", 5]);
41+
42+
ClassicAssert.IsInstanceOf<IFirst>(substitute);
43+
ClassicAssert.IsInstanceOf<ISecond>(substitute);
44+
ClassicAssert.IsInstanceOf<ClassWithCtorArgs>(substitute);
45+
}
46+
47+
[Test]
48+
public void Can_sub_for_concrete_type_and_implement_other_two_interfaces()
49+
{
50+
// test from docs
51+
var substitute = Substitute.For([typeof(IFirst), typeof(ISecond), typeof(ConcreteClassWithCtorArgs)],
52+
["hello world", 5]);
53+
54+
ClassicAssert.IsInstanceOf<IFirst>(substitute);
55+
ClassicAssert.IsInstanceOf<ISecond>(substitute);
56+
ClassicAssert.IsInstanceOf<ConcreteClassWithCtorArgs>(substitute);
57+
}
58+
3459
[Test]
3560
public void Partial_sub()
3661
{
@@ -90,8 +115,13 @@ public class Partial
90115
public virtual int Number() { return -1; }
91116
public int GetNumberPlusOne() { return Number() + 1; }
92117
}
118+
93119
public abstract class ClassWithCtorArgs(string s, int a)
94120
{
95121
public string StringFromCtorArg { get; set; } = s; public int IntFromCtorArg { get; set; } = a;
96122
}
123+
124+
public class ConcreteClassWithCtorArgs(string s, int a) : ClassWithCtorArgs(s, a)
125+
{
126+
}
97127
}

0 commit comments

Comments
 (0)