Reflection.Emit InvalidProgramException

В настоящее время я пытаюсь создать «макет» для интерфейса с использованием Reflection.Emit. Поэтому я создал базовый класс, который использую для всех динамически генерируемых моков. Для свойств в интерфейсе я хочу вызвать метод «Получить» в базовом классе, который возвращает значение свойства.

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);
    }
  }
}

К сожалению, я всегда получаю InvalidProgramException при попытке прочитать «издевательское» свойство. Установка свойства (которое также делегирует вызов какому-либо методу базового класса) работает нормально, то же самое для вызовов методов.

Для тестирования я создал довольно простой интерфейс:

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;

person chrisih    schedule 20.07.2017    source источник
comment
var setter = typeBuilder.DefineMethod("set_" + prop.Name, MethodAttributes.Public, typeof(void), Type.EmptyTypes); определяет метод, который не принимает аргументов (поскольку вы не указали типы для аргументов). Это неправильно. Кроме того, вы можете показать полный рабочий код? Включая код, который реализует ваш интерфейс, чтобы мы могли его воспроизвести?   -  person Rob    schedule 20.07.2017
comment
Кроме того, тестирование вашего кода, похоже, работает нормально после внесения изменений в установщик   -  person Rob    schedule 20.07.2017
comment
Спасибо за подсказку относительно TypeParameter. Я добавил их, но исключение все еще выдается...   -  person chrisih    schedule 20.07.2017


Ответы (1)


После небольшой отладки я нашел вашу проблему. Я заметил, что

wrapped.Text = "" входил в TryCallMethodOneWay, когда ясно написано, что он вызывает TrySetProperty.

Это потому, что foreach (var meth in typeof(TIf).GetMethods()) вернет вам методы получения и установки. Это; вы дважды определяете геттеры и сеттеры.

Это решается простой проверкой:

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 });

И ваш код должен работать без проблем

person Rob    schedule 20.07.2017