mardi 12 décembre 2017

Exception when creating C# Web API 2 Controller at Runtime

I want to generate Web API 2 controller dynamically on startup. In this case, I used reflection.

However, when I compiled it and started the service, I got exception when I call it via a HttpRequest. The exception looks like this:

{
    "Message": "An error has occurred.",
    "ExceptionMessage": "Unable to cast object of type 'System.Object[]' to type 'System.Web.Http.ParameterBindingAttribute[]'.",
    "ExceptionType": "System.InvalidCastException",
    "StackTrace": " at System.Web.Http.Controllers.ReflectedHttpParameterDescriptor.GetCustomAttributes[TAttribute]() at System.Web.Http.Controllers.HttpParameterDescriptor.FindParameterBindingAttribute() at System.Web.Http.Controllers.HttpParameterDescriptor.get_ParameterBinderAttribute() at System.Web.Http.ModelBinding.DefaultActionValueBinder.GetParameterBinding(HttpParameterDescriptor parameter) at System.Array.ConvertAll[TInput,TOutput](TInput[] array, Converter`2 converter) at System.Web.Http.ModelBinding.DefaultActionValueBinder.GetBinding(HttpActionDescriptor actionDescriptor) at System.Web.Http.Controllers.HttpActionDescriptor.get_ActionBinding() at System.Web.Http.Controllers.ApiControllerActionSelector.ActionSelectorCacheItem..ctor(HttpControllerDescriptor controllerDescriptor) at System.Web.Http.Controllers.ApiControllerActionSelector.GetInternalSelector(HttpControllerDescriptor controllerDescriptor) at System.Web.Http.Controllers.ApiControllerActionSelector.SelectAction(HttpControllerContext controllerContext) at System.Web.Http.ApiController.ExecuteAsync(HttpControllerContext controllerContext, CancellationToken cancellationToken) at System.Web.Http.Dispatcher.HttpControllerDispatcher.<SendAsync>d__1.MoveNext()"
}


Here is my code set.

// WebApiConfig.cs
// Register function in WebApiConfig

public static void Register(HttpConfiguration config)
{
    // Replace services by this line, to add my custom HttpControllerSelector
    config.Services.Replace(typeof(IHttpControllerSelector), new CustomHttpControllerSelector(config));

    // Web API routes
    config.MapHttpAttributeRoutes();

    config.Routes.MapHttpRoute(
        name: "DefaultApi",
        routeTemplate: "api/{controller}/{id}",
        defaults: new { id = RouteParameter.Optional }
    );
}


// CustomeHttpControllerSelector.cs
// CustomHttpControllerSelector class to override the default one (In this case, the same controller will be returned for all request)

public class CustomHttpControllerSelector : DefaultHttpControllerSelector
{
    private readonly HttpConfiguration _configuration;

    public CustomHttpControllerSelector(HttpConfiguration configuration) : base(configuration)
    {
        _configuration = configuration;
    }

    public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
    {
        var ctrl = ControllerBuilder.CreateNewObject();

        String controllerName = base.GetControllerName(request);
        return new HttpControllerDescriptor(_configuration, controllerName, ctrl.GetType());
    }
}


// ControllerBuilder.cs
// The builder to implement my custom controller. Seems the problem is here. Maybe I configured something wrong in this section?

public static class ControllerBuilder
{
    public static ApiController CreateNewObject()
    {
        var ctrlType = CompileResultType("TmpController", new Dictionary<string, string> { { "tmpField", "System.String" } });
        var ctrlObject = Activator.CreateInstance(ctrlType);

        return ctrlObject as ApiController;
    }

    public static Type CompileResultType(string typeSignature, Dictionary<string, string> propDic)
    {
        TypeBuilder tb = GetTypeBuilder(typeSignature);

        tb.SetParent(typeof(ApiController));

        ConstructorBuilder ctor = tb.DefineDefaultConstructor(MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.RTSpecialName);

        foreach (var item in propDic)
        {
            CreateProperty(tb, item.Key, Type.GetType(item.Value));
        }

        // For this controller, I only want a Get method to server Get request
        MethodBuilder myGetMethod =
           tb.DefineMethod("Get",
              MethodAttributes.Public,
              typeof(String), new Type[] { typeof(String) });
        // Generate IL for method.
        ILGenerator myMethodIL = myGetMethod.GetILGenerator();
        myMethodIL.Emit(OpCodes.Ldarg_1);
        myMethodIL.Emit(OpCodes.Ret);

        Type objectType = tb.CreateType();
        return objectType;
    }

    private static TypeBuilder GetTypeBuilder(string typeSignature)
    {
        var an = new AssemblyName(typeSignature);
        AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(an, AssemblyBuilderAccess.Run);
        ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
        TypeBuilder tb = moduleBuilder.DefineType(typeSignature,
                TypeAttributes.Public |
                TypeAttributes.Class |
                TypeAttributes.AutoClass |
                TypeAttributes.AnsiClass |
                TypeAttributes.BeforeFieldInit |
                TypeAttributes.AutoLayout,
                null);
        return tb;
    }

    private static void CreateProperty(TypeBuilder tb, string propertyName, Type propertyType)
    {
        FieldBuilder fieldBuilder = tb.DefineField("_" + propertyName, propertyType, FieldAttributes.Private);
        PropertyBuilder propertyBuilder = tb.DefineProperty(propertyName, PropertyAttributes.HasDefault, propertyType, null);
        MethodBuilder getPropMthdBldr = tb.DefineMethod("get_" + propertyName, MethodAttributes.Public | MethodAttributes.SpecialName | MethodAttributes.HideBySig, propertyType, Type.EmptyTypes);
        ILGenerator getIl = getPropMthdBldr.GetILGenerator();

        getIl.Emit(OpCodes.Ldarg_0);
        getIl.Emit(OpCodes.Ldfld, fieldBuilder);
        getIl.Emit(OpCodes.Ret);

        MethodBuilder setPropMthdBldr =
            tb.DefineMethod("set_" + propertyName,
              MethodAttributes.Public |
              MethodAttributes.SpecialName |
              MethodAttributes.HideBySig,
              null, new[] { propertyType });

        ILGenerator setIl = setPropMthdBldr.GetILGenerator();
        Label modifyProperty = setIl.DefineLabel();
        Label exitSet = setIl.DefineLabel();

        setIl.MarkLabel(modifyProperty);
        setIl.Emit(OpCodes.Ldarg_0);
        setIl.Emit(OpCodes.Ldarg_1);
        setIl.Emit(OpCodes.Stfld, fieldBuilder);

        setIl.Emit(OpCodes.Nop);
        setIl.MarkLabel(exitSet);
        setIl.Emit(OpCodes.Ret);

        propertyBuilder.SetGetMethod(getPropMthdBldr);
        propertyBuilder.SetSetMethod(setPropMthdBldr);
    }
}


I have tried to Invoke "Get" method of ctrlObject in CreateNewObject() by ctrlType.GetMethod("Get").Invoke(ctrlObject, new [] {"param1"}). However, I can get the return string "param1"!!!. So, may I know what problem is causing the exception that I quoted at the top while I request it as a real Web API controller?





Aucun commentaire:

Enregistrer un commentaire