web-dev-qa-db-fra.com

Injection du constructeur du contrôleur de base dans ASP.NET MVC avec Unity

J'ai un contrôleur de base dans mon projet MVC 5 qui implémente certaines fonctionnalités partagées. Cette fonctionnalité nécessite des dépendances. J'utilise Unity 3 pour injecter ces implémentations dans mes contrôleurs, un modèle qui a bien fonctionné jusqu'à ce que mes contrôleurs soient passés à l'héritage de ce contrôleur de base. Maintenant, je rencontre le problème suivant:

public class BaseController : Controller
{
    private readonly IService _service;

    public BaseController(IService service)
    {
        _service = service;
    }
}

public class ChildController : BaseController
{
    private readonly IDifferentService _differentService;

    public ChildController(IDifferentService differentService)
    {
        _differentService = differentService;
    }
}

Cela génère naturellement une erreur de 'BaseController' does not contain a constructor that takes 0 arguments. Unity ne résout pas la construction du BaseController, il ne peut donc pas y injecter les dépendances. Je vois deux manières évidentes de résoudre ce problème:

1.) Appelez explicitement le contrôleur BaseController et demandez à chaque contrôleur ChildController d'injecter les dépendances de BaseController

public class ChildController : BaseController
{
    private readonly IDifferentService _differentService;

    public ChildController(IDifferentService differentService,
                           IService baseService)
        : base(baseService)
    {
        _differentService = differentService;
    }
}

Je n’apprécie pas cette approche pour plusieurs raisons: premièrement, parce que les ChildControllers n’utilisent pas les dépendances supplémentaires (ce qui provoque une surcharge du constructeur dans les contrôleurs enfants sans raison), et plus important encore, si je modifie jamais la signature du constructeur du contrôleur de base, je dois modifier les signatures de constructeur de chaque contrôleur enfant.

2.) Implémenter les dépendances du BaseController via l'injection de propriété

public class BaseController : Controller
{
    [Dependency]
    public IService Service { get; set; }

    public BaseController() { }
}

J'aime mieux cette approche - je n'utilise aucune des dépendances dans le code constructeur du BaseController - mais cela rend la stratégie d'injection de dépendance du code incohérente, ce qui n'est pas idéal non plus.

Il existe probablement une approche encore meilleure qui implique une sorte de fonction de résolution des dépendances BaseController qui appelle le conteneur Unity pour trier la signature de la méthode du ctor, mais avant de commencer à écrire quelque chose de trop complexe, je me demandais si quelqu'un avait déjà résolu ce problème? J'ai trouvé quelques solutions sur le Web, mais il s'agissait de solutions de contournement telles que Service Locator, que je ne souhaite pas utiliser.

Merci!

27
NWard

La première chose à comprendre est que vous n’instanciez pas le contrôleur de base. Vous instanciez le contrôleur enfant, qui hérite de l'interface et des fonctionnalités des contrôleurs de base. Cette distinction est importante. Lorsque vous dites "les ChildControllers ne font pas usage des dépendances supplémentaires", vous vous trompez complètement. Parce que ChildControllerEST ÉGALEMENTBaseController. Il n'y a pas deux classes différentes créées. Juste une classe qui implémente les deux fonctionnalités.

Donc, depuis ChildController IS A BaseController, il n’ya rien d’erreur ou d’étrange à propos de la passation de paramètres dans le constructeur des contrôleurs enfants qui appelle le constructeur des classes de base. C'est comme cela que cela devrait être fait.

Si vous changez de classe de base, vous devrez probablement changer de toute façon vos classes d'enfants. Il n'y a aucun moyen d'utiliser l'injection de constructeur pour injecter des dépendances de classe de base qui ne sont pas incluses dans la classe enfant.

Je ne recommande pas l'injection de propriété, car cela signifie que vos objets peuvent être créés sans une initialisation appropriée et que vous devez vous rappeler de les configurer correctement.

BTW, les termes appropriés sont Subclass et Superclass. Un "enfant" est une sous-classe, le parent est la "superclasse".

28
Erik Funkenbusch

Avec ASP.Net 5 et sa construction en DI

public class BaseController : Controller
{
    protected ISomeType SomeMember { get; set; }

    public BaseController(IServiceProvider serviceProvider)
    {
        //Init all properties you need
        SomeMember = (SomeMember)serviceProvider.GetService(typeof(ISomeMember));
    }
}

public class MyController : BaseController  
{
public MyController(/*Any other injections goes here*/, 
                      IServiceProvider serviceProvider) : 
 base(serviceProvider)
{}
}

METTRE À JOUR

Il existe également une méthode d'extension dans Microsoft.Extensions.DependencyInjection pour la raccourcir

SomeMember = serviceProvider.GetRequiredService<ISomeMember>();
11
Vitaly

J'ai adopté une approche quelque peu différente (mais pour moi assez évidente et probablement commune). Cela fonctionne pour moi, mais il y a peut-être des pièges dont je ne suis pas au courant.

J'ai créé des propriétés de service communes dans ma classe BaseController que la plupart de mes contrôleurs utilisent. Ils sont instanciés quand ils sont nécessaires/référencés. 

S'il y a un service dont un contrôleur particulier a moins besoin, je l'injecte dans le constructeur de ce contrôleur normalement.

using JIS.Context;
using JIS.Managers;
using JIS.Models;
using JIS.Services;
using System;
using System.Collections.Generic;
using System.Web;
using System.Web.Mvc;

namespace JIS.Controllers
{
    public class BaseController : Controller
    {
        public BaseController()
        {
            ViewBag.User = UserManager?.User;
        }

        private IUserManager userManager;
        public IUserManager UserManager
        {
            get
            {
                if (userManager == null)
                {
                    userManager = DependencyResolver.Current.GetService<IUserManager>();
                }
                return userManager;
            }
            set
            {
                userManager = value;
            }
        }

        private ILoggingService loggingService;
        public ILoggingService LoggingService
        {
            get
            {
                if (loggingService == null)
                {
                    loggingService = DependencyResolver.Current.GetService<ILoggingService>();
                }
                return loggingService;
            }
            set { loggingService = value; }
        }

        private IUserDirectory userDirectory;
        public IUserDirectory UserDirectory
        {
            get
            {
                if (userDirectory == null)
                {
                    userDirectory = DependencyResolver.Current.GetService<IUserDirectory>();
                }
                return userDirectory;
            }
            set { userDirectory = value; }
        }

        private ApplicationDbContext appDb;
        public ApplicationDbContext AppDb
        {
            get
            {
                if (appDb == null)
                {
                    appDb = new ApplicationDbContext();
                }
                return appDb;
            }
            set
            {
                appDb = value;
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && appDb != null)
            {
                appDb.Dispose();
            }
            base.Dispose(disposing);
        }
    }
}

Dans mon contrôleur, je sous-classe simplement à partir de ce BaseController:

public class UsersController : BaseController
{
    // and any other services I need are here:
    private readonly IAnotherService svc;
    public UsersController(IAnotherService svc)
    {
        this.svc = svc;
    }
...
}

De cette façon, des services communs sont générés à la volée quand ils en ont besoin et sont disponibles pour mes contrôleurs sans beaucoup de passe-partout.

0
DBatesX