blog.pagesd.info

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

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.

mercredi 23 juin 2010

Activer l'intellisense pour le mapping NHibernate dans Visual Studio

Pour que Visual Studio propose l'intellisence quand on édite les fichier de configuration ou de mapping de NHibernate, il faut définir la propriété "Schema" pour qu'elle pointe sur le fichier "nhibernate-mapping.xsd" ou "nhibernate-configuration.xsd".

Mais plutôt que de faire ça à la main, il est beaucoup plus simple de référencer ces deux fichiers une bon une fois pour toute au niveau de Visual Studio. Pour cela, il suffit de copier les fichiers "nhibernate-mapping.xsd" et "nhibernate-configuration.xsd" dans le répertoire destiné à contenir les schémas pour Visual Studio. Généralement (en tout cas dans mon cas), ce répertoire est : C:\Program Files (x86)\Microsoft Visual Studio 9.0\Xml\Schemas.

Et c'est tout (à moins qu'il m'ai fallu relancer Visual Studio ?).

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.

vendredi 2 avril 2010

Faire fonctionner ASP.NET MVC sur IIS 6

Voici la façon la plus simple possible qui soit pour qu'une application ASP.NET MVC puisse être installée et fonctionner sur un serveur IIS 6 qui n'y est pas préparé. Personnellement, j'ai dû refaire ça hier et j'avais presque oublié comment m'y prendre. Donc, pour ne pas prendre de risque, je préfère noter tout ça pour la prochaine fois où je pourrais en avoir besoin.

Concrètement, je n'ai rien inventé et tout vient du billet de Phil Haack : ASP.NET MVC on IIS 6 Walkthrough.

Ajouter l'extension .aspx aux routes MVC

Pour que MVC fonctionne sur un IIS 6 où ASP.NET s'attend à avoir des URLs avec des extensions ".aspx", il faut commencer par mettre à jour les routes définies dans le fichier Global.asax.cs pour qu'elles prennent en compte cette extension.

Comment faire

Pour cela, il faut modifier la méthode RegisterRoutes comme ci-dessous :

public static void RegisterRoutes(RouteCollection routes)
{
  routes.IgnoreRoute("{resource}.axd/{*pathInfo}");

  routes.MapRoute(
    "Default",
    "{controller}.aspx/{action}/{id}",
    new { action = "Index", id = "" }
  );

  routes.MapRoute(
    "Root",
    "",
    new { controller = "Home", action = "Index", id = "" }
  );
}

La première route (nommée "Default") fait qu'on aura plus des URLs du style "http://www.example.com/Controller/Action/Id" mais "http://www.example.com/Action.aspx/Action/Id" que IIS 6 sait très bien gérer.

La seconde route (nommée "Root") est nécessaire pour gérer l'URL "http://www.example.com/" que la route "Default" ne sait plus prendre en compte puisqu'elle attend une extension ".aspx".

Remarque

Pour que l'URL "http://www.example.com/" fonctionne, il faut également penser à configurer IIS pour que le fichier Default.aspx soit un des documents par défaut du site (à la place ou en plus des fichiers Index.aspx, Index.htm...).

Attention

Etant donné que nos URLs prennent des ".aspx", il faut aussi modifier le fichier Web.Config pour mettre à jour l'URL qui y est paramétrée pour effectuer l'authentification. Rechercher "loginUrl=" et remplacer :

<forms loginUrl="~/Account/LogOn" ...

Par :

<forms loginUrl="~/Account.aspx/LogOn" ...

Après ce gros morceau, il ne devrait pas y avoir d'autre problème avec les URLs. Sauf si on a codé en dur des liens au lieu d'utiliser les helpers Html.ActionLink() ou Url.Action() qui vont bien... Mais ça, c'est le métier qui rentre.

Installer ASP.NET MVC sur le serveur IIS 6

Je rigole. C'est beaucoup trop compliqué à faire.

Par contre, ce qui est beaucoup plus fastoche, c'est de copier System.Web.Mvc.dll dans le répertoire "bin" de l'application !

Un dernier truc

Tant qu'à faire de modifier le Web.Config, autant en profiter pour lui ajouter la ligne :

<customErrors mode="Off" />

Au moins, avec ça, si jamais il y a un problème on est sûr d'avoir un bel écran jaune avec des messages d'erreur en pagaille.

mercredi 24 mars 2010

Contact Manager avec MongoDB et NoRM

Maintenant que j'ai enfin expédié mon billet consacré au portage hyper passionnant de l'application ContactManager sous NHibernate, je peux enfin revenir à des trucs beaucoup plus amusants.

Ca faisait un petit moment que j'avais envie de tester pour de bon une de ces nouvelles bases de données NoSQL dont on nous rabat les oreilles ces derniers temps.

Mais bon, ça demande du temps et il faut choisir quelle NoBase de données employer et il faut qu'il y ait des pilotes pour C# et il y a plein d'autres trucs à faire... Et un soir, Rob Conery sort un billet où non seulement il explique comment utiliser une base de données MongoDB avec LINQ mais en plus il présente le "provider" NoRM sur lequel il est en train de travailler avec quelques autres.

C'est trop beau pour être vrai => je télécharge, je trifouille et j'arrive à peu près à faire marcher ses bouts d'exemples.

C'est pas compliqué ! J'ai plus qu'à transformer Contact Manager pour qu'il fonctionne avec MongoDB et NoRM !!!

Oulla! Ne nous emballons pas. NoRM est en cours de développement donc y'a pas forcément tout ce qu'il faut et surtout j'ai pas encore tout assimilé en matière de base de données documents.

C'est pas compliqué ! J'ai qu'à refaire seulement la toute première étape !!!

Le faire, c'est bien et ça va même assez vite. Le dire, c'est mieux mais c'est un peu plus long.

Et donc, après quelques heures d'efforts, voici mon tutorial sur le développement d'une application de gestion de contacts avec ASP.NET MVC, MongoDB et NoRM qui reprend quasiment mot à mot la première étape du tutoriel de Microsoft. Comme l'original, ce tutoriel présente donc comment créer une application de gestion de contact de la façon la plus simple qui soit. Tout au long de celui-ci, vous pourrez voir comment mettre en place le support d'opérations CRUD classiques vers une base de données MongoDB : création, lecture, mise à jour et suppression d'enregistrements.

mardi 23 mars 2010

Porter Contact Manager sous NHibernate

Après avoir fait quelques essais très basiques avec NHibernate, j'ai voulu essayer d'être plus concret et d'aller un peu plus loin en tentant de porter l'application Contact Manager sous NHibernate. A priori, ça ne devait pas être trop compliqué dans la mesure où il y a quelques temps j'avais déjà remplacé Entity Framework par LINQ to SQL et que ça ne s'était pas trop mal passé. Sans compter, que je souhaitais juste faire en sorte que ça marche à peu près tel quel, sans trop chercher à découvrir ou à mettre au point la "bonne" façon d'utiliser NHibernate avec ASP.NET MVC.

Dans la pratique, ça m'a pris un peu plus de temps que ce que j'avais pensé y consacrer. En fait, j'aurais pu faire ce billet depuis un petit moment, mais avant il fallait que j'arrive à mettre au propre la solution que j'avais suivie. Parce que c'est là que le bât blesse, c'est qu'avec NHibernate il y a tout plein de façons de faire et que je n'ai pas réussi à trouver une documentation "officielle" (et récente) qui décrive clairement quel est l'état de l'art pour utiliser NHibernate dans le cadre d'une application ASP.NET.

Voici donc parmi tous mes essais (avec des réussites, des ratés et des abandons) une des façons de faire à laquelle je suis arrivé et qui est assez "simple" à expliquer. Ca marche correctement, mais je ne peux absolument pas affirmer que c'est la méthode à suivre.

Etape 1 : Installer NHibernate

Pour faire simple, le plus pratique est d'aller sur le site de Castle ActiveRecord et d'y télécharger la toute dernière version d'Active Record. Une fois l'archive décompressée, il suffit de créer un répertoire "lib" dans le répertoire racine de "ContactManager" et d'y copier les quelques fichiers suivants :

  • Iesi.Collections.dll
  • Iesi.Collections.xml
  • LinFu.DynamicProxy.dll
  • log4net.dll
  • log4net.xml
  • nhibernate-configuration.xsd
  • nhibernate-mapping.xsd
  • NHibernate.ByteCode.LinFu.dll
  • NHibernate.ByteCode.LinFu.xml
  • NHibernate.dll
  • NHibernate.Linq.dll
  • NHibernate.xml

En réalité, je n'utilise pas du tout l'approche Active Record. Mais l'avantage de passer par leur site c'est qu'on est certain d'avoir DLLs récentes avec des versions compatibles entre elles. Au cours de mes différents essais, j'avais récupéré des DLLs au fur et à mesure de mes besoins et d'un petit peu tous les côtés et ça m'avait joué quelques mauvais tours.

Une fois ces différentes librairies mises en place, il suffit de référencer les 4 éléments suivants dans l'application ContactManager existante :

  • log4net.dll
  • NHibernate.dll
  • NHibernate.ByteCode.LinFu.dll
  • NHibernate.Linq.dll

Et pour bien faire les choses, il faut aussi prendre le temps de dé-référencer les éléments qui correspondent à Entity Framework et par conséquent de supprimer les fichiers liés à EF dans le sous-répertoire Models.

Etape 2 : Configurer NHibernate

NHibernate est installé et référencé au niveau du projet. Il faut maintenant le configurer pour lui expliquer qu'on aimerait bien qu'il utilise notre base de données SQL Server habituelle. Et là commence la galère puisqu'il existe apparement des tonnes de façon différentes pour faire ça. J'en ai essayé quelques unes et je n'ai pas vraiment très bien compris quels sont les avantages et les inconvénients de telle ou telle façon de faire (mais c'est vrai que j'ai pas trop passé de temps dessus).

Par conséquent, je vais aller au plus simple et donner directement la méthode que j'ai repérée sur la vidéo de présentation de NHibernate proposée par TekPub. C'est juste un extrait, mais ça m'a suffi pour emprunter le source à l'application Kona de Rob Conery. Cela consiste à créer un fichier "nhibernate.config" à la racine de l'application ASP.NET MVC pour y définir tous les éléments nécessaires pour configurer NHibernate :

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-configuration xmlns="urn:nhibernate-configuration-2.2">

  <session-factory name="ContactManager">
    <property name="dialect">NHibernate.Dialect.MsSql2008Dialect</property>
    <property name="connection.driver_class">NHibernate.Driver.SqlClientDriver</property>
    <property name="connection.connection_string">server=.\SQLExpress;database=ContactManagerDB;Integrated Security=true;</property>
    <property name="show_sql">true</property>
    <property name="cache.use_query_cache">false</property>
    <property name="adonet.batch_size">16</property>
    <property name="proxyfactory.factory_class">NHibernate.ByteCode.LinFu.ProxyFactoryFactory, NHibernate.ByteCode.LinFu</property>
    <mapping assembly="ContactManager" />
  </session-factory>
  
</hibernate-configuration>

Par rapport à l'écriture de ces informations dans le Web.config, il me semble que c'est un peu plus clair d'isoler ça dans un fichier à part (sans compter que ça évite de complexitrifier le fichier Web.config inutilement). Sinon, il aurait aussi été possible d'utiliser directement le fichier "hibernate.cfg.xml" standard mais je trouve qu'un fichier ".config" c'est plus propre (et plus sûr ?) qu'un fichier ".xml".

Etape 3 : Gérer une session NHibernate

Dans la version Entity Framework de l'application Contact Manager, on utilise un objet ContactManagerDBEntities généré automatiquement par ADO.NET EF pour communiquer avec la base de données SQL Server. Dans la version portée sous LINQ to SQL, c'est un objet ContactManagerModelDataContext qui joue ce rôle.

En ce qui concerne NHibernate, les communications avec la base de données se font par l'intermédiaire d'un objet Session. Et là aussi, pour gérer / instancier cet objet Session, il existe tout plein de façons de faire.

Pour l'instant, je reprend de façon ultra simplifiée la méthode proposée par Rob Conery parce qu'il est possible de la mettre en oeuvre en très très peu de lignes au niveau du fichier global.asax.cs :

using System;
using System.IO;
using System.Web.Mvc;
using System.Web.Routing;
using NHibernate;
using NHibernate.Cfg;

namespace ContactManager
{

    public class MvcApplication : System.Web.HttpApplication
    {

        public static ISessionFactory SessionFactory = CreateSessionFactory();

        private static ISessionFactory CreateSessionFactory()
        {
            var cfg = new Configuration().Configure(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "nhibernate.config"));
            return cfg.BuildSessionFactory();
        }

Grâce à ça, il devient tout simple de remplacer la ligne :

private ContactManagerDBEntities _entities = new ContactManagerDBEntities();

par l'instruction suivante :

private ISession _session = MvcApplication.SessionFactory.GetCurrentSession();

Etape 4 : Faire le mapping entre les tables et les objets

Avec NHibernate, les classes Contact et Group ne sont pas générées automatiquement comme avec Entity Framework ou LINQ to SQL. Il faut donc créer à la main des classes POCO (c'est pas le plus compliqué) et il faut également créer un fichier de mapping XML pour associer chaque classe à la table correspondante dans la base de données (c'est déjà un peu plus mariole).

Je ne vais pas expliquer comment faire ni entrer dans les détails de comment ça marche (en plus je n'ai pas encore suffisament bien compris moi-même), mais voici ce à quoi je suis arrivé après quelques essais.

Models\Contact.cs

namespace ContactManager.Models
{
    public partial class Contact
    {
        public Contact()
        {
            Group = new Group();
        }
        public virtual int Id { get; set; }
        public virtual string FirstName { get; set; }
        public virtual string LastName { get; set; }
        public virtual string Phone { get; set; }
        public virtual string Email { get; set; }
        public virtual Group Group { get; set; }

        /// <summary>
        /// Create a new Contact object.
        /// </summary>
        /// <param name="id">Initial value of Id.</param>
        /// <param name="firstName">Initial value of FirstName.</param>
        /// <param name="lastName">Initial value of LastName.</param>
        /// <param name="phone">Initial value of Phone.</param>
        /// <param name="email">Initial value of Email.</param>
        public static Contact CreateContact(int id, string firstName, string lastName, string phone, string email)
        {
            Contact contact = new Contact();
            contact.Id = id;
            contact.FirstName = firstName;
            contact.LastName = lastName;
            contact.Phone = phone;
            contact.Email = email;
            return contact;
        }
    }

}

Mappings\Contact.hbm.xml

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">

  <class name="Contact" table="Contacts" dynamic-update="true">

    <cache usage="read-write" />

    <id name="Id" column="Id" type="integer">
      <generator class="identity" />
    </id>
    <property name="FirstName" type="string" />
    <property name="LastName" type="string" />
    <property name="Phone" type="string" />
    <property name="Email" type="string" />
    <many-to-one name="Group" column="groupId" not-null="true" />

  </class>

</hibernate-mapping>

Models\Group.cs

using System.Collections.Generic;

namespace ContactManager.Models
{
    public class Group
    {
        public Group()
        {
            Contacts = new List<Contact>();
        }
        public virtual int Id { get; set; }
        public virtual string Name { get; set; }
        public virtual IList<Contact> Contacts { get; set; }
    }
}

Mappings\Group.hbm.xml

<?xml version="1.0" encoding="utf-8" ?>
<hibernate-mapping xmlns="urn:nhibernate-mapping-2.2" namespace="ContactManager.Models" assembly="ContactManager">

  <class name="Group" table="Groups" dynamic-update="true">

    <cache usage="read-write" />

    <id name="Id" column="Id" type="integer">
      <generator class="identity" />
    </id>
    <property name="Name" type="string" />
    <bag
        name="Contacts"
        inverse="true"
        lazy="true">
      <key column="groupId" />
      <one-to-many class="Contact" />
    </bag>

  </class>

</hibernate-mapping>

En ce qui concerne les deux fichiers Contact.hbm.xml et Group.hbm.xml, il faut impérativement modifier leur propriété "Action de génération" au niveau de l'explorateur de projet pour la définir à "Ressource incorporée" sans quoi le mapping NHibernate ne fonctionne pas. C'est un problème très facile à identifier parce qu'on obtient une erreur "Association references unmapped class: ContactManager.Models.Contact" dès le lancement de l'application.

Redévelopper le repository avec NHibernate

Après ces 4 premières étapes, tout est enfin prêt pour pouvoir modifier la couche repository de l'application afin de remplacer Entity Framework par NHibernate :

  • NHibernate est installé et référencé
  • NHibernate est configuré pour accéder à la base de données SQL Server
  • Un objet NHibernate Session a été défini pour communiquer avec la base de données
  • Les objets données et les fichiers de mapping NHibernate sont en place

Par conséquent, il ne reste plus qu'à implémenter l'interface IContactManagerRepository en utilisant NHibernate et LINQ to NHibernate pour remplacer le fichier EntityContactManagerRepository.cs qui l'implémentait pour Entity Framework.

Models\NHContactManagerRepository.cs

using System.Collections.Generic;
using System.Linq;
using NHibernate;
using NHibernate.Linq;

namespace ContactManager.Models
{
    public class NHContactManagerRepository : IContactManagerRepository
    {
        private ISession _session = ContactManager.MvcApplication.SessionFactory.OpenSession();

        public Contact GetContact(int id)
        {
            return (from c in _session.Linq<Contact>()
                    where c.Id == id
                    select c).FirstOrDefault();
        }

        public Contact CreateContact(int groupId, Contact contactToCreate)
        {
            // Associate group with contact
            contactToCreate.Group = GetGroup(groupId);

            // Save new contact
            _session.SaveOrUpdate(contactToCreate);
            return contactToCreate;
        }

        public Contact EditContact(int groupId, Contact contactToEdit)
        {
            // Associate with new group
            contactToEdit.Group = GetGroup(groupId);

            // Save changes
            _session.SaveOrUpdate(contactToEdit);
            _session.Flush();
            return contactToEdit;
        }

        public void DeleteContact(Contact contactToDelete)
        {
            _session.Delete(contactToDelete);
            _session.Flush();
        }

        public Group CreateGroup(Group groupToCreate)
        {
            _session.SaveOrUpdate(groupToCreate);
            return groupToCreate;
        }

        public IEnumerable<Group> ListGroups()
        {
            return _session.Linq<Group>().OrderBy(o => o.Name).ToList();
        }

        public Group GetFirstGroup()
        {
            return _session.Linq<Group>().FirstOrDefault();
        }

        public Group GetGroup(int id)
        {
            return (from g in _session.Linq<Group>()
                    where g.Id == id
                    select g).FirstOrDefault();
        }

        public void DeleteGroup(Group groupToDelete)
        {
            _session.Delete(groupToDelete);
            _session.Flush();
        }

    }
}

Une fois encore, je n'ai pas cherché à faire "bien" mais simplement à ce que ça marche. Dans la pratique, il vaudrait mieux éviter tous ces "_session.Flush()" et passer par des transactions et des "using" mais pour l'instant, ce n'était absolument pas mon objectif.

Pour pouvoir compiler et vérifier que tout fonctionne comme prévu, il reste encore à modifier le constructeur ContactManagerService pour qu'il utilise ce repository en lieu et place de celui d'Entity Framework.

public ContactManagerService(IValidationDictionary validationDictionary)
    : this(validationDictionary, new NHContactManagerRepository())
{
}

Une fois cette ultime modification réalisée, la solution compile sans aucune erreur et elle m'offre bien les mêmes fonctionnalités que les versions avec Entity Framework ou LINQ to SQL :

  • lister les contacts rattachés à un groupe,
  • ajouter, modifier ou supprimer un contact,
  • ajouter ou supprimer un groupe de contacts.

Et juste pour le plaisir, je peux lancer les quelques tests unitaires de ContactManager.Test et tout va bien !

Conclusion

Par rapport au passage sous LINQ to SQL, ça a été beaucoup plus laborieux. Le problème, c'est qu'il y a à la fois beaucoup (trop) de façon de faire et beaucoup (trop) d'informations disponibles ce qui fait qu'il n'est vraiment pas évident de savoir dans quelle direction partir.

Mais malgré tout, c'est quand même un succès. Il a suffi de changer le repository (et de recréer les classes Contact et Group générées automatiquement) pour que l'application ContactManager de base fonctionne correctement avec NHibernate. C'est donc un nouveau bon point pour le pattern Repository (zut! juste au moment où j'avais presque envie de me (ré)orienter vers Active Record). Et avec la prochaine version de Entity Framework qui devrait être POCO friendly, on peut même supposer que ce sera de plus en plus facile de passer d'un ORM à l'autre.

Peut-être que plus tard (c'est à dire une fois que je saurai faire), j'écrirai un autre billet pour présenter une façon d'utiliser Nibernate avec ASPNET MVC qui soit un peu plus orthodoxe. Mais déjà, j'aimerai très vite essayer de voir ce que ça donne en changeant la base de données et en testant cette version de Contactmanager NHibernate avec MySQL ou Oracle.

jeudi 18 mars 2010

Localhost lent avec Firefox, Safari et Chrome

Ces derniers temps, je travaille sur une charte graphique pour ASP.NET MVC. Je teste au fur et à mesure sous Internet Explorer 8 (parce que c'est la cible de l'application) et sous Firefox 3.5 (pour être sûr de faire du code à peu près correct).

J'avais l'impression que tout allait bien, mais ces derniers temps je me suis rendu compte que l'affichage était vraiment lent. Et pourtant, je fais mes essais avec le projet ASP.NET MVC quasi vide créé par défaut par Visual Studio 2008. En particulier, l'image de fond mettait une bonne seconde avant de s'afficher. Alors que c'est un bête PNG de 350 octets ! Ca y est, mon disque dur SSD est devenu lent :(

Au début, je me suis dit que je verrai ça plus tard jusqu'à ce que je remarque que ce phénomène de lenteur était beaucoup plus sensible avec Firefox et quasiment inexistant avec Internet Explorer. (Ah! Ah! Microsoft qui est meilleur que Mozilla :). Et là ça a fait tilt. Je me suis souvenu que j'avais déjà vu ce genre de problème signalé quelque part : une application ASP.NET MVC qui devient lente quand on utilise Firefox.

Y'avait plus qu'à aller sur Google pour voir comment régler ça. Et en deux temps trois mouvements j'ai eu la réponse à mon problème : un bête problème d'IPv6 avec localhost (pas d'inquiétude, moi non plus je sais pas ce que ça veut dire).

D'abord, une bidouille spéciale pour Firefox (Fixing Firefox Slowness with localhost on Vista (or XP with IPv6) qui consiste à modifier la configuration avancée de Firefox :

  • taper about:config dans la barre d'adresse
  • promettre de bien faire attention
  • filtrer sur le paramètre network.dns.disableIPv6
  • double-cliquer sur la ligne obtenue pour passer de "false" (valeur par défaut) à "true" (valeur définie par l'utilisateur)
  • quitter et redémarre Firefox pour faire bonne mesure (pas sûr que cela soit vraiment nécessaire)

C'est déjà pas mal, mais en y regardant de plus près, j'ai vu que cette lenteur concernait aussi Chrome et Safari (Slow IIS on Vista with Firefox, Chrome or Safari). Si jamais il me reprend l'envie d'installer Safari ou Chrome (ou Opera ?) un jour ou l'autre, je suis bien obligé de faire les choses correctement.

Et étant donné que le contenu de mon fichier hosts ne ressemblait pas tout à fait à ce qui était indiqué, j'ai continué à chercher et je suis finalement tombé sur un truc un peu plus explicite sur StackOverflow : My Local Host goes so slow now that I am on windows 7 and Asp.net MVC.

Avec ça, je peux faire ma modification, si ce n'est que mon fidèle Notepad ne veut pas me laisser enregistrer le fichier et attend de moi que je lui donne un autre nom ou que je le sauvegarde dans "Mes Documents". Qu'à cela ne tienne, je quitte puis j'exécute Notepad en tant qu'administrateur et ce coup-ci plus de problème.

Mon fichier C:\Windows\System32\drivers\etc\hosts d'origine :

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host

# localhost name resolution is handled within DNS itself.
#       127.0.0.1       localhost
#       ::1             localhost

Le même fichier hosts après avoir dé-commenté la ligne 127.0.0.1 localhost :

# Copyright (c) 1993-2009 Microsoft Corp.
#
# This is a sample HOSTS file used by Microsoft TCP/IP for Windows.
#
# This file contains the mappings of IP addresses to host names. Each
# entry should be kept on an individual line. The IP address should
# be placed in the first column followed by the corresponding host name.
# The IP address and the host name should be separated by at least one
# space.
#
# Additionally, comments (such as these) may be inserted on individual
# lines or following the machine name denoted by a '#' symbol.
#
# For example:
#
#      102.54.94.97     rhino.acme.com          # source server
#       38.25.63.10     x.acme.com              # x client host

# localhost name resolution is handled within DNS itself.
127.0.0.1       localhost
#       ::1             localhost

Et même après avoir remis le paramètre network.dns.disableIPv6 de Firefox à "false", je peux débuguer ma charte graphique sous Firefox sans attendre des heures une seconde à chaque fois que je rafraichis la page.

- page 1 de 4