diff --git a/src/NSubstitute/Core/IProxyFactory.cs b/src/NSubstitute/Core/IProxyFactory.cs
index 31cd3ed8..9f0a6b9c 100644
--- a/src/NSubstitute/Core/IProxyFactory.cs
+++ b/src/NSubstitute/Core/IProxyFactory.cs
@@ -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);
}
\ No newline at end of file
diff --git a/src/NSubstitute/Core/ISubstituteFactory.cs b/src/NSubstitute/Core/ISubstituteFactory.cs
index 8c237f69..1640de60 100644
--- a/src/NSubstitute/Core/ISubstituteFactory.cs
+++ b/src/NSubstitute/Core/ISubstituteFactory.cs
@@ -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);
}
\ No newline at end of file
diff --git a/src/NSubstitute/Core/SubstituteFactory.cs b/src/NSubstitute/Core/SubstituteFactory.cs
index e55c2ffd..59fd76a6 100644
--- a/src/NSubstitute/Core/SubstituteFactory.cs
+++ b/src/NSubstitute/Core/SubstituteFactory.cs
@@ -36,6 +36,34 @@ public object CreatePartial(Type[] typesToProxy, object?[] constructorArguments)
return Create(typesToProxy, constructorArguments, callBaseByDefault: true, isPartial: true);
}
+ ///
+ /// Create a substitute for the given types, with calls configured to call the implementation on
+ /// where possible. (virtual) Parts of the instance can be substituted using
+ /// Returns().
+ ///
+ /// The instance whose implementation will be called if a corresponding member from is called.
+ ///
+ ///
+ ///
+ 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);
diff --git a/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs b/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
index a445f42f..c4030ed0 100644
--- a/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
+++ b/src/NSubstitute/Proxies/CastleDynamicProxy/CastleDynamicProxyFactory.cs
@@ -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);
@@ -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);
@@ -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 &&
@@ -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);
diff --git a/src/NSubstitute/Substitute.cs b/src/NSubstitute/Substitute.cs
index 97699644..e6484984 100644
--- a/src/NSubstitute/Substitute.cs
+++ b/src/NSubstitute/Substitute.cs
@@ -116,4 +116,23 @@ public static TInterface ForTypeForwardingTo(params object[]
var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
return (TInterface)substituteFactory.CreatePartial([typeof(TInterface), typeof(TClass)], constructorArguments);
}
+
+ ///
+ /// 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 When(() => call).DoNotCallBase() or by
+ /// setting a value to return value 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.
+ ///
+ /// The interface or class the substitute will implement.
+ /// The target instance providing implementation for (parts of) the interface
+ ///
+ /// 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`.
+ public static T ForTypeForwardingTo(object target, params object[] constructorArguments)
+ where T : class
+ {
+ var substituteFactory = SubstitutionContext.Current.SubstituteFactory;
+ return (T)substituteFactory.Create(target, [typeof(T)], constructorArguments);
+ }
}
\ No newline at end of file
diff --git a/tests/NSubstitute.Acceptance.Specs/TypeForwarding.cs b/tests/NSubstitute.Acceptance.Specs/TypeForwarding.cs
index af4a8fa9..b0593fc0 100644
--- a/tests/NSubstitute.Acceptance.Specs/TypeForwarding.cs
+++ b/tests/NSubstitute.Acceptance.Specs/TypeForwarding.cs
@@ -54,6 +54,17 @@ public void PartialSubstituteFailsIfClassDoesntImplementInterface()
() => Substitute.ForTypeForwardingTo());
}
+
+ [Test]
+ public void SubstitutePartialForwarding()
+ {
+ List wrappedInstance = [2];
+ var sub = Substitute.ForTypeForwardingTo>(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()
{