locked
Attribute routing not working when controller is loaded dynamically (using custom controller selector) RRS feed

  • Question

  • User1023628836 posted

    I'm creating a selfhost REST server that dynamically loads controllers.
    When my controller is defined in my server project, then attribute routing works fine.
    When same controller is dynamically loaded then it seems attribute routing is not working.

    Here is what i do:
    I'm creating a self hosted REST server (using Owin):

    string baseUri = string.Concat("http://+:12345/MyServer");
    myserver = WebApp.Start<Startup>(baseUri);
    Console.WriteLine("MyServer is listening at " + baseUri);
    ...

    I create a static controller just to make sure it works ok with my settings:

    public class SampleController : ApiController
    {
        public SampleController() { }
    
        [HttpGet]
        [Route("api/Sample/Test")]
        [Route("api/Sample/Test/{data}")]
        [ActionName("Test")]
        public string GetTest(string data)
        {
            return Test(data);
        }
    
        [HttpPost]
        [Route("api/Sample/Test")]
        public string Test([FromBody] string data)
        {
            return string.Concat("Test - Received ", data);
        }
    ...

    My startup class looks like this:

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            var webApiConfiguration = ConfigureWebApi();
            app.UseWebApi(webApiConfiguration);
        }
    
        private HttpConfiguration ConfigureWebApi()
        {
            HttpConfiguration config = new HttpConfiguration();
            config.MapHttpAttributeRoutes();
    
            config.Routes.MapHttpRoute(
                        "DefaultApi",
                        "api/{controller}/{action}/{id}",
                        new { id = RouteParameter.Optional});
    
            DynControllerSelector custom = new DynControllerSelector(config);
    
            config.Services.Replace(typeof(IHttpControllerSelector), custom);
    
            return config;
        }
    }

    Then i dynamically load another controller which has the same methods and definition exposed than the static one. To do that i use a custom controller selector:

    public DynControllerSelector(HttpConfiguration configuration) : base(configuration)
     {
        _Configuration = configuration;
        _ControlleDescriptorDict = new ConcurrentDictionary<string, HttpControllerDescriptor>();
     }
     public override HttpControllerDescriptor SelectController(HttpRequestMessage request)
     {
        HttpControllerDescriptor httpControllerDesc = null;
        try
        {
          IDictionary<string, HttpControllerDescriptor> controllers = GetControllerMapping();
          //string controllerName = base.GetControllerName(request); // is null for some reason
         // Just get the controler in a hack way for now
        string controllerName = request.RequestUri.LocalPath.Replace("/MyServer/api/", "");
        int idx = controllerName.IndexOf("/");
        controllerName = controllerName.Substring(0, idx);
    
        if (!controllers.ContainsKey(controllerName))
        {
           if (_ControlleDescriptorDict.TryGetValue(controllerName, out httpControllerDesc) == false)
           {
            lock (_mlock)
            {
                if (_ControlleDescriptorDict.TryGetValue(controllerName, out httpControllerDesc) == false) // Check that controller has not been created while we were waiting for the lock
                {
                  string assemblyName = string.Concat(controllerName, ".dll");
                  if (System.IO.File.Exists(assemblyName))
                  {
                     Assembly assembly = Assembly.LoadFrom(assemblyName);
                     var types = assembly.GetTypes();
                     var matchedTypes = types.Where(i => typeof(IHttpController).IsAssignableFrom(i)).ToList();
                     var matchedController = matchedTypes.FirstOrDefault(i => i.Name.ToLower() == controllerName.ToLower() + "controller");
                     if (matchedController == null) throw new Exception(string.Concat("Failed to find controller ", controllerName, "Controller"));
                     httpControllerDesc = new HttpControllerDescriptor(_Configuration, controllerName, matchedController);
    
                    _ControlleDescriptorDict.TryAdd(controllerName, httpControllerDesc);
                   }
                   else
                   {
                    throw new Exception(string.Concat("Failed to load assembly ", assemblyName));
                    }
                 }
              }
             }
          }
             else
                httpControllerDesc = base.SelectController(request);
    . . 

    They should both use the same routing settings.

    I can call (HTTP GET) my static controller using attribute routing: http://localhost:12345/MyServer/api/Sample/Test/hello2 
    That works fine.

    Problem is when i try the same thing on my dynamic controller: http://localhost:12345/MyServer/api/MyDynamic/Test/hello2 

    I get following error: { "Message": "The requested resource does not support http method 'GET'." }

    It seems somehow the attribute routing is not enabled for my dynamic controller.
    If i use query parameters, it works just fine: http://localhost:12345/MyServer/api/MyDynamic/Test?data=hello

    How can i make it work?

    Thanks in advance for your help

    Nick

    Thursday, October 17, 2019 4:41 PM

Answers

  • User61956409 posted

    Hi dtf017,

    To make dynamically loaded controller work with attribute routing, you can try to implement custom IAssembliesResolver like below.

    public class MyAssembliesResolver : DefaultAssembliesResolver
    {
        public override ICollection<Assembly> GetAssemblies()
        {
            ICollection<Assembly> baseAssemblies = base.GetAssemblies();
            List<Assembly> assemblies = new List<Assembly>(baseAssemblies);
    
            string thirdPartySource = @"D:\DynamicAPIsController\APIs\";
    
            if (!string.IsNullOrWhiteSpace(thirdPartySource))
            {
                if (Directory.Exists(thirdPartySource))
                {
                    foreach (var file in Directory.GetFiles(thirdPartySource, "*.*", SearchOption.AllDirectories))
                    {
                        if (Path.GetExtension(file) == ".dll")
                        {
                            var externalAssembly = Assembly.LoadFrom(file);
    
                            baseAssemblies.Add(externalAssembly);
                        }
                    }
                }
            }
            return baseAssemblies;
        }
    }

    In Startup.cs

    //...
    
    config.Services.Replace(typeof(IAssembliesResolver), new MyAssembliesResolver());
    config.MapHttpAttributeRoutes();
    
    //...

    Test Result

    With Regards,

    Fei Han

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, October 24, 2019 3:19 AM
  • User-474980206 posted

    .net does not support unloading a module. you can only unload an appdomain. you probably need create you own route loader, that updates the route table based on the attributes of the dynamic loaded modules. re-running the attribute route reader will still see the "old" assemblies.  

    note: asp.net core does not support appdomains and currently can not unload an assembly.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Tuesday, December 10, 2019 4:49 PM
  • User1023628836 posted

    Thanks for your valuable responses.
    I believe one option would be to overwrite ApiControllerActionSelector:

    _Config.Services.Replace(typeof(IHttpActionSelector), new MyHttpActionSelector());

    Indeed when assembly is dynamically loaded, the routing definition is not updated.
    You can clearly see that when this method is called.
    One way will then be to do some custom logic to update the routing table there when new assembly is loaded.
    Thanks again for the help

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, January 16, 2020 11:11 AM

All replies

  • User-474980206 posted

    Attribute routing runs at startup. It uses reflection on all loaded modules to build the route table. As you are loading controllers after this step, they are not “seen”. You need to go thru the attribute routing source code for the version you are using and see if there is a way to trigger a reload of the route table. 

    Thursday, October 17, 2019 6:45 PM
  • User1023628836 posted

    Hi Bruce,

    Thanks for your response. I figured that out but question is how to set the routing after the controller has been loaded.

    Also this should not impact other controller already dynamically loaded.

    That is the part i'm missing. Still thanks for your response.

    Monday, October 21, 2019 7:19 AM
  • User61956409 posted

    Hi dtf017,

    To make dynamically loaded controller work with attribute routing, you can try to implement custom IAssembliesResolver like below.

    public class MyAssembliesResolver : DefaultAssembliesResolver
    {
        public override ICollection<Assembly> GetAssemblies()
        {
            ICollection<Assembly> baseAssemblies = base.GetAssemblies();
            List<Assembly> assemblies = new List<Assembly>(baseAssemblies);
    
            string thirdPartySource = @"D:\DynamicAPIsController\APIs\";
    
            if (!string.IsNullOrWhiteSpace(thirdPartySource))
            {
                if (Directory.Exists(thirdPartySource))
                {
                    foreach (var file in Directory.GetFiles(thirdPartySource, "*.*", SearchOption.AllDirectories))
                    {
                        if (Path.GetExtension(file) == ".dll")
                        {
                            var externalAssembly = Assembly.LoadFrom(file);
    
                            baseAssemblies.Add(externalAssembly);
                        }
                    }
                }
            }
            return baseAssemblies;
        }
    }

    In Startup.cs

    //...
    
    config.Services.Replace(typeof(IAssembliesResolver), new MyAssembliesResolver());
    config.MapHttpAttributeRoutes();
    
    //...

    Test Result

    With Regards,

    Fei Han

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, October 24, 2019 3:19 AM
  • User1023628836 posted

    Hi Fei Han,

    Thanks a lot for your response. I really appreciate it.
    Indeed it does load the assembly after the service has been started and configuration/routing is applied properly when using a custom assembly resolver.
    That works fine.
    One thing i should have mentioned is that the overall goal of this is to be able to update on the fly the assemblies and load them at runtime.
    Meaning the assembly loaded may be updated several times at a later time. It means i need to find a way to reload it somehow to reflect the changes.
    This should be done without interruption to the service.

    That is what my DynamicControllerSelector class is achieving. It manages updates of the assembly.
    Basically the controller is first created using assembly say A. Server is loading that assembly when client is calling the controller apis.
    Now later controller is updated, a new assembly is generated (assembly called A.v2).
    The DynamicControllerSelector will load that assembly and will process the request using the new version of the controller.
    This is done with no interruption to the service. This works well. Only down side is that it does not seem to define the routine properly, thus causing
    the attribute routing to not be functional.
    Of course after some time, it may be needed to recreate the current AppDomain to clean/remove "obsolete" assemblies
    but this can be done once a day only if needed as it is not expected to get thousands of updates for these dynamic assemblies.

    So question is how to achieve this by using the custom assembly resolver. Is there a way to remove the controller somehow and force the service to call
    again the assemblies resolver?

    Thanks again for your feedback

    Tuesday, December 10, 2019 1:51 PM
  • User-474980206 posted

    .net does not support unloading a module. you can only unload an appdomain. you probably need create you own route loader, that updates the route table based on the attributes of the dynamic loaded modules. re-running the attribute route reader will still see the "old" assemblies.  

    note: asp.net core does not support appdomains and currently can not unload an assembly.

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Tuesday, December 10, 2019 4:49 PM
  • User1023628836 posted

    Thanks for your valuable responses.
    I believe one option would be to overwrite ApiControllerActionSelector:

    _Config.Services.Replace(typeof(IHttpActionSelector), new MyHttpActionSelector());

    Indeed when assembly is dynamically loaded, the routing definition is not updated.
    You can clearly see that when this method is called.
    One way will then be to do some custom logic to update the routing table there when new assembly is loaded.
    Thanks again for the help

    • Marked as answer by Anonymous Thursday, October 7, 2021 12:00 AM
    Thursday, January 16, 2020 11:11 AM