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() {