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".
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.
Implémentez IApplicationFeatureProvider<ControllerFeature>
.
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)
.
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.
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
Ceci pourrait ne pas être possible avec RC1, car DefaultControllerTypeProvider.IsController()
est marqué comme internal
.
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.
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.
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>
.
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").
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
{
}
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.
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:
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.");
}
}
}
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)
{
...
}