dimanche 4 janvier 2009

ASP.Net MVC RC1 prévue pour janvier

ASP.Net MVC sort en version RC (Release Candidate) courant janvier.
Release candidate = version presque définitive, candidate à la sortie sur le marché (RTM : Relase To Market) Plus d'infos ici (anglais)

Concrètement, ça signifie que de nombreuses modifications, simplifications et débuggages vont être mis en place.
Les version RC sont généralement très proches des version RTM (release to market), aussi on peut s'attendre à ce que le code créé avec la RC soit compatible à 99% avec la RTM.

Très simplement, ça signifie que ce blog va ralentir la cadence afin d'attendre la sortie de la version RC.
Nous allons avancer au mieux dans le modèle de donnée
. En revanche la partie web devra attendre.

samedi 3 janvier 2009

Solution de test avec Visual Studio

Visual Studio Test List Editor

Il est temps de classer et de les ordonner afin qu'ils soient un vrai outil de développement agile.

1) Ouvrez la fenêtre Test List Editor

Elle ne contient aucun test

2) Créer une première liste 'Models'

On y mettra tous les tests de MvcBlog.Models

3) Créer pareillement une liste 'Code' et une liste 'Controllers'

4) Créer dans 'Models' une sous-liste 'User', dans 'Code' une sous-liste 'Membership' et dans 'Controllers' une sous-liste 'Home'

5) Ouvrir maintenant la fenêtre Test View
Vous devez y trouver les tests suivants :

6) Glisser/déposer chaque test dans la liste appropriée

Il est maintenant possible d'éxécuter uniquement une partie des tests en cochant la(les) case(s) appropriée(s) et en cliquant sur le bouton run checked tests.

vendredi 2 janvier 2009

MembershipProvider : implémentation 2/2

MembershipProvider

1) Dans le projet MvcBlog, dans le répertoire Code, créer une classe MyMembershipProvider qui hérite de MembershipProvider

2) Demander à Visual Studio d'y implémenter les membres abstraits de MembershipProvider

On se retrouver avec un gros paquet de propriétés et de méthodes à implémenter.
Pas de panique, on ne codera pas tout dans la première version. Elle sera simple mais fonctionnelle.

Les première méthodes à regarder sont celles qui permettent de faire une sélection sur un ou plusieurs utilisateurs :

  • GetUser (2 surcharges)
  • GetUserNameByEmail
  • FindUsersByEmail
  • FindUsersByName
  • GetAllUsers

On note que les trois dernière méthodes retournent une collection de type MembershipUserCollection.
Comme notre couche métier remonte des List<User>, la première chose à faire consiste à créer une méthode utilitaire pour faire la conversion.
On en profitera pour gérer les autres paramètres communs à ces méthodes

3) Dans la région 'Private' / 'Tools', créer la méthode GetUsers


#region Private

#region Inner Tools

/// <summary>
/// Retourne une MembershipUserCollection depuis une List<User>
/// </summary>
private MembershipUserCollection GetUsers(List<User> users, int pageIndex, int pageSize, out int totalRecords)
{
MembershipUserCollection retour = null;
// initialisation du total
totalRecords = 0;

// si valide
if(users != null)
{
// on instancie la collection de retour
retour = new MembershipUserCollection();

// on passe les pages
users.Skip(pageIndex * pageSize);

// on boucle sur les utilisateur
foreach(User user in users)
{
// si valide
if(user != null)
{
// on instancie un MembershipUser
MyMembershipUser mUser = MyMembershipUser.FromUser(user);
// on l'ajoute ) la collection
retour.Add(mUser);
}
}
// on compte le nb de résultats
totalRecords = retour.Count;
}
return retour;
}

#endregion

#endregion

4) Dans la région 'Public' / 'Users selection', coder maintenant les 6 méthodes de sélection d'utilisateur en se basant sur GetUsers pour celles qui retournent une MembershipUserCollection


#region Public

#region Users selection

/// <summary>
/// Récupère un utilisateur par son login
/// 'userIsOnline' n'est pas utilisé
/// </summary>
public override MembershipUser GetUser(string login, bool userIsOnline)
{
MembershipUser retour = null;

// on récupère l'utilisateur associé
User user = User.GetByLogin(login);
// si réussi
if(user != null)
// on instancie
retour = MyMembershipUser.FromUser(user);

return retour;
}
/// <summary>
/// Récupère un utilisateur par son ID
/// 'userIsOnline' n'est pas utilisé
/// </summary>
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
MembershipUser retour = null;

// si la clef donnée est un entier
if(providerUserKey is Int32)
{
// on cast
int id = (int)providerUserKey;
// si valide
if(id > 0)
{
// on récupère l'utilisateur associé
User user = User.GetById(id);
// si réussi
if(user != null)
// on instancie
retour = MyMembershipUser.FromUser(user);
}
}
return retour;
}
/// <summary>
/// Récupère le login d'un utilisateur par son email
/// </summary>
public override string GetUserNameByEmail(string email)
{
string retour = null;

// on récupère l'utilisateur associé
User user = User.GetByEmail(email);
// si réussi
if(user != null)
// on retourne son login
retour = user.Login;

return retour;
}
/// <summary>
/// Récupère une collection d'utilisateur par un filtre sur l'email
/// </summary>
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
return this.GetUsers(User.ListByEmail(emailToMatch), pageIndex, pageSize, out totalRecords);
}
/// <summary>
/// Récupère une collection d'utilisateur par un filtre sur le login
/// </summary>
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
return this.GetUsers(User.ListByLogin(usernameToMatch), pageIndex, pageSize, out totalRecords);
}
/// <summary>
/// Récupère tous les utilisateurs
/// </summary>
public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
return this.GetUsers(User.List(), pageIndex, pageSize, out totalRecords);
}

#endregion

5) Il faut maintenant s'occuper des tests de cette classe.
On créé une classe TestMembership dans un nouveau répertoire Code.
Les tests étant les même, on va tricher et copier/coller les tests du fichier TestUser, et remplacer dedans, au fur et à mesure qu'on les code, les lignes qu'on peut gérer avec notre MembershipProvider.
Exemple :


/// <summary>
/// Test 3 : chargement par login
/// </summary>
[TestMethod]
public void TestChargementParLogin()
{
MyMembershipProvider provider = new MyMembershipProvider();

// chargement : succes
MembershipUser user = (provider.GetUser(Login1, false));
Assert.IsNotNull(user, "Erreur chargement utilisateur (succes)");

// chargement : echec
user = provider.GetUser(LoginWrong, false);
Assert.IsNull(user, "Erreur chargement utilisateur (echec)");
}

6) Mettre de côté, dans une région 'Methodes non implémentées', les méthodes qu'on implémentera pas, à savoir :

  • GetNumberOfUsersOnline
  • DeleteUser
  • UnlockUser
  • GetPassword
  • ChangePasswordQuestionAndAnswer
  • ResetPassword

7) Renseigner les informations pour les propriétés suivantes, qu'on placera dans une région 'MembershipProvider configuration' :

  • EnablePasswordReset
  • EnablePasswordRetrieval
  • RequiresQuestionAndAnswer
  • RequiresUniqueEmail
  • MaxInvalidPasswordAttempts
  • MinRequiredNonAlphanumericCharacters
  • MinRequiredPasswordLength
  • PasswordAttemptWindow
  • PasswordFormat
  • PasswordStrengthRegularExpression
  • ApplicationName

Ce qui doit donner :


#region MembershipProvider configuration

/// <summary>
/// Vrai si le provider autorise à réinitialiser le mot de passe
/// </summary>
public override bool EnablePasswordReset
{
get { return false; }
}
/// <summary>
/// Vrai si le provider autorise à retrouver un mot de passe
/// </summary>
public override bool EnablePasswordRetrieval
{
get { return false; }
}
/// <summary>
/// Vrai si le provider nécessite une question / réponse secrète
/// </summary>
public override bool RequiresQuestionAndAnswer
{
get { return false; }
}
/// <summary>
/// Vrai si le provider nécessite un email unique
/// </summary>
public override bool RequiresUniqueEmail
{
get { return true; }
}
/// <summary>
/// Nombre d'essais de mot de passe infructueux autorisés
/// </summary>
public override int MaxInvalidPasswordAttempts
{
get { return 0; }
}
/// <summary>
/// Nombres de caractères non-aplhanumériques autorisés dans le mot de passe
/// </summary>
public override int MinRequiredNonAlphanumericCharacters
{
get { return 0; }
}
/// <summary>
/// Taille minimum du mot de passe
/// </summary>
public override int MinRequiredPasswordLength
{
get { return 3; }
}
/// <summary>
/// Nombre de minutes à attendre si on ne veut pas que l'échec du mot de passe soit enregistré
/// </summary>
public override int PasswordAttemptWindow
{
get { return 1; }
}
/// <summary>
/// Format de stockage du mot de passe
/// </summary>
public override MembershipPasswordFormat PasswordFormat
{
get { return MembershipPasswordFormat.Hashed; }
}
/// <summary>
/// Expression régulière utilisée pour tester la force du mot de passe
/// </summary>
public override string PasswordStrengthRegularExpression
{
get { return "*"; }
}
/// <summary>
/// The name of the application using the custom membership provider.
/// </summary>
public override string ApplicationName
{
get { return "MvcBlog"; }
set
{
throw new ApplicationException("ApplicationName.set pas autorisé");
}
}

#endregion

Il ne reste plus que 4 méthodes, qu'on placera dans une région 'Securité principale'.
8) Implémenter les trois premières. C'est très faciles à coder grâce à notre travail sur User:

  • UpdateUser
  • ChangePassword
  • ValidateUser

#region Securité principale

/// <summary>
/// Met à jour un utilisateur
/// </summary>
public override void UpdateUser(MembershipUser user)
{
// on essaye de le caster en MyMembershipUser
MyMembershipUser mUser = (user as MyMembershipUser);
// si c'en est un
if(mUser != null)
// on demande la mise à jour
mUser.Update();
// si ce n'en n'est pas un
else
//on lance une exception
throw new InvalidOperationException("The given MembershipUser must be a MvcBlog.Code.MyMembershipUser");
}
/// <summary>
/// Met à jour le mot de passe d'un utilisateur
/// </summary>
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
bool retour = false;

// on charge l'utilisateur
User user = User.GetByLogin(username);
// si réussi
if(user != null)
// on met à jour son mot de passe
retour = user.UpdatePassword(oldPassword, newPassword);

return retour;
}
/// <summary>
/// Authentifie un utilisateur
/// </summary>
public override bool ValidateUser(string username, string password)
{
return User.Authenticate(username, password);
}

#endregion

La dernière méthode, CreateUser est un peu plus complexe, puisqu'elle doit remonter un MembershipCreateStatus.
Pour le renseigner, il va falloir s'appuyer sur les contraintes d'unicité de la base de donnée, car il n'est pas fiable de faire les tests avant la création (voir 'Creation d'un utilisateur' dans cet article)

9) Coder CreateUser, en prenant en compte les contraintes suivantes :

  • Vérification des paramètres en entrée
  • Remontée d'un statut cohérent si les paramètre en entrée sont incorrects
  • Si les paramètres sont corrects mais que la création échoue, test pour essayer d'en déterminer la cause
  • Remontée d'une statut cohérent en fonction de ces tests

Ce qui doit donner ceci :


/// <summary>
/// Créé un utilisateur
/// </summary>
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
MyMembershipUser retour = null;
status = MembershipCreateStatus.UserRejected;

// check if input is valid
if(!string.IsNullOrEmpty(username)
&& !string.IsNullOrEmpty(password)
&& (password.Length < this.MinRequiredPasswordLength)
&& !string.IsNullOrEmpty(email))
{
// create a user instance
User user = User.CreateNew(username, email, password);
// si réussi
if(user != null)
{
// succeeded
status = MembershipCreateStatus.Success;
// instanciate returning instance
retour = MyMembershipUser.FromUser(user);
}
// en cas d'échec de la création
else
{
// si un utilisateur a déjà cet email
if(User.GetByEmail(email) != null)
// problème de duplication d'email
status = MembershipCreateStatus.DuplicateEmail;

// si un utilisateur a déjà ce login
else if(User.GetByLogin(username) != null)
// problème de duplication d'email
status = MembershipCreateStatus.DuplicateUserName;
}
}
// check the constraints
else
{
// if username not valid
if(string.IsNullOrEmpty(username))
status = MembershipCreateStatus.InvalidUserName;
else if(string.IsNullOrEmpty(email))
status = MembershipCreateStatus.InvalidEmail;
else if((string.IsNullOrEmpty(password))
|| (password.Length < this.MinRequiredPasswordLength))
status = MembershipCreateStatus.InvalidPassword;
}
return retour;
}

10) pour finir, remplacer tous les tests de la classe TestMembership en utilisant notre MyMembershipProvider

Noter que vous recontrez des problèmes au bon déroulement de ces tests.
En effet, il va falloir travailler un peu notre solution de test pour la rendre plus fonctionnelle.
C'est le sujet du prochain article.


Configurer le MembershipProvider dans l'application web

11) Modifier la section membership comme suit


<membership defaultProvider="MyMembershipProvider">
<providers>
<clear />
<add name="MyMembershipProvider" type="MvcBlog.Code.MyMembershipProvider" />
</providers>
</membership>

Il est maintenant possible de naviguez dans votre site, d'enregistrer un nouvel utilisateur et de s'authentifier.
Notre MembershipProvider est fonctionnel.


Télécharger le code des fichier décrit dans cette série de deux articles

jeudi 1 janvier 2009

MembershipProvider : implémentation 1/2

Explications

Pourquoi créer son MembershipProvider ?

C'est une couche d'abstraction qui se place au dessus de la gestion des utilisateurs.
Son principal avantage c'est de pouvoir être modifiée et étendue sans impact sur le code qui s'appuie dessus.

De plus, ASP.Net fourni en standard des contrôles exploitant le MembershipProvider, permettant de les mettre très simplement en place

Enfin, c'est très facile à mettre en place, à étendre et à modifier au besoin car le MembershipProvider est configurable dans le fichier web.config

Comment créer son MembershipProvider ?

Il suffit, dans un premier temps, de créer une classe dérivée de MembershipProvider, et d'implémenter toutes les méthodes abstraites requises.

Néanmoins, comme nous souhaitons exploiter au mieux notre classe métier User, nous allons devoir créer également une classe dérivée de MembershipUser

MembershipUser : implémentation

1) Dans le projet MvcBlog, dans le répertoire Code, créer une classe MyMembershipUser qui hérite de MembershipUser

2) Rajouter un champ _user de type User dans la région 'Private' / 'Inner Data'
Notre MyMembershipUser sera donc un adapter de User


#region Private

#region Inner data

/// <summary>
/// objet métier User associé
/// </summary>
private User _user = null;

#endregion

#endregion

3) Rajouter deux constructeurs :

Un constructeur vide dans la région 'Private' / 'Construction'
Il sert à verrouiller le constructeur vide ét éviter les instanciations inconsistantes

Un constructeur prenant une instance de User en paramètre dans la région 'Protected' / 'Construction'
Ce sera le constructeur officiel.
Noter qu'on y rajoute une sécurité en plus pour éviter qu'un MyMembershipUser ne puisse contenir un User vide.


#region Protected

#region Construction

/// <summary>
/// Constructeur basé sur un User
/// </summary>
protected MyMembershipUser(User user)
{
// si valide
if(user != null)
// on stocke
this._user = user;
// si non valide
else
// erreur
throw new InvalidOperationException("Impossible d'instancier un MyMembershipUser without a valid MvcBlog.Models.User instance");
}

#endregion

#endregion

#region Private

#region Construction

/// <summary>
/// Constructeur vide protégé, pour éviter les instanciatations inconsistantes
/// </summary>
private MyMembershipUser()
{
}

#endregion
...

4) Créer la méthode statique FromUser permettant de récupérer une instance à partir d'une instance de User


#region Static

#region Factories

/// <summary>
/// Construction d'une instance de MyMembershipUser à partir d'un objet User
/// </summary>
public static MyMembershipUser FromUser(User user)
{
MyMembershipUser retour = null;

// si valide
if(user != null)
// on instancie
retour = new MyMembershipUser(user);

return retour;
}

#endregion

#endregion

4) Surcharger les données de MembershipUser pour les récupérer dans notre instance de User.


#region Données surchargée de MembershipUser

/// <summary>
/// Login
/// </summary>
public override string UserName
{
get { return this._user.Login; }
}
/// <summary>
/// Email
/// </summary>
public override string Email
{
get { return this._user.Email; }
set { this._user.Email = value; }
}
/// <summary>
/// Date de création
/// </summary>
public override DateTime CreationDate
{
get { return this._user.DateCreation; }
}
/// <summary>
/// Clef de l'utilisateur pour le provider, on se sert ici de l'ID interne
/// </summary>
public override object ProviderUserKey
{
get { return this._user.ID; }
}

#endregion

5) Rajouter nos données personnelles


/// <summary>
/// ID internal de l'utilisateur
/// </summary>
public int ID
{
get { return this._user.ID; }
set { this._user.ID = value; }
}
/// <summary>
/// ID externe de l'utilisateur
/// </summary>
public Guid IDExternal
{
get { return this._user.IdExternal; }
set { this._user.IdExternal = value; }
}

6) Pour terminer, implémenter la méthode publique Update pour mettre à jour notre instance


#region Methods

/// <summary>
/// Met à jour l'utilisateur
/// </summary>
public void Update()
{
this._user.Save();
}

#endregion

L'adapteur est terminé. Dans l'article suivant, nous coderons le MembershipProvider à proprement parlé.

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>