Accueil > Asp.Net MVC > Cycle de vie détaillé d’une requête MVC

Cycle de vie détaillé d’une requête MVC

Quand vous appelez une action dans une application MVC, tout se passe comme si vous invoquiez simplement une méthode de la classe contrôleur. Comment passe t’on d’une requête http à une invocation de méthode avec des arguments? Comme la magie n’existe pas, nous allons tenter de décrypter ici tout ce qui se passe lors du traitement d’une requête dans une application MVC. Dans quel ordre et dans quel but, que pouvons nous y dériver, étendre… Un petit voyage dans les rouages du framework toujours utile pour trouver des solutions élégantes à certaines problématiques récurrentes.

Nous mettrons ici de côté l’injection de dépendances qui (bien que très utile) introduit une complexité inutile pour la compréhension du cycle de vie. Sachez juste que toutes les interfaces représentant des points d’extension peuvent être résolues par injection de dépendance très facilement.

Le routage

Le premier mécanisme à se mettre en marche est le mécanisme de routage. Il va analyser la requête en fonction du contexte http et déterminer quel contrôleur et quelle action doit être appelée. Dans un contexte MVC, le but du routage est de créer un dictionnaire de données de route (RouteData) qui contient au minimum 2 entrées: « controller » et « action » (+ « area » si besoin). Ces 2 entrées permettront au système d’instancier le bon contrôleur et d’appeler la bonne action dans les prochaines étapes.

Le routage se présente sous la forme d’un module http : System.Web.Routing.UrlRoutingModule qui gère les résultats de routage mis en cache et un handler System.Web.Routing.UrlRoutingHandler qui gère la requête en elle même. Il s’appuie sur la liste des routes ajoutées dans la RouteTable. Chaque route contient un dictionnaire de contraintes qui spécifie les valeurs valides pour un paramètre d’url, un dictionnaire de DataTokens qui permet de stocker des valeurs personnalisées et un dictionnaire de données par défaut qui spécifie les valeurs par défaut des paramètres de l’url. C’est dans ce dernier dictionnaire qu’on trouvera le nom de l’action et du contrôleur à invoquer.

Chaque route fait également référence à un handler qui prendra en charge la suite de la requête. Quand on utilise MapRoute, on crée en fait une route en instanciant les 3 dictionnaires cités au dessus et on définit le handler MVC par défaut: MvcRouteHandler. Il est possible de spécifier un autre handler si besoin en n’utilisant pas MapRoute et en créant directement un objet Route avec le handler choisi. Ce handler devra implémenter l’interface IRouteHandler. Le handler par défaut ne fait pas grand chose excepté définir le comportement des sessions dans le contexte http et passer la main à un autre handler qui gère l’exécution MVC en elle même: le MvcHandler.
Ce qui nous amène à la prochaine étape: la création du contrôleur.

Création du contrôleur

Nous avons à ce point un objet RequestContext contenant des données qui vont nous permettre de retrouver le contrôleur à instancier. Le MvcHandler s’appuie par défaut sur un objet DefaultControllerFactory qui contient la logique nécessaire pour instancier le bon contrôleur en fonction du contexte. Il est possible d’étendre cette factory en implémentant l’interface IControllerFactory et en déclarant votre type dans l’Application_Start comme ceci:

ControllerBuilder.Current.SetDefaultControllerFactory(typeof(MyCustomFactory))

Cette technique permet de fixer des propriétés communes à tout vos contrôleurs plus élégamment qu’avec une classe abstraite.

La ControllerFactory contient une méthode CreateController qui retourne une instance de controller à partir d’une string. Notre contrôleur est prêt! Il faut maintenant invoquer notre action: c’est le rôle de l’object ActionInvoker.

Invocation de l’action

A ce point, il reste encore beaucoup de travail à faire avant de sortir notre résultat. L’ActionInvoker orchestre le reste de l’exécution. Il a beaucoup de responsabilités allant de la localisation de l’action à appeler jusqu’à l’exécution des filtres et des résultats en passant par la gestion des erreurs. L’interface à laquelle il obéit (IActionInvoker) est pourtant très simple et ne contient qu’une seule méthode InvokeAction. Le type par défaut utilisé dans MVC est ControllerActionInvoker et il est affecté au contrôleur via la méthode CreateActionInvoker() qui est protected et donc surchargeable par vos implémentations de contrôleur. Les différentes méthodes de la classe ControllerActionInvoker sont également extensibles si besoin.

La première méthode intéressante de cette classe est la méthode FindAction qui renvoie un objet ActionDescriptor représentant l’action à exécuter. A ce point, nous savons quelle action doit être appelée.
L’étape suivante consiste à récupérer tous les filtres qui s’appliquent à l’action. Il en existe de 4 types:
Les filtres d’autorisation (AuthorizationFilters), les filtres d’erreur (ExceptionFilters), les filtres d’exécution de l’action (ActionFilters) et les filtres d’exécution du résultat (ResultFilters).
Les premiers à être appelés sont les filtres d’autorisation: si l’utilisateur n’est pas autorisé à exécuter cette action, la requête s’arrête là et renvoie une erreur 403.
Si l’utilisateur est autorisé, les choses sérieuses peuvent commencer!

Mapping des arguments de l’action

Si l’action a des arguments, il faut leur donner une valeur. Pour cela, l’ActionIvoker fait appel à un objet ModelBinder. L’application contient une collection de model binders chacun associés à un type pour remplir ce travail.
Chaque ModelBinder obéit à l’interface IModelBinder qui ne contient qu’une seule méthode:
object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext)
Cette méthode est appelée pour chaque paramètre sur le ModelBinder correspondant à son type. Pour vos propres types, vous pouvez si besoin implémenter votre propre ModelBinder et l’ajouter à la collection de l’application dans Application_Start via:
ModelBinders.Binders.Add(typeof (MyType), new MyModelBinder())
Le ModelBinder instancie un objet et en remplit les propriétés mais où trouve-t’il les valeurs? La réponse se trouve dans le 2me argument de BindModel. L’objet ModelBindingContext contient un objet ValueProvider qui tient ce rôle.
L’application contient une collection d’objets de type ValueProviderFactory dont le but est d’instancier des IValueProvider. Ces objets permettent de faire le lien entre les élements de la requête http de base (query string, form,…)et l’objet qu’on est en train de binder. Imaginons qu’on aie un argument nommé « id » dans notre action. Le ModelBinder essaiera de retrouver sa valeur et demander aux ValueProviders si ils peuvent identifier la valeur correspondante (par exemple est-ce qu’un des champs du formulaire s’appelle « id »? est-ce que la query string contient la clé « id »? est-ce qu’un fichier a été uploadé avec l’identifiant « id »?). Les ValueProviders sont interrogés séquentiellement et le premier à répondre favorablement donne sa valeur à l’argument. Cet appel se fait via:
bindingContext.ValueProvider.GetValue(« id »).AttemptedValue

Il existe de base plusieurs ValueProviderFactory:

  • System.Web.Mvc.ChildActionValueProviderFactory
  • System.Web.Mvc.JsonValueProviderFactory
  • System.Web.Mvc.FormValueProviderFactory
  • System.Web.Mvc.HttpFileCollectionValueProviderFactory
  • System.Web.Mvc.QueryStringValueProviderFactory
  • System.Web.Mvc.RouteDataValueProviderFactory

Vous pouvez également créer les vôtres et les ajouter à la collection en ajoutant dans Application_Start:
ValueProviderFactories.Factories.Add(new MyValueProviderFactory())
Créer les votres peut être utile j’ai récemment eu à implémenter un CookieValueProvider qui évite pas mal de code de récupération de cookies dans les actions par exemple.

Exécution de l’action

Tout ça terminé, l’ActionInvoker lance l’exécution de l’action. Cette exécution commence par l’appel des méthodes OnActionExecuting de tous les ActionFilters, puis de l’action en elle même et enfin des méthodes OnActionExecuted des ActionFilters. Rien de particulier ici c’est de l’exécution standard.
On récupère à la fin un objet de type ActionResult contenant le résultat de l’action.

Résultat de l’action

Le résultat de l’action peut être de n’importe quel type dérivant de ActionResult. Il en existe beaucoup dont le ViewResult qui permet de renvoyer l’affichage d’une page à l’utilisateur. Le principe de tous les résultats est le même, ils contiennent une méthode ExecuteResult qui donne le résultat final. et la réponse est ensuite envoyée au client. De la même manière que les ActionFilters, les ResultFilters s’exécutent avant et après ExecuteResult.
Penchons nous plus particulièrement sur les ViewResult qui déclenchent le ViewEngine pour rendre une page.

ViewResult et ViewEngine

Le ViewResult est celui qui permet d’exécuter une page razor pour la renvoyer au client. Il s’appuie pour cela sur le ViewEngine. L’exécution se passe en plusieurs étapes:
En premier lieu, le ViewResult demande au ViewEngine quelle vue va devoir être exécutée en fonction du contexte via la méthode FindView. Cette méthode renvoit un objet ViewEngineResult contenant une vue qui implémente l’interface IView. IView contient la méthode Render qui permet d’exécuter la vue et d’écrire le résultat dans un TextWriter qui est ici lié directement au flux de réponse. Comme le ViewEngineResult utilisé est renvoyé directement par le ViewEngine, la méthode Render utilisée est bien celle du ViewEngine correspondant. Cela permet à l’objet ViewResult d’être indépendant du ViewEngine utilisé. Le plus connu est bien évidemment le RazorViewEngine mais vous pouvez aussi implémenter les vôtres comme nous l’avions fait dans cet article sur les vues mobiles

Conclusion

C’est la fin de notre petit voyage dans les entrailles de MVC. Je reviendrai prochainement sur chaque partie plus en détail avec des exemples d’extension plus concrets. Pour ceux qui seront arrivés au bout de la lecture de ce post très théorique, Merci!

  1. jbbi
    12/06/2012 à 2:06

    Très intéressant, un ou plusieurs schéma aurait surement été le bienvenue !

  2. eldorrado
    03/07/2012 à 9:45

    très enrichissant, j’attends les schémas avec impatience!

  3. Frederic Simonet
    09/03/2013 à 11:12

    Bonjour, j’ai lu dans plusieurs articles dont un sur Ruby on Rails que c’était le contrôleur qui générait la réponse au client. Qu’en est-il ?

    • 10/03/2013 à 10:19

      Bonjour,
      Le contrôleur génère en fait un objet de type ActionResult (selon l’action ça peut être un ViewResult, PartialViewResult, JSonResult… tous dérivent de ActionResult). C’est la méthode ExecuteResult de cet ActionResult qui crée la réponse qui sera envoyée au client en fonction de ce que lui a passé le contrôleur. Je ne sais pas si le mécanisme est le même en RoR.

  4. 10/01/2014 à 11:31

    Merci Christophe pour ces explications.
    Mais comme le dit eldorrado, un petit schéma permettrait de comprendre encore plus vite.

    En tout cas c’est du bon boulot de synthèse.

  1. 02/07/2014 à 10:53

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s