web-dev-qa-db-fra.com

Injection de dépendances dans les filtres d'action ASP.NET MVC 3. Quel est le problème avec cette approche?

Voici la configuration. Disons que j'ai un filtre d'action qui a besoin d'une instance d'un service:

public interface IMyService
{
   void DoSomething();
}

public class MyService : IMyService
{
   public void DoSomething(){}
}

J'ai ensuite un ActionFilter qui a besoin d'une instance de ce service:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService; // <--- How do we get this injected

   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _myService.DoSomething();
       base.OnActionExecuting(filterContext);
   }
}

Dans MVC 1/2, l'injection de dépendances dans des filtres d'action était un peu pénible. L'approche la plus courante consistait à utiliser un invocateur d'actions personnalisées comme on peut le voir ici: http://www.jeremyskinner.co.uk/2008/11/08/dependency-injection-with-aspnet-mvc-action -filters / La principale motivation derrière cette solution de contournement était que cette approche suivante était considérée comme un couplage bâclé et serré avec le conteneur:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService;

   public MyActionFilter()
      :this(MyStaticKernel.Get<IMyService>()) //using Ninject, but would apply to any container
   {

   }

   public MyActionFilter(IMyService myService)
   {
      _myService = myService;
   }

   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _myService.DoSomething();
       base.OnActionExecuting(filterContext);
   }
}

Ici, nous utilisons l'injection de constructeur et la surcharge du constructeur pour utiliser le conteneur et injecter le service. Je suis d'accord que cela couple étroitement le conteneur avec l'ActionFilter.

Ma question est la suivante: maintenant, dans ASP.NET MVC 3, où nous avons une abstraction du conteneur utilisé (via le DependencyResolver), tous ces cerceaux sont-ils toujours nécessaires? Permettez-moi de démontrer:

public class MyActionFilter : ActionFilterAttribute
{
   private IMyService _myService;

   public MyActionFilter()
      :this(DependencyResolver.Current.GetService(typeof(IMyService)) as IMyService)
   {

   }

   public MyActionFilter(IMyService myService)
   {
      _myService = myService;
   }

   public override void OnActionExecuting(ActionExecutingContext filterContext)
   {
       _myService.DoSomething();
       base.OnActionExecuting(filterContext);
   }
}

Maintenant, je sais que certains puristes pourraient se moquer de cela, mais sérieusement, quel serait l'inconvénient? Il est toujours testable car vous pouvez utiliser le constructeur qui prend un IMyService au moment du test et injecter un service simulé de cette façon. Vous n'êtes lié à aucune implémentation du conteneur DI puisque vous utilisez DependencyResolver, y a-t-il des inconvénients à cette approche?

Soit dit en passant, voici une autre bonne approche pour le faire dans MVC3 en utilisant la nouvelle interface IFilterProvider: http://www.thecodinghumanist.com/blog/archives/2011/1/27/structuremap-action-filters-and-dependency -injection-in-asp-net-mvc-

77
BFree

Je ne suis pas positif, mais je crois que vous pouvez simplement utiliser un constructeur vide (pour l'attribut ) et avoir ensuite un constructeur qui injecte réellement la valeur (pour le filtre partie). *

Edit : Après un peu de lecture, il semble que la façon acceptée de le faire soit via l'injection de propriété:

public class MyActionFilter : ActionFilterAttribute
{
    [Injected]
    public IMyService MyService {get;set;}

    public override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        MyService.DoSomething();
        base.OnActionExecuting(filterContext);
    }
}

Concernant la question pourquoi ne pas utiliser un localisateur de service: Cela réduit principalement la flexibilité de votre injection de dépendance. Par exemple, que se passe-t-il si vous injectez un service de journalisation et que vous souhaitez donner automatiquement au service de journalisation le nom de la classe dans laquelle il est injecté? Si vous utilisez l'injection de constructeur, cela fonctionnerait très bien. Si vous utilisez un résolveur de dépendance/localisateur de service, vous n'auriez pas de chance.

Mise à jour

Étant donné que cela a été accepté comme réponse, je voudrais dire officiellement que je préfère approche de Mark Seeman car cela sépare la responsabilité du filtre d'action de l'attribut. De plus, l'extension MVC3 de Ninject propose des moyens très puissants de configurer des filtres d'action via des liaisons. Voir les références suivantes pour plus de détails:

Mise à jour 2

Comme @usr l'a souligné dans les commentaires ci-dessous, ActionFilterAttributes sont instanciés lorsque la classe est chargée, et ils durent toute la durée de vie de l'application. Si l'interface IMyService n'est pas censée être un Singleton, elle finit par être Captive Dependency . Si son implémentation n'est pas thread-safe, vous pourriez être très pénible.

Chaque fois que vous avez une dépendance avec une durée de vie plus courte que la durée de vie prévue de votre classe, il est sage d'injecter une usine pour produire cette dépendance à la demande, plutôt que de l'injecter directement.

30
StriplingWarrior

Oui, il y a des inconvénients, comme il y a beaucoup de problèmes avec IDependencyResolver lui-même, et à ceux-ci, vous pouvez ajouter l'utilisation d'un Singleton Localisateur de services, ainsi que Injection bâtarde .

Une meilleure option consiste à implémenter le filtre en tant que classe normale dans laquelle vous pouvez injecter les services que vous souhaitez:

public class MyActionFilter : IActionFilter
{
    private readonly IMyService myService;

    public MyActionFilter(IMyService myService)
    {
        this.myService = myService;
    }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if(this.ApplyBehavior(filterContext))
            this.myService.DoSomething();
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    {
        if(this.ApplyBehavior(filterContext))
            this.myService.DoSomething();
    }

    private bool ApplyBehavior(ActionExecutingContext filterContext)
    {
        // Look for a marker attribute in the filterContext or use some other rule
        // to determine whether or not to apply the behavior.
    }

    private bool ApplyBehavior(ActionExecutedContext filterContext)
    {
        // Same as above
    }
}

Remarquez comment le filtre examine le filterContext pour déterminer si le comportement doit être appliqué ou non.

Cela signifie que vous pouvez toujours utiliser des attributs pour contrôler si le filtre doit être appliqué ou non:

public class MyActionFilterAttribute : Attribute { }

Cependant, maintenant cet attribut est complètement inerte.

Le filtre peut être composé avec la dépendance requise et ajouté aux filtres globaux dans global.asax:

GlobalFilters.Filters.Add(new MyActionFilter(new MyService()));

Pour un exemple plus détaillé de cette technique, bien qu'appliquée à l'API Web ASP.NET au lieu de MVC, consultez cet article: http://blog.ploeh.dk/2014/06/13/passive-attributes =

91
Mark Seemann

La solution suggérée par Mark Seemann semble élégante. Cependant assez complexe pour un problème simple. L'utilisation du cadre en implémentant AuthorizeAttribute semble plus naturelle.

Ma solution a été de créer un AuthorizeAttribute avec une fabrique de délégués statique à un service enregistré dans global.asax. Il fonctionne pour n'importe quel conteneur DI et se sent légèrement mieux qu'un localisateur de service.

Dans global.asax:

MyAuthorizeAttribute.AuthorizeServiceFactory = () => Container.Resolve<IAuthorizeService>();

Ma classe d'attributs personnalisés:

[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
public class MyAuthorizeAttribute : AuthorizeAttribute
{
    public static Func<IAuthorizeService> AuthorizeServiceFactory { get; set; } 

    protected override bool AuthorizeCore(HttpContextBase httpContext)
    {
        return AuthorizeServiceFactory().AuthorizeCore(httpContext);
    }
}
6
Jakob