Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Help Needed] Support call forwarding to a provided instance for types it has in common with the substitute. #840

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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 src/NSubstitute/Core/IProxyFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ namespace NSubstitute.Core;
public interface IProxyFactory
{
object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments);
object GenerateProxy(object targetObject, ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments);
}
1 change: 1 addition & 0 deletions src/NSubstitute/Core/ISubstituteFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ public interface ISubstituteFactory
{
object Create(Type[] typesToProxy, object[] constructorArguments);
object CreatePartial(Type[] typesToProxy, object[] constructorArguments);
object Create(object targetObject, Type[] typesToProxy, object?[] constructorArguments);
}
28 changes: 28 additions & 0 deletions src/NSubstitute/Core/SubstituteFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,34 @@ public object CreatePartial(Type[] typesToProxy, object?[] constructorArguments)
return Create(typesToProxy, constructorArguments, callBaseByDefault: true, isPartial: true);
}

/// <summary>
/// Create a substitute for the given types, with calls configured to call the implementation on <paramref name="targetObject"/>
/// where possible. (virtual) Parts of the instance can be substituted using
/// <see cref="SubstituteExtensions.Returns{T}(T,T,T[])">Returns()</see>.
/// </summary>
/// <param name="targetObject">The instance whose implementation will be called if a corresponding member from <paramref name="typesToProxy"/> is called.</param>
/// <param name="typesToProxy"></param>
/// <param name="constructorArguments"></param>
/// <returns></returns>
public object Create(object targetObject, Type[] typesToProxy, object?[] constructorArguments)
{
return Create(targetObject, typesToProxy, constructorArguments, callBaseByDefault: false, isPartial: false);
}

private object Create(object targetObject, Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault, bool isPartial)
{
var substituteState = substituteStateFactory.Create(this);
substituteState.CallBaseConfiguration.CallBaseByDefault = callBaseByDefault;

var primaryProxyType = GetPrimaryProxyType(typesToProxy);
var canConfigureBaseCalls = callBaseByDefault || CanCallBaseImplementation(primaryProxyType);

var callRouter = callRouterFactory.Create(substituteState, canConfigureBaseCalls);
var additionalTypes = typesToProxy.Where(x => x != primaryProxyType).ToArray();
var proxy = proxyFactory.GenerateProxy(targetObject, callRouter, primaryProxyType, additionalTypes, isPartial, constructorArguments);
return proxy;
}

private object Create(Type[] typesToProxy, object?[] constructorArguments, bool callBaseByDefault, bool isPartial)
{
var substituteState = substituteStateFactory.Create(this);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ public object GenerateProxy(ICallRouter callRouter, Type typeToProxy, Type[]? ad
: GenerateTypeProxy(callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
}

public object GenerateProxy(object targetObject, ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
return typeToProxy.IsDelegate()
? !targetObject.GetType().IsDelegate()
? throw new NotSupportedException()
: throw new NotImplementedException() // TODO: Technically, there could be a use case for this. Implement if needed.
: GenerateTypeProxy(targetObject, callRouter, typeToProxy, additionalInterfaces, isPartial, constructorArguments);
}

private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);
Expand All @@ -38,6 +47,28 @@ private object GenerateTypeProxy(ICallRouter callRouter, Type typeToProxy, Type[
return proxy;
}

private object GenerateTypeProxy(object targetObject, ICallRouter callRouter, Type typeToProxy, Type[]? additionalInterfaces, bool isPartial, object?[]? constructorArguments)
{
VerifyClassHasNotBeenPassedAsAnAdditionalInterface(additionalInterfaces);

var proxyIdInterceptor = new ProxyIdInterceptor(typeToProxy);
var forwardingInterceptor = CreateForwardingInterceptor(callRouter);

var proxyGenerationOptions = GetOptionsToMixinCallRouterProvider(callRouter);

var proxy = CreateProxyUsingCastleProxyGenerator(
targetObject,
typeToProxy,
additionalInterfaces,
constructorArguments,
[proxyIdInterceptor, forwardingInterceptor],
proxyGenerationOptions,
isPartial);

forwardingInterceptor.SwitchToFullDispatchMode();
return proxy;
}

private object GenerateDelegateProxy(ICallRouter callRouter, Type delegateType, Type[]? additionalInterfaces, object?[]? constructorArguments)
{
VerifyNoAdditionalInterfacesGivenForDelegate(additionalInterfaces);
Expand Down Expand Up @@ -111,6 +142,45 @@ private object CreateProxyUsingCastleProxyGenerator(Type typeToProxy, Type[]? ad
interceptors);
}

private object CreateProxyUsingCastleProxyGenerator(object targetObject, Type typeToProxy, Type[]? additionalInterfaces,
object?[]? constructorArguments,
IInterceptor[] interceptors,
ProxyGenerationOptions proxyGenerationOptions,
bool isPartial)
{
if (isPartial)
return CreatePartialProxy(targetObject, typeToProxy, additionalInterfaces, constructorArguments, interceptors, proxyGenerationOptions, isPartial);

// We make a proxy/wrapper for the target object type.
// We forward only implementation of the specified base type/interfaces to the target, so we don't want to use its type as typeToProxy.
if (typeToProxy.GetTypeInfo().IsInterface)
{
VerifyNoConstructorArgumentsGivenForInterface(constructorArguments);

var interfacesArrayLength = additionalInterfaces != null ? additionalInterfaces.Length + 1 : 1;
var interfaces = new Type[interfacesArrayLength];

interfaces[0] = typeToProxy;
if (additionalInterfaces != null)
{
Array.Copy(additionalInterfaces, 0, interfaces, 1, additionalInterfaces.Length);
}

// We need to create a proxy for the object type, so we can intercept the ToString() method.
// Therefore, we put the desired primary interface to the secondary list.
typeToProxy = typeof(object);
additionalInterfaces = interfaces;
}


return _proxyGenerator.CreateClassProxyWithTarget(typeToProxy,
additionalInterfaces,
targetObject,
proxyGenerationOptions,
constructorArguments,
interceptors);
}

private object CreatePartialProxy(Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, bool isPartial)
{
if (typeToProxy.GetTypeInfo().IsClass &&
Expand All @@ -137,6 +207,16 @@ private object CreatePartialProxy(Type typeToProxy, Type[]? additionalInterfaces
interceptors);
}

private object CreatePartialProxy(object targetObject, Type typeToProxy, Type[]? additionalInterfaces, object?[]? constructorArguments, IInterceptor[] interceptors, ProxyGenerationOptions proxyGenerationOptions, bool isPartial)
{
return _proxyGenerator.CreateClassProxyWithTarget(typeToProxy,
additionalInterfaces,
targetObject,
proxyGenerationOptions,
constructorArguments,
interceptors);
}

private ProxyGenerationOptions GetOptionsToMixinCallRouterProvider(ICallRouter callRouter)
{
var options = new ProxyGenerationOptions(_allMethodsExceptCallRouterCallsHook);
Expand Down
19 changes: 19 additions & 0 deletions src/NSubstitute/Substitute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,23 @@ public static TInterface ForTypeForwardingTo<TInterface, TClass>(params object[]
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
return (TInterface)substituteFactory.CreatePartial([typeof(TInterface), typeof(TClass)], constructorArguments);
}

/// <summary>
/// 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.
/// 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
/// by using <see cref="WhenCalled{T}.DoNotCallBase()">When(() => call).DoNotCallBase()</see> or by
/// <see cref="SubstituteExtensions.Returns{T}(T,T,T[])">setting a value to return value</see> for that member.
/// 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.
/// </summary>
/// <typeparam name="T">The interface or class the substitute will implement.</typeparam>
/// <param name="target">The target instance providing implementation for (parts of) the interface</param>
/// <param name="constructorArguments"></param>
/// <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
/// overridden via `Returns` and `When..DoNotCallBase`.</returns>
public static T ForTypeForwardingTo<T>(object target, params object[] constructorArguments)
where T : class
{
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
return (T)substituteFactory.Create(target, [typeof(T)], constructorArguments);
}
}
11 changes: 11 additions & 0 deletions tests/NSubstitute.Acceptance.Specs/TypeForwarding.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,17 @@ public void PartialSubstituteFailsIfClassDoesntImplementInterface()
() => Substitute.ForTypeForwardingTo<ITestInterface, TestRandomConcreteClass>());
}


[Test]
public void SubstitutePartialForwarding()
{
List<int> wrappedInstance = [2];
var sub = Substitute.ForTypeForwardingTo<IReadOnlyList<int>>(wrappedInstance);
using var _ = Assert.EnterMultipleScope();
Assert.That(sub.Count, Is.EqualTo(1));
Assert.That(sub[0], Is.EqualTo(2));
Assert.That(sub.FirstOrDefault(), Is.EqualTo(2));
}
[Test]
public void PartialSubstituteFailsIfClassIsAbstract()
{
Expand Down
Loading