2

I'm currently switching from .net framework to .net core 3.1.

Defining Api Controllers inside the namespace is all fine and works.

Now I have the case, that I need to declare the Api Controllers within another class, like this:

namespace Api.Controllers
{
    public class MainClass : BaseClass
    {
        public MainClass()
        {
        }

        [ApiController]
        [Route("Test")]
        public class TestController : ControllerBase
        {
            [HttpGet]
            public int GetResult()
            {
                return 0;
            }
        }
    }
}

The result is, that the Api Controller can't be found after calling the "AddControllers" method inside the "ConfigureServices" method of the startup class.

The MainClass is instantiated before the Startup class will be called.

I've tried to change the global route and defining an area like "{area=Test}/{controller=Test}", or set the ApiController attribute above the MainClass, but none of them worked.

Is there a way to get this working?

satma0745
  • 667
  • 1
  • 7
  • 26
Mello1337
  • 21
  • 2
  • 3
    Why would you want to do this though? Nested classes can be useful but they almost always introduce complexity for no benefit. – DavidG Mar 29 '21 at 09:03
  • @DavidG in my case, the `MainClass` contains a lot of methods to send or request queries to/from database. In addition to that, the whole class is dynamically created, meaning it's an dynamic assembly that will be loaded at runtime. So the controllers inside this will be different for each "compilation". There won't be a more complexe construct which could conflict with other controllers or something like this. – Mello1337 Mar 30 '21 at 10:22
  • Then you should be injecting services that perform these tasks as dependencies into your controllers. Nested controllers are not the way to do this. Another option would be to use a base abstract controller which has shared methods. – DavidG Mar 30 '21 at 10:28

1 Answers1

1

Looks like the default ControllerFeatureProvider does not treat nested controller types as controller. You can add (don't need to replace) your custom provider to change that behavior, like this:

public class NestedControllerFeatureProvider : ControllerFeatureProvider
{
    protected override bool IsController(TypeInfo typeInfo)
    {
        if(!typeInfo.IsClass) return false;
        if(typeInfo.IsAbstract) return false;
        var isNestedType = typeInfo.DeclaringType != null;
        var isPublic = true;
        var parentType = typeInfo.DeclaringType;
        while(parentType != null){
            isPublic = parentType.IsPublic;
            parentType = parentType.DeclaringType;                 ​
       ​ }
       ​ return isNestedType && isPublic;                  ​
     }
}

Then add that provider to the ApplicationPartManager in Startup.ConfigureServices like this:

​​services
    .AddMvc()
  ​​  .ConfigureApplicationPartManager(appPart => {
        ​​appPart.FeatureProviders.Add(new NestedControllerFeatureProvider());
    });

If you want to replace the default ControllerFeatureProvider, just find it in the FeatureProviders and remove it. Of course then you need to ensure that your custom one should handle everything just like what done by the default logic, something like this:

​​//for IsController
return base.IsController(typeInfo) || <...your-custom-logic...>;

NOTE: You can refer to the default implementation of ControllerFeatureProvider to learn some standard logic to implement your own logic correctly. The code above is just a basic example. To me, as long as the classes inherits from ControllerBase and not abstract, they can work fine as a controller to serve requests. There would be no serious troubles except some weird conventions (e.g: class name not ending with Controller is still a controller or some standard attributes applied on the controller class are not working ...).

We should not use nested controller classes. Each controller class should be put in a separate file (as a good practice). However the point of this answer (the most interesting part that I'm pretty sure not many know about, is the use of ControllerFeatureProvider which can help you customize the features set in other scenarios). And really if you really have to stick with your design somehow, you of course have to use this solution, no other way.

satma0745
  • 667
  • 1
  • 7
  • 26
King King
  • 61,710
  • 16
  • 105
  • 130
  • This potentially matches far too many types and could cause a lot of trouble. For example, private controllers or abstract classes. Also, just because something is possible, doesn't mean we should be doing it. – DavidG Mar 29 '21 at 10:03
  • @DavidG yes, I just provided a way to help him achieve what he wants, the detailed implementation can be much more complicated (we can view the source code for the default implementation of `ControllerFeatureProvider` to learn some logic). BTW, I don't think the code here could cause ***a lot of trouble*** as long as the controller type is a `ControllerBase` and non-abstract, it's almost fine. – King King Mar 29 '21 at 15:36
  • @DavidG you can see the default implementation here is actually not very complicated https://github.com/dotnet/aspnetcore/blob/c925f99cddac0df90ed0bc4a07ecda6b054a0b02/src/Mvc/Mvc.Core/src/Controllers/ControllerFeatureProvider.cs#L41 – King King Mar 29 '21 at 15:37
  • I know, but it's the kind of code that can cause weird things to happen later on, with peculiar bugs that are very hard to track down. I'm just saying I would have either not answered (my preference) or included the other checks in the base code. – DavidG Mar 29 '21 at 15:39
  • Thank you, that did it's job. I added all checks from the default, except the IsPublic check. I just want to note, that I will never have the case, that controllers will be added outside this class, or that the (controller) class(es) are abstract, or the controllers declared as private. – Mello1337 Mar 30 '21 at 10:18
  • @Mello1337 you're welcome, if it solved your issue, you should accept the answer by clicking on the tick mark, that's how SO works. – King King Mar 30 '21 at 11:30