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é.