Discovering Generic Controllers in ASP.NET Core

前端 未结 4 1559
时光取名叫无心
时光取名叫无心 2020-12-03 01:18

I am trying to create a generic controller like this:

[Route(\"api/[controller]\")]
public class OrdersController : Controller where T : IOrder
{
           


        
相关标签:
4条回答
  • 2020-12-03 02:01

    To get a list of controllers in RC2, just get ApplicationPartManager from DependencyInjection and do this:

        ApplicationPartManager appManager = <FROM DI>;
    
        var controllerFeature = new ControllerFeature();
        appManager.PopulateFeature(controllerFeature);
    
        foreach(var controller in controllerFeature.Controllers)
        {
            ...
        }
    
    0 讨论(0)
  • 2020-12-03 02:13

    Short Answer

    Implement IApplicationFeatureProvider<ControllerFeature>.

    Question and Answer

    Does anyone know what "service" interface is responsible for [discovering all available controllers]?

    The ControllerFeatureProvider is responsible for that.

    And does anyone know of a way to make ASP.NET Core "dump" the names of all the controllers it discovered?

    Do that within ControllerFeatureProvider.IsController(TypeInfo typeInfo).

    Example

    MyControllerFeatureProvider.cs

    using System;
    using System.Linq;
    using System.Reflection;
    using Microsoft.AspNetCore.Mvc.Controllers;
    
    namespace CustomControllerNames 
    {
        public class MyControllerFeatureProvider : ControllerFeatureProvider 
        {
            protected override bool IsController(TypeInfo typeInfo)
            {
                var isController = base.IsController(typeInfo);
    
                if (!isController)
                {
                    string[] validEndings = new[] { "Foobar", "Controller`1" };
    
                    isController = validEndings.Any(x => 
                        typeInfo.Name.EndsWith(x, StringComparison.OrdinalIgnoreCase));
                }
    
                Console.WriteLine($"{typeInfo.Name} IsController: {isController}.");
    
                return isController;
            }
        }
    }
    

    Register it during startup.

    public void ConfigureServices(IServiceCollection services)
    {
        services
            .AddMvcCore()
            .ConfigureApplicationPartManager(manager => 
            {
                manager.FeatureProviders.Add(new MyControllerFeatureProvider());
            });
    }
    

    Here is some example output.

    MyControllerFeatureProvider IsController: False.
    OrdersFoobar IsController: True.
    OrdersFoobarController`1 IsController: True.
    Program IsController: False.
    <>c__DisplayClass0_0 IsController: False.
    <>c IsController: False.
    

    And here is a demo on GitHub. Best of luck.

    Edit - Adding Versions

    .NET Version

    > dnvm install "1.0.0-rc2-20221" -runtime coreclr -architecture x64 -os win -unstable
    

    NuGet.Config

    <?xml version="1.0" encoding="utf-8"?>
    <configuration>
      <packageSources>
        <clear/>
        <add key="AspNetCore" 
             value="https://www.myget.org/F/aspnetvnext/api/v3/index.json" />  
      </packageSources>
    </configuration>
    

    .NET CLI

    > dotnet --info
    .NET Command Line Tools (1.0.0-rc2-002429)
    
    Product Information:
     Version:     1.0.0-rc2-002429
     Commit Sha:  612088cfa8
    
    Runtime Environment:
     OS Name:     Windows
     OS Version:  10.0.10586
     OS Platform: Windows
     RID:         win10-x64
    

    Restore, Build, and Run

    > dotnet restore
    > dotnet build
    > dotnet run
    

    Edit - Notes on RC1 vs RC2

    This might not be possible is RC1, because DefaultControllerTypeProvider.IsController() is marked as internal.

    0 讨论(0)
  • 2020-12-03 02:15

    What happens by default

    During the controller discovery process, your open generic Controller<T> class will be among the candidate types. But the default implementation of the IApplicationFeatureProvider<ControllerFeature> interface, DefaultControllerTypeProvider, will eliminate your Controller<T> because it rules out any class with open generic parameters.

    Why overriding IsController() doesn't work

    Replacing the default implementation of the IApplicationFeatureProvider<ControllerFeature> interface, in order to override DefaultControllerTypeProvider.IsController(), will not work. Because you don't actually want the discovery process to accept your open generic controller (Controller<T>) as a valid controller. It is not a valid controller per se, and the controller factory wouldn't know how to instantiate it anyway, because it wouldn't know what T is supposed to be.

    What needs to be done

    1. Generate closed controller types

    Before the controller discovery process even starts, you need to generate closed generic types from your open generic controller, using reflection. Here, with two sample entity types, named Account and Contact:

    Type[] entityTypes = new[] { typeof(Account), typeof(Contact) };
    TypeInfo[] closedControllerTypes = entityTypes
        .Select(et => typeof(Controller<>).MakeGenericType(et))
        .Select(cct => cct.GetTypeInfo())
        .ToArray();
    

    We now have closed TypeInfos for Controller<Account> and Controller<Contact>.

    2. Add them to an application part and register it

    Application parts are usually wrapped around CLR assemblies, but we can implement a custom application part providing a collection of types generated at runtime. We simply need to have it implement the IApplicationPartTypeProvider interface. Therefore, our runtime-generated controller types will enter the controller discovery process like any other built-in type would.

    The custom application part:

    public class GenericControllerApplicationPart : ApplicationPart, IApplicationPartTypeProvider
    {
        public GenericControllerApplicationPart(IEnumerable<TypeInfo> typeInfos)
        {
            Types = typeInfos;
        }
    
        public override string Name => "GenericController";
        public IEnumerable<TypeInfo> Types { get; }
    }
    

    Registration in MVC services (Startup.cs):

    services.AddMvc()
        .ConfigureApplicationPartManager(apm =>
            apm.ApplicationParts.Add(new GenericControllerApplicationPart(closedControllerTypes)));
    

    As long as your controller derives from the built-in Controller class, there is no actual need to override the IsController method of the ControllerFeatureProvider. Because your generic controller inherits the [Controller] attribute from ControllerBase, it will be accepted as a controller in the discovery process regardless of its somewhat bizarre name ("Controller`1").

    3. Override the controller name in the application model

    Nevertheless, "Controller`1" is not a good name for routing purposes. You want each of your closed generic controllers to have independent RouteValues. Here, we will replace the name of the controller with that of the entity type, to match what would happen with two independent "AccountController" and "ContactController" types.

    The model convention attribute:

    public class GenericControllerAttribute : Attribute, IControllerModelConvention
    {
        public void Apply(ControllerModel controller)
        {
            Type entityType = controller.ControllerType.GetGenericArguments()[0];
    
            controller.ControllerName = entityType.Name;
        }
    }
    

    Applied to the controller class:

    [GenericController]
    public class Controller<T> : Controller
    {
    }
    

    Conclusion

    This solution stays close to the overall ASP.NET Core architecture and, among other things, you will keep full visibility of your controllers through the API Explorer (think "Swagger").

    It has been tested successfully with both conventional and attribute-based routing.

    0 讨论(0)
  • 2020-12-03 02:15

    Application Feature Providers examine application parts and provide features for those parts. There are built-in feature providers for the following MVC features:

    • Controllers
    • Metadata Reference
    • Tag Helpers
    • View Components

    Feature providers inherit from IApplicationFeatureProvider, where T is the type of the feature. You can implement your own feature providers for any of MVC's feature types listed above. The order of feature providers in the ApplicationPartManager.FeatureProviders collection can be important, since later providers can react to actions taken by previous providers.

    By default, ASP.NET Core MVC ignores generic controllers (for example, SomeController). This sample uses a controller feature provider that runs after the default provider and adds generic controller instances for a specified list of types (defined in EntityTypes.Types):

    public class GenericControllerFeatureProvider : IApplicationFeatureProvider<ControllerFeature>
    {
        public void PopulateFeature(IEnumerable<ApplicationPart> parts, ControllerFeature feature)
        {
            // This is designed to run after the default ControllerTypeProvider, 
            // so the list of 'real' controllers has already been populated.
            foreach (var entityType in EntityTypes.Types)
            {
                var typeName = entityType.Name + "Controller";
                if (!feature.Controllers.Any(t => t.Name == typeName))
                {
                    // There's no 'real' controller for this entity, so add the generic version.
                    var controllerType = typeof(GenericController<>)
                        .MakeGenericType(entityType.AsType()).GetTypeInfo();
                    feature.Controllers.Add(controllerType);
                }
            }
        }
    }
    

    The entity types:

    public static class EntityTypes
    {
        public static IReadOnlyList<TypeInfo> Types => new List<TypeInfo>()
        {
            typeof(Sprocket).GetTypeInfo(),
            typeof(Widget).GetTypeInfo(),
        };
    
        public class Sprocket { }
        public class Widget { }
    }
    

    The feature provider is added in Startup:

    services.AddMvc()
        .ConfigureApplicationPartManager(p => 
            p.FeatureProviders.Add(new GenericControllerFeatureProvider()));
    

    By default, the generic controller names used for routing would be of the form GenericController`1[Widget] instead of Widget. The following attribute is used to modify the name to correspond to the generic type used by the controller:

    using Microsoft.AspNetCore.Mvc.ApplicationModels; using System;

    namespace AppPartsSample
    {
        // Used to set the controller name for routing purposes. Without this convention the
        // names would be like 'GenericController`1[Widget]' instead of 'Widget'.
        //
        // Conventions can be applied as attributes or added to MvcOptions.Conventions.
        [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
        public class GenericControllerNameConvention : Attribute, IControllerModelConvention
        {
            public void Apply(ControllerModel controller)
            {
                if (controller.ControllerType.GetGenericTypeDefinition() != 
                    typeof(GenericController<>))
                {
                    // Not a GenericController, ignore.
                    return;
                }
    
                var entityType = controller.ControllerType.GenericTypeArguments[0];
                controller.ControllerName = entityType.Name;
            }
        }
    }
    

    The GenericController class:

    using Microsoft.AspNetCore.Mvc;
    
    namespace AppPartsSample
    {
        [GenericControllerNameConvention] // Sets the controller name based on typeof(T).Name
        public class GenericController<T> : Controller
        {
            public IActionResult Index()
            {
                return Content($"Hello from a generic {typeof(T).Name} controller.");
            }
        }
    }
    

    Sample: Generic controller feature

    0 讨论(0)
提交回复
热议问题