Skip to content

Commit

Permalink
First (failed) attempt
Browse files Browse the repository at this point in the history
  • Loading branch information
rbeurskens committed Nov 5, 2024
1 parent 72d146d commit 0cd7450
Show file tree
Hide file tree
Showing 6 changed files with 140 additions and 0 deletions.
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 @@ -109,4 +109,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

0 comments on commit 0cd7450

Please sign in to comment.