À la découverte des contrôleurs génériques ASP.NET noyau

je suis en train de créer un contrôleur générique comme ceci:

[Route("api/[controller]")]
public class OrdersController<T> : Controller where T : IOrder
{
    [HttpPost("{orderType}")]
    public async Task<IActionResult> Create(
        [FromBody] Order<T> order)
    {
       //....
    }
}

j'ai l'intention que la variable {orderType} URI segment contrôle le type générique du contrôleur. J'expérimente avec les deux une coutume IControllerFactory et IControllerActivator, mais rien ne fonctionne. Chaque fois que j'essaie d'envoyer une demande, je reçois une réponse 404. Le code de mon Controller personnalisé factory (et activateur) n'est jamais exécuté.

évidemment le problème est que ASP.NET le noyau s'attend à être valide les contrôleurs à la fin, avec le suffixe "Contrôleur", mais mon contrôleur générique à la place, a la (réflexion) suffixe "Contrôleur " 1". Ainsi, les routes basées sur les attributs qu'elle déclare passent inaperçues.

<!--10 ASP.NET MVC, at least in its early days,DefaultControllerFactory était responsable de la découverte de tous les contrôleurs disponibles. Il testé pour le "Contrôleur" suffixe:

le framework MVC fournit une usine de controller par défaut (correctement nommée DefaultControllerFactory) qui va chercher dans tous les assemblages d'une appldomaine à la recherche de tous les types qui implémentent IController et dont le nom se termine par "Controller."

apparemment, dans ASP.NET Core, l'usine du contrôleur n'a plus cette responsabilité. Comme je l'ai dit plus tôt, mon contrôleur personnalisé en usine exécute pour les contrôleurs "normaux", mais n'est jamais invoqué pour les contrôleurs génériques. Il y a donc quelque chose d'autre, plus tôt dans le processus d'évaluation, qui régit la découverte de contrôleurs.

est-ce que quelqu'un sait quelle interface "service" est responsable de cette découverte? Je ne connais pas l'interface de personnalisation ou le point "crochet".

et est-ce que quelqu'un connaît un moyen de faire ASP.NET noyau "balancer" les noms de tous les contrôleurs qu'il a découvert? Ce serait génial d'écrire un test d'unité qui vérifie que n'importe quelle découverte de contrôleur personnalisé que j'attends fonctionne effectivement.

D'ailleurs, s'il y a un "crochet" qui permet de découvrir des noms de Controllers génériques, cela implique que les substitutions de routes doivent aussi être normalisées:

[Route("api/[controller]")]
public class OrdersController<T> : Controller { }

quelle que soit la valeur de T est donné, le nom [controller] doit rester un simple nom de base-générique. En utilisant le code ci-dessus comme exemple, la valeur [controller] serait "Orders". Ce ne serait pas" ordres 1 "ou"ordres de quelque chose".

Remarque:

ce problème pourrait également être résolu en déclarant explicitement fermé les types génériques, au lieu de générer au moment de l'exécution:

public class VanityOrdersController : OrdersController<Vanity> { }
public class ExistingOrdersController : OrdersController<Existing> { }

Les travaux ci-dessus, mais il produit des URI des chemins que je n'aime pas:

~/api/VanityOrders
~/api/ExistingOrders

Ce que j'avais réellement voulu, c'était ceci:

~/api/Orders/Vanity
~/api/Orders/Existing

un Autre réglage qui me reçoit l'URI je suis à la recherche de:

[Route("api/Orders/Vanity", Name ="VanityLink")]
public class VanityOrdersController : OrdersController<Vanity> { }
[Route("api/Orders/Existing", Name = "ExistingLink")]
public class ExistingOrdersController : OrdersController<Existing> { }

cependant, bien que cela semble fonctionner, cela ne répond pas vraiment à ma question. Je voudrais utiliser mon contrôleur Générique directement à l'exécution, plutôt qu'indirectement (par codage manuel) au moment de la compilation. Fondamentalement, cela signifie que j'ai besoin ASP.NET Core pour pouvoir "voir" ou "découvrir" mon contrôleur Générique, malgré le fait que son nom de réflexion de temps d'exécution ne se termine pas avec le suffixe "Controller" attendu.

14
demandé sur Tratcher 2016-04-17 22:05:37

4 réponses

Courte Réponse

implémenter IApplicationFeatureProvider<ControllerFeature>.

la Question et la Réponse

est-ce que quelqu'un sait quelle interface "service" est responsable de [découvrir tous les contrôleurs disponibles]?

ControllerFeatureProvider en est responsable.

et est-ce que quelqu'un connaît un moyen de faire ASP.NET noyau "dump" les noms de tous les contrôleurs il découvert?

dans les ControllerFeatureProvider.IsController(TypeInfo typeInfo).

Exemple

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;
        }
    }
}

L'Enregistrer au démarrage.

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

voici quelques exemples de sortie.

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

Et voici une démo sur GitHub. Le meilleur de la chance.

Modifier - Ajouter Les 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

Restaurer, créer et Exécuter

> dotnet restore
> dotnet build
> dotnet run

Modifier - Notes sur la RC1 vs RC2

Cela pourrait ne pas être possible est RC1, parce que DefaultControllerTypeProvider.IsController() est marqué comme internal.

13
répondu Shaun Luttin 2016-04-22 14:11:24

Ce qui se passe par défaut

pendant le processus de découverte du contrôleur, votre Controller<T> la classe sera parmi les types de candidats. Mais le défaut de mise en œuvre de l' IApplicationFeatureProvider<ControllerFeature> interface,DefaultControllerTypeProvider, éliminera votre Controller<T> parce qu'elle exclut toute classe ouverte paramètres génériques.

pourquoi remplacer IsController () ne fonctionne pas

remplace l'implémentation par défaut de IApplicationFeatureProvider<ControllerFeature> interface, afin de remplacer DefaultControllerTypeProvider.IsController(), ne fonctionnera pas. Parce que vous ne voulez pas réellement que le processus de découverte accepte votre contrôleur générique ouvert (Controller<T>) comme contrôleur valide. C'est un controller valide en soi, et l'usine du controller ne saurait pas comment l'instancier de toute façon, parce qu'elle ne saurait pas ce que T est censé être.

Ce qui doit être fait

1. Générer des types de contrôleurs fermés

avant même que le processus de découverte du contrôleur ne commence, vous besoin de générer des types génériques fermés à partir de votre contrôleur open generic, En utilisant la réflexion. Ici, avec deux types d'entités, nommé Account et Contact:

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

Nous avons maintenant fermé TypeInfosController<Account> et Controller<Contact>.

2. Les ajouter à une partie d'une application, et de l'inscrire

les pièces D'Application sont habituellement enroulées autour des assemblages CLR, mais nous pouvons implémenter une pièce d'application personnalisée en fournissant une collection de types générés à l'exécution. Nous il suffit de le faire implémenter le IApplicationPartTypeProvider interface. Par conséquent, nos types de Controllers générés par l'exécution vont entrer dans le processus de découverte du controller comme tout autre type intégré le ferait.

L'application personnalisée partie:

public class GenericControllerApplicationPart : ApplicationPart, IApplicationPartTypeProvider
{
    public GenericControllerApplicationPart(IEnumerable<TypeInfo> typeInfos)
    {
        Types = typeInfos;
    }

    public override string Name => "GenericController";
    public IEnumerable<TypeInfo> Types { get; }
}

inscription aux services MVC (Startup.cs):

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

aussi longtemps que votre controller dérive duController classe, il n'y a pas besoin de remplacer les IsController méthode de l' ControllerFeatureProvider. Parce que votre le contrôleur Générique hérite du [Controller] attribut ControllerBase, il sera accepté comme contrôleur dans le processus de découverte quel que soit son nom quelque peu bizarre ("Controller`1").

3. Remplacer le nom du contrôleur dans le modèle d'application

néanmoins, "Controller' 1 " n'est pas un bon nom à des fins de routage. Vous voulez chacun de vos fermé générique contrôleurs indépendants RouteValues. Ici, nous remplacerons le nom du contrôleur par celui du type d'entité, pour correspondre à ce qui se passerait avec deux types indépendants "AccountController" et "ContactController".

Le modèle de convention de l'attribut:

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

        controller.ControllerName = entityType.Name;
    }
}

appliqué à la classe controller:

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

Conclusion

cette solution reste proche de l'ensemble ASP.NET l'architecture de base et, entre autres choses, vous garderez une visibilité complète de vos contrôleurs à travers L'API Explorer (pensez à "Swagger").

Il a a été testé avec succès avec le routage classique et basé sur les attributs.

10
répondu Mathieu Renda 2017-08-17 04:52:11

les fournisseurs de fonctions d'Application examinent les parties de l'application et fournissent des fonctions pour ces parties. Il existe des fournisseurs de fonctionnalités intégrés pour les fonctionnalités MVC suivantes:

  • Contrôleurs
  • Référence Des Métadonnées
  • Balise Aides
  • Voir Les Composants

les fournisseurs de fonctionnalités héritent de IApplicationFeatureProvider, où T est le type de la fonctionnalité. Vous pouvez implémenter vos propres fournisseurs de fonctionnalités pour n'importe laquelle des fonctionnalités de MVC les types énumérés ci-dessus. L'ordre des fournisseurs de fonctionnalités dans L'ApplicationPartManager.La collecte de FeatureProviders peut être importante, car les fournisseurs ultérieurs peuvent réagir aux mesures prises par les fournisseurs précédents.

par défaut, ASP.NET le noyau MVC ignore les controllers génériques (par exemple, SomeController). Cet exemple utilise un fournisseur de fonctions de controller qui court après le fournisseur par défaut et ajoute des instances de controller génériques pour une liste spécifiée de types (définis dans EntityTypes.Les 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);
            }
        }
    }
}

Les types d'entité:

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

    public class Sprocket { }
    public class Widget { }
}

le fournisseur de fonctionnalités est ajouté au démarrage:

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

par défaut, les noms génériques du contrôleur utilisés pour le routage seraient de la forme GenericController`1[Widget] au lieu de Widget. L'attribut suivant est utilisé pour modifier le nom de correspondre au type générique utilisé par le contrôleur:

utilisation de Microsoft.AspNetCore.Mvc.ApplicationModels; utiliser Système;

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;
        }
    }
}

la classe GenericController:

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.");
        }
    }
}

Exemple: contrôleur Générique la fonction

1
répondu Mohammad Akbari 2017-12-05 13:05:48

pour obtenir une liste des controllers dans RC2, il suffit D'obtenir ApplicationPartManager de DependencyInjection et de faire ceci:

    ApplicationPartManager appManager = <FROM DI>;

    var controllerFeature = new ControllerFeature();
    appManager.PopulateFeature(controllerFeature);

    foreach(var controller in controllerFeature.Controllers)
    {
        ...
    }
0
répondu bang 2016-06-13 12:27:01