mercredi 31 décembre 2008

Blogger du code avec blogger.com

Pas de tutoriel aujourd'hui

Suite à de nombreux déboires pour essayer de trouver une solution efficace pour blogger du code, j'ai fini par passer une bonne partie de la journée à coder un parser HTML pour améliorer mon optimisateur HTML.

Si vous avez déjà essayé de blogger du code, vous connaissez sans doute le problème... et avec les Generics du C# c'est encore pire (il ressemblent à des balises HTML).

C'est maintenant résolu, grâce au script SyntaxHighlighter couplé à l'optimisateur HTML


Blogger du code : recette

Configurer SyntaxHighlighter

La première bonne nouvelle c'est qu'on peut directement faire des liens sur les scripts existants.
Pas de problématique d'hébergement.

1) Rajouter dans l'entête de votre template HTML, la feuille CSS suivante :


<link href='http://howard.ross.work.googlepages.com/SyntaxHighlighter.css' rel='stylesheet' type='text/css'/>

2) Rajouter, tout en bas de votre page, avant la fermeture de votre balise body, le script suivant :


<script language="javascript" src="http://howard.ross.work.googlepages.com/shCore.js"></script>
<script language="javascript" src="http://howard.ross.work.googlepages.com/shBrushCSharp.js"></script>
<script language="javascript" src="http://howard.ross.work.googlepages.com/shBrushXml.js"></script>
<script language="javascript" src="http://howard.ross.work.googlepages.com/shBrushSql.js"></script>
<script language="javascript" src="http://howard.ross.work.googlepages.com/shBrushJScript.js"></script>
<script language="javascript">
dp.SyntaxHighlighter.ClipboardSwf = 'http://howard.ross.work.googlepages.com/clipboard.swf';
dp.SyntaxHighlighter.BloggerMode(); dp.SyntaxHighlighter.HighlightAll('code');
</script>

Utiliser SyntaxHighlighter

Vous pouvez dès maintenant utiliser SyntaxHightlighter.
Pour cela il suffit de faire une balise pre (texte préformaté) comme suit :


<pre name="code" class="c#">
using System;

public class A<T>
{
public A(string text)
{
this._text = text;
}
private string _text;
}</pre>

Vous pouvez utiliser ce système pour différents langages en changeant simplement l'attribut class de la balise pre.
Les scripts que j'ai donné en exemple permettent le classes suivantes :

  • c#
  • xml
  • js
  • sql

Le seul inconvénient notable c'est qu'il faut terminer la balise pre sur la dernière ligne de code, sinon SyntaxHighlighter rajoute un ligne de code vide.


Optimisation HTML : la cerise sur le gâteau

http://www.blogger.com est un moteur de blog intéressant.
Néanmoins, je n'utilise pas son éditeur, trop limité à mon goût, j'écris directement en HTML.
Du coup, je rencontre quelques soucis avec certains caractères, notamment ceux des Generics du C#.

Pour contourner ça, intervient l'optimisateur HTML, qui va :

  • Réduire la taille du HTML en enlevant les espaces inutiles
  • Améliorer le HTML en remplaçant les accents par les équivalents HTML (&acute;, etc.), ce qui est également une best-practice pour le référencement
  • Laisser le code en paix en ne touchant pas au contenu de la balise pre, à l'exception des > et < qui vont se transformer en &gt; &gt;

3) Copier l'intégralité de l'article en HTML dans la textarea 'Entrez votre HTML'
Cliquer sur 'Optimize' et récupérer le contenu optimisé

html optimisation

Demain nous allons créer notre propre MembershipProvider, en utilisant notre classe User

mardi 30 décembre 2008

La classe métier User

L'étape d'aujourd'hui consiste à coder la première classe de l'application : la classe User.

Entity Framework : extension et bon usage

Detach(object entity)

L'Entity Framework fourni par défaut un mécanisme de tracking des modifications sur les entités.
Ce mécanime a pour objectif de retenir les modifications effectuées pour savoir quel entités mettre à jour en base lors de l'appel à SaveChanges().

L'inconvénient dans notre cas est la dépendance qui existe entre l'entité et le contexte.
Il est préférable pour l'instant que le contexte ait une durée de vie très courte pour des raisons de performance.
Pour résoudre ce problème, il exite une méthode Detach(object entity) pour détacher une entité du contexte et que nous allons utiliser.

AttachAsModified(object entity)

La version actuelle de l'Entity Framwork ne fournie pas de méthode simple pour mettre à jour un objet détaché de la base.
En effet, la méthode Attach(object entity) attahce l'objet comme 'non modifié'.
Il y a deux méthodes pour contourner ceci :

  • Marquer tous les champs de l'entité comme 'modifié' avant l'appel à SaveChanges()
  • Requêter le contexte pour récupérer l'objet et le mettre à jour, via la méthode ApplyPropertyChanges

Nous allons choisir la première méthode, car la seconde présente l'inconvénvient de faire une requête au contexte, et donc à la base.
Pour mettre en place cette premère méthode, nous allons utiliser une méthode d'extension founir par Daniel Simmons, de l'équipe de développement de l'Entity Framework chez Microsoft.
Voir l'article original (en anglais)

La classe statique EntityFrameworkExtension contenant le code de la méthode d'extension est fournie avec les fichiers du jour

Hashage du mot de passe

1) La première étape consiste à gérer le hashage du mot de passe.
Le framework .Net nous fourni tout le nécessaire.


/// <summary>
/// Hash a chaine donnée avec l'algo MD5
/// </summary>
public static string Hash(string text)
{
string retour = string.Empty;

// le service de hashage
MD5CryptoServiceProvider hasher = new MD5CryptoServiceProvider();
// l'encodeur
UTF8Encoding encoder = new UTF8Encoding();
// on hash
byte[] hash = hasher.ComputeHash(encoder.GetBytes(text));
// on boucle sur la chaine d'octets retournée
for(int i = 0; i < hash.Length; i++)
// on la colle dans le résultat sous forme de caractère
retour += hash[i].ToString();

return retour;
}

Constructeurs

2) Comme vu dans l'article précédent, il nous faut masquer les constructeurs.
On va déclarer deux constructeurs : le constructeur vide et le constructeur pour gérer un nouvel utilisateur. Ces constructeurs seront protectedOn place le tout dans une région 'Protected' et dans une sous-région 'Construction', pour bien ranger notre code :


#region Protected

#region Construction

/// <summary>
/// Constructeur de base.
/// </summary>
protected User()
{
}
/// <summary>
/// Constructeur pour un nouvel utilisateur
/// Contient les paramètres obligatoires
/// </summary>
protected User(string login, string email, string password)
{
this._Login = login;
this._Email = email;
// on hash le mot de passe pour le stocker de façon sécurisée
this._PasswordHash = Hash(password);

// on créé un nouvel identifiant extérieur (GUID)
this._IdExternal = Guid.NewGuid();
// on défini la date de création
this.DateCreation = DateTime.Now;
}

#endregion

#endregion

Authentification

3) Grâce à la classe MvcBlogEntities, l'authentification est très facile à coder.


/// <summary>
/// Vérifie l'authentification d'une utilisateur
/// </summary>
public static bool Authenticate(string login, string password)
{
bool retour = false;

// on hash le mot de passe
string hashPwd = User.Hash(password);

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// on récupère le premier utilisateur avec ce login et ce mot de passe
User user = (from u in ctx.Users
where (u.Login == login
&& u.PasswordHash == hashPwd
)
select u).FirstOrDefault();

// si on en a un : succès
retour = (user != null);
}
return retour;
}

4) Rajouter une méthode de test d'authentification à notre test unitaire TestUser.
Vous noterez qu'on ne peut pas l'essayer tout de suite : le constructeur de User étant inaccessible, notre première méthode TestCreation ne fonctionne plus.


Création d'un utilisateur

5) Créer ensuite la méthode de création d'un utilisateur.
On place celle-ci dans une région 'Static' dans une sous-région 'Factory'.
A noter qu'on vérifie scrupuleusement les paramètres en entrée. Si ceux-ci ne sont pas corrects, on retourne null
Si une erreur se produit lors de la création en base, comme un problème de contrainte d'unicité ou d'accès à la base, on retourne null également.

A noter également : on ne vérifie pas les contraintes d'unicité avant l'insertion, car ceci est relativement inutile : si deux utilisateur crééent le même login en même temps, la contrainte sera de toute façon violée.
Par désign, on laisse Sql Server s'occuper de cette partie.


#region Static

#region Factories

/// <summary>
/// Créé un nouvel utilisateur en base
///
/// Retourne null en cas d'échec
/// </summary>
public static User CreateNew(string login, string email, string password)
{
User retour = null;

// si les paramètres sont valides
if(!string.IsNullOrEmpty(login)
&& !string.IsNullOrEmpty(password)
&& !string.IsNullOrEmpty(email))
{
using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// créé une instance
retour = new User(login, email, password);
try
{
// l'ajoute au contexte
ctx.AddToUsers(retour);

// tente un enregistrement en base
if(1 != ctx.SaveChanges())
// en cas d'échec, retourne null
retour = null;
}
// pas de traitement d'exception ici
catch { }
}
}
return retour;
}

#endregion

#endregion

6) Modifier la méthode TestCreation dans notre test unitaire TestUser pour utiliser cette nouvelle methode.
Vous pouvez maintenant tester la création puis l'authentification.
La classe de test finale est fournie à la fin de ce post.


Chargement

7) Coder les différentes méthodes de chargement standard pour un utilisateur.
On aura besoin de :

  • GetByID
  • GetByIdExternal
  • GetByLogin
  • GetByEmail
  • List
  • ListByLogin
    • Liste les utilisateurs dont le login contient une partie de la chaine donnée en parametre. Utilisé dans les pages d'administration.
  • ListByEmail
    • Liste les utilisateurs dont l'email contient une partie de la chaine donnée en parametre. Utilisé dans les pages d'administration.

/// <summary>
/// Retourne l'utilisateur demandé
/// </summary>
public static User GetById(int id)
{
User retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// on fait un appel direct suivir de FirstOrDefault
// car on sait qu'il ne peut y avoir qu'un seul résultat maximum
retour = (from u in ctx.Users
where u.ID == id
select u
).FirstOrDefault();

// si valide
if(retour != null)
// on le détache du contexte
ctx.Detach(retour);
}
return retour;
}
/// <summary>
/// Retourne l'utilisateur avec l'ID externe donné
/// </summary>
public static User GetByIdExternal(Guid idExternal)
{
User retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// on fait un appel direct suivir de FirstOrDefault
// car on sait qu'il ne peut y avoir qu'un seul résultat maximum
retour = (from u in ctx.Users
where u.IdExternal == idExternal
select u
).FirstOrDefault();

// si valide
if(retour != null)
// on le détache du contexte
ctx.Detach(retour);
}
return retour;
}
/// <summary>
/// Retourne l'utilisateur avec le login donné
/// </summary>
public static User GetByLogin(string login)
{
User retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// on fait un appel direct suivir de FirstOrDefault
// car on sait qu'il ne peut y avoir qu'un seul résultat maximum
retour = (from u in ctx.Users
where u.Login == login
select u
).FirstOrDefault();

// si valide
if(retour != null)
// on le détache du contexte
ctx.Detach(retour);
}
return retour;
}
/// <summary>
/// Retourne l'utilisateur avec l'email donné
/// </summary>
public static User GetByEmail(string email)
{
User retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// on fait un appel direct suivir de FirstOrDefault
// car on sait qu'il ne peut y avoir qu'un seul résultat maximum
retour = (from u in ctx.Users
where u.Email == email
select u
).FirstOrDefault();

// si valide
if(retour != null)
// on le détache du contexte
ctx.Detach(retour);
}
return retour;
}
/// <summary>
/// Retourne tous les utilisateurs
/// </summary>
public static List<User> List()
{
List<User> retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
retour = (from u in ctx.Users
select u
).ToList();

// si valide
if(retour != null)
{
// on les détache du contexte
foreach(User user in retour)
ctx.Detach(user);
}
}
return retour;
}
/// <summary>
/// Retourne les utilisateurs dont l'email contient une partie de la chaine donnee
/// </summary>
public static List<User> ListByLogin(string filtre)
{
List<User> retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
retour = (from u in ctx.Users
where u.Login.Contains(filtre)
select u
).ToList();

// si valide
if(retour != null)
{
// on les détache du contexte
foreach(User user in retour)
ctx.Detach(user);
}
}
return retour;
}
/// <summary>
/// Retourne les utilisateurs dont l'email contient une partie de la chaine donnee
/// </summary>
public static List<User> ListByEmail(string filtre)
{
List<User> retour = null;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
retour = (from u in ctx.Users
where u.Email.Contains(filtre)
select u
).ToList();

// si valide
if(retour != null)
{
// on les détache du contexte
foreach(User user in retour)
ctx.Detach(user);
}
}
return retour;
}

8) Ajouter des méthodes de test unitaire pour chacun de ces méthodes à notre classe de test TestUser


Mise à jour

9) Enfin, rajouter deux méthodes d'instance publiques de mise à jour.
Ces méthodes seront placées dans une région 'Public' / 'Methods'.

  • Save
    • Mise à jour globale de l'utilisateur
  • UpdatePassword
    • Mise à jour du mot de passe de l'utilisateur

#region Public

#region Methods

/// <summary>
/// Enregistre l'utilisateur
/// </summary>
/// <returns>Vrai en cas de succès</returns>
public bool Save()
{
bool retour = false;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
try
{
// attache l'objet en tant que 'modifié'
ctx.AttachAsModified(this);
// on sauve, si 1 ligne a été modifiée : succès
retour = (1 == ctx.SaveChanges());
}
// pas de gestion d'exception ici
catch(Exception exc)
{

Console.WriteLine(exc.ToString());

}
finally
{
// on détache l'objet de nouveau
ctx.Detach(this);
}
}
return retour;
}
/// <summary>
/// Met à jour le mot de passe si l'ancien est donné
/// </summary>
public bool UpdatePassword(string oldPassword, string newPassword)
{
bool retour = false;

// on hash les mots de passe
string hashOld = User.Hash(oldPassword);
string hashNew = User.Hash(newPassword);

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// get the user with the given username and the given password has
User user = (from u in ctx.Users
where (u.ID == this.ID
&& u.PasswordHash == hashOld
)
select u).FirstOrDefault();

// si trouvé
if(user != null)
{
// on change le password
user.PasswordHash = hashNew;
try
{
// on sauve
// si 1 ligne a été modifiée : succès
retour = (1 == ctx.SaveChanges());
}
catch { }
}
}
return retour;
}

#endregion

#endregion

10) Développer les tests pour ces deux méthodes.


Le code complet de cette classe, ainsi que du test unitaire et de la classe d'extension se trouvent ici.

lundi 29 décembre 2008

Règles de codage

Pourquoi des règles de codage ?

Maintenant que nous avons vu avec quelle facilité on peut manipuler ses données, il est tentant de s'en servir un peu partout à la demande.
Néanmoins, ce tutoriel a pour vocation de mettre en place un véritable projet.
Il est donc nécessaire de poser quelques règles pour faire du code propre et éviter le pire.

Les objectifs :

  • Réutilisation : certaines partie du code peuvent être réutilisée dans d'autres projet ou pour d'autres extensions au site.
  • Maintenance : au moment où on développer unprojet, on a l'ensemble de ce projet en tête, mais quelques mois plus tard, c'est oublié. Se plier à des règles permet de se retrouver beaucoup plus rapidement.
  • Ouverture : des règles permettent de transmettre le projet à d'autres développeurs. Typiquement quand on n'a plus le temps ou que le projet prend une grand envergure et qu'il faut recruter.

Comment les atteindre

  • Utilisation de convention de nommage
  • Règles et contraintes d'utilisation des objets métier
  • Une programmation défensive

Conventions de nommage

Pourquoi des conventions de nommage ?

La raison semble évidente, mais pour en savoir plus, le sujet est traité ici.

Quelles convention de nommage ?

  • De manière générale, on utilise la notation camelCase pour tout nommer
  • Les champs privés sont préfixés par un '_' pour les reconnaitres d'un coup d'oeil.
  • Les variables locales et les paramètres commencent par une minuscule
  • Les noms des classes, des évènement et des méthodes commencent par une majuscule
  • Le nom d'une classe est un nom commun
  • Le nom d'une classe ou d'une variable doit de préférence commencer par indiquer sa nature
    'Quoi' toujours en premier. Ca permet d'optimiser l'utilisation de l'intellisense.
    • ex : IdUser : c'est un identifiant pour un utilisateur
    • ex : UrlImage : c'est l'URL de l'image
  • Le nom d'une méthode commence par un verbe
  • On utilise les régions du C# pour séparer les classes en fonction de leur accessiblité

Contraintes sur les objets métiers

Masquer les constructeurs

Afin d'éviter qu'un développeur ne créé une instance inconsistente, on masque les constructeurs.
Le développeur devra passer par un jeu de methodes statiques pour récupérer une instance de travail.

Masquer les appels à la couche donnée

Seul le code de la couche métier doit avoir connaissance des mécanismes internes de persistence. Ceci permet de pouvoir changer ces mécanismes sans impact sur le reste du système.

Ainsi, les appels à MvcBlogEntities sont autorisés uniquement :

  • Dans les tests unitaires
  • Dans les classes de MvcBlog.Models
    • En particulier les classes contrôleur ne doivent jamais faire appel à MvcBlogEntities.
      • Contrôleur = gestion de l'interface
      • MvcBlogEntities = gestion de l'accès aux données
      • Ces deux couches ne doivent pas collaborer directement, c'est le modèle qui doit faire l'intermédiaire

Adopter de bonnes pratiques

On verra tout au long du déroulement un certain nombre de bonnes pratiques.
Celles-ci peuvent sembler contraignant, mais elles sont là pour faciliter le travail a prosteriori et permettre au développeur de se focaliser sur le travail à accomplir sans se prendre la tête sur des détail du code

La première de ces bonnes pratiques, c'est de n'utiliser qu'une seule instruction return dans une fonction.
On ne sais jamais quelle complexité peut prendre une fonction. Faire un unique return et des conditions de parcours de la méthode permet d'éviter d'avoir à comprendre tout le code de cette méthode pour s'assurer que le code que l'on rajoute soit bien exécuté.
Ainsi, une fonction a une entrée et une sortie. Pas de 'return' caché, pas de surprise.

Autre règle : un champ est toujours privé.
On doit passer par un accesseur (propriété) pour y accéder.
Au besoin, on peut coder un accesseur protégé, ou protéger le set de sa propriété.


Programmation défensive

Exceptions

A quelques rares exceptions, aucun objet de notre framework ne devra lancer d'exception.
On réservera ce mécanisme aux cas réellement exceptionnels, à savoir les cas non prévu par le développeur, ce qui est son but normal.
Beaucoup de développeurs se servent des exceptions pour remplacer la gestion d'erreur standard, mais ce ne sera pas notre cas.

Vérification de paramètres

Avant de commencer tout traitement, une méthode publique ou protégée, il faut vérifier la validité des paramètres fournis pour le bon déroulement du traitement.

Les méthodes privées n'étant appelée que depuis la classe, on admet que le développeur de cette classe aura fait la vérification avant d'effectuer l'appel

Log

Afin de tracer les erreurs, on effectuera des logs lors des situations qui demandent l'attention du développeur.
Pour ce faire, on utilisera une petite classe utilitaire que nous allons installer dans le projet à la fin de cet article.


Classes utilitaires

Téléchargement

1) Télécharger le zip ici, il contient quelques classes utilitaires pour la suite de notre projet.

Il contient en particulier :

  • La classe Failer qui permet de faire des logs sur disque.
  • La classe ConfigurationHelper qui facilite l'utilisation d'un fichier de configuration.

2) Créer un répertoire 'Code' dans la solution McvBlog

3) Ajouter les deux classes dans ce répertoire

Configuration

La classe Failer peut être configurée pour indiquer le répertoire dans lequel les logs sont créés

Pour cela rajouter la ligne suivante dans la section appSettings des fichiers de configuration des deux projets (n'oubliez pas le projet de test)


<appSettings>
<!-- Répertoire contenant les logs d'erreur -->
<add key="PathFailer" value="C:\temp\Logs\" />
</appSettings>

dimanche 28 décembre 2008

Tests unitaires et premières lignes de code

Configuration du projet de test

Maintenant que la couche donnée est fonctionnelle, procédons aux premiers tests

Notre solution contient un second projet : le projet de tests unitaires.
1) Définir ce projet comme projet de démarrage et le lancer.

VS test par defaut

Par défaut, la solution effectue deux tests sur un des contrôleurs

2) Copier/coller la chaîne de connexion du modèle EntityFramework depuis le web.config du projet web

3) Dans cette chaîne, remplacer MonBlogMVC par MonBlogMVC.Test pour que nos tests s'effectuent sur une base vierge

<add name="MvcBlogEntities" connectionString="metadata=res://*/Models.MvcBlogModel.csdl|res://*/Models.MvcBlogModel.ssdl|res://*/Models.MvcBlogModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=LYRE\SQLEXPRESS;Initial Catalog=MonBlogMVC.Test;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />

4) Créer un répertoire 'Models' à la racine de 'McvBlog.Tests'
On placera dans ce répertoire tous les tests unitaires les classes de McvBlog.Models.

Premier test unitaire

5) Créer un premier test unitaire ('ajouter' / 'test unitaire') nommé TestUser
Par défaut il ressemblera à ça :


using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MvcBlog.Tests.Models
{
/// <summary>
/// Summary description for TestUser
/// </summary>
[TestClass]
public class TestUser
{
public TestUser()
{
//
// TODO: Add constructor logic here
//
}

private TestContext testContextInstance;

/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get
{
return testContextInstance;
}
set
{
testContextInstance = value;
}
}

#region Additional test attributes
//
// You can use the following additional attributes as you write your tests:
//
// Use ClassInitialize to run code before running the first test in the class
// [ClassInitialize()]
// public static void MyClassInitialize(TestContext testContext) { }
//
// Use ClassCleanup to run code after all tests in a class have run
// [ClassCleanup()]
// public static void MyClassCleanup() { }
//
// Use TestInitialize to run code before running each test
// [TestInitialize()]
// public void MyTestInitialize() { }
//
// Use TestCleanup to run code after each test has run
// [TestCleanup()]
// public void MyTestCleanup() { }
//
#endregion

[TestMethod]
public void TestMethod1()
{
//
// TODO: Add test logic here
//
}
}
}

6) Modifier cette classe. Elle contiendra tous nos tests unitaires de la classe MvcBlog.Models.User :

  • Rajouter un using pour MvcBlog.Models
  • Supprimer le constructeur (inutile)
  • Déplacer le champ et la propriété du TestContext dans une classe de base qui servira à tous nos tests unitaires
  • Supprimer les 'additionnal test attributes', sauf MyClassCleanup qui va nous servir à faire le ménage
  • Ecrire le premier test : la création d'un utilisateur
  • Dans MyClassCleanup, rajouter une routine de suppression de tous les utilisateurs. La base doit être nettoyée après le test.s

Votre code doit maintenant ressembler à ceci :


using System;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;

using MvcBlog.Models;

namespace MvcBlog.Tests.Models
{
/// <summary>
/// Tests unitaires pour la classe User
/// </summary>
[TestClass]
public class TestUser
{
#region Initialisation / nettoyage

/// <summary>
/// Après que tous les test soient terminés
/// </summary>
[ClassCleanup()]
public static void MyClassCleanup()
{
using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// supprime tous les utilisateurs
foreach(User obj in ctx.Users)
ctx.DeleteObject(obj);

ctx.SaveChanges();
}
}

#endregion

/// <summary>
/// Test 1 : création d'un utilisateur
/// </summary>
[TestMethod]
public void TestCreation()
{
int count = 0;

using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// créer une nouvelle instance
User user = new User("Mose", "mose@mose.mo", "azerty");

// l'ajouter à nos entités
ctx.AddToUsers(user);
// enregistrer en base
count = ctx.SaveChanges();
}

// si on a bien 1 ligne affectée, c'est réussi
Assert.AreEqual(1, count, "Chargement échoué : trop ou pas assez de ligne affectées");
}
}
}

Notre classe de test de base :


using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MvcBlog.Tests
{
/// <summary>
/// Unit test base class
/// </summary>
public class TestUnitBase
{
#region TestContext management

/// <summary>
/// test context which provides information about and functionality for the current test run.
///</summary>
private TestContext _testContextInstance;
/// <summary>
///Gets or sets the test context which provides
///information about and functionality for the current test run.
///</summary>
public TestContext TestContext
{
get { return this._testContextInstance; }
set { this._testContextInstance = value; }
}

#endregion
}
}

Lancez le test : il semble que tout fonctionne.
Néanmoins nous ne sommes pas sûrs que l'utilisateur a réellement été créé.
Pour le vérifier, trois possibilités que je vous invite à tester :

  • Faire un point d'arrêt au début de MyClassCleanup et vérifier directement dans la base de donnée
  • Commenter le code de nettoyage et lancer deux fois le test : les contraintes d'unicité du login et de l'e-mail vont provoquer une exception
  • Rajouter un test de chargement

7) Ajouter le test pour tester le chargement de l'utilisateur créé précédemment
On utilise la syntaxe de LinQ que vous devez déjà connaître puisque c'est un pré-requis à ce tutoriel.


/// <summary>
/// Test 2 : chargement de l'utilisateur
/// </summary>
[TestMethod]
public void TestChargement()
{
using(MvcBlogEntities ctx = new MvcBlogEntities())
{
// on tente un chargement
var queryUsers = from u in ctx.Users
where u.UserName == "Mose"
select u;

// on vérifie qu'on a bien 1 seul et unique résultat
Assert.AreEqual(1, queryUsers.Count(), "Chargement échoué : trop ou pas assez d'utilisateurs retournés");
}
}

Aucune surprise : le test passe très bien
Nous avons donc maintenant :

  • Une couche donnée qui peut aller créer et récupérer des données en base
  • Un projet de test avec ses premiers tests unitaire afin de valider le fonctionnement de cette couche donnée

Vous pouvez dès maintenant écrire d'autres tests unitaires afin de manipuler un peu vos entités.
Nous reviendrons sur ce point ultérieurement, et nous ferons d'autres tests unitaires après avoir enrichi nos modèles.


Important

Comme l'a très exactement fait remarqué un lecteur attentif, on ne peut pas parler ici de 'test unitaire'.

Un test unitaire est un test qui se suffit à lui même, qui n'a pas besoin des autres tests pour réussir, qu'on peut lancer seul.
Or nos tests ici sont liés les uns aux autre : TestChargement nécessite la réussite de TestCreation. Cette approche a été choisie délibérément pour simplifier la mise en place des tests, et parce qu'il est très pénible de faire exclusivement des tests unitaires.

L'unité de nos tests sera donc l'objet métier.

samedi 27 décembre 2008

Entity Framework : création de la couche donnée

Chaîne de connexion

1) Spécifier, dans le web.config, la connexion vers la base


<add name="local" connectionString="Data Source=LYRE\SQLEXPRESS;Initial Catalog=MonBlogMVC;Integrated Security=True" providerName="System.Data.SqlClient" />

Le fichier edmx

2) Dans le répertoire "Models", rajouter un nouvel élément de type ADO.Net Entity Data Model (en français ça doit ressembler beaucoup)

entity framework : add new item

3) Choisir "generate from base" comme contenu

entity framework : choisir le contenu

4) Choisir la chaîne de connection de l'étape 1 (sélectionnée par défaut)

5) Définir "McvBlogEntities" comme nom dans la case "save the connection settings...".
Attention : la légende porte très mal son nom, il ne s'agit pas que du nom dans la base, mais surtout du nom du repository de nos données, un peu l'équivalent du DataContext de LinQ-to-SQL

entity framework : connexion a la base

6) Cocher "Tables", pour importer uniquement les tables de notre base

7) Entrer "Models" comme namespace, afin que toutes les classes générées par Entity Framework soient accessibles dans MvcBlog.Models

entity framework : choisir quoi importer

C'est terminé. En allant vérifier dans le web.config, vous verrez une chaîne de connexion d'un genre un peu particulier.
Celle-ci contient notamment le chemin vers les 3 fichiers XML définissant le mapping avec la base de donnée, ici en ressource incluse dans un assembly
Pour en savoir plus : voir MSDN


<add name="MvcBlogEntities" connectionString="metadata=res://*/Models.MvcBlogModel.csdl|res://*/Models.MvcBlogModel.ssdl|res://*/Models.MvcBlogModel.msl;provider=System.Data.SqlClient;provider connection string="Data Source=LYRE\SQLEXPRESS;Initial Catalog=MonBlogMVC;Integrated Security=True;MultipleActiveResultSets=True"" providerName="System.Data.EntityClient" />

Votre modèle doit ressembler à ceci :

entity framework : modele brut

Renommage

Le nommage par défaut de nos entités de travail n'est pas satisfaisant. Les préfixes utilisés dans la base n'ont pas leur place dans notre code et les propriétés de navigation ne sont pas intuitives.

Entités et ensembles d'entités

8) En utilisant la fenêtre "Propriété" (F4), renommer toutes les tables (propriété Name) et tous les ensembles (propriété Entity Set Name)
Les "EntitySet" sont des propriétés qui nous permettront d'accéder à nos listes d'entités, pour un code clair et lisible, il est important que leur nommage soit intuitif.

Entity framework : entity et entity set name

Les propriétés de navigation (Navigation Properties) permettent d'accéder aux entités liées à une autre entité.
ex : le propriété TEM_Template dans l'entité blog est un accesseur vers l'instance du template associé à ce blog.

9) Renommer les propriétés de navigation avec des noms intuitifs, en faisant attentions aux cardinalité (fenêtre 'Propriété') et au mapping (fenêtre 'mapping')

Entity framework : renommer les associations

A la fin, votre schéma doit ressembler au suivant.
A noter que si vous n'utilisez pas le même nommage, il vous faudra adapter votre code LinQ en fonction lors des étapes suivantes.

Entity framework : modele apres renommage

Le renommage est l'étape la plus fastidieuse de cette création de la couche donnée.
Néanmoins, ça permet d'avoir un aperçu complet de l'ensemble des associations entre les différentes entités que nous manipulerons ultérieurement.

vendredi 26 décembre 2008

Base de données

Vous devez avoir une instance de SQL Server sur votre machine. SQL Server Express (la version gratuite) fonctionnera parfaitement.

Créer la base de donnée

1) Télécharger le script de création de la base de données : script de la base zippé

2) Créer une base de donnée nommée "MonBlocMVC"

3) Passer le script sur cette base

4) Créer une base de donnée nommée "MonBlocMVC.Test", qui servira pour nos tests unitaires.

5) Passer le script sur cette base


Schéma de la base de donnée

On notera ici que chaque table est préfixée, en prévision de l'extension du système.

  • US_ : informations utilisateur / personnalisations
  • SYS_ : fondamentaux du système
  • BLO_ : contenu des blogs
  • TEM_ : gestion des templates
  • TAG_ : gestion des tags
blog mvc : base de donnee

Description de la base de donnée

Table User

  • ID : identifiant entier, plus léger à utiliser qu'un Guid
  • DateCreation/DateUpdate : Date de creation et de dernière mise à jour
  • IdExternal : identifiant externe, Guid, utilisé pour identifier un utilisateur à l'extérieur du système
  • Login / Email : les infos de l'utilisateur
  • PasswordHash : le mot de passe hashé de l'utilisateur (indécryptable)

Table Entity
Contient les différentes entités (template / post / blog) du projet et leurs points communs

  • ID : identifiant de l'entité
  • Name : nom de l'entité
  • DateCreation/DateUpdate : Date de creation et de dernière mise à jour
  • IdCreator : utilisateur ayant créé l'entité
  • IdUpdator : utilisateur ayant fait la dernière mise à jour

Tables Tag et EntityTag

  • ID / Text : identifiant + texte du tag
  • Une liaison n...n avec la table des entités

Table Template

  • ID : identifiant du template, clef étrangère sur la table des entités
  • TemplateBlog : chaine de caractère : code HTML de la page contenant des balises pour injectet les différents paramètres
  • TemplatePost : chaine de caractère : code HTML d'un post avec des balises également

Table Blog

  • ID : identifiant du blog, clef étrangère sur la table des entités
  • Description : texte d'en-tête d'un blog
  • IdTemplate : l'ID du template de ce blog

Table Post

  • ID : identifiant du post, clef étrangère sur la table des entités
  • HTMLContent : le contenu (en html) du post
  • DatePublication : la date et heure où le post doit apparaître dans le blog. Permet de planifier sa publication
  • IdBlog : le blog contenant ce post

jeudi 25 décembre 2008

ASP.Net MVC : Création du projet

Vous devez avoir déjà installé ASP.Net MVC (si non, le télécharger ici)

1) Créer une solution vierge nommée "MVC"

2) Ajouter un nouveau projet nommé "MvcBlog" de type ASP.Net MVC web application

aspnet mvc nouveau projet

3) Oui, vous voulez créer un projet de test unitaire pour ce projet

unit test aspnet mvc

Votre solution est maintenant créée, voyons un peu ce qu'elle contient :

  • Content doit contenir les fichiers associés au site (CSS/Image/...)
  • Controllers doit contenir les différents contrôleurs de l'application
  • Models doit contenir le modèle de donnée
  • Scripts doit contenir les fichiers Javascript utilisés, on notera que jQuery est nativement ajouté à la solution, ainsi que quelques autres scripts Microsoft
  • Views contient les vues utilisées dans notre application. On notera que ces vues sont des fichiers .aspx et qu'il y a déjà dans notre solution des vues de pages pour se logguer, enregister un nouvel utilisateur et changer son mot de passe. On notera aussi le répertoire "Shared" qui contient un UserControl (.ascx) et une MasterPage.
  • Default.aspx est une page ASP.Net qui sert uniquement lors des requètes directes à la racine du site
  • global.asax que vous connaissez déjà mais qui présente quelques différentes des sites ASP.Net classiques
  • le web.config par défaut qui contient déjà toutes les déclaration des assemblies/handlers et modules utilisés par ASP.Net MVC
solution aspnet mvc

4) Essayez votre site : lancez directement le projet (F5), acceptez la modification du web.config pour activer le débuggage.

Vous avez un squelette complet d'application ASP.Net MVC, déjà fonctionnel.

Si vous créez un compte, vous noterez que l'application créé automatiquement un fichier .mdf dans le répertoire App_Data. En explorant les données de ce fichier, vous reconnaitrez le système de Membership introduit par ASP.Net 2.0.

Vous pouvez supprimer ce fichier : nous allons customiser la gestion des utilisateurs.

mercredi 24 décembre 2008

Spécifications

Quel est l'objectif du projet ?

Faire un moteur de blog simple :

  • Un blog est une page web qui contient
    • un titre
    • des posts
  • Un blog est associé à un unique template de blog
  • Un template de blog défini le squelette HTML de la page web, ainsi que le modèle HTML de chaque post
  • Un blog contient des posts
  • Un utilisateur peut administrer uniquement ses templates / blogs / posts
  • On peut associer des tags a un post ou a un blog

Fonctionnalités :

  • Affichage d'un blog
  • Gestion/création de templates de blogs
  • Gestion/création de blogs
  • Gestion/création de posts dans un blog
  • Gestion des tags

Spécificités :

Afin que ce tutoriel aborde son sujet en profondeur, on rajoute quelques contraintes :

  • On ne devra pas utiliser les table aspnet_Users, aspnet_Membership de base, il faudra partir de notre propre gestion des utilisateurs
  • On devra enregistrer la date et l'identité de l'utilisateur lors d'une création ou d'une mise à jour de template, blog ou post

mardi 23 décembre 2008

Entity Framework : présentation

Entity Framework est une surcouche d'ADO.Net, la couche qui gère l'accès aux données dans le framework.Net

Entity Framework a pour objectif de fournir un couche d'accès aux données avec un minimum d'efforts. C'est donc un framework d'ORM au même titre que son plus célèbre concurrent NHibernate

Brièvement, Entity Framework fourni :

  • Un framework permettant de manipuler des données issues de sources diverses
  • Un compatibilité avec LinQ, qui permet de requêter efficacement dans ces sources de donnée
  • Une modélisation complexe permettant de personnaliser son mapping de la base de donnée
  • Un outil graphique permettant d'importer un schéma de base et de créer son mapping simplement
  • Une intégration native dans Visual Studio 2008 SP1

Entity Framework est encore récent, mais il va être poussé en avant par Microsoft comme solution privilégiée d'accès aux données, au détriment de Linq-to-SQL.

Ce tutoriel utilise cet outils parce que :

  • Tout le travail standard d'interaction avec la base de donnée est déjà fait
  • C'est simple et efficace à mettre en place
  • C'est une solution Microsoft, donc parfaitement intégrée à l'environnement de développement
  • C'est une technologie en cours de maturation

C'est une technologie encore jeune, elle présente donc quelques limitations. Ce tutoriel n'a pas pour vocation d'en faire l'apologie ni de la critiquer, mais simplement de montrer son usage dans le cadre d'un projet complet.

Si vous souhaitez en savoir plus, vous trouverez ici un tutoriel très complet sur Entity Framework écrit par Paul Musso sur developpez.com, mais ce n'est pas un pré-requis pour le projet que nous allons développer.

lundi 22 décembre 2008

ASP.Net MVC : présentation

ASP.Net MVC est l'implémentation du modèle Modèle / Vue / Contrôleur (MVC) pour la plateforme ASP.Net

Le modèle MVC est une méthode de conception de la couche interface d'une application

Le modèle MVC a pour objectif la separation of concern, la séparation des objectifs de chacun des composants de cette interface.

Brièvement :

  • Le modèle gère la récupération et les traitements des données du système
  • La vue gère l'affichage de ces données
  • Le contrôleur est de cerveau de l'affaire puisqu'il contiendra toute l'intelligence de l'interface
    • il gère la synchronisation entre le modèle et les informations affichées
    • il gère les évènements se produisant dans l'interface, et leurs impacts sur la vue et sur le modèle

Il existe plusieurs frameworks MVC pour la plateforme Microsoft.Net, le plus connu étant le Castle Project

Avec ASP.Net MVC, Microsoft a voulu ouvrir sa plateforme ASP.Net à ce genre d'architecture qui connait un succès grandissant notamment pour le Test Driven development.

Les différences majeures avec l'ASP.Net classique, sont :

  • la disparition de la notion de PostBack
  • la disparition du ViewState
  • la disparition du cycle de vie des pages

En d'autres termes : il va falloir revoir complètement votre façon d'aborder un projet web

dimanche 21 décembre 2008

Introduction

Ce blog est un tutoriel dont l'objectif est de créer un moteur de blog simple en utilisant les dernières technologies fournies par Microsoft (en décembre 2008)

Technologies utilisées :

  • ASP.Net MVC, surnommé System.Web.MVC, version beta
  • Entity Framework, version 1

Pré-requis logiciel :

  • Visual Studio 2008 Professionnel SP1
  • ASP.NET MVC version beta

Connaissances requises :

  • C# v3.5
  • HTML / CSS
  • ASP.Net v2.0

Initialement, seul le script de création du schéma de base de donnée est fourni.

C'est vous qui allez coder la solution complète