web-dev-qa-db-fra.com

Injection de plusieurs implémentations avec injection de dépendances

Je travaille actuellement sur un projet ASP.NET Core et je souhaite utiliser la fonctionnalité intégrée d'Injection de dépendances (DI).

Eh bien, j'ai commencé avec une interface:

ICar
{
    string Drive();
}

et souhaitez implémenter l'interface ICar plusieurs fois comme

public class BMW : ICar
{
    public string Drive(){...};
}

public class Jaguar : ICar
{
    public string Drive(){...};
}

et ajoutez ce qui suit dans la classe Startup

public void ConfigureServices(IServiceCollection services)
{
     // Add framework services.
     services.AddMvc();
     services.AddTransient<ICar, BMW>(); 
     // or 
     services.AddTransient<ICar, Jaguar>();
 }

Maintenant, je dois prendre une décision entre deux implémentations et ma classe décidée sera définie dans chaque constructeur qui a besoin d'une implémentation ICar. Mais mon idée était de dire, si le contrôleur demandé est BMWController, alors utilisez l'implémentation BMW ou utilisez Jaguar si le JaguarController est demandé .

Sinon, DI n'a pas de sens pour moi. Comment puis-je gérer ce problème correctement?

Pour mieux comprendre mon problème, jetez un œil à cette photo: https://media-www-asp.azureedge.net/media/44907/dependency-injection-golf.png ? raw = true Comment fonctionne le résolveur de dépendances et où puis-je le configurer dans ASP.NET Core?

Dans Unity, il est possible de créer quelque chose comme ça container.RegisterType<IPerson, Male>("Male");container.RegisterType<IPerson, Female>("Female");

et appeler le bon type comme celui-ci

[Dependency("Male")]IPerson malePerson
13
DrScrum Milestone

La fonctionnalité que vous recherchez n'est pas facile à implémenter, du moins lorsque vous l'utilisez dans le contrôleur car les contrôleurs sont traités un peu spécialement (par défaut, les contrôleurs ne sont pas enregistrés avec ServiceCollection et ne sont donc pas résolus/instancié par le conteneur et plutôt instancié par ASP.NET Core lors de la requête, voir aussi l'explication et l'exemple sur ma réponse associée ).

Avec le conteneur IoC intégré, vous ne pouvez le faire que via la méthode d'usine, ici avec un exemple sur une classe BmwCarFactory:

services.AddScoped<ICar, BmwCar>();
services.AddScoped<BmwCar>();
services.AddScoped<BmwCarFactory>(p => new BmwCarFactory(p.GetRequiredService<BmwCar>())));

Le conteneur IoC par défaut est intentionnellement simple pour fournir les bases de l'injection de dépendances pour vous permettre de démarrer et pour que d'autres conteneurs IoC puissent facilement s'y connecter et remplacer l'implémentation par défaut.

Pour les scénarios plus avancés, les utilisateurs sont encouragés à utiliser l'IoC de leur choix qui prend en charge des fonctionnalités plus avancées (analyse d'assemblage, décorateurs, dépendances conditionnelles/paramétrées, etc.

AutoFac (que j'utilise dans mes projets) prend en charge ces scénarios avancés. Dans la documentation AutoFac, il y a 4 scénarios (en tout avec le 3ème que @pwas a suggéré dans les commentaires):

1. Repensez vos cours

Nécessite des frais supplémentaires pour refactoriser votre code et la hiérarchie des classes, mais simplifie considérablement la consommation des services injectés

2. Modifiez les inscriptions

Les documents le décrivent ici , si vous ne voulez pas ou ne pouvez pas changer le code.

// Attach resolved parameters to override Autofac's
// lookup just on the ISender parameters.
builder.RegisterType<ShippingProcessor>()
       .WithParameter(
         new ResolvedParameter(
           (pi, ctx) => pi.ParameterType == typeof(ISender),
           (pi, ctx) => ctx.Resolve<PostalServiceSender>()));
builder.RegisterType<CustomerNotifier>();
       .WithParameter(
         new ResolvedParameter(
           (pi, ctx) => pi.ParameterType == typeof(ISender),
           (pi, ctx) => ctx.Resolve<EmailNotifier>()));
var container = builder.Build();

3. Utilisation de services à clé ( ici )

Il est assez similaire à l'approche précédente de 2. mais résout les services en fonction d'une clé, plutôt que de leur type concret

4. Utilisez les métadonnées

C'est assez similaire à 3. mais vous définissez les clés via l'attribut.

D'autres conteneurs comme nity ont des attributs spéciaux, comme DependencyAttribute que vous pouvez utiliser pour annoter la dépendance, comme

public class BmwController : Controller
{
    public BmwController([Dependency("Bmw")ICar car)
    {
    }
}

Mais cela et la 4ème option d'Autofac font fuir le conteneur IoC dans vos services et vous devriez considérer les autres approches.

Alternativement, vous créez des classes et des usines qui résolvent vos services en fonction de certaines conventions. Par exemple, un ICarFactory:

public ICarFactory
{
    ICar Create(string carType);
}

public CarFactory : ICarFactory
{
    public IServiceProvider provider;

    public CarFactory(IServiceProvider provider)
    {
        this.provider = provider;
    }

    public ICar Create(string carType)
    {
        if(type==null)
            throw new ArgumentNullException(nameof(carType));

        var fullQualifedName = $"MyProject.Business.Models.Cars.{carType}Car";
        Type carType = Type.GetType(fullQualifedName);
        if(carType==null)
            throw new InvalidOperationException($"'{carType}' is not a valid car type.");

        ICar car = provider.GetService(carType);
        if(car==null)
            throw new InvalidOperationException($"Can't resolve '{carType.Fullname}'. Make sure it's registered with the IoC container.");

        return car;
    }
}

Ensuite, utilisez-le comme

public class BmwController : Controller
{
    public ICarFactory carFactory;

    public BmwController(ICarFactory carFactory)
    {
        this.carFactory = carFactory;

        // Get the car
        ICar bmw = carFactory.Create("Bmw");
    }
}

Alternative à IServiceProvider

// alternatively inject IEnumerable<ICar>
public CarFactory : ICarFactory
{
    public IEnumerable<ICar> cars;

    public CarFactory(IEnumerable<ICar> cars)
    {
        this.cars = cars;
    }

    public ICar Create(string carType)
    {
        if(type==null)
            throw new ArgumentNullException(nameof(carType));

        var carName = ${carType}Car";
        var car = cars.Where(c => c.GetType().Name == carName).SingleOrDefault();

        if(car==null)
            throw new InvalidOperationException($"Can't resolve '{carName}.'. Make sure it's registered with the IoC container.");

        return car;
    }
}
19
Tseng