Reflection.Emit InvalidProgramException

Reflection.Emit InvalidProgramException

我目前正在尝试使用 Reflection.Emit 为界面创建 "Mock"。 因此我创建了一个基础 class 用于所有动态生成的模拟。 对于接口中的属性,我想在基础 class 中调用 "Get" 方法,即 returns 属性 值。

public class Mock
{
  public static TIf Wrap<TIf>() where TIf : class
  {
    if (!typeof(TIf).IsInterface)
      throw new Exception(typeof(TIf) + " is no interface");

    var asmBuilder = Thread.GetDomain().DefineDynamicAssembly(new AssemblyName("Test"), AssemblyBuilderAccess.Run);
    var modBuilder = asmBuilder.DefineDynamicModule("Mock", true);
    var typename = "ImplOf" + typeof(TIf).Name.Replace(" ", "") + ".Mock";
    var typeBuilder = modBuilder.DefineType(typename, TypeAttributes.Public, typeof(WrapperBase));

    typeBuilder.AddInterfaceImplementation(typeof(TIf));

    // methods
    foreach (var meth in typeof(TIf).GetMethods())
    {
      var del = typeof(WrapperBase).GetMethod(meth.ReturnType != typeof(void) ? "TryCallMethod" : "TryCallMethodOneWay");

      var mb = typeBuilder.DefineMethod(meth.Name, meth.Attributes ^ MethodAttributes.Abstract);
      mb.SetParameters(meth.GetParameters().Select(p => p.ParameterType)?.ToArray());
      mb.SetReturnType(meth.ReturnType);
      var mbil = mb.GetILGenerator();
      mbil.Emit(OpCodes.Ldarg_0);
      mbil.Emit(OpCodes.Ldstr, meth.Name);
      for (var i = 0; i < meth.GetParameters().Length; i++)
      {
        mbil.Emit(OpCodes.Ldarg, i + 1);
      }

      mbil.Emit(OpCodes.Call, del);
      mbil.Emit(OpCodes.Ret);
    }

    // properties
    foreach (var prop in typeof(TIf).GetProperties())
    {
      var propertyBuilder = typeBuilder.DefineProperty(prop.Name, prop.Attributes, prop.PropertyType, null);

      if (prop.CanRead)
      {
        var getterDelegate = typeof(WrapperBase).GetMethod("TryGetProperty");
        var getter = typeBuilder.DefineMethod("get_" + prop.Name, MethodAttributes.Public, prop.PropertyType, Type.EmptyTypes);

        var gil = getter.GetILGenerator();
        gil.Emit(OpCodes.Ldarg_0);
        gil.Emit(OpCodes.Ldstr, prop.Name);
        gil.Emit(OpCodes.Callvirt, getterDelegate);
        gil.Emit(OpCodes.Castclass, prop.PropertyType);
        gil.Emit(OpCodes.Ret);
        propertyBuilder.SetGetMethod(getter);
      }

      if (prop.CanWrite)
      {
        var setterDelegate = typeof(WrapperBase).GetMethod("TrySetProperty");
        var setter = typeBuilder.DefineMethod("set_" + prop.Name, MethodAttributes.Public, typeof(void), Type.EmptyTypes);

        var sil = setter.GetILGenerator();
        sil.Emit(OpCodes.Ldarg_0);
        sil.Emit(OpCodes.Ldstr, prop.Name);
        sil.Emit(OpCodes.Ldarg_1);
        sil.Emit(OpCodes.Call, setterDelegate);
        sil.Emit(OpCodes.Ret);
        propertyBuilder.SetSetMethod(setter);
      }
    }

    var retType = typeBuilder.CreateType();
    return retType.GetConstructor(new Type[0]).Invoke(new object[0]) as TIf;
  }

  public abstract class WrapperBase
  {
    public event Func<string, object[], object> OnTryCallMethod;
    public event Action<string, object[]> OnTryCallMethodOneWay;
    public event Func<string, object> OnTryGetProperty;
    public event Action<string, object> OnTrySetProperty;

    /// <inheritdoc />
    public object TryCallMethod(string name, object[] pars)
    {
      return OnTryCallMethod?.Invoke(name, pars);
    }

    /// <inheritdoc />
    public void TryCallMethodOneWay(string name, object[] pars)
    {
      OnTryCallMethodOneWay?.Invoke(name, pars);
    }

    /// <inheritdoc />
    public object TryGetProperty(string name)
    {
      return OnTryGetProperty?.Invoke(name);
    }

    /// <inheritdoc />
    public void TrySetProperty(string name, object value)
    {
      OnTrySetProperty?.Invoke(name, value);
    }
  }
}

不幸的是,我在尝试读取 "mocked" 属性 时总是得到 InvalidProgramException。 设置 属性(这也会将调用委托给某些基本 class 方法)工作正常,方法调用也是如此。

为了测试,我创建了一个非常简单的界面:

public interface ITest
{
  void Show(string text);

  string Text { get; set; }
}

现在我这样调用模拟:

  var wrapped = Mock.Wrap<ITest>();

  // ***************** works - EventHandler is called with correct parameters!
  ((Mock.WrapperBase)wrapped).OnTryCallMethodOneWay += (s, objects) => { };
  wrapped.Show("sss");

  // ***************** works - EventHandler is called with correct parameters!
  wrapped.Text = "";
  ((Mock.WrapperBase)wrapped).OnTrySetProperty += (s, val) => { };

  // ***************** does NOT work - getting InvalidProgramException
  ((Mock.WrapperBase)wrapped).OnTryGetProperty += s => "";
  var t = wrapped.Text;

经过一些调试,我发现了您的问题。我注意到

wrapped.Text = "" 进入 TryCallMethodOneWay 时清楚地写为调用 TrySetProperty.

这是因为 foreach (var meth in typeof(TIf).GetMethods()) 将要 return 您 getter 和 setter 方法。那是;您定义了 getter 和 setter 两次。

这通过简单的检查解决了:

var properties = typeof(TIf).GetProperties();
var propertyMethods = properties.SelectMany(p => new[] { p.GetGetMethod(), p.GetSetMethod() }).ToLookup(p => p);

foreach (var meth in typeof(TIf).GetMethods())
{
    if (propertyMethods.Contains(meth))
        continue;
    ...
}               

现在,如果要实现接口,您还必须将实现方法标记为 Virtual。因此,您需要将代码更改为以下内容:

var getter = typeBuilder.DefineMethod("get_" + prop.Name, MethodAttributes.Public | MethodAttributes.Virtual, prop.PropertyType, Type.EmptyTypes);

var setter = typeBuilder.DefineMethod("set_" + prop.Name, MethodAttributes.Public | MethodAttributes.Virtual, typeof(void), new[] { prop.PropertyType });

而且您的代码应该可以正常工作