Nouvelles Chroniques d'Amethyste

Penser au Sens, pas au Verbe

Les projets ASP.NET 5 – partie 2/4

Poster un commentaire

Important 28/11/2016: Remise à jour et beaucoup améliorations de l’article d’origine. La version testée est Asp.Net Core 1.0
On a présenté l’architecture d’un projet Core dans l’article qui précède, voyons maintenant ce qui change du côté du site.

MVC 6: l’ère de l’unification

MVC rejoint maintenant WebApi dans la liste des middlewares OWIN et de fait les deux modèles fusionnent. On retrouve comme avant le modèle MVC avec les même vue/modèle et contrôleur, mais l’architecture sous-jacente n’est plus orientée spécifiquement IIS. En fait on dissocie l’hébergement de l’application de son socle technique qui est ici http.
Le nouveau cycle de vie d’une requête devient:
as41
Je pense que le flux est simple à comprendre, à l’exception de la première étape qui demande quelques commentaires.
Cassini disparaît et laisse la place à Kestrel, le nouveau serveur web pour Asp.Net basé sur la librairie open-source libuv.
L’intérêt de Kestrel est d’être multi-plateforme.
Il est bien entendu toujours possible de lancer le projet dans d’autres hôtes comme IIS.
En réalité Core propose en standard 3 hôtes d’hébergement:
  1. IISExpress
  2. Kestrel
  3. WebListener
Nous verrons plus loin comment les mettre en œuvre avec les commandes.
Le mieux est je pense de partir d’un projet vide et de le remonter étape par étape. Ainsi donc on sélectionne:
2016-11-28_13-45-54
Et le point de départ pour la suite est:
2016-11-28_13-47-09
Si on fait F5 on constate que quelque chose s’affiche:
as44
Pourtant on n’a pas de contrôleur, c’est même un projet vide.
Le centre nerveux d’un projet MVC 6 est OWIN. On s’attend donc à une classe appelé par défaut Startup qui expose une méthode Configure qui elle-même attend une instance de IApplicationBuilder.
C’est la méthode qui permet de configurer le pipeline OWIN. Tout ce qui n’est pas déclaré dans le pipeline n’existe pas et ne sera pas chargé. Cette approche par modules appelés middleware est très différente des approches précédentes dans laquelle on chargeait un gigantesque pipeline web et on y puisait ce dont on avait besoin.
Le pipeline fournit par défaut est celui-ci:
2016-11-28_13-49-13
La ligne significative pour l’instant est la dernière qui retourne le message que nous avons vu lors du test. Tel quel ce n’est guère utile, mais on va le faire évoluer…
Note: En l’état le projet va être exécuté sous Kestrel. Si vous souhaitez lancer le projet sous IIS un petit tuto de Rick Strahl devrait vous aider:
Si la partie du code avec app.Run() ne vous parle pas vraiment, je conseille de lire cet article auparavant:
Notez également que VS propose des templates tout fait pour créer des middlewares. Le même template que celui proposé dans l’article.
2016-11-28_14-10-47
Pour la suite supprimez les lignes en app.Run().

Pages statiques

Faisons une première tentative simple avec des pages statiques.
On commence par ajouter le package Nuget Microsoft.AspNetCore.StaticFiles.
2016-11-28_14-25-09
Puis on ajoute le middleware qui va bien:
public void Configure(IApplicationBuilder app)
{
   app.UseStaticFiles();
}
 Evidemment on n’oublie pas d’ajouter un fichier html statique dans wwwroot, disons default.html. Il ne reste plus qu’à tester.
as47
Repérez bien l’url. On confirme bien que le répertoire racine est wwwroot.

MVC

 La première étape est d’ajouter la dépendance nécessaire à Microsoft.AspNetCore.Mvc:
2016-11-28_14-28-31
Après un Restore Packages tout est OK.
L’architecture MVC est globalement la même avec MVC 6. Il y a surtout des ajouts, on a donc pas grand-chose à réapprendre. On commence comme avant par créer des répertoires Controllers, Views et Models:
as49
On construit le contrôleur HomeController. On peut le construire à la main comme une classe normale ou bien utiliser un des nouveaux templates:
2016-11-28_14-30-44
Au final on va partir de ce code:
public class HomeController : Controller
{
   public IActionResult Index()
   {
      return Content("Hello Amethyste");
   }
}
MVC passe aussi par des middlewares. Rendez-vous donc dans Startup pour ajouter le middleware MVC:

public void Configure(IApplicationBuilder app)
{
   app.UseStaticFiles();

   app.UseMvc(routes =>
   {
      routes.MapRoute(
      name: "default",
      template: "{controller=Home}/{action=Index}/{id?}");
   });
}

Nous reparlerons plus loin de la définition de la route.
Il ne reste plus qu’à lancer et voir ce qui se passe:
2016-11-28_14-32-46
Nous découvrons le nouveau formulaire par défaut d’affichage des messages d’erreur!
Tout en bas des informations systèmes utiles.
Ce qui nous intéresse est le message d’erreur. Habituez-vous y car vous allez le voir souvent! C’est là que se niche en effet une différence importante avec les versions précédentes.
Voyons donc comment résoudre le problème.

L’injection de dépendances

MVC et WebApi ont toujours supporté l’inversion de contrôle sous la forme du pattern d’injection de dépendance (presque toujours dans le cas de MVC), mais il était facultatif. Maintenant il est obligatoire.
La plomberie est apportée par l’ajout de la méthode ConfigureServices() évoquée dans le message d’erreur. On doit donc ajouter ceci dans la classe Startup, mais normalement le template le fait pour vous.
public void ConfigureServices(IServiceCollection services)
{
}
Microsoft fournit un framework d’injection prêt à l’emploi supporté par IServiceCollection, mais il est possible de choisir le framework de son choix. On a juste à ajouter ceci:
public void ConfigureServices(IServiceCollection services)
{
   services.AddMvc();
}
Qui ajoute le service MVC au conteneur d’injection de dépendances. On reteste.
Cette fois nous avons bien l’affichage attendu:
 as52
Vous découvrirez les différentes variantes des méthodes du type services.AddXXX(). Qui permettent de gérer différents scénarios comme la création de scope ou de singleton.
Voici une petite bibliographie:
Si vous souhaitez utiliser un framework d’injection autre:
http://developer.telerik.com/featured/dependency-injection-in-asp-net-mvc6/
Je fais simplement remarquer à l’auteur qu’il est possible d’injecter avec les propriétés. Il y a un attribut que nous découvrirons plus loin!!!
La discussion autour de cet article est intéressante.
Un dernier tuto très bien fait comme j’aime avec des tas de dessins:
http://docs.asp.net/en/latest/fundamentals/dependency-injection.html

Routage en MVC

 Revenons sur la déclaration de la route:
app.UseMvc(routes =>
{
   routes.MapRoute(
   name: "default",
   template: "{controller=Home}/{action=Index}/{id?}");
});
On remarque une nouvelle syntaxe des valeurs par défaut nettement plus intuitive.
Note: selon diverses démos que j’ai pu voir c’est aussi la route par défaut et un simple app.UseMvc() devrait suffire. Malheureusement pas moyen d’y arriver chez moi!? Par contre la route par défaut des WebApi fonctionne bien.
Bien entendu dans une application réelle on peut avoir plusieurs routes à mettre en place. La déclaration dans le middleware est possible en appelant plusieurs fois MapRoute() dans la lambda.
MVC 6 supporte une autre méthode bien plus claire. On la connaît déjà puisqu’elle provient de MVC 5: les attributs.
[Route("Home")]
public class HomeController : Controller
{
   [Route("Default")]
   public IActionResult Default()
   {
      return Content("Hello Amethyste");
   }
}
 MVC 6 apporte toutefois une évolution de la syntaxe qui peut devenir:
[Route("[Controller]")]
public class HomeController : Controller
{
   [Route("[Action]")]
   public IActionResult Default()
   {
      return Content("Hello Amethyste");
   }
}
Repérez les crochets ([ ]).
La différence est que même si je renomme le contrôleur ou la méthode d’action, je n’ai pas à modifier la route, juste l’url. Bien sûr, si je souhaite déclarer un contrôleur et une action par défaut:
[Route("[Controller]"), Route("")]
public class HomeController : Controller
{
   [Route("[Action]"), Route("")]
   public IActionResult Index()
   {
      return Content("Hello Amethyste");
   }
}
Si le routage vous intéresse (comme si on pouvait ne pas se sentir concerné!) je recommande cet article qui approfondit les choses:

Configurer l’application

Le modèle de gestion de la configuration d’une appli Asp.Net traditionnelle reposait sur une hiérarchie de fichier XML qui partaient du niveau machine, jusqu’au niveau d’un sous répertoire du site. Bref compliqué.
MVC 6 change les choses. La configuration est prise en charge uniquement dans Startup. Aucun fichier caché, pas de fichiers planqués quelque part ailleurs que dans l’application.
Note: le SDK associé a changé à quasiment chaque nouvelle version et changé en profondeur. Attendez vous à ramer un peu pour faire fonctionner quelque chose.
Il s’agit bien entendu d’un fichier JSON, son nom peut être celui que vous souhaitez. Appelons le config.json par exemple et ajoutons le au projet avec ce contenu:

{
"Message": "Message in the bottle"
}

On peut déclarer une structure plus complexe avec des sous-objets, peu importe. Vous déclarez ensuite un POCO qui permettra de désérialiser le json:
public class AppSettings
{
   public string Message { get; set; }
}
MVC fournit un framework (les espaces de noms en Microsoft.Extensions.*) pour lire les fichiers de configuration que l’on doit ajouter en tant que dépendance:
2016-11-28_17-00-11
Puis on complète Startup et son constructeur ainsi:
var builder = new ConfigurationBuilder()
   .SetBasePath(env.ContentRootPath)
   .AddJsonFile("config.json", optional: true, reloadOnChange: true)
   .AddEnvironmentVariables();

Configuration = builder.Build();

On commence par créer une fabrique de gestionnaire de configuration avec ConfigurationBuilder. A l’aide de ses différentes méthodes Add on lui ajoute des sources pour trouver ses configurations. Ici on lui donne accès à config.json et aux variables d’environnement.

SetBasePath indique où ConfigurationManager ira chercher les fichiers de configuration. Ici il s’agit de l’emplacement à la racine du projet. On peut préférer déployer le fichier en même temps que les exécutables, on utilisera plutôt:

SetBasePath(System.AppContext.BaseDirectory)

Et on complète project.json avec une déclaration CopyToOutput:

2016-11-28_17-03-35

Si une variable existe à la fois dans le fichier de configuration et l’environnement, c’est la variable d’environnement qui l’emporte. C’est très pratique pour les applications Azure Web Apps d’autant plus que l’on a plus de notion de transformation comme avec les fichiers web.config.
On construit ensuite le gestionnaire de configuration de type IConfigurationRoot que l’on ajoute pour rendre compilable le code précédent.
public IConfigurationRoot Configuration { get; }
Si on veut récupérer le fichier de configuration dans les contrôleurs (et c’est le cas en général), il est pratique de le faire par injection de dépendance, donc on complète ConfigureServices avec:
services.AddOptions();
services.Configure<AppSettings>(Configuration);
 Côté contrôleur on complète le constructeur ainsi:

AppSettings _appSettings;

public HomeController(IOptions<AppSettings> appSettings)
{
   _appSettings = appSettings.Value;
}

Et la méthode d’action:

public IActionResult Index()
{
   return Content(_appSettings.Message);
}

Maintenant il s’affiche:
2016-11-28_17-10-15

La classe Startup en détail

Startup est le point d’entrée de la nouvelle architecture. En fait ce n’est pas son seul nom car il existe une version de Startup par environnement. Et dans un environnement donné le moteur MVC recherche en priorité la version de l’environnement en cours.
Clarifions ce point. L’environnement est définit dans les propriétés du projet:
2016-11-28_17-14-12
L’environnement par défaut est donc Development.
On pourrait donc définir une version de Startup spécifique en créant StartupDevelopment. Faites un essai et constatez que c’est StartupDevelopment et pas Startup qui démarre.
On est pas obligé de créer une classe spécifique, on peut aussi créer juste une méthode Configure spécifique à l’environnement:
public void ConfigureDevelopment(IApplicationBuilder app)
{
}
Le contexte d’environnement peut être retrouvé en interrogeant la variable d’environnement ASPNET_ENV ou bien IHostingEnvironment. D’où sort cette interface?
Il existe plusieurs surcharges pour le constructeur de Startup:

public Startup()
{
}

public Startup(IApplicationEnvironment applicationEnvironment)
{
}

public Startup(IApplicationEnvironment applicationEnvironment, IHostingEnvironment hostingEnvironment)
{
}

La première ne pose bien sûr pas de difficultés:

  • IApplicationEnvironment
    Informations sur l’application comme son nom, sa version, le chemin physique vers l’application…
  • IHostingEnvironment
    Informations sur l’environnement d’hébergement de l’application
Il existe également deux surcharges de la méthode Configure:

public void Configure(IApplicationBuilder applicationBuildert)
{
}

public void Configure(IApplicationBuilder applicationBuilder, IHostingEnvironment hostingEnvironment)
{
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
}
MVC traite particulièrement les sémantiques d’environnement les plus courants, déjà en fournissant la classe:
public static class EnvironmentName
{
   public static readonly string Development = "Development";
   public static readonly string Staging = "Staging";
   public static readonly string Production = "Production";
}
Mais également des méthodes d’extensions à IHostingEnvironment:
  • IsDevelopment
  • IsStaging
  • IsProduction
Une des avancées de ce modèle est que l’environnement est fourni par une interface: IHostingEnvironment. Cela signifie qu’elle peut être injectée ce qui peut être nécessaire dans certains scénarios de tests unitaires.
Une bibliographie est peut-être intéressante:
http://docs.asp.net/en/latest/fundamentals/environments.html
Et un autre tuto extrêmement complet avec bien plus de choses que j’en ai abordé:
http://dotnetliberty.com/index.php/2015/10/18/asp-net-5-mvc6-configuration-in-9-steps/
D’ailleurs je recommande vraiment de suivre son blog, il est rempli de tutos super bien faits et très clairs comme je les aime.

WebApi

Si vous avez déjà installé la librairie MVC comme précédemment, il n’y a rien de plus à faire, on est prêt à créer un service WebApi.
2016-11-28_17-14-12
On sélectionne le template qui correspond et on obtient:
as57
On constate tout d’abord que la classe de base est Controller, la même que celle du contrôleur MVC. Le reste de la syntaxe est celle à laquelle on est habitué et en particulier les attributs tels HttpGet.
Notez que la route par défaut est de la forme ~/api/Message. Contrairement à MVC, je n’ai pas de problème avec la route par défaut.
Pour finir je voudrai conseiller un article dans lequel l’auteur a testé systématiquement les combinaisons de réglage entre contrôleur et requête Http pour faire l’inventaire de ce qui marche ou pas:
C’est un point d’amélioration très clair à mon avis, peut-être aussi est-ce parce que la doc manque.

 Bibliographie

Publicités

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