web-dev-qa-db-fra.com

Découverte de contrôleurs génériques dans ASP.NET Core

J'essaie 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)
    {
       //....
    }
}

Je souhaite que la variable de segment URI {orderType} contrôle le type générique du contrôleur. J'expérimente à la fois avec une variable IControllerFactory et une IControllerActivator personnalisées, mais rien ne fonctionne. Chaque fois que j'essaie d'envoyer une demande, je reçois une réponse 404. Le code de ma fabrique de contrôleurs personnalisés (et de mon activateur) n'est jamais exécuté. 

Il est évident que le problème est que ASP.NET Core s'attend à ce que les contrôleurs valides se terminent par le suffixe "Controller", alors que mon contrôleur générique a plutôt le suffixe "Controller`1" (basé sur la réflexion). Ainsi, les routes basées sur les attributs déclarées passent inaperçues.

Dans ASP.NET MVC, au moins à ses débuts, la DefaultControllerFactory était responsable de la découverte de tous les contrôleurs disponibles . Il a testé le suffixe "Controller":

L'infrastructure MVC fournit une fabrique de contrôleurs par défaut (nommément nommée DefaultControllerFactory) qui recherchera dans tous les assemblys d'un domaine d'application à 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 de contrôleurs n’assume plus cette responsabilité. Comme je l'ai indiqué précédemment, ma fabrique de contrôleurs personnalisés s'exécute pour les contrôleurs "normaux", mais n'est jamais appelée pour les contrôleurs génériques. Il y a donc autre chose, plus tôt dans le processus d'évaluation, qui régit la découverte des contrôleurs.

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

Et est-ce que quelqu'un connaît un moyen de faire en sorte qu'ASP.NET Core "vider" les noms de tous les contrôleurs qu'il a découverts? Il serait bien d’écrire un test unitaire qui vérifie que toute découverte de contrôleur personnalisé à laquelle je m'attends fonctionne réellement.

Incidemment, s’il existe un "hook" permettant de découvrir les noms de contrôleurs génériques, cela implique que les substitutions de route doivent également être normalisées:

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

Quelle que soit la valeur donnée pour T, le nom du [contrôleur] doit rester un nom générique de base simple. En utilisant le code ci-dessus à titre d'exemple, la valeur de [contrôleur] serait "Commandes". Ce ne serait pas "Orders`1" ou "OrdersOfSomething".

Remarque

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

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

Ce qui précède fonctionne, mais il génère des chemins d’URI que je n’aime pas:

~/api/VanityOrders
~/api/ExistingOrders

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

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

Un autre ajustement me donne l'URI que je cherche:

[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. J'aimerais utiliser mon contrôleur générique directement au moment de l'exécution, plutôt qu'indirectement (via un codage manuel) au moment de la compilation. Cela signifie fondamentalement que j'ai besoin que ASP.NET Core soit capable de "voir" ou de "découvrir" mon contrôleur générique, malgré le fait que son nom de réflexion au moment de l'exécution ne se termine pas par le suffixe "Controller" attendu.

14
Brent Arias

Réponse courte

Implémentez IApplicationFeatureProvider<ControllerFeature>

Question et réponse

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

Le ControllerFeatureProvider en est responsable. 

Et est-ce que quelqu'un connaît un moyen de faire en sorte qu'ASP.NET Core "vider" les noms de tous les contrôleurs découverts?

Faites cela dans 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;
        }
    }
}

Enregistrez-le lors du démarrage. 

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

Voici un exemple 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 . Bonne chance.

Edition - Ajout de versions

Version .NET

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

CLI .NET

> 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, construire et exécuter

> dotnet restore
> dotnet build
> dotnet run

Edit - Notes sur RC1 vs RC2

Ceci pourrait ne pas être possible avec RC1, car DefaultControllerTypeProvider.IsController() est marqué comme internal

13
Shaun Luttin

Qu'est-ce qui se passe par défaut

Pendant le processus de découverte du contrôleur, votre classe Controller<T> générique ouverte fera partie des types de candidats. Mais l'implémentation par défaut de l'interface IApplicationFeatureProvider<ControllerFeature>, DefaultControllerTypeProvider, éliminera votre Controller<T> car elle exclut toute classe avec des paramètres génériques ouverts.

Pourquoi écraser IsController () ne fonctionne pas

Le remplacement de l'implémentation par défaut de l'interface IApplicationFeatureProvider<ControllerFeature>, 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>) en tant que contrôleur valide. C'est pas un contrôleur valide en tant que tel, et l'usine du contrôleur ne saurait de toute façon pas l'instancier, car 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 de contrôleur ne démarre, vous devez générer des types génériques fermés à partir de votre contrôleur générique ouvert, à l'aide de la réflexion. Ici, avec deux exemples de types d'entités, nommés 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é TypeInfos pour Controller<Account> et Controller<Contact>.

2. Ajoutez-les à une partie de l'application et enregistrez-la

Les pièces d'application sont généralement entourées d'assemblages CLR, mais nous pouvons implémenter une pièce d'application personnalisée fournissant une collection de types générés au moment de l'exécution. Nous avons simplement besoin qu'il implémente l'interface IApplicationPartTypeProvider. Par conséquent, nos types de contrôleurs générés à l'exécution entreront dans le processus de découverte de contrôleurs comme tout autre type intégré.

La partie application personnalisée:

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

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

Enregistrement dans les services MVC (Startup.cs):

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

Tant que votre contrôleur dérive de la classe Controller intégrée, il n'est pas nécessaire de remplacer la méthode IsController de ControllerFeatureProvider. Étant donné que votre contrôleur générique hérite de l'attribut [Controller] de ControllerBase, il sera accepté en tant que contrôleur dans le processus de découverte, quel que soit son nom un peu bizarre ("Controller`1").

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

Néanmoins, "Controller`1" n'est pas un bon nom pour le routage. Vous voulez que chacun de vos contrôleurs génériques fermés ait une RouteValues indépendante. Ici, nous allons remplacer 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".

L'attribut convention du modèle:

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

        controller.ControllerName = entityType.Name;
    }
}

Appliqué à la classe de contrôleur:

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

Conclusion

Cette solution reste proche de l'architecture globale ASP.NET Core et vous garderez, entre autres choses, une visibilité complète de vos contrôleurs via l'API Explorer (pensez "Swagger").

Il a été testé avec succès avec un routage conventionnel et basé sur des attributs.

10
Mathieu Renda

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

  • Contrôleurs
  • Référence de métadonnées
  • Tag Helpers
  • 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 tous les types de fonctionnalités de MVC répertoriés ci-dessus. L'ordre des fournisseurs de fonctions dans la collection ApplicationPartManager.FeatureProviders peut être important, car les fournisseurs ultérieurs peuvent réagir aux actions entreprises par les fournisseurs précédents.

Par défaut, ASP.NET Core MVC ignore les contrôleurs génériques (par exemple, SomeController). Cet exemple utilise un fournisseur de fonctions de contrôleur exécuté après le fournisseur par défaut et ajoute des instances de contrôleur génériques pour une liste de types spécifiée (définie dans 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);
            }
        }
    }
}

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 de contrôleur génériques utilisés pour le routage seraient de la forme GenericController`1 [Widget] au lieu de Widget. L'attribut suivant permet de modifier le nom afin qu'il corresponde au type générique utilisé par le contrôleur:

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

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: Fonction de contrôleur générique

1
Mohammad Akbari

Pour obtenir une liste des contrôleurs dans RC2, procurez-vous simplement ApplicationPartManager à partir de DependencyInjection et procédez comme suit:

    ApplicationPartManager appManager = <FROM DI>;

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

    foreach(var controller in controllerFeature.Controllers)
    {
        ...
    }
0
bang