Comment enregistrer plusieurs implémentations de la même interface dans Asp.Net Core?

J’ai des services dérivés de la même interface

public interface IService { } public class ServiceA : IService { } public class ServiceB : IService { } public class ServiceC : IService { } 

En règle générale, d’autres conteneurs IOC tels que Unity vous permettent d’enregistrer des implémentations concrètes par une Key qui les distingue.

Dans Asp.Net Core, comment puis-je enregistrer ces services et les résoudre à l’exécution en fonction de certaines clés?

Je ne vois aucune des méthodes Add Service qui prend le paramètre key ou name généralement utilisé pour distinguer l’implémentation concrète.

  public void ConfigureServices(IServiceCollection services) { // How do I register services here of the same interface } public MyController:Controller { public void DoSomeThing(ssortingng key) { // How do get service based on key } } 

Le motif d’usine est-il la seule option ici?

Mise à jour1
J’ai parcouru l’article ici qui montre comment utiliser le modèle d’usine pour obtenir des instances de service lorsque nous avons une implémentation de concrétisation multiple. Cependant, ce n’est toujours pas une solution complète. Lorsque j’appelle la méthode _serviceProvider.GetService() , je ne peux pas injecter de données dans le constructeur. Par exemple, considérons cet exemple

 public class ServiceA : IService { private ssortingng _efConnectionSsortingng; ServiceA(ssortingng efconnectionSsortingng) { _efConnecttionSsortingng = efConnectionSsortingng; } } public class ServiceB : IService { private ssortingng _mongoConnectionSsortingng; public ServiceB(ssortingng mongoConnectionSsortingng) { _mongoConnectionSsortingng = mongoConnectionSsortingng; } } public class ServiceC : IService { private ssortingng _someOtherConnectionSsortingng public ServiceC(ssortingng someOtherConnectionSsortingng) { _someOtherConnectionSsortingng = someOtherConnectionSsortingng; } } 

Comment _serviceProvider.GetService() peut- _serviceProvider.GetService() injecter une chaîne de connexion appropriée? Dans Unity ou dans tout autre CIO, nous pouvons le faire au moment de l’enregistrement de type. Je peux utiliser IOption mais cela nécessitera que je injecte tous les parameters, je ne peux pas injecter une chaîne de connexion particulière dans le service.

Notez également que j’essaie d’éviter d’utiliser d’autres conteneurs (y compris Unity) car je dois également enregistrer tout le rest (par exemple les contrôleurs) avec un nouveau conteneur.

L’utilisation d’un modèle de fabrique pour créer une instance de service contre DIP étant donné que l’usine augmente le nombre de dépendances qu’un client est obligé de dépendre des détails ici

Donc, je pense que le défaut par défaut dans le kernel ASP.NET manque de 2 choses
1> Enregistrer des instances à l’aide de la clé
2> Injecter des données statiques dans le constructeur lors de l’inscription

J’ai fait une solution simple en utilisant Func quand je me suis retrouvé dans cette situation.

 services.AddTransient(); services.AddTransient(); services.AddTransient(); services.AddTransient>(serviceProvider => key => { switch(key) { case "A": return serviceProvider.GetService(); case "B": return serviceProvider.GetService(); case "C": return serviceProvider.GetService(); default: throw new KeyNotFoundException(); // or maybe return null, up to you } }); 

Et l’utiliser à partir de n’importe quelle classe enregistrée avec DI comme:

 public class Consumer { private readonly Func _serviceAccessor; public Consumer(Func serviceAccessor) { _serviceAccessor = serviceAccesor; } public void UseServiceA() { //use _serviceAccessor field to resolve desired type _serviceAccessor("A").DoIServiceOperation(); } } 

METTRE À JOUR

Gardez à l’esprit que dans cet exemple, la clé pour la résolution est une chaîne, dans un souci de simplicité et parce que OP demandait ce cas particulier.

Mais vous pouvez utiliser n’importe quel type de résolution personnalisé comme clé, car vous ne voulez généralement pas un énorme commutateur n-code pour pourrir votre code. Dépend de la façon dont votre application évolue.

Une autre option consiste à utiliser la méthode d’extension GetServices partir de Microsoft.Extensions.DependencyInjection .

Enregistrez vos services en tant que:

 services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); 

Puis résoudre avec un peu de Linq:

 var services = serviceProvider.GetServices(); var serviceB = services.First(o => o.GetType() == typeof(ServiceB)); 

ou

 var serviceZ = services.First(o => o.Name.Equals("Z")); 

(en supposant que IService a une propriété de chaîne appelée “Nom”)

Assurez-vous d’ using Microsoft.Extensions.DependencyInjection;

Mettre à jour

AspNet 2.1 source: GetServices

Il n’est pas pris en charge par Microsoft.Extensions.DependencyInjection .

Mais vous pouvez twigr un autre mécanisme d’dependency injections, comme StructureMap Voir sa page d’accueil et son projet GitHub .

Ce n’est pas difficile du tout:

  1. Ajoutez une dépendance à StructureMap dans votre project.json :

     "Structuremap.Microsoft.DependencyInjection" : "1.0.1", 
  2. Injectez-le dans le pipeline ASP.NET dans ConfigureServices et enregistrez vos classes (voir la documentation)

     public IServiceProvider ConfigureServices(IServiceCollection services) // returns IServiceProvider ! { // Add framework services. services.AddMvc(); services.AddWhatever(); //using StructureMap; var container = new Container(); container.Configure(config => { // Register stuff in container, using the StructureMap APIs... config.For().Add(new Cat("CatA")).Named("A"); config.For().Add(new Cat("CatB")).Named("B"); config.For().Use("A"); // Optionally set a default config.Populate(services); }); return container.GetInstance(); } 
  3. Ensuite, pour obtenir une instance nommée, vous devrez demander le IContainer

     public class HomeController : Controller { public HomeController(IContainer injectedContainer) { var myPet = injectedContainer.GetInstance("B"); ssortingng name = myPet.Name; // Returns "CatB" 

C’est tout.

Pour l’exemple à construire, vous avez besoin de

  public interface IPet { ssortingng Name { get; set; } } public class Cat : IPet { public Cat(ssortingng name) { Name = name; } public ssortingng Name {get; set; } } 

J’ai fait face au même problème et je veux partager comment je l’ai résolu et pourquoi.

Comme vous l’avez mentionné, il y a deux problèmes. La première:

Dans Asp.Net Core, comment puis-je enregistrer ces services et les résoudre à l’exécution en fonction de certaines clés?

Alors, quelles options avons-nous? Les gens suggèrent deux:

  • Utilisez une fabrique personnalisée (comme _myFactory.GetServiceByKey(key) )

  • Utilisez un autre moteur DI (comme _unityContainer.Resolve(key) )

Le motif d’usine est-il la seule option ici?

En fait, les deux options sont des fabriques car chaque conteneur IoC est également une fabrique (hautement configurable et compliquée). Et il me semble que d’autres options sont aussi des variations du motif Factory.

Alors quelle option est la meilleure alors? Ici, je suis d’accord avec @Sock qui a suggéré d’utiliser une fabrique personnalisée, et c’est pourquoi.

Premièrement, j’essaie toujours d’éviter d’append de nouvelles dépendances quand elles ne sont pas vraiment nécessaires. Donc, je suis d’accord avec vous sur ce point. De plus, l’utilisation de deux frameworks DI est pire que la création d’abstraction personnalisée en usine. Dans le second cas, vous devez append une nouvelle dépendance au paquet (comme Unity) mais dépendre d’une nouvelle interface d’usine est moins mauvais ici. Je crois que l’idée principale d’ASP.NET Core DI est la simplicité. Il maintient un ensemble minimal de fonctionnalités suivant le principe KISS . Si vous avez besoin de quelques fonctionnalités supplémentaires, utilisez le bricolage ou utilisez un Plungin correspondant qui implémente la fonctionnalité souhaitée (principe fermé ouvert).

Deuxièmement, nous avons souvent besoin d’injecter de nombreuses dépendances nommées pour un service unique. Dans le cas de Unity, vous devrez peut-être spécifier des noms pour les parameters du constructeur (en utilisant InjectionConstructor ). Cet enregistrement utilise la reflection et une logique intelligente pour deviner les arguments du constructeur. Cela peut également conduire à des erreurs d’exécution si l’enregistrement ne correspond pas aux arguments du constructeur. D’autre part, lorsque vous utilisez votre propre usine, vous avez le contrôle total sur la manière de fournir les parameters du constructeur. C’est plus lisible et résolu à la compilation. Principe KISS à nouveau.

Le deuxième problème:

Comment _serviceProvider.GetService () peut-il injecter une chaîne de connexion appropriée?

Tout d’abord, je suis d’accord avec vous que dépendre de nouvelles choses comme IOptions (et donc sur le package Microsoft.Extensions.Options.ConfigurationExtensions ) n’est pas une bonne idée. J’ai vu des discussions sur IOptions où il y avait des opinions différentes sur ses avantages. Encore une fois, j’essaie d’éviter d’append de nouvelles dépendances quand elles ne sont pas vraiment nécessaires. Est-ce vraiment nécessaire? Je pense que non. Sinon, chaque implémentation devrait en dépendre sans qu’il y ait un besoin clair de cette implémentation (pour moi, cela ressemble à une violation du FAI, où je suis également d’accord avec vous). Cela est également vrai en fonction de l’usine, mais dans ce cas, cela peut être évité.

ASP.NET Core DI fournit une très bonne surcharge à cet effet:

 var mongoConnection = //... var efConnection = //... var otherConnection = //... services.AddTransient( s => new MyFactoryImpl( mongoConnection, efConnection, otherConnection, s.GetService(), s.GetService()))); 

Vous avez raison, le conteneur intégré ASP.NET Core n’a pas la notion d’enregistrer plusieurs services et d’en récupérer un, comme vous le suggérez, une usine est la seule solution réelle dans ce cas.

Alternativement, vous pouvez passer à un conteneur tiers comme Unity ou StructureMap qui fournit la solution dont vous avez besoin (documenté ici: https://docs.asp.net/en/latest/fundamentals/dependency-injection.html?#replacing- le-default-services-container ).

Apparemment, vous pouvez simplement injecter IEnumerable de votre interface de service! Et puis trouvez l’instance que vous voulez utiliser LINQ.

Mon exemple concerne le service AWS SNS mais vous pouvez faire la même chose pour n’importe quel service injecté.

Commencez

 foreach (ssortingng snsRegion in Configuration["SNSRegions"].Split(',', SsortingngSplitOptions.RemoveEmptyEnsortinges)) { services.AddAWSService( ssortingng.IsNullOrEmpty(snsRegion) ? null : new AWSOptions() { Region = RegionEndpoint.GetBySystemName(snsRegion) } ); } services.AddSingleton(); services.Configure(Configuration); 

SNSConfig

 public class SNSConfig { public ssortingng SNSDefaultRegion { get; set; } public ssortingng SNSSMSRegion { get; set; } } 

appsettings.json

  "SNSRegions": "ap-south-1,us-west-2", "SNSDefaultRegion": "ap-south-1", "SNSSMSRegion": "us-west-2", 

Usine SNS

 public class SNSFactory : ISNSFactory { private readonly SNSConfig _snsConfig; private readonly IEnumerable _snsServices; public SNSFactory( IOptions snsConfig, IEnumerable snsServices ) { _snsConfig = snsConfig.Value; _snsServices = snsServices; } public IAmazonSimpleNotificationService ForDefault() { return GetSNS(_snsConfig.SNSDefaultRegion); } public IAmazonSimpleNotificationService ForSMS() { return GetSNS(_snsConfig.SNSSMSRegion); } private IAmazonSimpleNotificationService GetSNS(ssortingng region) { return GetSNS(RegionEndpoint.GetBySystemName(region)); } private IAmazonSimpleNotificationService GetSNS(RegionEndpoint region) { IAmazonSimpleNotificationService service = _snsServices.FirstOrDefault(sns => sns.Config.RegionEndpoint == region); if (service == null) { throw new Exception($"No SNS service registered for region: {region}"); } return service; } } public interface ISNSFactory { IAmazonSimpleNotificationService ForDefault(); IAmazonSimpleNotificationService ForSMS(); } 

Vous pouvez maintenant obtenir le service SNS pour la région de votre choix dans votre service ou contrôleur personnalisé

 public class SmsSender : ISmsSender { private readonly IAmazonSimpleNotificationService _sns; public SmsSender(ISNSFactory snsFactory) { _sns = snsFactory.ForSMS(); } ....... } public class DeviceController : Controller { private readonly IAmazonSimpleNotificationService _sns; public DeviceController(ISNSFactory snsFactory) { _sns = snsFactory.ForDefault(); } ......... } 

Bien qu’il semble que @Miguel A. Arilla l’ait clairement fait remarquer et que j’ai voté pour lui, j’ai créé une solution intéressante, mais qui nécessite beaucoup plus de travail.

Cela dépend de la solution ci-dessus. Donc, fondamentalement, j’ai créé quelque chose de similaire à Func> et je l’ai appelé IServiceAccessor tant IServiceAccessor , puis j’ai dû append d’autres extensions à IServiceCollection tant que telles:

 public static IServiceCollection AddSingleton( this IServiceCollection services, ssortingng instanceName ) where TService : class where TImplementation : class, TService where TServiceAccessor : class, IServiceAccessor { services.AddSingleton(); services.AddSingleton(); var provider = services.BuildServiceProvider(); var implementationInstance = provider.GetServices().Last(); var accessor = provider.GetServices().First(); var serviceDescriptors = services.Where(d => d.ServiceType == typeof(TServiceAccessor)); while (serviceDescriptors.Any()) { services.Remove(serviceDescriptors.First()); } accessor.SetService(implementationInstance, instanceName); services.AddSingleton(prvd => accessor); return services; } 

Le service Accessor ressemble à:

  public interface IServiceAccessor { void Register(TService service,ssortingng name); TService Resolve(ssortingng name); } 

Le résultat final, vous pourrez enregistrer des services avec des noms ou des instances nommées comme nous le faisions avec d’autres conteneurs. Par exemple:

  services.AddSingleton("Symmesortingc"); services.AddSingleton("Asymmesortingc"); 

Cela suffit pour le moment, mais pour que votre travail soit complet, il est préférable d’append les méthodes d’extension possibles pour couvrir tous les types d’enregistrement en suivant la même approche.

Il y avait un autre article sur stackoverflow, mais je ne le trouve pas, où l’affiche explique en détail pourquoi cette fonctionnalité n’est pas prise en charge et comment la contourner, fondamentalement similaire à ce que @Miguel a déclaré. C’était bien beau, même si je ne suis pas d’accord avec chaque point car je pense qu’il y a une situation où vous avez vraiment besoin d’instances nommées. Je posterai ce lien ici une fois que je le retrouverai.

En fait, vous n’avez pas besoin de passer ce sélecteur ou cet accessoire:

J’utilise le code suivant dans mon projet et cela a bien fonctionné jusqu’à présent.

  ///  /// Adds the singleton. ///  /// The type of the t service. /// The type of the t implementation. /// The services. /// Name of the instance. /// IServiceCollection. public static IServiceCollection AddSingleton( this IServiceCollection services, ssortingng instanceName ) where TService : class where TImplementation : class, TService { var provider = services.BuildServiceProvider(); var implementationInstance = provider.GetServices().LastOrDefault(); if (implementationInstance.IsNull()) { services.AddSingleton(); provider = services.BuildServiceProvider(); implementationInstance = provider.GetServices().Single(); } return services.RegisterInternal(instanceName, provider, implementationInstance); } private static IServiceCollection RegisterInternal(this IServiceCollection services, ssortingng instanceName, ServiceProvider provider, TService implementationInstance) where TService : class { var accessor = provider.GetServices>().LastOrDefault(); if (accessor.IsNull()) { services.AddSingleton>(); provider = services.BuildServiceProvider(); accessor = provider.GetServices>().Single(); } else { var serviceDescriptors = services.Where(d => d.ServiceType == typeof(IServiceAccessor)); while (serviceDescriptors.Any()) { services.Remove(serviceDescriptors.First()); } } accessor.Register(implementationInstance, instanceName); services.AddSingleton(prvd => implementationInstance); services.AddSingleton>(prvd => accessor); return services; } // // Summary: // Adds a singleton service of the type specified in TService with an instance specified // in implementationInstance to the specified Microsoft.Extensions.DependencyInjection.IServiceCollection. // // Parameters: // services: // The Microsoft.Extensions.DependencyInjection.IServiceCollection to add the service // to. // implementationInstance: // The instance of the service. // instanceName: // The name of the instance. // // Returns: // A reference to this instance after the operation has completed. public static IServiceCollection AddSingleton( this IServiceCollection services, TService implementationInstance, ssortingng instanceName) where TService : class { var provider = services.BuildServiceProvider(); return RegisterInternal(services, instanceName, provider, implementationInstance); } ///  /// Registers an interface for a class ///  /// The type of the t interface. /// The services. /// IServiceCollection. public static IServiceCollection As(this IServiceCollection services) where TInterface : class { var descriptor = services.Where(d => d.ServiceType.GetInterface(typeof(TInterface).Name) != null).FirstOrDefault(); if (descriptor.IsNotNull()) { var provider = services.BuildServiceProvider(); var implementationInstance = (TInterface)provider?.GetServices(descriptor?.ServiceType)?.Last(); services?.AddSingleton(implementationInstance); } return services; } 

Une approche d’usine est certainement viable. Une autre approche consiste à utiliser l’inheritance pour créer des interfaces individuelles héritant d’IService, implémenter les interfaces héritées dans vos implémentations IService et enregistrer les interfaces héritées plutôt que la base. Que l’ajout d’une hiérarchie d’inheritance ou de fabriques soit le modèle “correct”, tout dépend de qui vous parlez. Je dois souvent utiliser ce modèle pour traiter plusieurs fournisseurs de bases de données dans la même application qui utilise un générique, tel que IRepository , comme base pour l’access aux données.

Exemple d’interfaces et implémentations:

 public interface IService { } public interface IServiceA: IService {} public interface IServiceB: IService {} public IServiceC: IService {} public class ServiceA: IServiceA {} public class ServiceB: IServiceB {} public class ServiceC: IServiceC {} 

Récipient:

 container.Register(); container.Register(); container.Register(); 

Bien que l’implémentation prête à l’emploi ne l’offre pas, voici un exemple de projet qui vous permet d’enregistrer des instances nommées, puis d’injecter INamedServiceFactory dans votre code et d’extraire les instances par nom. Contrairement à d’autres solutions de façade, il vous permettra d’enregistrer plusieurs instances de la même implémentation mais configurées différemment

https://github.com/macsux/DotNetDINamedInstances

Ma solution pour ce que ça vaut … a envisagé de passer à Castle Windsor car je ne peux pas dire que j’aimais l’une des solutions ci-dessus. Pardon!!

 public interface IStage : IStage { } public interface IStage { void DoSomething(); } 

Créez vos différentes implémentations

 public class YourClassA : IStage { public void DoSomething() { ...TODO } } public class YourClassB : IStage { .....etc. } 

enregistrement

 services.AddTransient, YourClassA>() services.AddTransient, YourClassB>() 

Utilisation du constructeur et de l’instance …

 public class Whatever { private IStage ClassA { get; } public Whatever(IStage yourClassA) { ClassA = yourClassA; } public void SomeWhateverMethod() { ClassA.DoSomething(); ..... } 

Que diriez-vous d’un service pour les services?

Si nous avions une interface INamedService (avec la propriété .Name), nous pourrions écrire une extension IServiceCollection pour .GetService (nom de chaîne), où l’extension prendrait ce paramètre de chaîne et ferait un .GetServices () sur lui-même, et chaque fois retourné Par exemple, recherchez l’instance dont INamedService.Name correspond au nom donné.

Comme ça:

 public interface INamedService { ssortingng Name { get; } } public static T GetService(this IServiceProvider provider, ssortingng serviceName) where T : INamedService { var candidates = provider.GetServices(); return candidates.FirstOrDefault(s => s.Name == serviceName); } 

Par conséquent, votre IMyService doit implémenter INamedService, mais vous obtiendrez la résolution basée sur les clés souhaitée, n’est-ce pas?

Pour être honnête, devoir même avoir cette interface INamedService semble moche, mais si vous voulez aller plus loin et rendre les choses plus élégantes, alors le code de cette classe pourrait contenir un [NamedServiceAtsortingbute (“A”)] extension, et ça fonctionnerait tout aussi bien. Pour être encore plus juste, Reflection est lent, donc une optimisation peut être en ordre, mais honnêtement, c’est quelque chose que le moteur DI aurait dû aider. Rapidité et simplicité sont les principaux consortingbuteurs au TCO.

Globalement, il n’y a pas besoin de fabrique explicite, car “trouver un service nommé” est un concept tellement réutilisable, et les classes d’usine ne sont pas une solution. Et un Func <> semble correct, mais un bloc de commutation est tellement grave , et encore une fois, vous écrirez des Func aussi souvent que vous le feriez en écrivant des Factories. Commencez simple, réutilisable, avec moins de code, et si cela ne se fait pas pour vous, alors allez complexe.

Je viens d’injecter simplement un IEnumerable

ConfigureServices dans Startup.cs

 Assembly.GetEntryAssembly().GetTypesAssignableFrom().ForEach((t)=> { services.AddScoped(typeof(IService), t); }); 

Dossier Services

 public interface IService { ssortingng Name { get; set; } } public class ServiceA : IService { public ssortingng Name { get { return "A"; } } } public class ServiceB : IService { public ssortingng Name { get { return "B"; } } } public class ServiceC : IService { public ssortingng Name { get { return "C"; } } } 

MyController.cs

 public class MyController { private readonly IEnumerable _services; public MyController(IEnumerable services) { _services = services; } public void DoSomething() { var service = _services.Where(s => s.Name == "A").Single(); } ... } 

Extensions.cs

  public static List GetTypesAssignableFrom(this Assembly assembly) { return assembly.GetTypesAssignableFrom(typeof(T)); } public static List GetTypesAssignableFrom(this Assembly assembly, Type compareType) { List ret = new List(); foreach (var type in assembly.DefinedTypes) { if (compareType.IsAssignableFrom(type) && compareType != type) { ret.Add(type); } } return ret; } 

J’ai fait quelque chose de similaire il y a quelque temps, et je pense que l’implémentation a été agréable.

Disons que vous devez traiter les messages entrants, chaque message a un type, donc l’approche courante ici est d’utiliser un commutateur et de faire des choses comme ceci:

 @Override public void messageArrived(Ssortingng type, Message message) throws Exception { switch(type) { case "A": do Something with message; break; case "B": do Something else with message; break; ... } } 

Nous pouvons éviter ce code de commutation, et nous pouvons également éviter le besoin de modifier ce code chaque fois qu’un nouveau type est ajouté, en suivant ce modèle:

 @Override public void messageArrived(Ssortingng type, Message message) throws Exception { messageHandler.getMessageHandler(type).handle(message); } 

C’est bien non? Alors, comment pouvons-nous réaliser quelque chose comme ça?

Définissons d’abord une annotation et aussi une interface

 @Retention(RUNTIME) @Target({TYPE}) public @interface MessageHandler { Ssortingng value() } public interface Handler { void handle(MqttMessage message); } 

et maintenant, utilisons cette annotation dans une classe:

 @MessageHandler("myTopic") public class MyTopicHandler implements Handler { @override public void handle(MqttMessage message) { // do something with the message } } 

La dernière partie consiste à écrire la classe MessageHandler. Dans cette classe, il vous suffit de stocker dans une carte toutes vos instances de gestionnaires, la clé de la carte sera le nom du sujet et la valeur et l’instance de la classe. Fondamentalement, la méthode getMessageHandler ressemblera à:

 public Handler getMessageHandler(Ssortingng topic) { if (handlerMap == null) { loadClassesFromClassPath(); // This method should load from classpath all the classes with the annotation MessageHandler and build the handlerMap } return handlerMap.get(topic); } 

La bonne chose avec cette solution, c’est que si vous devez append un nouveau gestionnaire pour un nouveau message, il vous suffit d’écrire la nouvelle classe, d’append l’annotation à la classe et c’est tout. Le rest du code rest le même car il est générique et utilise la reflection pour charger et créer des instances.

J’espère que ça vous aide un peu.