-
-
Notifications
You must be signed in to change notification settings - Fork 801
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
Ability to specify proxy generator or invalidate it #925 #1227
base: main
Are you sure you want to change the base?
Conversation
Thanks for your interest in working on this! I haven't finished looking through all of your PR, however my first impression is that this exposes far too many Moq internals that shouldn't be made public. Exposing those would needlessly tie our hands in refactoring Moq in the future. The main problem in this PR is that Many other parts of your PR appear to be a direct consequence of exposing |
This is promising. Would you be willing to share your Cecil-based implementation of |
Thank you for your reply :) Our Cecil implementation is still very messy and needs cleaning up, but I can at least share the code of our internal class CecilProxyFactory : ProxyFactory
{
private readonly ConcurrentDictionary<CacheKey, Type> proxyTypes;
public CecilProxyFactory()
{
this.proxyTypes = new ConcurrentDictionary<CacheKey, Type>();
}
public override object CreateProxy(Type mockType, IInterceptor interceptor, Type[] interfaces, object[] arguments)
{
var type = this.GetProxyType(mockType, interfaces, arguments.Length);
var argsWithInterceptor = new object[arguments.Length + 1];
argsWithInterceptor[0] = interceptor;
arguments.CopyTo(argsWithInterceptor, 1);
var instance = Activator.CreateInstance(type, argsWithInterceptor);
if(mockType.IsDelegateType())
{
return Delegate.CreateDelegate(mockType, instance, instance.GetType().GetMethod("Invoke"));
}
return instance;
}
private Type GetProxyType(Type mockType, Type[] interfaces, int length)
{
var key = CacheKey.Create(mockType, interfaces);
return this.proxyTypes.GetOrAdd(key, key => this.CreateProxyType(mockType, interfaces));
}
private Type CreateProxyType(Type mockType, Type[] interfaces)
{
using (var builder = new AssemblyGenerator())
{
if (mockType.IsInterface)
{
var typeName = builder.DefineInterfaceProxy(mockType, interfaces);
var type = builder.BuildAssembly().GetType(typeName);
return type;
}
else if (mockType.IsDelegateType())
{
var typeName = builder.DefineDelegateContainer(mockType, interfaces);
var type = builder.BuildAssembly().GetType(typeName);
return type;
}
else
{
// Class proxy
var typeName = builder.DefineClassProxy(mockType, interfaces);
var type = builder.BuildAssembly().GetType(typeName);
return type;
}
}
throw new NotImplementedException();
}
public override bool IsMethodVisible(System.Reflection.MethodInfo method, out string messageIfNotVisible)
{
messageIfNotVisible = null;
return true;
}
public override bool IsTypeVisible(Type type)
{
return true;
}
private sealed class CacheKey
{
private readonly MemberInfo[] orderedTypes;
private readonly int hashCode;
private CacheKey(params System.Reflection.MemberInfo[] orderedTypes)
{
this.orderedTypes = orderedTypes;
this.hashCode = this.CalculateHashCode();
}
public override bool Equals(object obj)
{
var other = obj as CacheKey;
if (other == null || this.orderedTypes.Length != other.orderedTypes.Length)
{
return false;
}
for (var i = 0; i < this.orderedTypes.Length; i++)
{
if (!this.orderedTypes[i].Equals(other.orderedTypes[i]))
{
return false;
}
}
return true;
}
public override int GetHashCode()
{
return this.hashCode;
}
private int CalculateHashCode()
{
// TODO: Improve whenever
var hash = this.orderedTypes.Length;
for (var i = 0; i < this.orderedTypes.Length; i++)
{
hash = (hash * 7) ^ this.orderedTypes[i].GetHashCode();
}
return hash;
}
internal static CacheKey Create(Type t1, Type[] moreTypes)
{
var h = new HashSet<MemberInfo>() { t1 };
h.UnionWith(moreTypes);
var orderedTypes = h.OrderBy(x => x.Name, StringComparer.Ordinal).ToArray();
return new CacheKey(orderedTypes);
}
}
} As you can see, we are using the provided using Moq.Async;
using System;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
namespace Moq
{
/// <summary/>
public class CecilInvocation : Moq.IInvocation
{
/// <summary>
/// Internal type to mark invocation results as "exception occurred during execution". The type just
/// wraps the Exception so a thrown exception can be distinguished from an <see cref="System.Exception"/>
/// return value.
/// </summary>
private readonly struct ExceptionResult
{
public ExceptionResult(Exception exception)
{
Exception = exception;
}
public Exception Exception { get; }
}
private readonly object proxyInstance;
private object result;
private MethodInfo methodImplementation;
/// <summary/>
public CecilInvocation(Type proxyType
, System.Reflection.MethodInfo method
, object proxyInstance
, params object[] arguments
)
{
this.proxyInstance = proxyInstance;
this.ProxyType = proxyType;
this.Method = method;
this.Arguments = arguments;
}
/// <summary>
///
/// </summary>
public MethodInfo Method { get; }
/// <summary>
///
/// </summary>
public MethodInfo MethodImplementation
{
get
{
if (this.methodImplementation == null)
{
this.methodImplementation = this.Method.GetImplementingMethod(this.ProxyType);
}
return this.methodImplementation;
}
}
/// <summary>
///
/// </summary>
public object[] Arguments { get; }
/// <summary>
///
/// </summary>
public ISetup MatchingSetup { get; private set; }
/// <summary>
///
/// </summary>
public Type ProxyType { get; }
/// <summary>
///
/// </summary>
public bool IsVerified { get; }
/// <summary>
///
/// </summary>
public object ReturnValue
{
get => this.result is ExceptionResult ? null : this.result;
set
{
this.result = value;
}
}
/// <summary>
///
/// </summary>
public Exception Exception
{
get => this.result is ExceptionResult r ? r.Exception : null;
set
{
this.result = new ExceptionResult(value);
}
}
/// <summary/>
public object CallBase()
{
if (this.Method.IsAbstract)
{
throw new NotSupportedException();
}
// call the base method
var ftn = this.Method.MethodHandle.GetFunctionPointer();
var delegateType = this.GetDelegateType();
var d =(Delegate)Activator.CreateInstance(delegateType, this.proxyInstance, ftn);
var result = d.DynamicInvoke(this.Arguments);
return result;
}
/// <summary>
///
/// </summary>
/// <param name="awaitableFactory"></param>
public void ConvertResultToAwaitable(IAwaitableFactory awaitableFactory)
{
if (this.result is ExceptionResult r)
{
this.result = awaitableFactory.CreateFaulted(r.Exception);
}
else if (this.result != null && !this.Method.ReturnType.IsAssignableFrom(this.result.GetType()))
{
this.result = awaitableFactory.CreateCompleted(this.result);
}
}
/// <summary>
///
/// </summary>
/// <param name="setup"></param>
public void MarkAsMatchedBy(ISetup setup)
{
this.MatchingSetup = setup;
}
/// <summary>
///
/// </summary>
/// <returns></returns>
private Type GetDelegateType()
{
var types = this.Method.GetParameters()
.Select(p => p.ParameterType)
.Concat(new[] { this.Method.ReturnType });
return Expression.GetDelegateType(types.ToArray());
}
}
} Perhaps it could be possible to create another abstraction on top of |
|
This is a draft for the changes as per our discussion in issue #1083.
I am aware that it might need cleaning up, documentation, and so on. In this first stage, I mainly want feedback on this approach, if it's the right way to go at all.
I opted to make
ProxyFactory
public, which I assume is desirable if we want external projects to provide their own implementations. That in turn cascaded a bit, and required me to make some more types public.In order to not leak too much implementation, I decided to expose the interface types instead of the abstract base classes. This might require some more boilerplate for alternative proxy factory implementations, but I would argue that it's worth it.
I can also confirm that with these rather crude changes, we are now able to plug in our own proxy generator, and by utilizing Cecil, we have managed to cut our test execution times nearly 2 hours down to 15 minutes.