Почему вызов метода, сгенерированного IL, вызывает InvalidProgramException? - PullRequest
0 голосов
/ 10 января 2020

Я генерирую контроллеры Web API во время выполнения (. Net Core 3.0). У меня есть базовый контроллер c, от которого я наследую сгенерированный контроллер, и при настройке действий http я хочу вызвать метод из базового класса и вернуть его значение. Когда я вызываю / api / Foo / 1, возникает InvalidProgramException.

EDIT : я упростил код и добавил работающий пример (см. Рабочий пример проекта ) .

Чего я хочу добиться:

public class FooController : GenericBaseController<Foo, int> {

    [HttpGet]
    public Foo Get(int id){
        return base.DoGet(id);
    }
}

Как выглядит базовый контроллер:

[Route("api/[controller]")]
[ApiController]
public abstract class GenericBaseController<T, TId> : ControllerBase {

    protected T DoGet(TId id)
    { 
        return default;
    }
}

Как выглядит код компоновщика типов, устанавливающий метод:

        MethodBuilder builder = tb.DefineMethod("Get", MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.Final, modelType, new Type[] {typeof(int)});

        builder.DefineParameter(1, ParameterAttributes.None, "id");

        ConstructorInfo httpCtorInfo = typeof(HttpGetAttribute).GetConstructors().First();
        CustomAttributeBuilder httpAttrBuilder = new CustomAttributeBuilder(httpCtorInfo, new object[0]);
        builder.SetCustomAttribute(httpAttrBuilder);

        ILGenerator emitter = builder.GetILGenerator();

        emitter.Emit(OpCodes.Nop);

        emitter.Emit(OpCodes.Ldarg_0); 
        emitter.Emit(OpCodes.Ldarg_1); 

        MethodInfo baseMethod = baseCrudController.GetMethod("DoGet", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
        emitter.Emit(OpCodes.Call, baseMethod);

        emitter.Emit(OpCodes.Stloc_0);
        emitter.Emit(OpCodes.Br_S, 0011);
        emitter.Emit(OpCodes.Ldloc_0);

        emitter.Emit(OpCodes.Ret);

Я попытался восстановить из того, что я получил, когда я использую коды операций, используя читатель, найденный здесь https://www.codeproject.com/Articles/14058/Parsing-the-IL-of-a-Method-Body

0000 : nop 
0001 : ldarg.0 
0002 : ldarg.1 
0003 : call instance FooProject.Controllers.GenericBaseController`1[System.Int32]::DoGet() 
0008 : stloc.0 
0009 : br.s 0011 
0011 : ldloc.0 
0012 : ret

1 Ответ

1 голос
/ 10 января 2020

Мне пришлось немного помассировать ваш код, чтобы он скомпилировался и запустился. Как только я это сделал, все прошло отлично, поэтому я не уверен, какая часть моего массажа устранила проблему, с которой вы столкнулись. Изменения, которые я сделал:

  1. Когда вы определяете свой tb (вызывая DefineType), вам нужно убедиться, что он наследуется от baseCrudController. Поскольку вы объявляете baseCrudController позже , я не уверен, что вы это делали. Но трудно сказать, потому что этот код не в вашем вопросе
  2. При вызове tb.DefineMethod строки в C# нуждаются в двойных кавычках, а не в одинарных кавычках
  3. В том же самом вызов, вам нужно typeof(Foo), new[] { typeof(int) } не typeof(Foo), typeof(int), так как вам нужно передать массив типов параметров
  4. Вам нужно использовать typeof(HttpGetAttribute), а не typeof(HttpGet)
  5. HttpGetAttribute имеет несколько конструкторов, и вы не можете полагаться на порядок, в котором они возвращаются. Явно укажите, что вам нужен конструктор без параметров
  6. Вам не нужен nops, и вам не нужен stloc / ldloc после call. Они добавляются только в сборках отладки, чтобы помочь отладчику
  7. На самом деле, поскольку вы не объявили локальный слот 0, stloc.0 и ldloc.0 недопустимы. Когда вы используете System.Reflection.Emit, если вам нужно сохранить значение в локальном слоте, используйте LocalBuilder и не указывайте адреса слотов в явном виде.
  8. Когда вы вызываете базовый метод, его имя "DoGet", а не "GetInBase"

Внесите все эти изменения, и вы получите следующее:

var assemblyName = new AssemblyName("Test");
var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
var moduleBuilder = assemblyBuilder.DefineDynamicModule("Test");
Type baseCrudController = typeof(GenericBaseController<,>).MakeGenericType(typeof(Foo), typeof(int));
var tb = moduleBuilder.DefineType("FooController", TypeAttributes.Public, baseCrudController);

MethodBuilder builder = tb.DefineMethod("Get", MethodAttributes.Public | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(Foo), new[] { typeof(int) });

ConstructorInfo httpCtorInfo = typeof(HttpGetAttribute).GetConstructor(Type.EmptyTypes);
CustomAttributeBuilder httpAttrBuilder = new CustomAttributeBuilder(httpCtorInfo, new object[0]);
builder.SetCustomAttribute(httpAttrBuilder);

ILGenerator emitter = builder.GetILGenerator();

emitter.Emit(OpCodes.Ldarg_0);
emitter.Emit(OpCodes.Ldarg_1);

MethodInfo baseTypeMethod = baseCrudController.GetMethod("DoGet", BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.DeclaredOnly);
emitter.Emit(OpCodes.Call, baseTypeMethod);

emitter.Emit(OpCodes.Ret);

// Test
var resultType = tb.CreateType();
var instance = Activator.CreateInstance(resultType);
var result = resultType.GetMethod("Get").Invoke(instance, new object[] { 3 });
...