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.
Aucun commentaire:
Enregistrer un commentaire