blog.pagesd.info

Aller au contenu | Aller au menu | Aller à la recherche

mercredi 8 septembre 2010

ASP.NET MVC 2 : Nouvelle syntaxe <%: %> pour encoder la sortie HTML

Ceci est la traduction du billet "New <%: %> Syntax for HTML Encoding Output in ASP.NET 4 (and ASP.NET MVC 2)" de Scott Guthrie.

Le billet d'aujourd'hui va couvrir une petite (mais malgré tout très utile) nouveauté syntaxique qui est apparue avec ASP.NET 4. Il s'agit de la possibilité d'encoder automatiquement le code HTML inséré à l'intérieur des pages. Cela contribue à protéger vos applications et vos sites contre le Cross-site scripting (XSS) et les attaques par injection HTML, et cela grâce à une nouvelle syntaxe à la fois élégante et concise.

Encoder le HTML

Le Cross-site scripting (XSS) et les attaques par injection HTML représentent deux des principales failles de sécurité qui gangrènent les sites internet et les applications. Elles surviennent lorsque des hackers découvrent un moyen pour injecter un script côté client ou du code HTML dans des pages web qui seront ensuite consultées par d'autres visiteurs du site. Cela peut aussi bien servir à vandaliser le site qu'à permettre aux hackers de lancer des scripts côté client pour voler les informations des cookies ou bien usurper l'identité d'un utilisateur sur le site pour réaliser des actions malveillantes.

Une des solutions pour parvenir à réduire ce risque est de s'efforcer à ce que le contenu HTML renvoyé au navigateur client soit toujours encodé. Cela permet de s'assurer que toute information qui aurait pû être créée ou modifiée par un utilisateur final ne pourra jamais être renvoyée vers une page côté client en contenant des balises telles que <script> ou <img>.

Comment encodait-on le HTML jusqu'à présent ?

Les applications ASP.NET (et en particulier celles qui utilisent ASP.NET MVC) s'appuient très souvent sur la syntaxe <%= %> pour générer certains éléments. Actuellement, les développeurs peuvent utiliser les méthodes Server.HtmlEncode() ou HttpUtility.Encode() à l'intérieur de ce bloc pour y encoder le HTML avant qu'il ne soit renvoyé côté client. C'est ce qui est fait avec un code tel que celui-ci :

Bien que cela fonctionne parfaitement, cela présente deux inconvénients :

  1. C'est assez verbeux
  2. Les développeurs oublient souvent d'appeler la méthode Server.HtmlEncode() – et il n'existe pas de moyen facile pour vérifier qu'elle est bien utilisée tout au long de l'application.

Encoder avec la nouvelle syntaxe <%: %>

Avec ASP.NET 4, nous avons introduit une nouvelle syntaxe <%: %> pour générer des éléments comme le fait un bloc <%= %>, mais qui en plus l'encode automatiquement avant de le générer. Cela évite d'avoir à convertir explicitement le contenu HTML comme nous l'avons fait dans l'exemple ci-dessus. Au lieu de ça, vous pouvez maintenant vous contenter d'écrire le code ci-dessous pour faire exactement la même chose :

Nous avons opté pour la syntaxe <%: %> afin qu'il soit facile et rapide de remplacer les morceaux de code <%= %> existants. Cela vous permet également de rechercher facilement les éléments <%= %> dans votre code et de contrôler les cas où vous n'utilisez pas l'encodage HTML dans votre application pour vous assurer que c'est bien ce que vous souhaitiez faire.

Eviter le double encodage

Bien que l'encodage HTML soit souvent une bonne pratique, il existe quelques cas où le contenu que vous renvoyez doit être du HTML ou qu'il est déjà encodé, auquel cas vous ne souhaitez pas l'encoder à nouveau.

ASP.NET 4 comporte une nouvelle interface IHtmlString (ainsi que son implémentation concrète : HtmlString) que vous pouvez implémenter sur vos types pour indiquer que leur valeur est déjà correctement encodée (ou tout au moins contrôlée) pour pouvoir être affichée en tant que HTML, et que par conséquent cette valeur ne doit pas être (ré)encodée. Le bloc <%: %> teste la présence de cette interface IHtmlString dans les expressions qu'il contient et le cas échéant ne vas pas faire d'encodage HTML pour les valeurs qui implémentent cette interface. Cela évite aux développeurs d'avoir à se poser la question au cas par cas pour savoir s'il faut utiliser la syntaxe <%: %> ou non. Vous pouvez vous contenter de toujours utiliser des blocs <%: %> et faire en sorte que toutes les propriétés ou types qui sont déja encodées implémentent bien l'interface IHtmlString.

Utiliser les helpers ASP.NET MVC avec <%: %>

Prenons le cas où vous utilisez des méthodes helpers en ASP.NET MVC pour avoir un exemple concret qui montre dans quel cas ce mécanisme d'encodage HTML s'avère utile. Les méthodes helper renvoient habituellement du HTML. A titre d'exemple, la méthode helper Html.TextBox() renvoie une balise du style <input type="text"/>. Avec ASP.NET MVC 2, ces méthodes helper renvoient maintenant des types HtmlString par défaut, de façon à indiquer que l'affichage du contenu renvoyé est sûr et qu'il n'a pas à être encodé par le bloc <%: %>.

Cela vous permet d'utiliser ces méthodes aussi bien avec des blocs <%= %> :

Qu'à l'intérieur des nouveaux blocs <%: %> :

Dans les deux cas ci-dessus, le contenu HTML renvoyé par la méthode helper sera envoyé vers le client en tant que HTML et le bloc <%: %> ne cherchera pas à faire un double encodage.

Cela vous permet de toujours utiliser la syntaxe <%: %> par défaut plutôt que des blocs <%= %> dans vos applications. Si vous êtes du genre intraitable, vous pouvez même créer une règle de build dont le but sera de traquer l'utilisation de blocs <%= %> dans votre application pour que vous soyez certain que rien n'échappe à l'encodage HTML

Génération des vues ASP.NET MVC 2

Lorsque vous utilisez VS 2010 (ou sa version gratuite Visual Web Developer 2010 Express) pour construire des applications ASP.NET MVC 2, vous pouvez voir que les vues qui sont générées par l'intermédiaire de la boite de dialogue "Add View" utilisent désormais des blocs <%: %> par défaut quand il s'agit de renvoyer du contenu. Dans l'exemple ci-dessous, j'ai demandé à générer une simple vue "Edit" pour un objet Article. Vous pouvez voir que la syntaxe <%: %> est employée trois fois pour le label, la textbox et le message de validation (tous trois obtenus grâce à des méthodes helper) :

Résumé

La nouvelle syntaxe <%: %> offre une méthode concise pour encoder automatiquement le contenu puis le renvoyer vers la sortie. Cela vous permet de rendre votre code un peu moins verbeux et d'assez facilement vérifier / contrôler que vous faites toujours de l'encodage HTML dans tout votre site. Ce qui participe à la protection de votre application contre les attaques de type Cross-site scripting (XSS) et injection HTML.

Ceci est la traduction du billet "New <%: %> Syntax for HTML Encoding Output in ASP.NET 4 (and ASP.NET MVC 2)" de Scott Guthrie.

vendredi 6 août 2010

ASP.NET MVC 2 : Validation basée sur le modèle

Ceci est la traduction du billet "ASP.NET MVC 2: Model Validation" de Scott Guthrie.

Ceci est le deuxième billet d'une série consacrée à la prochaine sortie de ASP.NET MVC 2. Ce billet aborde les améliorations apportées à la validation dans la future version de ASP.NET MVC 2.

La validation dans ASP.NET MVC 2

Valider les saisies utilisateurs et appliquer des règles métiers est un besoin élémentaire dans la plupart des applications web. ASP.NET MVC 2 propose un tas de nouvelles fonctionnalités qui simplifient de façon significative la validation de la saisie utilisateur et l'application de règles de validation sur les objets Model ou ViewModel. Ces fonctionnalités sont conçues de façon à ce que la logique de validation soit toujours appliquée côté serveur et qu'en option elle puisse aussi être applicable côté client via du code Javascript. L'infrastructure et les fonctionnalités de validation d'ASP.NET MVC 2 sont conçues avec deux objectifs :

  1. Les développeurs peuvent facilement employer les DataAnnotations pour gérer la validation. Basées sur de simples déclarations, les DataAnnotations donnent la possibilité d'ajouter des règles de validation aux objets et aux propriétés avec un minimum de code.
  2. S'ils le souhaitent, les développeurs peuvent intégrer leur propre système de validation ou employer un framework de validation existant, tel que Castle Validator ou EntLib Validation Library. Les fonctionnalités de validation de ASP.NET MVC 2 sont conçues pour faciliter l'incorporation de tout type d'architecture de validation - tout en conservant les avantages de la nouvelle infrastructure de validation de ASP.NET MVC 2 (comprenant la validation côté client, la validation du binding avec le modèle, etc...)

Grâce à tout cela, la validation est vraiment très simple dans la majorité des cas, tout en restant suffisamment flexible pour les scénarios d'utilisation les plus évolués.

Utiliser les DataAnnotations pour la validation en ASP.NET MVC 2

Pour apprendre à tirer parti du support des DataAnnotations pour valider les données, nous allons étudier un exemple de scénario CRUD tout simple. Et nous allons plus précisément développer un formulaire "Create" qui permettra à l'utilisateur de saisir les coordonnées de ses amis :

Notre objectif est de vérifier que les informations saisies sont valides avant de les enregistrer dans la base de données et si ce n'est pas le cas afficher les messages d'erreurs correspondant :

Nous souhaitons que cette validation s'effectue à la fois côté serveur et côté client (via du Javascript). Et nous voulons que notre code respecte l'approche DRY ("Don't Repeat Yourself") - ce qui implique que nous devons appliquer les règles de validation en un seul endroit et qu'elles soient respectées par les contrôleurs, les actions et les vues.

Je vais utiliser VS 2010 et ASP.NET MVC 2 pour mettre en oeuvre le scénario décrit ci-dessus. Mais vous pouvez tout aussi bien employer VS 2008 et ASP.NET MVC 2 pour arriver au même résultat.

Etape 1: Implémenter FriendsController (sans validation pour commencer)

Nous commencerons par ajouter une classe "Person" toute simple à notre nouveau projet ASP.NET MVC 2 :

Celle-ci contient 4 propriétés, implémentées en utilisant les propriétés automatiques de C# (un truc que VB gère aussi dans VS 2010 - Youhou !).

Nous allons ensuite ajouter une classe contrôleur "FriendsController" à notre projet qui exposera deux méthodes d'action "Create". La première action est appelée lorsque une requête HTTP-GET est faite sur l'URL /Friends/Create. Cela affichera un formulaire vide pour y saisir les informations d'une personnes. La seconde action est appelée lorsque une requête HTTP-POST est postée vers l'URL /Friends/Create. Elle fait correspondre les zones de saisie du formulaire avec un objet Person, contrôle qu'il n'y a pas d'erreur de correspondance, et si c'est correct va éventuellement l'enregistrer dans la base de données (nous verrons cet aspect du travail plus loin dans ce tutoriel). Si les données envoyées par le formulaire sont incorrectes, la méthode d'action ré-affiche le formulaire de saisi avec les messages d'erreur :

Une fois que nous avons programmé notre contrôleur, nous pouvons faire un clic-droit à l'intérieur d'une de ces méthodes d'action et sélectionner la commande "Add View" de Visual Studio - ce qui affiche la boite de dialogue "Add View". Nous demandons à générer une vue de type "Create" basée sur un objet Person :

Visual Studio va alors générer automatiquement un fichier Create.aspx dans le répertoire \Views\Friends de notre projet. Vous pouvez pouvoir voir ci-dessous comment il tire parti des nouveaux helpers HTML fortement typés gérés par ASP.NET MVC 2 (ce qui offre un contrôle à la compilation et une meilleure intellisense).

Et maintenant, lorsque nous lançons l'application et appelons l'URL /Friends/Create nous arrivons sur un formulaire vide dans lequel nous pouvons effectuer notre saisie :

Cependant, étant donné que nous n'avons pas encore implémenté l'aspect validation, rien ne nous empêche de saisir des informations erronées dans ce formulaire et de les publier vers le serveur.

Etape 2: Activer la Validation avec les DataAnnotations

Nous allons maintenant mettre à jour notre application pour qu'elle respecte quelques règles de validation de base. Nous implémenterons ces règles sur notre objet "Person" - et pas dans le contrôleur "FriendsController" ou la vue "Create". L'avantage d'implémenter ces règles au niveau de l'objet "Person" nous assure que ces règles seront suivies dans tous les cas où notre application utilisera l'objet "Person" (pour gérer l'action "Edit" par exemple). Cela nous aide à éviter de répéter nos règles de validation plusieurs fois dans notre application et donc à respecter la philosophie DRY.

ASP.NET MVC 2 permet aux développeurs d'ajouter facilement des attributs de validation sous forme de déclaration aux différentes classes Model ou ViewModel et ensuite que ces règles de validation soient automatiquement appliquées chaque fois que l'application fera du binding de données pour ces classes. Pour mettre cela en application, nous allons modifier notre classe Person pour y définir quelques attributs de validation. Pour cela, nous commençons par faire un "using" de l'espace de noms "System.ComponentModel.DataAnnotations" dans l'en-tête du fichier source - puis préfixer les propriétés de l'objet Person avec les attributs de validation [Required], [StringLength], [Range], et [RegularExpression] (qui sont tous disponible via cet espace de nom) :

Remarque: Nous avons défini les messages d'erreurs sous forme de chaines. Il est aussi possible de les définir dans des fichiers de ressources puis de les adapter en fonction de la langue des visiteurs. Vous avez plus d'informations sur la façon de localiser les messages d'erreur dans le billet «Localizing ASP.NET MVC Validation».

Maintenant que nous avons ajoutés des attributs de validation à notre classe Person, nous pouvons relancer l'application et regarder ce qui se passe quand nous saisissons des données incorrectes puis que nous les envoyons au serveur :

Vous pouvez constater que notre application offre désormais une gestion des erreurs tout à fait satisfaisante. Les zones de saisies avec des données incorrectes sont surlignées en rouge et les messages d'erreur de validation que nous avons définis sont portés à la connaissance de l'utilisateur. D'autre part, le formulaire a conservé le contenu des éléments saisis - si bien que l'utilisateur n'a qu'à les corriger et pas à les saisir à nouveau.

Vous vous demandez sans doute comment ça marche ?

Pour bien comprendre ce comportement, regardons de plus près la méthode d'action Create qui gère la requête POST envoyée par le formulaire :

Quand notre formulaire HTML est envoyé vers le serveur, la méthode ci-dessus est appelée. Etant donnée que cette méthode d'action attend un objet "Person" en paramètre, ASP.NET MVC va créer un objet "Person" et automatiquement initialiser ses propriétés à partir des informations du formulaire. Au cours de cette phase, il va aussi contrôler si les attributs de validation DataAnnotation sont valides pour cet objet Person. Si tout est correct, alors la propriété ModelState.IsValid renverra vrai et dans ce cas nous sauvegarderons (bientôt) cette personne dans la base de données puis nous ferons une redirection vers la page d'accueil.

S'il y a la moindre erreur de validation pour notre objet "Person", la méthode d'action va ré-afficher le formulaire avec les données saisies. C'est ce dont se charge la dernière ligne de code dans l'exemple ci-dessus.

Les messages d'erreurs sont alors affichés dans la vue grâce au fait que notre formulaire Create contient des méthodes helper <%= Html.ValidationMessageFor() %> en face de chaque helper <%= Html.TextBoxFor() %>. Ces helpers vont afficher le message d'erreur correspondant pour chaque propriété incorrecte qui aura été passée à à vue :

Le plus sympa avec cette approche, c'est qu'elle est carrément simple à mettre en œuvre et qu'elle nous permet très facilement d'ajouter ou de modifier des règles de validation à notre classe Person sans avoir à mettre à jour le moindre code dans nos vues ou nos contrôleurs. Cette capacité de définir les règles de validation à un seul endroit pour qu'elles soient prises en compte et respectées partout ailleurs nous permet de faire évoluer notre application et ses règles extrêmement rapidement avec un minimum de travail et en suivant au mieux la philosophie DRY.

Etape 3: Activer la validation côté client

Actuellement, la validation pour notre application ne s'exécute que côté serveur, ce qui signifie que nos utilisateurs devront valider le formulaire pour l'envoyer vers le serveur afin de savoir si leur saisie est correcte.

Un des aspects intéressant de l'architecture de validation de ASP.NET MVC 2 c'est qu'elle gère à la fois le côté serveur et le côté client. Tout ce que nous avons à faire pour que cela fonctionne, c'est de référencer deux Javascripts dans notre vue et d'ajouter une ligne de code :

Une fois que nous avons ajouté ces trois lignes, ASP.NET MVC 2 va utiliser les méta-données de validation que nous avons ajouté à notre classe Person et s'occupera pour nous de tous les traitements de validation en Javascript côté client. Cela signifie que les utilisateurs auront un retour immédiat sur les erreurs de validation dès qu'ils quitteront une zone de saisie.

Pour voir ce que donne cette prise en charge Javascript côté client dans le cas de notre application, nous n'avons qu'à relancer l'application et remplir les trois premières zones de saisie avec des valeurs correctes avant d'essayer de cliquer sur le bouton "Create". Comme vous pouvez le voir ci-dessous, nous obtenons aussitôt un message d'erreur sans que le formulaire ait besoin d'être envoyé vers le serveur :

Si nous saisissons alors n'importe quoi d'incorrect, le message d'erreur passe immédiatement de "Email Required" à "Not a valid email" (tout deux correspondant aux messages d'erreurs que nous avons définis quand nous avons ajouté les règles de validation à la classe Person) :

Le message d'erreur disparait dès que nous saisissons une adresse email valide et le fond de la zone de saisie revient à sa couleur normale :

Ce qu'il y a de bien, c'est que nous n'avons pas eu à écrire nous même le moindre morceau de JavaScript pour que cela fonctionne. Et notre code pour la validation reste fidèle au principe DRY - nous pouvons définir les règles à un seul endroit et elles s'appliquent dans toute l'application - aussi bien côté serveur que côté client.

Ce qu'il faut savoir, c'est que pour des raisons de sécurité, les règles de validations seront toujours appliquées côté serveur, même dans le cas où vous les avez déjà activées côté client. De cette façon, vous être protégé des pirates qui pourraient essayer de contourner les règles de validation côté client pour tromper votre serveur.

Avec ASP.NET MVC 2, le support de la validation côté client en JavaScript peut fonctionner avec n'importe quel framework ou système de validation que vous pouvez utiliser. Cela ne nécessite pas d'avoir choisi l'approche DataAnnotation - l'ensemble du système fonctionne de façon totalement indépendante des DataAnnotations et peut aussi bien être employée avec Castle Validator, EntLib Validation Block ou tout autre méthode de validation personnalisée que vous pouvez décider de mettre en place.

Si vous ne souhaitez pas utiliser nos fichiers JavaScript côté client, vous pouvez également les remplacer par un plugin de validation en jQuery et employer cette librairie à la place. Le téléchargement de ASP.NET MVC Futures intègrera la possibilité d'utiliser une validation jQuery en complément de la validation ASP.NET MVC 2 côté serveur.

Etape 4: Créer un attribut de validation personnalisé

L'espace de nom System.ComponentModel.DataAnnotations du framework .NET contient un certain nombre d'attributs de validation tous prêts que vous pouvez utiliser. Nous en avons déjà vu 4 jusqu'à présent : [Required], [StringLength], [Required], et [RegularExpression].

Vous avez aussi la possibilité de définir et d'employer vos propres attributs de validations personnalisés. Vous pouvez développer des attributs spécialisés à partir de zéro en utilisant la classe de base ValidationAttribute disponible dans l'espace de noms System.ComponentModel.DataAnnotations. Une autre solution consiste à hériter d'un des attributs de validation déjà existant et à simplement étendre certaine de ses fonctionnalités.

Par exemple, pour arriver à nettoyer un peu le code de la classe Person, nous pourrions créer un nouvel attribut de validation [Email] qui utiliserait l'expression régulière servant à vérifier les adresses email. Pour cela, il nous suffit d'hériter de la classe d'attribut RegularExpression et de simplement appeler le constructeur de l'objet de base avec la bonne expression régulière :

Nous pouvons alors mettre à jour la classe Person pour qu'elle utilise notre nouvel attribut [Email] au lieu de l'expression régulière que nous utilisions jusqu'ici - ce qui rend notre code beaucoup plus clair et concis :

Lors de la création d'attributs de validation personnalisés, vous pouvez spécifier une logique de validation qui s'exécute à la fois sur le serveur et sur le client via JavaScript.

Etape 5: Sauvegarder dans la base de données

Nous allons maintenant développer les traitements nécessaires pour enregistrer nos amis dans la base de données :

Pour l'instant, nous travaillons seulement avec une bonne vieille classe C# (aussi connue sous le nom de classe "POCO" pour "Plain Old CLR (ou C#) Object"). Nous aurions pu écrire un morceau de code supplémentaire pour assurer la persistance en faisant correspondre les éléments de cette classe à la base de données. Les solutions de mapping objet-relationnel (ORM) telles que NHibernate gèrent désormais très bien ce genre de mapping POCO / PI (Persistence Ignorant). la nouvelle version de ADO.NET Entity Framework (EF) qui va être livrée avec .NET 4 supportera elle aussi le mapping POCO / PI et comme NHibernate, elle permettra également de spécifier le mapping pour la persistance directement au niveau du code (sans qu'il soit besoin de passer par des fichiers de configuration ou des concepteurs visuels).

Si notre objet Person avait été mappé à la base de données d'une de ces façons, nous n'aurions rien eu d'autre à faire, ni au niveau de la classe Person, ni au niveau des règles de validation : tout aurait continué à fonctionner parfaitement.

Mais qu'est-ce qui se passe dans le cas où nous utilisons un outil graphique pour effectuer notre mapping objet-relationnel ?

De nos jours, la plupart des développeurs qui utilisent Visual Studio n'écrivent pas eux même le code dont ils ont besoin pour le mapping et la persistance. Ils font confiance pour cela au concepteurs graphiques intégrés de Visual Studio qui s'en chargent pour eux.

Une question qui revient souvent quand on utilise les DataAnnotations (ou tout autre forme de validation basée sur des attributs) est : "Comment faire pour les appliquer lorsque le modèle d'objet avec lequel on travaille est créé / mis à jour via un concepteur visuel ?". Par exemple, que se passerait-il si au lieu d'avoir une classe Person de type POCO comme celle que nous utilisons depuis le début, nous avions mis au point cette classe Person avec un outil graphique tel que le concepteur LINQ to SQL ou celui de ADO.NET EF ?

La copie d'écran ci-dessus montre une classe Person crée à l'aide du concepteur ADO.NET EF dans VS 2010. La fenêtre du haut défini la classe Person et la fenêtre du bas montre l'éditeur de mapping qui sert à assurer la correspondance entre ses propriétés et une table "People" dans la base de données. Quand on clique sur "Save", le concepteur génère automatiquement une classe Person à votre place dans le projet. C'est très bien, sauf qu'à chaque fois que vous changez quelque chose et re-cliquez sur "Save", il va re-générer cette classe. Et ainsi tous les attributs de validation que vous auriez pu y définir seraient perdu.

Une méthode qui s'offre à nous pour ajouter des méta-données sous forme d'attribut (tels que les attributs de validations) à une classe qui est générée et maintenue par un concepteur de Visual Studio est d'employer la technique de la "buddy class". Cela consiste à créer une autre classe avec nos attributs de validation et autres méta-données puis à la relier à la classe qui a été généré par Visual Studio en appliquant un attribut "MetadataType" à une troisième classe partielle qui sera combinée avec la classe générée lors de la compilation. Prenons l'exemple des règles de validation que nous avons utilisées jusqu'à maintenant. Si nous voulons les appliquer à une classe Person qui provient des concepteurs LINQ to SQL ou ADO.NET EF, nous n'avons qu'à placer ce code de validation dans une classe à part nommée "Person_Validation" puis à faire le lien avec la classe "Person" générée par VS en utilisant le code ci-dessous :

L'approche ci-dessus n'est sans doute pas aussi élégante qu'une approche purement POCO mais présente l'avantage de fonctionner plutôt bien avec n'importe quel code généré par un outil ou un concepteur de Visual Studio.

Dernière étape – Enregistrer en base de données

Notre dernière étape - que nous utilisions une classe "Person" de type POCO ou générée automatiquement - sera de sauvegarder nos amis convenables dans la base de données.

Faire cela demande juste de remplacer le "Todo" laissé dans la classe FriendsController par 3 lignes de code qui enregistrent notre nouvel ami dans la base de données. Le code ci-dessous présente la totalité de la classe FriendsController dans le cas où on passe par ADO.NET EF pour gérer la persistance des données.

Et maintenant, quand nous allons sur l'URL /Friends/Create nous pouvons facilement ajouter des personnes à la base de données de nos amis :

La validation de toutes ces données est appliquée aussi bien par le navigateur client que par le serveur. Nous pouvons donc facilement ajouter, modifier ou supprimer des règles de validation à un seul endroit et avoir la certitude qu'elles seront prises en compte par tous les contrôleurs et toutes les vues de notre application.

Conclusion

ASP.NET MVC 2 simplifie énormément la gestion de la validation dans les applications web. Il favorise une approche DRY avec une validation qui s'appuie sur le modèle ce qui permet de s'assurer que les règles de validation seront appliquées de façon identique dans toute l'application. Avec la prise en charge des DataAnnotations par ASP.NET MVC 2, la mise en oeuvre de la plupart des scénarios classiques de validation devient un véritable jeu d'enfant. Et les possibilités d'extension au niveau de l'infrastructure de validation de ASP.NET MVC 2 vous permettent de gérer un grande variété de scénarios de validation plus poussés - et d'intégrer n'importe quel framework ou système de validation existant ou personnalisé.

Ceci est la traduction du billet "ASP.NET MVC 2: Model Validation" de Scott Guthrie.

samedi 31 juillet 2010

ASP.NET MVC 2 : Des helpers HTML fortement typés

Ceci est la traduction du billet "ASP.NET MVC 2: Strongly Typed Html Helpers" de Scott Guthrie.

Ceci est le premier billet d'une série consacrée à la prochaine sortie de ASP.NET MVC 2. Ce billet présente les nouveaux helpers fortement typés qui ont fait leur apparition dans ASP.NET MVC 2.

Les helpers HTML actuels

ASP.NET MVC 1 est fourni avec un certain nombre de helpers HTML que vous pouvez utiliser dans les vues pour simplifier la génération de l'interface utilisateur en HTML. Par exemple, pour afficher une textbox, vous pouvez utiliser le helper Html.TextBox() de la façon suivante dans une de vos vues :

Le premier paramètre de la méthode helper ci-dessus fournit le nom et l'id pour la textbox et le second paramètre défini la valeur qu'elle doit contenir. Ce helper aura comme effet de produire le code HTML suivant :

Les nouveaux helpers HTML fortement typés

Une des fonctionnalités qui nous a le plus été demandée était de gérer des helpers HTML fortement typés basés sur des expressions lambda pour référencer les objets Model ou ViewModel passés aux vues. Cela permet une meilleure vérification des vues au moment de la compilation (et donc de découvrir les bugs lors de la programmation et pas de l'exécution) et aussi la possibilité de profiter de l'intellisense dans les vues.

Les nouveaux helpers HTML fortement typés font désormais parti de ASP.NET MVC 2. Ces méthodes utilisent la convention de nommage "Html.HelperNameFor()", comme par exemple : Html.TextBoxFor(), Html.CheckBoxFor(), Html.TextAreaFor()... Ils autorisent l'utilisation d'une expression lambda pour définir en une fois les attributs id/name et la valeur du contrôle à générer.

Par exemple, avec ASP.NET MVC 2, nous pouvons maintenant utiliser le nouveau helper Html.TextBoxFor() en plus du Html.TextBox() classique :

Vous remarquerez dans le code ci-dessus que nous n'avons plus besoin d'utiliser la chaine "ProductName". Les expressions lambda sont suffisamment souples pour permettre de retrouver le nom de la propriété ou du champ dans notre modèle d'objets en plus de sa valeur.

Etant donné que les helpers HTML sont fortement typés, cela nous permet d'avoir accès à l'intellisense de Visual Studio lorsque nous écrivons l'expression lambda :

Le code HTML généré est exactement le même que pour la version présentée dans le premier exemple :

Les helpers HTML fortement typés fournis avec ASP.NET MVC 2

ASP.NET MVC 2 intègre d'ores et déjà les helpers HTML fortement typés suivants :

Eléments HTML

  • Html.TextBoxFor()
  • Html.TextAreaFor()
  • Html.DropDownListFor()
  • Html.CheckboxFor()
  • Html.RadioButtonFor()
  • Html.ListBoxFor()
  • Html.PasswordFor()
  • Html.HiddenFor()
  • Html.LabelFor()

Autres helpers

  • Html.EditorFor()
  • Html.DisplayFor()
  • Html.DisplayTextFor()
  • Html.ValidationMessageFor()

Je reviendrai sur les nouvelles méthodes helpers Html.EditorFor() et Html.DisplayFor() dans un autre billet de cette série quand j'aborderai les évolutions apportées à la fonctionalité d'auto-scaffold en ASP.NET MVC 2. Nous verrons aussi le helper Html.ValidationMessageFor() dans le prochain billet de cette série qui présentera les nouveautés en matière de validation apportées par ASP.NET MVC 2.

Les helpers HTML fortement typés et le Scaffolding

VS 2008 et VS 2010 utilisent désormais tous les deux les nouveaux helpers fortement typés pour auto-générer les vues fortement typées à partir de la commande "Add View".

Par exemple, supposons que nous ayons une classe "ProductsController" toute simple comme ci-dessous avec une action Edit() qui renvoie un formulaire de modification pour un objet "Product" :

Dans Visual Studio, nous pouvons faire un clic-droit à l'intérieur de l'action Edit() et choisir la commande "Add View" dans le menu contextul pour créer une nouvelle vue. Nous choisirons de créer une vue selon le modèle "Edit" qui sera basée sur un objet de type "Product" :

Avec ASP.NET MVC 2, la vue qui a été créée par défaut utilise désormais les nouveaux helpers HTML fortement typés pour faire référence à l'objet Product :

Conclusion

Les helpers HTML fortement typés intégrés avec ASP.NET MVC 2 fournissent une méthode pratique pour obtenir un contrôle du type de données à l'intérieur de nos vues. Ils permettent un meilleur contrôle de nos vues dès la compilation (ce qui vous permet de détecter les erreurs à la compilation et pas à l'utilisation) et ils offrent un support de l'intellisense plus puissant lorsque vous codez vos vues depuis Visual Studio.

Ceci est la traduction du billet "ASP.NET MVC 2: Strongly Typed Html Helpers" de Scott Guthrie.

mardi 1 juin 2010

MVC Music Store / Raven DB : StoreManagerController (2° partie)

Ceci est la traduction du billet "Porting MVC Music Store to Raven: StoreManagerController, part 2", le dernier de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Le contrôleur StoreManagerController contient encore deux méthodes que nous n'avons pas étudiées. Nous allons devoir les aborder de façon un peu différente.

Est-ce que vous devinez pourquoi ?

C'est parce qu'au départ nous étions d'accord sur le fait qu'il n'y avait aucune raison valable de gérer les artistes en tant que document spécifique. Après tout, il ne s'agit que de données de référence. Sauf que maintenant nous devons y faire référence.

C'est sûr qu'on pourrait créer une série de documents artistes, ce qui faciliterait énormément la migration du code :

Mais je continue à penser que les artistes n'existent pas réellement en tant qu'entité indépendante dans notre modèle. Par conséquent, au lieu de suivre cette voie nous allons plutôt faire une projection.

Nous commençons par définir un index "Artists" à l'aide des requêtes linq map / reduce suivantes :

// map 
from album in docs.Albums
select new { album.Artist.Id, album.Artist.Name }

// reduce 
from artist in results
group artist by new { artist.Id, artist.Name } into g
select new { g.Key.Id, g.Key.Name }

Si vous regardez attentivement ce code, vous pouvez voir que sa fonctionnalité principale est de faire un distinct sur l'ensemble des artistes de tous les albums.

Si bien que maintenant nous pouvons coder nos deux dernières méthodes comme ceci :

Il faut bien comprendre un truc : ça ne coûte rien de faire des requêtes avec Raven, parce que Raven ne permet les requêtes que sur des index et que ces index sont créés en tache de fond, ce qui contribue à rendre les requêtes très rapides.

Et cela remet en cause la façon dont vous allez concevoir votre système et de votre modèle de données. Avec Raven, vous cherchez à axer la plupart de vos traitements sur des index et interroger ces index, parce que c'est ce qui est le moins gourmand.

lundi 31 mai 2010

MVC Music Store / Raven DB : StoreManagerController

Ceci est la traduction du billet "Porting MVC Music Store to Raven: StoreManagerController", le douzième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

La dernière partie du portage de MVC Music Store sous Raven concerne toute la partie administration, implémentée au niveau du contrôleur StoreManagerController. Je vais commencer par une rapide comparaison de toutes les méthodes où le passage sous Raven n'apporte rien de nouveau puis je mettrai l'accent sur une différence de conception plutôt intéressante entre les deux implémentations.

Code d'origine

Portage sous Raven

Le code pour Raven est beaucoup plus court pour la bonne et simple raison que j'ai fait disparaitre tout la pseudo gestion d'erreur qu'il contenait.

Code d'origine

Portage sous Raven

Là encore, le fait de faire disparaitre une gestion d'erreur qui n'est là que pour la gallerie a un impact plus que certain sur la taille du code.

Code d'origine

Portage sous Raven

Ici nous avons une différence plus intéressante. Le code d'origine a pour effet d'effacer les commandes qui contiennent l'album supprimé. Ce que ne fait pas le code porté sous Raven.

La notion d'intégrité référentielle n'existe pas sous Raven (ou de façon plus générale sous les bases de données document). Cela peut être un avantage ou un inconvénient. Mais dans ce cas précis, cela s'avère être un avantage puisque nous pouvons supprimer un album sans perdre de commandes. Je ne sais pas pour vous, mais en ce qui me concerne ça ne me déplait pas de conserver toutes mes commandes :)

Avec Raven, les documents sont indépendants les uns des autres. Par conséquent, le fait de modifier un document n'a aucun impact sur les autres documents.

Il reste encore deux méthodes à étudier en ce qui concerne le contrôleur StoreManagerController, mais je verrai ça dans mon prochain billet.

dimanche 30 mai 2010

MVC Music Store / Raven DB : Gérer le règlement

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Porting the checkout process", le onzième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Dans MVC Music Store, la gestion du règlement est composée de deux parties : renseigner l'adresse et le mode de paiement puis terminer la commande.

Le code pour gérer l'adresse et le mode de paiement dans la version d'origine :

Et ce même code correspondant à la version portée sous Raven :

Comme vous pouvez le constater, ils sont presque identiques. Mais le code pour Raven n'est pas tout à fait complet.

Si vous vous souvenez, nous avions décidé de stocker une propriété CountSold dans le document Album, pour pouvoir plus facilement faire un classement en fonction de ce compteur. Il nous reste donc à réaliser l'incrémentation de ce compteur, ce que j'ai codé immédiatement après l'appel à CreateOrder :

Le truc essentiel dans ce code, c'est que nous avons chargé tous les documents albums en une seule requête. Et lorsque nous sauvegardons, Raven va effectuer un seul appel (batch) au serveur.

Et maintenant, pour être tout à fait complet, voyons ce que donnait la méthode Complete() :

Et dans la nouvelle version sous Raven :

Je pense qu'arrivé à ce point, vous êtes en mesure de comprendre comment fonctionne les deux versions.

Mon prochain billet concernera la partie administration de l'application.

vendredi 28 mai 2010

MVC Music Store / Raven DB : ShoppingCartController

Ceci est la traduction du billet "Porting MVC Music Store to Raven: ShoppingCart", le dixième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Le contrôleur ShoppingCartController est considérablement impacté par tous les changements que nous avons apportés à la classe ShoppingCart. Penchons-nous sur ces modifications, en commençant par l'action Index() dans le code d'origine :

Celui-ci exécute deux requêtes différentes pour gérer la commande alors que la version pour Raven exécute seulement une requête au niveau de la méthode FindShoppingCart () :

Ce code implémente seulement la logique pour charger le panier depuis Raven ou créer un nouveau panier (pour l'identifiant spécifié). Notez-bien que nous n'enregistrons pas le nouveau panier dans la base de données, mais associons seulement ce nouveau panier avec la session. Nous n'avons pas besoin de sauvegarder étant donné que pour l'instant il ne contient rien de significatif. Lorsque nous appellerons SaveChanges(), le nouveau panier sera envoyé vers Raven pour stockage.

Maintenant, voyons-voir l'action AddToChart d'origine :

Et la version portée sous Raven :

Elles sont très similaires, si ce n'est que dans le cas de Raven, la méthode AddToCart() de la classe ShoppingCart est uniquement concerné par l'ajout d'un nouvel article au panier ou par la mise à jour de la quantité d'un article existant. Ainsi, il n'y a absolument aucun accès à la base de données dans la version pour Raven de la méthode ShoppingCart.AddToCart().

La différence c'est donc que dans l'approche pour Raven, nous appelons la méthode session.SaveChanges() au niveau de l'action. Pour la bonne et simple raison que c'est le bon endroit où faire cela étant donné que le code appelant est en charge de l'environnent, y compris la sauvegarde lorsque cela est nécessaire.

Je pense que le code pour Raven est plutôt facile à suivre. Il y a juste un truc un peu étrange à faire remarquer au niveau de la dernière ligne : id.Split(). Pourquoi diable fait-on ça ?

Et bien c'est parce que Raven utilise des identifiants de la forme "albums/616" et que la valeur DeleteId sera utilisé par le code Javascript appelant pour retrouver un élément à partir de son identifiant. Et comme l'identifiant d'un élément HTML ne peut pas contenir de "/", nous ne renvoyons que la partie numérique de l'identifiant. Ce n'est pas un problème puisque dans ce cas précis nous ne gérons que des albums.

Encore une fois, on ne peut pas faire plus simple. Par conséquent, je ferai seulement remarquer qu'avec l'approche suivie par Raven on peut profiter du cache de l'unit of work alors qu'avec le code d'origine non.

Dans mon prochain billet, je m'occuperai de la gestion de la commande.

jeudi 27 mai 2010

MVC Music Store / Raven DB : ShoppingCart

Ceci est la traduction du billet "Porting MVC Music Store to Raven: ShoppingCart", le neuvième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

ShoppingCart

La classe ShoppingCart de MVC Music Store est ma tête à claque du moment. C'est plus fort que moi.

Vous pouvez admirer à quoi elle ressemble dans l'illustration de droite. Le souci avec elle, c'est que c'est le genre de code qui fait l'amalgame entre deux responsabilités différentes :

  • Les opérations au sujet du panier
    • GetCart
    • GetCartId
    • GetCartItems
    • GetCount
  • Les traitements du contenu du panier
    • AddToCart
    • CreateOrder
    • EmptyCart
    • MigrateCart
    • RemoveFromCart

Vous avez sans doute remarqué que toutes les opérations au sujet du panier correspondent à des fonctions get. Tous les traitements du contenu du panier sont relatifs aux articles du panier, ils représentent la logique métier du panier et sa raison d'être. Les opérations Get ne sont pas rattachées aux articles du panier, elles dépendent d'un autre genre d'objet qui gère les instances de paniers.

Dans la plupart des applications, cet objet s'appellerait un Repository. Je ne suis pas certain que cela nous soit utile dans le cas présent. Si on étudie les méthodes Get de plus près, on se rend compte que la seule justification de leur existence vient du fait que l'on a choisi de stocker uniquement les articles du panier. Comme il n'existe pas d'entité panier, nous sommes obligé d'effectuer des requêtes explicites pour obtenir ces données.

Avec Raven, nous procèderons différemment, ce qui fait que la seule chose dont nous devrions avoir besoin est GetCart() et peut-être GetCartId().

Voici donc à quoi un document panier ressemblera :

Ce qui donnera en tant qu'entité :

La méthode GetTotal a été remplacée par une propriété Total. Contrairement à la méthode GetTotal qui va générer un accès à la base de données, cette propriété fonctionne uniquement à partir des données en mémoire. Cela constitue une autre différence majeure de Raven par rapport à une autre solution ORM : on ne va pas faire de lazy loading. C'est quelque chose d'inhérent aux bases de données documents : les données du modèle ont rarement besoin de parcourir d'autres données en dehors de leur propre document. Parcourir le document avec Raven ne risque pas de provoquer du lazy loading ou de nous entrainer dans de redoutables problèmes de type SELECT N+1.

Et maintenant, occupons nous de gérer les opérations au sujet du panier. Les plus importantes sont GetCartId et GetCart. Je considère que ces méthodes n'ont rien à faire là. J'ai donc créé une nouvelle classe ShoppingCartFinder qui ressemble à ceci :

Vous pouvez voir que nous n'exposons plus la méthode GetCartId. Il s'agit d'un élément interne qui n'a pas à être connu des clients de cette classe. La méthode SetShoppingCartId() est là parce que nous devons gérer l'initialisation de l'identifiant d'un panier étant donné que nous voulons gérer le transfert d'un panier (lorsqu'un utilisateur anonyme se connecte). Comme nous n'avons pas besoin des autres méthodes, je les ai supprimées.

Continuons avec les traitements du contenu du panier. Pour mémoire, voici la méthode AddToCart() d'origine :

Et voici cette méthode portée sous Raven :

Le code pour Raven fonctionne entièrement en mémoire et sans se soucier de tout l'aspect persistance. Le code d'origine se charge explicitement de tout ce qui est persistance. Ce n'est pas un problème en soi, mais ce n'est pas le bon endroit pour s'occuper de persistance.

Passons à RemoveFromCart() :

Vous pouvez voir que sa taille diminue de façon très significative et qu'une fois encore, il ne s'agit plus que d'un traitement en mémoire. La méthode EmptyCart() n'est pas implémentée puisqu'avec Raven cela correspond juste à un Lines.Clear().

Un truc intéressant à voir, c'est que l'ancienne implémentation de EmptyCart() aurait généré N requêtes (N étant le nombre d'articles dans le panier) alors qu'avec Raven cela engendre une seule requête.

La méthode CreateOrder() d'origine :

Et celle pour Raven pour laquelle il n'y a pas grand chose à dire si ce n'est que l'ancien code exacuterait N * 2 requêtes là où le code pour Raven continuera de se contenter d'une seule requête :-)

MigrateCart() est plus intéressant parce que son implémentation est complètement différente. Dans le code d'origine, on met à jour tous les articles du panier un par un :

Avec Raven, nous allons faire quelque chose de radicalement différent :

L'identifiant du panier sert pour définir la clé du document et donc, en initialisant cet identifiant (soit avec le nom de l'utilisateur soit avec une valeur stockée en session), nous pouvons charger le panier à l'aide d'une méthode Load (sur la clé primaire pour faire une comparaison avec le monde des bases relationnelles). Le transfert du panier est alors une opération toute simple. Tout ce que vous avez à faire, c'est de changer sa clé. Etant donné que Raven ne permet pas de la renommer, nous allons faire une suppression puis une insertion qui s'exécuteront dans la même transaction.

Le code pour appeler la méthode MigrateCart() est le suivant :

Etant donné que SaveChanges est atomique et transactionnel, cela a le même effet que de faire un Rename.

Et c'est tout pour le panier. Je consacrerai mon prochain billet au contrôleur ShoppingCartController qui utilise cette classe.

mercredi 26 mai 2010

MVC Music Store / Raven DB : StoreController

Ceci est la traduction du billet "Porting MVC Music Store to Raven: StoreController", le huitième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Commençons par la méthode Index() :

Il y a un truc dans ce code qui me gène, c'est qu'il va exécuter deux requêtes sur la base de données. Mais je ne vais pas m'appesantir étant donné que nous allons modifier tout ça.

Et voici ma version portée sous Raven :

Comme vous pouvez le constater, c'est à peut près pareil et donc pas très intéressant. Voyons voir ce que nous avons d'autre :

Ce qu'il faut bien voir, c'est que ce code cherche à faire une recherche sur le libellé d'un genre. Le problème c'est que le libellé du genre n'est pas la clé primaire, et que pour couronner le tout, il n'y a même pas d'index sur cette colonne libellé. Bon, c'est vrai que la table genre ne contient que 10 lignes, mais c'est une question de principe (si vous êtes très sympa, vous n'aurez droit qu'à un sermon du DBA pour avoir osé faire une requête sans index sur la base de production).

Avec Raven, il nous serait donc très simple d'implémenter ça en suivant la même approche, mais je ne vois pas d'excuse pour faire ça. Le genre que nous récupérons dans la méthode Browse() dépend des données que nous avons renvoyées avec la méthode Index(). Il n'y a donc pas de raison pour ne pas faire passer directement l'identifiant du genre. J'ai donc modifié l'action Index() pour renvoyer l'objet genre complet et pas seulement son libellé et par la suite renvoyer l'identifiant à l'action Browse() au lieu du libellé.

J'avais donc commencé à implémenter ça mais je me suis retrouvé coincé par l'association entre les albums et les genres.

Normalement, les bases de données documents n'ont pas d'associations et pas de jointures non plus. Alors, comment gérer ça ?

Depuis le temps vous devez commencer à vous douter de la réponse : en créant un un index :)

// AlbumsByGenre
from album in docs.Albums
where album.Genre != null
select new { Genre = album.Genre.Id }

Et cet index nous permet d'écrire le code suivant :

Et pour finir, il nous reste l'action GenreMenu :

Que nous pouvons facilement porter de la façon suivante :

Et nous en avons terminé avec StoreController.

MVC Music Store / Raven DB : Faire une migration plus poussée

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Advanced Migrations", le septième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Je me suis rendu compte qu'à cause d'une faute de frappe lorsque j'avais fait la reprise des données, les informations sur l'artiste étaient enregistrées en tant que "Arist" au lieu de "Artist". Cela va me donner l'occasion de montrer comment faire une migration du modèle de données un peu plus sophistiquée.

using (var documentStore = new DocumentStore { Url = "http://localhost:8080" })
{
    documentStore.Initialise();

    var count = 0;

    do
    {
        var queryResult = documentStore.DatabaseCommands.Query("Raven/DocumentsByEntityName", new IndexQuery
        {
            Query = "Tag:`Albums`",
            PageSize = 128,
            Start = count
        });


        if (queryResult.Results.Length == 0)
            break;

        count += queryResult.Results.Length;
        var cmds = new List<ICommandData>();
        foreach (var result in queryResult.Results)
        {
            var arist = result.Value<JObject>("Arist");
            if(arist == null)
                continue;
                        
            result["Artist"] = arist;
            result.Remove("Arist");

            cmds.Add(new PutCommandData
            {
                Document = result,
                Metadata = result.Value<JObject>("@metadata"),
                Key = result.Value<JObject>("@metadata").Value<string>("@id"),
            });
        }

        documentStore.DatabaseCommands.Batch(cmds.ToArray());

    } while (true);
    
}

Je ne pense pas que le code soit très compliqué à suivre. Vous pouvez voir comment on peut manipuler les documents en travaillant directement au niveau du document JSON plutôt que de passer par une couche objet.

mardi 25 mai 2010

MVC Music Store / Raven DB : Faire évoluer le modèle de données

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Migrations", le sixième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Dans mon dernier billet, j'ai indiqué que nous devions ajouter une propriété CountSold à tous les albums, généralement quelque chose d'assez pénible à faire dans l'univers des bases de données SQL. La commande pour ajouter une colonne est toute simple, mais c'est une vrai galère d'en venir à bout, de la déployer et de la versionner. Avec Raven, quand vous ajoutez une propriété, elle sera automatiquement ajoutée à votre document la prochaine fois que vous le sauvegarderez. Vous n'avez rien d'autre à faire. Et même, c'est pareil si vous décidez de supprimer une propriété. Raven s'occupera de fera le ménage après vous.

Mais comment faire quand on veut initialiser cette propriété avec une valeur définie, et pas se contenter de la valeur par défaut ? Dans ce cas là, il faut un peu mettre la main à la pâte, mais ça reste très simple :

using (var documentStore = new DocumentStore { Url = "http://localhost:8080" })
{
    documentStore.Initialise();
    using (var session = documentStore.OpenSession())
    {
        IDictionary<string,int> albumToSoldCount = new Dictionary<string, int>();
        int count = 0;

        do
        {
            var results = session.Query<SoldAlbum>("SoldAlbums")
                .Take(128)
                .Skip(count)
                .ToArray();

            if (results.Length == 0)
                break;
            count += results.Length;
            foreach (var soldAlbum in results)
            {
                albumToSoldCount[soldAlbum.Album] = soldAlbum.Quantity;
            }
        } while (true);

        count = 0;
        do
        {
            var albums = session.Query<Album>()
                .Skip(count)
                .Take(128)
                .ToArray();
            if (albums.Length == 0)
                break;

            foreach (var album in albums)
            {
                int value;
                albumToSoldCount.TryGetValue(album.Id, out value);

                album.CountSold = value;
            }

            count += albums.Length;

            session.SaveChanges();
            session.Clear();
        } while (true);
    }
}

Pour ceux d'entre-vous qui n'ont pas pris la peine de lire le code, cette fonction parcours l'index SoldAlbums que nous avons créé auparavant et mémorise ses valeurs. Puis nous parcourons les albums par lot de 128 et nous mettons à jour leur compteur CountSold. L'un dans l'autre, c'est plutôt facile.

Un des autres avantages de ce script, c'est que vous pouvez l'exécuter autant de fois que vous le voulez sans que cela fausse vos données.

MVC Music Store / Raven DB : Refaire HomeController, la bonne méthode

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Porting the HomeController, the Right Way", le cinquième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Comme je l'ai indiqué dans le billet précédent, nous pouvons solutionner le problème de la méthode GetTopSellingAlbums() grâce au map/reduce, mais cela n'est pas vraiment la bonne façon de faire les choses. Le problème en procédant de la sorte (en plus des regards effrayés et des cris de détresse que vous suscitez dès que vous mentionnez cette solution), c'est qu'on essaie de résoudre le problème selon une logique relationnelle. Et d'ailleurs, la solution précédente est quasiment identique à la façon dont une base de données relationnelle pourrait traiter ce genre de requête. Voyons plutôt quelle serait l'approche d'une base de données documents pour résoudre ce genre de problème.

La réponse est évidente à trouver : rappelez-vous que les documents sont indépendants et réfléchissez à nouveau à la question. Ce que nous cherchons à savoir, c'est quels sont les albums les plus vendus. Si nous ajoutions une propriété CountSold à l'album, cela deviendrait immédiatement bien plus simple de répondre à cette question. Et pour cela, il nous suffit de mettre à jour les différents albums qui font parti de la commande lorsque celle-ci est validée. C'est quelque chose de tout à fait acceptable et ce genre d'opération est couramment effectuée, y compris avec des bases de données SQL.

Pour l'instant, laissons de côté la façon de créer la propriété CountSold et de l'initialiser avec les bonnes valeurs (je verrai ça dans mon prochain billet). Nous supposerons donc que c'est déjà fait et qu'il ne nous reste plus qu'à trouver comment résoudre le problème de notre méthode GetTopSellingAlbums().

Et bien, c'est plutôt simple. Tout ce que nous avons à faire, c'est de définir un index sur CountSold.

// AlbumsByCountSold
from album in docs.Albums
select new { album.CountSold };

Avec ça, nous pouvons implémenter la fonction GetTopSellingAlbums() de la façon suivante :

C'est fait : simple, efficace et même élégant (même si c'est moi qui le dit).

vendredi 21 mai 2010

MVC Music Store / Raven DB : Refaire HomeController, méthode map/reduce

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Porting the HomeController, the map/reduce way", le quatrième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Actuellement, le contrôleur HomeController contient le code suivant :

Je n'aime vraiment pas quand un contrôleur se charge de faire des requêtes, mais ça n'est pas le sujet pour l'instant.

Grâce à EF Prof, on peut voir à quoi ressemble cette requête :

Et là on se trouve face à un problème très intéressant : il ne nous est pas possible de reproduire cette requête. En effet, cette requête porte sur plusieurs tables que dans notre modèle nous avons réparties dans différents documents.

Il existe plusieurs méthodes pour résoudre cela. Une des façon de faire serait de définir un index map / reduce au niveau des documents orders.

Note: Oui, je sais ce que vous allez dire.

La méthode que je suis sur le point de vous montrer n'est pas celle que je conseillerais dans la réalité. Mais je veux malgré tout vous la présenter. Dans mon prochain billet, je vous expliquerai la façon dont Raven permet de gérer ça dans les formes.

Avec Raven, le map / reduce consiste simplement en quelques requêtes Linq. Il n'y a donc pas de raison de s'affoler. Pour mémoire, nous avons défini les documents suivant dans notre base de données :

Nous créons l'index "SoldAlbums" à l'aide des requêtes suivantes :

// map
from order in docs.Orders
from line in order.Lines
select new{ line.Album, line.Quantity }

// reduce
from result in results
group result by result.Album into g
select new{ Album = g.Key, Quantity = g.Sum(x=>x.Quantity) }

Comme vous pouvez le voir, il s'agit de deux requêtes Linq toute simples.

Leur résultat devrait être le suivant :

Dès lors que nous avons cela, c'est un jeu d'enfant d'en faire découler GetTopSellingAlbums. En fait, la fonction ci-dessous implémente exactement la même logique et renvoie le même résultat que l'implémentation d'origine :

La façon dont elle fonctionne est très simple. Nous sélectionnons les albums les plus vendus (en triant les quantités par ordre décroissant), puis nous les chargeons depuis la base de données. Et dans le cas où nous aurions moins d'albums vendus que ce que nous comptons afficher, nous complétons avec d'autres albums normaux.

Au final ce code exécute 2 ou 3 requêtes. Je n'aime vraiment pas ça, mais sur ma machine, cela prend environ moins de 10 ms pour faire ces trois requêtes, ce qui est tout à fait supportable.

Je vous ai présenté cette solution parce que je voulais vous montrer que c'était une approche du problème, mais pas la solution recommandée pour le résoudre. Nous verrons une meilleure approche dans le billet suivant.

jeudi 20 mai 2010

MVC Music Store / Raven DB : Migrer les données

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Data migration", le troisième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Voici le code nécessaire pour lire les données dans la base de données de MVC Music Store et les transformer en documents comme attendu par Raven :

using (var documentStore = new DocumentStore { Url = "http://localhost:8080" })
{
    documentStore.Initialise();
    using (var session = documentStore.OpenSession())
    {
        foreach (var album in storeDB.Albums.Include("Artist").Include("Genre"))
        {
            session.Store(new
            {
                Id = "albums/" + album.AlbumId,
                album.AlbumArtUrl,
                Arist = new { album.Artist.Name, Id = "artists/" + album.Artist.ArtistId },
                Genre = new { album.Genre.Name, Id = "genres/" + album.Genre.GenreId },
                album.Price,
                album.Title,
            });
        }
        foreach (var genre in storeDB.Genres)
        {
            session.Store(new
            {
                genre.Description,
                genre.Name,
                Id = "genres/" + genre.GenreId
            });
        }
        session.SaveChanges();
    }
}

Comme vous pouvez le constater, c'est plutôt simple. Et même si c'est moi qui le dit, plutôt bien foutu.

J'ai utilisé des types anonymes parce que je me contente de migrer les données. Je ne m'occupe pas vraiment de savoir comment gérer les types pour l'instant.

mercredi 19 mai 2010

MVC Music Store / Raven DB : Configurer l'application

Ceci est la traduction du billet "Porting MVC Music Store to Raven: Setting up the application", le deuxième de la série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Juste quelques mots sur la façon de configurer Raven pour l'utiliser avec l'application MVC Music Store avant de me lancer dans le développement du reste du code.

  • Le fonctionnement retenu est (délibérément) très proche de celui employé avec NHibernate. Nous initialisons un objet DocumentStore au démarrage de l'application.
  • Puis nous gérons l'ouverture / fermeture des sessions dans le cadre de la requête HTTP, complété par une méthode CurrentSession() pour accéder à la session en cours.
  • Si l'application avait employé un conteneur, j'aurais fait en sorte que les contrôleurs récupèrent une instance de la session par son intermédiaire. Mais comme il n'y en a pas, je m'en tiens à une méthode statique.
    • Si cela ne vous plait vraiment pas, n'hésitez pas à proposer autre chose.
public class MvcApplication : System.Web.HttpApplication
{
    private const string RavenSessionKey = "Raven.Session";
    private static DocumentStore _documentStore;

    protected void Application_Start()
    {
        _documentStore = new DocumentStore { Url = "http://localhost:8080/" };
        _documentStore.Initialise();

        AreaRegistration.RegisterAllAreas();

        RegisterRoutes(RouteTable.Routes);
    }

    public MvcApplication()
    {
        BeginRequest += (sender, args) => HttpContext.Current.Items[RavenSessionKey] = _documentStore.OpenSession();
        EndRequest += (o, eventArgs) =>
        {
            var disposable = HttpContext.Current.Items[RavenSessionKey] as IDisposable;
            if (disposable != null)
                disposable.Dispose();
        };
    }

    public static IDocumentSession CurrentSession
    {
        get { return (IDocumentSession)HttpContext.Current.Items[RavenSessionKey]; }
    }
}

Et c'est à peu près tout, du moins en ce qui concerne l'initialisation de Raven.

mardi 18 mai 2010

MVC Music Store / Raven DB : Modèle de données

Ceci est la traduction du billet "Porting MVC Music Store to Raven: The data model", le premier d'une série consacrée au portage de l'application MVC Music Store sous RavenDB par Oren Eini, alias Ayende Rahien.

Le tutoriel "MVC Music Store" est venu à point nommé pour moi. Je souhaitais faire une application de démonstration pour Raven DB et le fait que quelqu'un d'autre ait déjà fait tout le travail ingrat (l'interface utilisateur :) à ma place et que je n'ai plus qu'à refaire l'accès aux données est une situation rêvée. Mon objectif est de ne rien toucher du tout au Javascript ou au code HTML et de me contenter de remplacer les contrôleurs. Ca devrait être intéressant de voir si je peux y arriver.

Le modèle de base de données dans le tutoriel d'origine est le suivant :

On peut déjà remarquer deux ou trois choses intéressantes dans ce schéma :

  • Il serait plus correct que la table Cart soit nommée CartLineItem puisqu'elle stocke une ligne par article dans le panier
    • CartId n'est pas une clé étrangère, mais référence le nom de l'utilisateur ou l'identifiant de la session
  • La table Artist gère uniquement le nom de l'artiste et rien d'autre.

A partir de ces informations, je considère que le modèle de données suivant devrait convenir.

Albums

  • Le document Album contient à la fois une référence pour le Genre et pour le libellé du genre. Cela nous permet d'afficher l'album sans avoir à référencer le document Genre.
  • Et pour les mêmes raisons, le document Album contient aussi le nom et l'identifiant de l'artiste.
  • Il n'y a pas un ensemble de documents Artists dans la base de données. Nous ne gérons aucune information sur les artistes, si ce n'est leur nom, et je ne vois donc pas de raison pour définir un document Artist pour l'instant.

Genre

Le document Genre est la réplique exacte de la table Genre, rien d'extraordinaire à ce niveau.

Cart

Le document Cart suit un format de document plutôt classique. Nous avons un simple document qui contient un tableau d'éléments là où le modèle de données relationnel contient un ensemble de lignes. Vous pouvez voir que le UserIdentifier nous sert pour stocker l'identifiant de l'utilisateur ou celui de la session pour le panier.

Orders

Order constitue une autre document plutôt standard. Nous regroupons toutes les information de la commande dans un simple document et nous stockons les informations liées à celle-ci (Address) dans un noeud spécifique.

Artist

Il n'y a pas de document Artist.

Pourquoi un document pour Genre et pas pour Artist ?

Pour la bonne raison que l'application va faire quelque chose avec les Genres (en plus d'afficher leur description) alors que la seule chose que l'on fait avec les Artistes est d'afficher leur nom dans la cadre d'un album. Pour l'instant, je considère que Artist fait partie intégrante de Album et qu'il n'y a donc pas à définir un document spécifique pour lui. La seule raison au fait qu'il existe un identifiant artiste en plus du nom est que je suppose (d'après les données) que la source pour les artistes est un système externe qui fait quelque chose d'un peu plus utile que simplement stocker le nom de l'artiste.

Nous n'avons fait que la moitié du travail à faire. Nous avons défini le modèle de données, maintenant nous devons étudier comment il sera utilisé dans un contexte plus large et lui ajouter le modèle de requête en utilisant des index. Nous verrons cela dans un prochain billet.

mercredi 28 avril 2010

C'est Quoi cette Expression Lambda ?

Ceci est la traduction du billet "What on Earth is a Lambda Expression?" de Simon Ince. Bien qu'il s'agisse d'une traduction, celle-ci s'inscrit dans une série de tutoriels dont le but est de me familiariser avec les nouveautés de C# 2, 3 et 4 :

  1. Les propriétés automatiques du c#
  2. Les types nullables en c#
  3. Les types implicites en c#

Ces derniers temps, j’ai eu à faire à quelques clients qui se demandaient ce qu’était une Expression Lambda, ce qui n’a pas manqué de me surprendre. Il semblerait donc qu’une seconde vague de développeurs se mette à utiliser les Lambdas (sans doute ceux qui n’étaient pas passé à C# 3.0 dès sa sortie) et qu’ils ont besoin de quelques pistes. C’est pourquoi ce billet est destiné à vous aider à comprendre ce que représentent les expressions lambdas.

Je ne vais pas chercher à répondre au comment, au pourquoi, au quand ou à quoi que ce soit dans ce genre. Il existe déjà de bien meilleurs billets sur le sujet. Non, je vais juste vous dire « bon sang, mais ça veut dire quoi une syntaxe pareille ? ».

Prenons un exemple concret

Je vais utiliser une situation classique auquel tout développeur est confronté de nos jours : filtrer une liste de Lamas en tenant compte de leur taille de leur propension à ronchonner (Ouais, je sais que c’est plus typique du pays de Candy que de celui de l’informatique, mais le vendredi après-midi c’est permis).

Supposons que nous ayons une liste de nos Lamas préférés :

private static List<Lama> Lamas = new List<Lama>()
{
    new Lama { Nom = "Larry", Taille = 10, EstRonchon = true },
    new Lama { Nom = "Loulou", Taille = 12, EstRonchon = false },
    new Lama { Nom = "Lara", Taille = 8, EstRonchon = true },
    new Lama { Nom = "Lorry", Taille = 4, EstRonchon = true },
    new Lama { Nom = "Laurel", Taille = 20, EstRonchon = false },
    new Lama { Nom = "Louise", Taille = 17, EstRonchon = true }
};

Maintenant, imaginez que je veuille obtenir une liste de tous les Lamas qui sont à la fois grands et du genre ronchonneur. On pourrait y arriver de la façon suivante :

var results = new List<Lama>();
foreach (var lama in Lamas)
{
    bool include = lama.EstRonchon && lama.Taille > 9;
    if (include)
        results.Add(lama);
}

Refactoriser en Lambda

Le problème c’est que ça fait un paquet de code pour appliquer un filtre tout bête… Alors que nous savons bien qu’il existe de supers méthodes d’extensions en LINQ qui permettent d’exécuter une commande Where sur une collection d’objets.

On va modifier un peu notre syntaxe de départ pour nous orienter dans la bonne direction. Notez bien que la plupart du code C# contenu dans le reste de ce billet est délibérément faux, étant donné que je cherche à vous conduire vers la solution. Je vous préviendrai la prochaine fois que vous aurez à faire à une syntaxe correcte !

Imaginons que Where prenne comme paramètre le nom d’une méthode qui réalise le filtrage de la liste. Notre code pourrait alors se présenter comme ceci :

var results = Lamas.Where(Filter);

... avec une méthode helper qu'on appellerait Filter :

private bool Filter(Lama lama)
{
    return lama.EstRonchon && lama.Taille > 9;
}

C’est pas complètement idiot ? La méthode Where appelle la méthode Filter en lui passant chaque Lama un par un pour vérifier s’il doit faire parti des résultats ou non.

Méthode anonyme en ligne

Ouais mais quand même : notre méthode Filter ne sert qu’à un seul endroit pour filtrer nos Lamas. On pourrait donc se simplifier la vie et éviter d’avoir à la déclarer en faisant une méthode en ligne à la place. Pourquoi pas quelque chose comme ci-dessous (encore une fois, c’est une syntaxe fictive comme la plupart du code dans ce billet) :

var results = Lamas.Where(
    bool Filter(Lama lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Ca c’est fait. Et en plus on s’est débarrassé du mot clé private puisque la méthode n’est plus un membre de la classe. Mais alors, à quoi ça sert qu’elle ait encore un nom ? Y’a qu’à le virer :

var results = Lamas.Where(
    bool (Lama lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Ca c’est déjà plus concis. Suivez-bien et je vous traduis ce que ça veut dire : « cette méthode renvoie un Booléen, et attend un Lama en entrée », suivi du code pour le corps de la méthode.

Types implicites

Attendez-voir. Le compilateur C# est quand même vachement intelligent, pas vrai ? Alors pourquoi est-ce que je me décarcasse à lui dire que la méthode renvoie un Booléen puisqu’il sait bien que la méthode Where a besoin d’un Booléen et qu’il est assez grand peut se rendre compte que la commande « EstRonchon && Taille > 9 » est une expression de type Booléen ? Tchao Tchao le Booléen :

var results = Lamas.Where(
    (Lama lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Et on sait bien que la méthode Where s’applique à une List<Lama>, ce qui fait que le seul argument possible pour cette méthode est de type Lama… Alors arrêtons d’écrire des trucs inutiles dans notre code :

var results = Lamas.Where(
    (lama)
    {
        return lama.EstRonchon && lama.Taille > 9;
    });

Constructeurs inutiles

Le truc c’est que notre méthode n’est rien de plus qu’une seule et ridiculement simple ligne d’expression Booléenne. Alors pourquoi avoir encore besoin du mot clé return ? Ou du point-virgule pour terminer la ligne ? On sait très bien ce qu’elle fait. Et on n’a quand même pas besoin des accolades pour une expression d’une seule ligne, pas vrai ?

var results = Lamas.Where(
    (lama)
        lama.EstRonchon && lama.Taille > 9
    );

Mais ça se complique un peu si on se met à supprimer les espaces inutiles :

var results = Lamas.Where( (lama) lama.EstRonchon && lama.Taille > 9 );

Là ça devient un peu plus coton à lire. Il nous faudrait trouver une autre façon de séparer les paramètres en entrée du corps de notre expression. Et avec C#, c’est justement à ça que sert l’opérateur « => » :

var results = Lamas.Where( (lama) => lama.EstRonchon && lama.Taille > 9 );

Et on n’a pas non plus besoin des parenthèses autour du paramètre en entrée puisqu’on en a un seul :

var results = Lamas.Where( lama => lama.EstRonchon && lama.Taille > 9 );

Et pourquoi diable gaspiller toutes ces lettres pour écrire « lama » à chaque fois alors qu’on pourrait très bien se contenter d’un simple « l » ?

var results = Lamas.Where( l => l.EstRonchon && l.Taille > 9 );

Résumé

Il s’avère que les trois dernières commandes ci-dessus sont des Expressions Lambdas valides qui filtrent une liste de Lamas pour nous. L’objectif de ce code n’a pas varié d’un iota et il continue à avoir la même signification :

Tiens. C'est une méthode qui prend un paramètre nommé « l » et renvoie un résultat Booléen en appliquant l’expression suivante au paramètre en entrée. T"as qu'à t'en servir pour filtrer les Lamas, steuplé !

Et maintenant, à vous de découvrir ce que la syntaxe ci-dessous peut bien vouloir dire :

grandRonchonLamas.ForEach(l => Console.WriteLine(l.Nom));

Je souhaite de tout cœur que cette approche un peu décalée a réussi à vous expliquer comment utiliser les Expressions Lambdas. Maintenant, vous n’avez plus qu’à approfondir tout ça et à vous documenter un peu pour comprendre des trucs comme Expression<>, Func<>, Action<>, etc…

Amusez-vous bien !

Ceci est la traduction du billet "What on Earth is a Lambda Expression?" de Simon Ince.