Skip to content

Commit bf120c8

Browse files
committed
First (failed) attempt
1 parent dfef728 commit bf120c8

File tree

6 files changed

+140
-0
lines changed

6 files changed

+140
-0
lines changed

src/NSubstitute/Core/IProxyFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,5 @@ namespace NSubstitute.Core;
33
public interface IProxyFactory
44
{
55
object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments);
6+
object GenerateProxy(object targetObject, ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments);
67
}

src/NSubstitute/Core/ISubstituteFactory.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ public interface ISubstituteFactory
44
{
55
object Create(Type[] typesToProxy, object[] constructorArguments);
66
object CreatePartial(Type[] typesToProxy, object[] constructorArguments);
7+
object Create(object targetObject, Type[] typesToProxy, object?[] constructorArguments);
78
}

src/NSubstitute/Core/SubstituteFactory.cs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,34 @@ public object CreatePartial(Type[] typesToProxy, object?[] constructorArguments)
3636
return Create(typesToProxy, constructorArguments, callBaseByDefault: true, isPartial: true);
3737
}
3838

39+
/// <summary>
40+
/// Create a substitute for the given types, with calls configured to call the implementation on <paramref name="targetObject"/>
41+
/// where possible. (virtual) Parts of the instance can be substituted using
42+
/// <see cref="SubstituteExtensions.Returns{T}(T,T,T[])">Returns()</see>.
43+
/// </summary>
44+
/// <param name="targetObject">The instance whose implementation will be called if a corresponding member from <paramref name="typesToProxy"/> is called.</param>
45+
/// <param name="typesToProxy"></param>
46+
/// <param name="constructorArguments"></param>
47+
/// <returns></returns>
48+
public object Create(object targetObject, Type[] typesToProxy, object?[] constructorArguments)
49+
{
50+
return Create(targetObject, typesToProxy, constructorArguments, callBaseByDefault: false, isPartial: false);
51+
}
52+
53+
private object Create(object targetObject, Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault, bool isPartial)
54+
{
55+
var substituteState = substituteStateFactory.Create(this);
56+
substituteState.CallBaseConfiguration.CallBaseByDefault = callBaseByDefault;
57+
58+
var primaryProxyType = GetPrimaryProxyType(typesToProxy);
59+
var canConfigureBaseCalls = callBaseByDefault || CanCallBaseImplementation(primaryProxyType);
60+
61+
var callRouter = callRouterFactory.Create(substituteState, canConfigureBaseCalls);
62+
var additionalTypes = typesToProxy.Where(x => x != primaryProxyType).ToArray();
63+
var proxy = proxyFactory.GenerateProxy(targetObject, callRouter, primaryProxyType, additionalTypes, isPartial, constructorArguments);
64+
return proxy;
65+
}
66+
3967
private object Create(Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault, bool isPartial)
4068
{
4169
var substituteState = substituteStateFactory.Create(this);

src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,15 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? ad
1717
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
1818
}
1919

20+
public object GenerateProxy(object targetObject, ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
21+
{
22+
return typeToProxy.IsDelegate()
23+
? !targetObject.GetType().IsDelegate()
24+
? throw new NotSupportedException()
25+
: throw new NotImplementedException() // TODO: Technically, there could be a use case for this. Implement if needed.
26+
: GenerateTypeProxy(targetObject, callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
27+
}
28+
2029
private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
2130
{
2231
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);
@@ -38,6 +47,28 @@ private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[
3847
return proxy;
3948
}
4049

50+
private object GenerateTypeProxy(object targetObject, ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
51+
{
52+
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);
53+
54+
var proxyIdInterceptor = new ProxyIdInterceptor(typeToProxy);
55+
var forwardingInterceptor = CreateForwardingInterceptor(callRouter);
56+
57+
var proxyGenerationOptions = GetOptionsToMixinCallRouterProvider(callRouter);
58+
59+
var proxy = CreateProxyUsingCastleProxyGenerator(
60+
targetObject,
61+
typeToProxy,
62+
additionalInterfaces,
63+
constructorArguments,
64+
[proxyIdInterceptor, forwardingInterceptor],
65+
proxyGenerationOptions,
66+
isPartial);
67+
68+
forwardingInterceptor.SwitchToFullDispatchMode();
69+
return proxy;
70+
}
71+
4172
private object GenerateDelegateProxy(ICallRouter callRouter, Type delegateType, Type[]? additionalInterfaces, object?[]? constructorArguments)
4273
{
4374
VerifyNoAdditionalInterfacesGivenForDelegate(additionalInterfaces);
@@ -111,6 +142,45 @@ private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? ad
111142
interceptors);
112143
}
113144

145+
private object CreateProxyUsingCastleProxyGenerator(object targetObject, Type typeToProxy, Type[]? additionalInterfaces,
146+
object?[]? constructorArguments,
147+
IInterceptor[] interceptors,
148+
ProxyGenerationOptions proxyGenerationOptions,
149+
bool isPartial)
150+
{
151+
if (isPartial)
152+
return CreatePartialProxy(targetObject, typeToProxy, additionalInterfaces, constructorArguments, interceptors, proxyGenerationOptions, isPartial);
153+
154+
// We make a proxy/wrapper for the target object type.
155+
// We forward only implementation of the specified base type/interfaces to the target, so we don't want to use its type as typeToProxy.
156+
if (typeToProxy.GetTypeInfo().IsInterface)
157+
{
158+
VerifyNoConstructorArgumentsGivenForInterface(constructorArguments);
159+
160+
var interfacesArrayLength = additionalInterfaces != null ? additionalInterfaces.Length + 1 : 1;
161+
var interfaces = new Type[interfacesArrayLength];
162+
163+
interfaces[0] = typeToProxy;
164+
if (additionalInterfaces != null)
165+
{
166+
Array.Copy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.Length);
167+
}
168+
169+
// We need to create a proxy for the object type, so we can intercept the ToString() method.
170+
// Therefore, we put the desired primary interface to the secondary list.
171+
typeToProxy = typeof(object);
172+
additionalInterfaces = interfaces;
173+
}
174+
175+
176+
return _proxyGenerator.CreateClassProxyWithTarget(typeToProxy,
177+
additionalInterfaces,
178+
targetObject,
179+
proxyGenerationOptions,
180+
constructorArguments,
181+
interceptors);
182+
}
183+
114184
private object CreatePartialProxy(Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, bool isPartial)
115185
{
116186
if (typeToProxy.GetTypeInfo().IsClass &&
@@ -137,6 +207,16 @@ private object CreatePartialProxy(Type typeToProxy, Type[]? additionalInterfaces
137207
interceptors);
138208
}
139209

210+
private object CreatePartialProxy(object targetObject, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, bool isPartial)
211+
{
212+
return _proxyGenerator.CreateClassProxyWithTarget(typeToProxy,
213+
additionalInterfaces,
214+
targetObject,
215+
proxyGenerationOptions,
216+
constructorArguments,
217+
interceptors);
218+
}
219+
140220
private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter callRouter)
141221
{
142222
var options = new ProxyGenerationOptions(_allMethodsExceptCallRouterCallsHook);

src/NSubstitute/Substitute.cs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,23 @@ public static TInterface ForTypeForwardingTo<TInterface, TClass>(params object[]
116116
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
117117
return (TInterface)substituteFactory.CreatePartial([typeof(TInterface), typeof(TClass)], constructorArguments);
118118
}
119+
120+
/// <summary>
121+
/// Creates a proxy for a class that implements an interface or class, forwarding methods and properties to an instance of the class, effectively mimicking a real instance.
122+
/// The proxy will log calls made to the interface and/or virtual class members and delegate them to an instance of the target if it implements them. Specific members can be substituted
123+
/// by using <see cref="WhenCalled{T}.DoNotCallBase()">When(() => call).DoNotCallBase()</see> or by
124+
/// <see cref="SubstituteExtensions.Returns{T}(T,T,T[])">setting a value to return value</see> for that member.
125+
/// 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.
126+
/// </summary>
127+
/// <typeparam name="T">The interface or class the substitute will implement.</typeparam>
128+
/// <param name="target">The target instance providing implementation for (parts of) the interface</param>
129+
/// <param name="constructorArguments"></param>
130+
/// <returns>An object implementing the selected interface or class. Calls will be forwarded to the actual methods if possible, but allows parts to be selectively
131+
/// overridden via `Returns` and `When..DoNotCallBase`.</returns>
132+
public static T ForTypeForwardingTo<T>(object target, params object[] constructorArguments)
133+
where T : class
134+
{
135+
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
136+
return (T)substituteFactory.Create(target, [typeof(T)], constructorArguments);
137+
}
119138
}

tests/NSubstitute.Acceptance.Specs/TypeForwarding.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,17 @@ public void PartialSubstituteFailsIfClassDoesntImplementInterface()
5454
() => Substitute.ForTypeForwardingTo<ITestInterface, TestRandomConcreteClass>());
5555
}
5656

57+
58+
[Test]
59+
public void SubstitutePartialForwarding()
60+
{
61+
List<int> wrappedInstance = [2];
62+
var sub = Substitute.ForTypeForwardingTo<IReadOnlyList<int>>(wrappedInstance);
63+
using var _ = Assert.EnterMultipleScope();
64+
Assert.That(sub.Count, Is.EqualTo(1));
65+
Assert.That(sub[0], Is.EqualTo(2));
66+
Assert.That(sub.FirstOrDefault(), Is.EqualTo(2));
67+
}
5768
[Test]
5869
public void PartialSubstituteFailsIfClassIsAbstract()
5970
{

0 commit comments

Comments
 (0)