FriendlyUrl, les urls amicales ?

Cet article présente la fonctionnalité FriendlyUrls qui permet facilement d’utiliser des segments d’urls comme paramètre dans des WebForms afin de profiter d’un routage similaire à celui utilisé en MVC ou WebApi.
Cette fonctionnalité, proposée par Scott Hanselmann est disponible dans tous les nouveaux projets .NET depuis visual studio 2012 mais elle peut aussi être ajoutée avec Nuget.
(Temps de lecture 9minutes)

Situation actuelle des paramètres

Si vous développez avec WebForm et que vous n’utilisez pas les FriendlyUrl, il y a fort à parier que vous utiliser le querystring pour passer des paramètres aux pages.
Le querystring est cette portion de l’url après le ‘?’ qui ne fait pas partie de l’adresse. Cette chaine de caractère peut contenir pratiquement autant de paramètres que vous voulez et est composée sous la forme paramètre=valeur.

Typiquement, si vous avez une page detail.aspx et que vous voulez accéder à l’élément avec l’identifiant 24, l’url sera /detail.aspx?identifiant=24.
Pour accéder à cette valeur dans le code, vous utilisez l’instruction Request.QueryString[“identifiant”].
Pour renseigner plus d’informations, il faut ajouter des paramètres avec le séparateur &. Par exemple /detail.aspx?identifiant=24&sous-categorie=4

Proposition de solution par url

Les FriendlyUrl sont une technique qui permet d’adapter le routage d’une application pour découper des sections d’url en paramètre.
Le but principal de cette solution est de simplifier les url et de les rendre humainement compréhensibles.
Leur utilisation implique un changement dans votre manière de récupérer vos paramètres, comme présenté dans l’exemple ci-après

Pour la page /detail.aspx?identifiant=24&sous-categorie=4, le code pour récupérer les informations est souvent structuré de la manière suivante :

private void ChargerDonnes()
{
//...

var identifiant = ObtenirIntQueryString("identifiant");
var sousCategorie = ObtenirIntQueryString("sous-categorie");
if(!identifiant.HasValue || !sousCategorie.HasValue)
{
AfficherErreur();
return;
}
//...

}


private int? ObtenirIntQueryString(string nomCle)
{
int valeur;
var valeurChaine = Request.QueryString[nomCle];
if (!string.IsNullOrEmpty(valeurChaine) && int.TryParse(valeurChaine, out valeur))
{
return valeur;
}
else
{
return null;
}
}

Avec les FriendlyUrl, l’adresse de votre page serait /detail/24/4 et la récupération des informations serait :

private int? identifiant;
private int? sousCategorie;
private void RecuperIdentifiantUrl()
{
IList<string> segments = Request.GetFriendlyUrlSegments();
identifiant = ConvertirSegmentInt(segments, 0);
sousCategorie = ConvertirSegmentInt(segments, 1);
}

private int? ConvertirSegmentInt(IList<string> segments, int index)
{
int valeur;
if (segments==null ||
index > segments.Count ||
string.IsNullOrEmpty(segments[index]) ||
!int.TryParse(segments[index], out valeur))
{
return null;
}
else
{
return valeur;
}
}

(Pour que cela fonctionne, il faut ajouter l’instruction using Microsoft.AspNet.FriendlyUrls.)

Avantage sur le querystring ?

1. A première vue, à part simplifier l’url, il n’y a pas d’avantage.
Au contraire, le fait de devoir récupérer un paramètre selon sa position plutôt que par son nom peut ressembler à un inconvénient…
C’est parce que le but est justement de simplifier l’url ! L’adresse peut maintenant représenter une sémantique et faire partie de votre interface utilisateur.
Ce point a déjà été couvert par Jakob Nielsen en 1999 dans l’article URL as UI et Microsoft Research a même démontré en 2007[PDF] que les gens passent une bonne partie du temps à regarder les urls dans les résultats de recherche.
Enfin, Google le recommande dans ses consignes relatives au contenu.

2. Par contre, cela requière plus de réflexion de votre part. Réaliser une adresse de manière sémantique implique de découper les différents paramètres du plus général au plus spécifique. (Découpe qui se limitait souvent à une API de type REST)
Ceci vous permet de mieux découper votre site mais il permet aussi à vos utilisateurs de se retrouver dans ce qu’ils font.

Habituellement, l’url se découpe sous la forme suivante
http://domaine/section_applicative/page/élément/action/

Dans le lien https://www.nngroup.com/articles/url-as-ui/, il est facile de déterminer qu’il référence l’élément “url as ui” de la section articles du site nngroup.com

Dans le lien https://www.amazon.ca/s/ref=nb_sb_noss_2?__mk_fr_CA=%C3%85M%C3%85%C5%BD%C3%95%C3%91&url=search-alias%3Daps&field-keywords=sgh-i337, il est facile de déterminer que le site est amazon.ca … et c’est tout !

3. En lien avec l’article sur l’OutputCache, si vous utilisez une URL /detail.aspx?identifiant=24, vous devez spécifier le VaryByParam=”identifiant” dans votre directive OutputCache.
Si vous désirez mettre en cache l’url /detail.aspx?identifiant=24&categorie=4, vous devez adapter la directive pour que ce soit VaryByParam=”identifiant,categorie”.
À chaque évolution, vous devrez adapter la directive et, croyez-moi, cette adaptation est souvent oubliée…

Dans le cas des FriendlyUrl, vu que l’adresse change à chaque fois et que par défaut, OutputCache lie les éléments dans la cache à l’url, chaque adresse différente fait l’objet d’un élément différent dans la cache.

4. Les urls ainsi générées sont proches du modèles mis en place par MVC.
Ceci vous permettra de facilement faire une migration vers MVC sans devoir reconstruire tous vos liens ni devoir mettre en place une page “votre page a été déplacée”, que –personnellement- je trouve irritante et qui possède son propre nom “LinkRot”.

Désavantages:

1. Si vous avez une structure de répertoire dans IIS qui est identique à une URL que vous désirez utiliser, IIS ne vous amènera pas à votre page mais essaiera d’utiliser le répertoire.
Pourquoi ? Vu que la route est déterminée lorsque vous accédez à votre page, cela se passe après que IIS ait contacté ASP.NET. Or, si le répertoire existe, IIS ne contactera pas ASP.NET.

Ex : Une page Content.Aspx et le répertoire /Content dans votre application. Si vous essayez d’accéder à /content/451, IIS va essayer de trouver le fichier 451 dans le répertoire Content.

2. Cela ne peut couvrir qu’un ensemble restreint de paramètres. Au delà de 3 ou 4, cela devient difficile à gérer dans les applications et n’apporte plus de gain pour les utilisateurs.

Dette technique – Partie 1

Comme plusieurs développeurs, j’ai toujours préféré travailler dans un contexte de nouveau développement et non en maintenance ou en entretien. Pour moi, ces deux derniers termes étaient péjoratifs parce qu’ils étaient synonymes de « patch » et de « workaround », voire de frustration. Un logiciel qui dégénère semblait être à cette époque une évolution normale et une fatalité. Après quelques années de bon service, on devait prévoir une réécriture. Ça ne donnait pas vraiment le goût à quiconque de travailler dans un contexte semblable. Produire quelque chose en sachant qu’on prévoit déjà le jeter n’est pas très motivant.

Pourquoi fallait-il toujours en venir à ça? Qu’est-ce qui fait que les logiciels vieillissent souvent mal? Malheureusement, il n’y a pas consensus dans l’industrie sur la manière de nommer les éléments qui impactent négativement le vieillissement d’une application. Certains vont parler de santé d’un système, d’autres de qualités non-fonctionnelles ou encore de qualité tout court. Un terme à la mode depuis quelques années, surtout en agilité, pour parler des éléments techniques qui sont problématiques, est celui de la dette technique. Je vous propose de creuser un peu le sujet pour voir ce qui en retourne.
(Temps de lecture 10 minutes)

Méthaphore

La métaphore de la dette technique a été introduite par M. Ward Cunningham en 1992 :

Shipping first time code is like going into debt. A little debt speeds development so long as it is paid back promptly with a rewrite… The danger occurs when the debt is not repaid. Every minute spent on not-quite-right code counts as interest on that debt. Entire engineering organizations can be brought to a stand-still under the debt load of an unconsolidated implementation, object-oriented or otherwise.

Si on paraphrase, la notion de dette technique fait le parallèle entre la notion de dette financière et le développement logiciel. Si on livre du code qui n’est pas de première qualité, on aura à le refaire éventuellement (rapidement svp!) et ça aura donc coûté plus cher à développer au final (capital + intérêts). En fait, tout le temps supplémentaire effectué sur ce code de pietre qualité doit être considéré comme des intérêts (difficulté de lecture, maintenance compliquée, etc.).

Une notion de stratégie est aussi sous-jacente sans quoi on parlerait simplement de mauvais code. Dans la définition de M. Cunningham, contracter une dette technique peut être une bonne chose si un autre objectif est atteint pour compenser. Le gain peut être le délai de commercialisation ou time to market plus court, rétroaction plus rapide ou autre.

Cette métaphore ciblait donc essentiellement du code, ce qui ne semble plus tout à fait être le cas aujourd’hui. La notion de dette technique a évolué et on y inclut maintenant à peu près tout ce qui n’est pas idéal du point de vue technique. On peut donc parler de dette de code, de connaissance, de test, d’architecture et de dette technologique. Voyons un peu les différents types de dette.

 

Types de dette

Dette de code

La dette de code regroupe tous les éléments reliés directement au code. Les avertissements du compilateurs, les indices de maintenabilité ou de complexité cyclomatique, longueur des méthodes ou des classes, etc.. D’autres éléments sont toutefois plus difficile à déceler. Quelle sont les qualités non-fonctionnelles que du code devrait avoir? Facile à modifier, couplage faible, respect des principes SOLID, facile à lire, représentatif du domaine d’affaire, etc.. Dépendamment du contexte, ces éléments sont d’une importance relative, ce qui explique probablement qu’il n’y a pas encore de consensus…

Dette de connaissance

Est-ce que votre documentation est à jour en tout point? Probablement pas… Il ne faut pas s’en vouloir, je n’ai pas encore vu une seule organisation où c’était le cas. Ce qui varie, c’est le degré de fiabilité de cette dernière. Si les documents ne sont pas à jour ou sont incomplets, comment fait-on pour se retrouver? Il faut demander au super-héros qui était là lors du dernier projet? Espérons qu’il a une bonne mémoire… Et si on parlait de PowerBuilder, VB6 et les autres? Par ailleurs, êtes-vous certains d’avoir bien compris le domaine d’affaire? Il suffit de prendre deux experts du domaine d’affaire et de les faire discuter ensemble pour se rendre compte que les termes employés sont les mêmes, mais que la définition est différente d’une personne à l’autre.

Dette de test

Le manque de couverture par les tests automatisés est une dette incontestable. Une application vieillira mal si on ne peut pas valider facilement et rapidement les changements qu’on y fait. On peut toujours s’en sortir avec les tests manuels, mais ils sont coûteux, lents et ils ne sont pas infaillibles. Par ailleurs, qui est capable de savoir ce qu’il faut tester lorsqu’on change la classe Dieu (God class)? La majorité des équipes ne prennent pas de chance et test l’application au complet. L’autre option est de ne pas modifier l’application…

Dette d’architecture

Cette notion de dette est plus difficile à cerner étant donné qu’il n’existe pas de principes architecturaux clairs et réglementés en développement logiciel comme c’est le cas pour les bâtiments. Un conseiller en architecture pourra toujours dire que le couplage est acceptable pour le moment étant donné les besoins actuels et, comme il est difficile de prévoir les changements, l’architecture est donc correcte. Ce dernier aura raison, mais c’est un jeu risqué. Par ailleurs, certains choix architecturaux pourront être fait en début de projet et, s’ils ne sont pas remis en question, il faudra vivre avec les intérêts de la dette ainsi contractée. C’est souvent cette étape qui fait mal en maintenance ou en entretien de système. On évite des changements fondamentaux pour ne pas déstabiliser l’application. Mais c’est une erreur parce qu’on commence alors à ajouter des nouvelles classes avec des noms proches de celles qui existent déjà et on ajoute des condition à un if else if qui n’en finit plus et on rend le code plus difficile à modifier, car on ajoute de la complexité.

Dette technologique

La dette technologique est plus facile à comprendre à mon avis. Toutes les librairies ont une version. Même chose pour les systèmes d’exploitation et les serveurs de base de données. Ne rien faire lors de la sortie d’une nouvelle version contribue à accroître la dette. On doit mettre à jour les technologies sur lesquelles reposent nos applications, sinon on doit gérer le risque qu’on encoure. Par ailleurs, mette à jour une version à la fois d’un produit est souvent moins coûteux que de passer trois versions en même temps.

 

Pourquoi avons-nous de la dette technique?

Selon la définition de M. Cunningham, la réponse est simple : c’est voulu. C’est une décision d’équipe qui est éclairée. Avec la définition élargie dont on parle maintenant, les raisons sont plus subtiles. La raison qui revient le plus souvent est celle de la pression de la date de livraison. On devait livrer alors on a coupé les coins ronds pour y arriver. Cependant, un changement externe à notre application peut engendrer de la dette technique. Le changement de version d’un API en est un exemple, mais ce n’est pas toujours aussi facile à détecter. Par ailleurs, il faut convaincre les clients qu’il doivent investir pour payer la dette. Certains clients sont plus coriaces que d’autres… Pourtant, la dette ne s’éliminera pas toute seule.

Votre logiciel n’est peut-être pas facilement modifiable. Si la couverture de code par les tests est insuffisante ou inexistante, on voudra limiter les changements ou les faire en périphérie, ce qui aura pour effet de contrevenir à l’évolution du système ou de l’application et à ralentir la maintenance.

L’obsession des nouvelles fonctionnalité n’est pas à négliger. Certains clients qui veulent toujours plus de fonctionnalités sans retravailler l’existant s’expose à des problèmes. Il ne faut pas mal interpréter l’agilité ou le SCRUM. Il est vrai que le but est toujours de produire de la valeur pour le client à chaque itération. Mais il faut s’habituer à garder une portion du budget pour l’entretien et le refactoring pour avoir une saine évolution.

 

Et ensuite?

Un fois qu’on comprend bien les types de dette, il importe de s’en occuper. Pour ce faire, on devra tenter de la repérer, de la mesurer et d’y remédier. C’est ce que nous verrons dans un prochain billet.

 

Journalisation asynchrone avec ActionFilterAttribute en ASP.NET MVC

Cet article présente l’attribut ActionFilter qui permet d’ajouter du comportement sur certaines actions de contrôleurs en ASP.NET MVC.
Nous allons utiliser cet attribut pour mettre en place, dans une application ASP.NET MVC, une journalisation asynchrone avec un service web REST.
La solution présentée ici permet de facilement ajouter la journalisation à une application existante en minimisant les impacts et sans devoir retester l’application à grandeur.
Nous n’allons pas modifier le code existant, nous allons juste le “décorer” d’un attribut sans impact sur la logique actuelle.
(Temps de lecture 16minutes)

ActionFilterAttribute

Il existe quatre catégories de filtres qui permettent d’adapter le comportement d’un contrôleur en ASP.NET MVC :

  1. AuthorizationFilter
  2. ActionFilter
  3. ResultFilter 
  4. ExceptionFilter

Le cycle de leur utilisation est :

cycle_filtres

Ces filtres sont représentés en tant qu’interface dans le framework mais il existe déjà des implémentation qui sont fournies en standards et permettent de configurer le comportement de la cache (OutputCache), gérer les erreurs qui peuvent survenir dans l’exécution d’un contrôleur (HandleError) ou adapter la procédure d’autorisation (Authorize).

IActionFilter est une interface qui peut être utilisée pour créer un attribut qui pourra exécuter du code avant et après la méthode du contrôleur, mais après l’autorisation.
C’est l’endroit idéal pour ajouter du comportement global qui n’est pas lié à la logique d’affaire de l’application.

Journalisation non technologique

Dans le cadre de cet article, nous allons utiliser le filtre IActionFilter pour ajouter une journalisation non technologique à une application existante.
En effet, il n’est pas rare qu’une application possède déjà de la journalisation mais celle-ci est souvent très technique, voire parfois uniquement composé d’instructions SQL.
Lors d’une analyse de ces journaux, les utilisateurs ne sont pas autonomes et requièrent l’aide des services informatiques.

De plus, ce type de journalisation indique les éléments technologiques qui ont été touchés mais ne contient pas toujours des informations ayant une portée sémantique au niveau affaire.
Le message “2016-09-14_11h34.556 – Utilisateur xyz – INSERT INTO TABLE Action(dossier,date_action,type_action) values (‘DgM00928’,‘2016-10-01’,14)” n’est pas parlant et si le système n’est pas très connu ou actif, il faudra qu’un développeur fouille dans le code pour savoir quand cet évènement peut survenir.

A mettre en contraste avec le message “2016-09-14_11h34.556 – Dossier DgM00928, ajout d’une action de type ‘Recontacter fournisseur’ pour le 2016/10/01, réalisé par xyz”.

L’application Exemple

Les vues et le contrôleur de notre application exemple ont été créées directement avec Visual Studio et elle est disponible sur notre github.

Le but de l’application est de gérer des clients et elle est composée des vues suivantes :

  • Liste de clients (Index.cshtml)
  • Détail d’un client pour consulter, modifier, ajouter (create.cshtml, delete.cshtml,details.cshtml & edit.cshtml)

solution

Le contrôleur associé à ces vues (ClientsController.cs) utilise le contexte de base  de données (SampleDBContext.cs) et les informations sont dans la classe Client.cs

public class Client
{
[DataType(DataType.Text)]
public string Name { get; set; }
public string ClientNumber { get; set; }
public int ClientID { get; set; }
[DataType(DataType.Text)]
public string StreetAddress { get; set; }
[DataType(DataType.Text)]
public string PostCode { get; set; }
[DataType(DataType.Text)]
public string State { get; set; }
[DataType(DataType.Text)]
public string Country { get; set; }

[DataType(DataType.EmailAddress)]
public string Email { get; set; }
}

Fonction Create dans le contrôleur de clients :

[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Create([Bind(Include = "ClientID,Name,StreetAddress,PostCode,State,Country,Email")] Client client)
{
if (ModelState.IsValid)
{
SampleDBContext.AddClient(client);
return RedirectToAction("Index");
}

return View(client);
}

Le contexte de base de données simule l’obtention et la gestion des données avec une liste statique de clients. L’exemple ci-après et purement pour fin de simulation. (D’ailleurs, il n’est pas thread-safe)

public class SampleDBContext
{
private static List<ActionFilterMVC.Models.Client> Clients { get; set; }
static SampleDBContext()
{
Clients = new List<Client>();
Clients.Add(new Client() { ClientID = 1, Name = "Client 1", ClientNumber = "CLI-001" });
Clients.Add(new Client() { ClientID = 2, Name = "Client 2", ClientNumber = "CLI-002" });
}
internal static IEnumerable<Client> GetClientList()
{
return Clients;
}
public static void Modify(Client client)
{
var localClient = Clients.FirstOrDefault(x => x.ClientID == client.ClientID);
if (localClient == null) return;
localClient.Country = client.Country;
localClient.Email = client.Email;
localClient.Name = client.Name;
localClient.PostCode = client.PostCode;
localClient.State = client.State;
localClient.StreetAddress = client.StreetAddress;
}
internal static Client GetClient(string number)
{
return Clients.Find(x => x.ClientNumber == number);
}
internal static void AddClient(Client client)
{
client.ClientID = Clients.Max(x => x.ClientID) + 1;
client.ClientNumber = "CLI-" + client.ClientID.ToString("000");
Clients.Add(client);
}
internal static void RemoveClient(string number)
{
Clients.RemoveAll(x => x.ClientNumber == number);
}
}

 

Le service de journalisation

Le service de journalisation est composé d’un contrôleur WebAPI qui implémente la méthode POST. Typiquement, ce service devrait journaliser dans des fichiers, dans le journal des événements ou dans une base de données.
Chacune de ces 3 solutions est intéressantes et possède des avantages et des inconvénients :

  • Fichier : Système de stockage simple et fiable qui peut être compressé par le système d’exploitation. De plus, le système de backup est facile à mettre en oeuvre. Par contre, il faut s’assurer de sécuriser les répertoires de fichier. Il faut aussi s’assurer de découper ces répertoires d’une manière logique – par client, par mois,… – au sinon vous risquez d’avoir un nombre ingérable de fichier. Typiquement, nous créons un répertoire par système qui utilise la journalisation et un fichier par jour. Nous gardons 18mois d’historique.
  • Journal des événements : système intégré à l’architecture Windows et qui peut être facilement consultable sans devoir accéder au système de fichier. Par contre, le backup des informations n’est pas très aisé. De plus, les applications asp.net qui tournent dans le pool applicatif par défaut ne peuvent pas écrire dans le journal des événements.
    Enfin, si la configuration n’est pas adéquate et que beaucoup d’événement sont générés dans un court laps de temps, les anciennes informations sont effacées.
  • Base de données : souvent plus lent que le système de fichier et avec un backup un peu plus compliqué à mettre en oeuvre, une base de données pour contenir des informations de journalisation peut être recommandée dans les situations où la journalisation est fréquemment consultée.

Dans notre exemple, la méthode fait simplement un Thread.Sleep(250) pour simuler un délai et un Debug.WriteLine() du message afin de pouvoir la tester. Le message apparaît alors dans la fenêtre de sortie de Visual Studio.

Journalisation asynchrone

De manière générale, il faut privilégier une journalisation asynchrone car la journalisation, bien qu’importante, ne devrait pas ralentir le traitement d’un système.
En utilisant une journalisation asynchrone, l’utilisateur n’est pas ralenti dans son action par un procédé qui ne lui est pas directement bénéfique.

Dans notre cas, nous avons une classe Journal sous le patron du singleton qui démarre un thread à son initialisation.

/// <summary>
/// Constructeur privé. Il faut passer par la propriété Journaliseur.Instance pour accéder à la fonctionnalité de journalisation.
/// </summary>
private Journal()
{
StartThread();
}

Ce thread utilise l’option IsBackground=false afin de s’assurer que l’information sera journalisée même si l’application est arrêtée.

private void StartThread()
{
thread = new Thread(new ThreadStart(ManageQueue));

thread.IsBackground = false;

thread.Start();
}

L’appel à la fonction de journalisation ajoute dans une queue un appel à la fonction interne de journalisation (avec le message en paramètre) et utilise un ManualResetEvent pour indiquer la présence d’élément dans la queue.

public void Log(string message)
{

lock (requestQueue)
{
requestQueue.Enqueue(() => InternalLog(message));
}

newElementPresent.Set();
}

La méthode associée au thread s’assure de vider la queue d’actions lorsque le ManualResetEvent est utilisé.

private void ManageQueue()
{
while (true)
{
waitingForRequest.Set();

newElementPresent.WaitOne();

newElementPresent.Reset();

waitingForRequest.Reset();

ExecuteQueueActions();
}
}

La fonction ExecuteQueueActions s’occupe d’exécuter tous les appels en attente dans la queue.

private void ExecuteQueueActions()
{
Queue<Action> copieQueue;

lock (requestQueue)
{
copieQueue = new Queue<Action>(requestQueue);
requestQueue.Clear();
}

foreach (var actionJournalisation in copieQueue)
{
actionJournalisation.Invoke();
}

}

ActionFilterAttribute pour lier le tout

Afin de facilement ajouter la journalisation au contrôleur, nous allons créer une classe JournalisationFilter qui hérite d’ActionFilterAttribute.

public class JournalisationFilterAttribute : ActionFilterAttribute
{
public string Message { get; set; }
public string ProprietesAJournaliser { get; set; }

public override void OnActionExecuting(ActionExecutingContext filterContext)
{

}
public override void OnActionExecuted(ActionExecutedContext filterContext)
{

}

}

Cette classe permet d’aisément associer un message de journalisation à une méthode de contrôleur en ajoutant un attribut sur l’action du contrôleur.
De cette manière, nous ne touchons pas à la logique existante dans le contrôleur.

[HttpPost]
[ValidateAntiForgeryToken]
[JournalisationFilter(Message = "L'utilisateur {utilisateur} vient de modifier le client {number} ({StreetAddress}{PostCode}).",
ProprietesAJournaliser = "StreetAddress,PostCode" )
]
public ActionResult Edit([Bind(Include = "ClientID,Name,StreetAddress,PostCode,State,Country,Email")] Client client)
{
if (ModelState.IsValid)
{
SampleDBContext.Modify(client);
return RedirectToAction("Index");
}
return View(client);
}

Pour nous permettre d’implémenter la logique de remplissage du message, la classe JournalisationFilter implémente la méthode OnActionExecuting pour obtenir les informations avant l’action et OnActionExecuted pour appeler le singleton de journalisation après succès.
La méthode OnActionExecuting utilise la propriété ActionParameters du paramètre filterContext pour obtenir créer un dictionnaire avec les valeurs des propriétés reçues lors de l’appel et qui sont définies dans ProprietesAJournaliser.

La classe de filtre est instanciée avant l’appel au contrôleur et c’est la même instance qui est utilisée pour tous les appels, il faut associer le dictionnaire à un élément partagé entre le OnActionExecuting et le OnActionExecuted pour qu’il puisse être utilisé par la méthode OnActionExecuted. Ce partage d’information peut être fait en ajoutant le dictionnaire aux items de HttpContext.

En résumé, OnActionExecuting récupère des informations disponibles uniquement avant l’action pour que OnActionExecuted puisse les utiliser lors de la génération du message.

public override void OnActionExecuting(ActionExecutingContext filterContext)
{
base.OnActionExecuting(filterContext);

var parametreAction = filterContext.ActionParameters.Values.FirstOrDefault();
if (parametreAction != null)
{
var dic = InitialiserDictionnaireProprietes(parametreAction);

filterContext.HttpContext.Items.Add("dic",dic);
}
}

La méthode InitialiserDictionnaireProprietes va créer un dictionnaire avec les valeurs voulues.

private void InitialiserDictionnaireProprietes(object parametreAction)
{
var dictionnaireProprietes = new Dictionary<string, string>();
if (string.IsNullOrEmpty(ProprietesAJournaliser))
{
return null;
}

var splitted = ProprietesAJournaliser.Split(',');
foreach (string nomPropriete in splitted)
{
var valeur = parametreAction.GetType().GetProperty(nomPropriete).GetValue(parametreAction, null) ?? string.Empty;

dictionnaireProprietes.Add(nomPropriete, valeur.ToString());
}
return dictionnaireProprietes;

}

Lors de l’appel à la méthode OnActionExecuted, le message sera généré en fonction du format spécifié par l’attribut.

public override void OnActionExecuted(ActionExecutedContext filterContext)
{
base.OnActionExecuted(filterContext);

Message = ReplaceValues(filterContext, Message);

Journal.Instance.Log(Message);
}

De plus, dans mon cas, j’ai ajouté la possibilité d’utiliser les valeurs {utilisateur} (déterminé sur base de l’utilisateur connecté à IIS ) et {number} qui sera récupéré de la route.

private string ReplaceValues(ActionExecutedContext filterContext, string message)
{
if (filterContext.RequestContext.RouteData.Values.ContainsKey("number"))
{
var number = filterContext.RequestContext.RouteData.Values["number"].ToString();
message = message.Replace("{number}", number);
}
if (DictionnaireProprietes != null && DictionnaireProprietes.Any())
{
foreach (string nomPropriete in DictionnaireProprietes.Keys)
{
message = message.Replace("{" + nomPropriete + "}", DictionnaireProprietes[nomPropriete]);
}
}

message = message.Replace("{utilisateur}", HttpContext.Current.User.Identity.Name);

return message;
}

Scénarios d’utilisation de la cache en ASP.NET WebForms

Cet article présente les fonctionnalités de base d’une cache ainsi que les différentes possibilités de l’utiliser dans les application ASP.NET WebForms avec les composants ViewState, HttpContext.Cache, MemoryCache et OutputCache.
Nous proposons aussi un rappel afin de déterminer les cas où l’utilisation d’une cache peut s’avérer intéressante et où il vaut mieux l’éviter !
De plus, nous aborderons le cycle de vie d’un élément dans une cache.  (Temps de lecture : 18 minutes)

Qu’est-ce que la cache

La cache est un endroit qui permet d’héberger temporairement des informations afin d’améliorer les performances d’une application. Elle est le plus souvent utilisée pour éviter des aller-retour avec une base de données ou un service de données externe à l’application.

Dans la situation d’une application obtenant ses informations d’une base de données et n’utilisant pas de cache, chaque consultation d’une information implique un accès à la base de données.
Diapositive1

Dans une application avec cache, un accès à la BD est requis pour la première consultation – qui va placer les données dans la cache – et toutes les autres consultations de la même informations prendront les données de la cache :
Diapositive2

Cette situation permet de diminuer fortement les accès à la base de données, et donc de diminuer la charge de la base de données qui va pouvoir accepter plus de trafic.

Dans le cadre d’une application ASP.NET, la cache peut aussi être du côté client, dans le navigateur :
Diapositive3

De manière générale, le contenu statique (image, javascript, css) est automatiquement mis en cache par le navigateur.

Pourquoi mettre en cache ?

Le but de la cache est d’alléger le travail des serveurs en limitant la communication avec d’autres services de données (comme les base de données) et/ou en limitant l’accès à des ressources plus lentes (disque dur, réseau).

Quoi mettre en cache ?

Des informations de sécurité ne doivent jamais être mises en cache !

En effet, une erreur dans la gestion de votre cache et certaines personnes pourraient avoir encore accès à certaines ressources alors que la sécurité vient d’être modifiée ! Pire, certaines personnes pourraient avoir accès à des informations qui ne leur sont pas destinées.

Seules des informations affichées à l’utilisateur peuvent être mises en cache!

Il va de soi que la cache ne doit être utilisée que pour gagner du temps/des ressources pour l’affichage.
Toute décision de logique d’affaire doit être basée sur la version la plus récente de l’information.

Comment enlever des informations de la cache ?

Comme l’a dit Phil Karlton : Il n’y a que deux choses difficiles en informatique : l’invalidation de la cache et nommer les choses.

Une technique pour invalider la cache sera présentée pour chacune des solutions présentées ci-après.

Solutions actuelles de cache pour ASP.NET WebForms

.NET permet d’utiliser la cache de manière transparente ou de manière manuelle pour satisfaire tous vos besoins !

Option manuelle client : mettre les données en cache dans la page

Le ViewState est un dictionnaire qui peut être utilisé pour garder des informations dans des aller-retour entre le serveur et le client. Ces informations seront transmises par le client à chaque POST sur une page et seront accessible dans le code avec l’instruction ViewState[“cle”].
Lors de la génération du rendu de la page, le contenu complet du ViewState est sérialisé et placé dans un champ de type hidden accompagné d’une clé de hachage basé sur l’adresse MAC de la machine.
viewstate

ASP.NET valide le viewstate à chaque appel, c’est pourquoi le viewstate possède une clé de hachage.

De la même manière, un champ Hidden au sein de votre page pourrait servir de cache et permettre ainsi à l’application de ne pas devoir recontacter la base de données. Attention, la valeur d’un champ hidden peut être modifiée par le client, il faut donc valider son contenu avant de l’utiliser.
Si vous ne voulez pas mettre en place une technique de validation de votre champ hidden, le plus simple est de se limiter à l’utiliser pour garder en cache des informations simple et non critiques.
Par ailleurs, cette technique n’est utile que si la page est ensuite transmise vers le serveur.

Que placer dans cette cache ? Principalement des informations liées à l’utilisateur qui sont peu critiques.

Comment retirer un élément de cette cache ? Dès que l’information est obsolète, elle doit être manuellement remplacée. Dans cette situation, la suppression de l’information est manuelle et simple à mettre en oeuvre.

Option manuelle serveur : la Session

Chaque élément d’une application ASP.NET peut accéder à un objet Session, lié à la session de travail en cours de l’utilisateur. Cet objet permet d’associer des objets à un utilisateur avec l’instruction Session[“cle”]=monObjet.
Elle est typiquement utilisée pour garder des informations liées à l’authentification (à ne pas confondre avec l’autorisation). Il faut éviter d’y stocker des informations de navigation car l’objet Session est partagé entre toutes les pages consultées par un même utilisateur dans la même session de travail.

Il est possible de l’utiliser pour garder d’autres informations en cache sauf que l’objet Session est fait pour fonctionner avec une faible quantité d’informations. Ses performances se dégradent proportionnellement à la taille des informations présentes. Chaque objet qui sera placé dans la session doit être sérialisable.

Que placer dans cette cache ? Principalement des informations liées à l’utilisateur qui sont peu critiques.

Comment retirer un élément de cette cache ? Ici encore, la suppression de l’information est manuelle et simple à mettre en oeuvre.

Option manuelle serveur: HttpContext.Cache

HttpContext.Cache est un dictionnaire restreint à l’AppDomain de votre application et qui est accessible à tout le code de l’application exécuté dans cet AppDomain.

Dans les composants Web (Page, UserControl), il est accessible par la propriété Cache.

private void AfficherValeur()
{
var cleCache = "clé";
string valeur = Cache[cleCache] as string;
if (valeur == null)
{
valeur = ObtenirValeurDeServiceDistant();
Cache[cleCache] = valeur;
}
lblValeur.Text = valeur;
}

Vu que la cache est partagée entre plusieurs processus, il faut éviter de l’appeler pour vérifier la présence d’un élément et ensuite l’appeler une seconde fois pour obtenir cet élément. Une situation de compétition (race condition) pourrait amener deux processus à faire la même vérification au même moment et à essayer d’obtenir l’information du service distant plusieurs fois.

private void AfficherValeurPasBien()
{
var cleCache = "clé";
if (Cache[cleCache] == null)
{
//Condition de course, plusieurs processus pourraient passer dans la condition du if
//entre ces deux instructions
Cache[cleCache] = ObtenirValeurDeServiceDistant();
}
lblValeur.Text = Cache[cleCache] as string;
}

Que placer dans cette cache ? Des informations utilisées par plusieurs utilisateurs et dont l’obtention est jugée coûteuse.
Comment déterminer si cela est coûteux ? Il faut ici faire preuve de pragmatisme et se baser sur les résultats d’analyse de performance.

Par exemple, j’ai déjà travaillé sur un système ou tous les intitulés d’interface utilisateur dans plusieurs langues (Titre de l’application, titre de section, texte d’un label,…) étaient présents dans la base de données car ils devaient pouvoir être modifiés rapidement par les utilisateurs. Une analyse de performance du serveur de base de données a indiqué que la table des intitulés était devenu un point chaud et couvrait 95% des appels à la base de données. Ces informations changeaient habituellement lors de la livraison d’une nouvelle version de l’application … ou sur base d’une plainte d’un intervenant car dans certaines langues “Scolaire” et “Académique” ne sont pas des synonymes.

Comment retirer un élément de cette cache ? De manière automatique, la cache sera vidée lorsque le pool d’application sera recyclé.
Par défaut, ce pool est recyclé toutes les 29 heures mais il est possible que votre infrastructure soit différente.
C’est cette technique que nous avons utilisé dans l’exemple des intitulés ci-dessus.

Vous pouvez manuellement retirer des informations de le cache lorsque vous détectez qu’elle devient obsolète. Par exemple, l’écran de modification d’un intitulé invalidait l’information de la cache pour un intitulé lorsque l’utilisateur enregistrait.

De manière avancée, il est possible de spécifier un délai d’expiration lorsque vous ajoutez l’information dans la cache.
Ce délai peut être exprimé selon un facteur temporel absolu (ex : “dans 4 minutes”) ou selon un facteur temporel de glissement (ex : “4 minutes après sa dernière utilisation”).
Le facteur de glissement vous permet donc de garder en cache quelque chose de fréquemment utilisé et de le retirer automatiquement lorsqu’il l’est moins.
Cette utilisation avancée permet aussi :

  • de spécifier une dépendance entre une entrée dans la cache et un(des) fichier(s) sur le disque (ex : un fichier XML qui contient les données utilisées par l’application), ou entre différentes entrées de la cache
  • d’associer une méthode de callback à l’élément de la cache. Cette méthode de callback sera exécutée lorsque l’élément est retiré de la cache tout en indiquant la raison du retrait (expirée, le système à besoin de mémoire, …)
private void AjouterValeurExpirationGlissement()
{
var cleCache = "clé";
string valeur = Cache[cleCache] as string;
if (valeur == null)
{
valeur = ObtenirValeurDeServiceDistant();

TimeSpan glissementExpiration = new TimeSpan(hours: 0, minutes: 4, seconds: 0);

CacheDependency dependanceDeCache = null;
CacheItemRemovedCallback callbackSurSuppressionItem = null;

Cache.Add(cleCache,
valeur,
dependanceDeCache,
Cache.NoAbsoluteExpiration,
glissementExpiration,
CacheItemPriority.Default,
callbackSurSuppressionItem);

}
lblValeur.Text = valeur;
}

Option automatique serveur: OutputCache

OutputCache est un attribut qui doit être ajouté à la page et qui permet d’indiquer à ASP.NET de garder le rendu à plusieurs endroit, que ce soit sur le serveur ou sur le client. Lors de l’utilisation de cette cache, il faut indiquer la période (en seconde) pendant laquelle les éléments doivent être gardés en cache.
Si une page présente des informations différentes en fonction de ses paramètres d’accès, il faut indiquer que la cache du rendu doit suivre ce paramètre : l’attribut VaryByParam permet de l’indiquer facilement.

Les exemples suivants se basent sur le code présent sur notre github.

<%@ OutputCache Duration="60" VaryByParam="Id" Location="ServerAndClient" %>

Avec l’instruction ci-dessus, tout changement de la valeur du paramètre Id va impliquer une autre entrée dans la cache  /OutputCache1.aspx?id=1 et /OutputCache1.aspx?id=2 seront deux entrées différentes dans la cache d’ASP.NET.
outputcache3aoutputcache3boutputcache3c

 

Si l’attribut n’indique pas de VaryByParam (voir OutputCache0.aspx), le rendu de /OutputCache0.aspx?id=1 et /OutputCache0.aspx?id=2 seront identiques !
outputcache1outputcache2

 

Que placer dans cette cache ? Cette cache concerne une page au complet plutôt qu’une information particulière
(Il est possible de spécifier certaines sections avec OutputCache, cela fera l’objet d’un autre article…)

Ce qui est certain, c’est que si vous ne spécifiez pas de VaryByParam, il ne faut pas montrer d’informations qui diffère d’un usager à l’autre !
Typiquement, cette manière de cacher les informations est utilisée pour des informations partageables entre les usagers.
Par exemple, le détail des coordonnées d’un fournisseur.

Comment retirer un élément de cette cache ?  L’instruction HttpResponse.RemoveOutputCacheItem(path) permet d’invalider la cache pour un chemin de page donné.

ATTENTION

Comme l’a dit Donald Knuth : l’optimisation prématurée est la racine de tous les maux.

L’utilisation d’une cache est de l’optimisation, il faut donc grandement se méfier de sa mise en place.

En effet, cela impose une compréhension complète de la durée de vie des informations présentes dans la cache.

Diapositive2
Dans l’exemple ci-dessus, si l’information x a été modifiée entre le deuxième et le troisième appel, le troisième appel recevra une information erronée.

Dans le cas du résultat d’une recherche sur Internet, ce n’est pas trop grave, dans le cas d’informations financières sur un client permettant de prendre une décision, cela peut être très problématique.
Cela peut être aussi problématique dans la situation où celui qui a modifié l’information est le même que celui qui la consulte ! Il perdra confiance dans le système car il pensera que ses modifications ont été perdues.
Il est donc primordial de savoir exactement quand indiquer à la cache qu’une information est obsolète.

De plus, dans le cas d’une base de données, les informations fréquemment consultées sont déjà dans la cache du SGBD. Tout bon SGBD qui se respecte garde en cache une partie des informations les plus consultées et gère déjà correctement le cycle de vie de ces informations.
Il n’est donc peut-être pas requis d’avoir la même information présente dans deux caches différentes, cela pourrait être vu comme un gaspillage de ressources.

L’optimisation prématurée est la racine de tous les maux.”
L’utilisation de fonctionnalité de cache ne doit se faire que si vous avez démontré que votre système souffrait !
Pour ce faire, il faut avoir :

  • une analyse des performances avant la mise en cache
  • la démonstration que la base de données (ou le service distant) est le goulot d’étranglement
  • un indicateur de volatilité des informations que vous voulez mettre en cache. (Par exemple, des données gérées dans un système distant et transférées par un lot toutes les nuits ont une très faible volatilité, elles ne changent pas pendant la journée. Le cours d’une action change pendant toute la période d’ouverture de la bourse et elle ne change pas pendant la nuit.)
  • un indicateur d’impact si une information n’est pas à jour (l’annuaire téléphonique de l’entreprise peut-il montrer une information erronée pour quelqu’un qui a changé de bureau et de téléphone hier?)

Typiquement, la mise en cache concerne des informations qui changent rarement pendant le processus de travail d’un utilisateur.

Si vous n’avez aucune mesure de performance et aucun problème de performance, il vaut mieux ne pas mettre de cache en place.
De manière générale, ces mesures de performances sont faites après la réalisation de l’application ou lorsque son développement touche à sa fin, pas au début !

Autohotkey pour améliorer son day-to-day

Cet article présente des cas d’utilisation du logiciel AutoHotkey, un logiciel qui permet de créer des macros et de les assigner à un raccourci du clavier. Il permet autant d’automatiser des tâches banales et quotidiennes que de réaliser des solutions complexes de récupération de données.
Cet article ne se veut pas un guide complet d’autohotkey mais plutôt une présentation de quelques cas d’utilisations pratiques. (Temps de lecture : 13 minutes)

Autohotkey ?

Autohotkey, logiciel gratuit et open source, permet donc d’associer des macros à des raccourcis clavier (ou souris) ou à des mots-clés. Le plus souvent, ces macros seront composées d’instruction d’envoi de touches du clavier mais il est possible de faire de scripts très évolués: (voir DNS Reverse lookup, Afficher le clavier visuel avec les boutons Volume d’une tablette, Dictionnaire automatique pour toutes les applications)
Attention : les exemples ci-dessous sont tous fonctionnels, néanmoins, dans le cas des applications exécutées en mode administrateur, il faut que Autohotkey soit aussi exécuté en mode administrateur pour pouvoir interagir avec l’application.

Automatisation simple dans visual studio 

Il existe plusieurs manières d’étendre les fonctionnalités de Visual Studio, des extensions disponibles sur la gallerie visual studio, des macros (qui ont été supprimées d’une version mais sont revenues en tant qu’extension).
Visual Studio intègre aussi une fonctionnalité de snippet de code qui permet d’automatiser certaines tâches répétitives (comme la création d’une propriété) mais la gestion de ces snippets pourrait être plus simple.

Dans Authotkey, le script ci-dessous permet d’entourer une variable de l’instruction Convert.ToInt32()
::cv::Convert.ToInt32({end})
Dans ce script, le ::cv indique le mot-clé à écouter “cv” et le ::Convert.ToInt32({end}) indique l’instruction à envoyer  Convert.ToInt32({end}). Le {end} est un mot réservé pour Authotkey et est identique à la touche Fin du clavier qui amène à la fin de la ligne.

Son utilisation est simple, il suffit de placer le curseur juste avant la variable et d’inscrire cv suivi d’un espace, Autohotkey se chargera d’insérer le texte après le “::”.

Dans le même style, le script ci-dessous permet d’envoyer select * from
::ssf::select * from

Ces deux exemples sont des scripts sur une seule ligne mais il est possible de réaliser des scripts complexes composés d’instructions, de condition,…

Le script ci-dessous, associé au raccourci Windows+U avec #u,  permet de créer l’instruction d’une propriété en c# sur base d’un mot unique reprenant le type de la variable et son nom.
Son action est la suivante :

  • copier tout le mot avant le curseur
  • extraire les 3 premiers caractères
  • déterminer le type de la propriété sur base de ces caractères
  • créer l’instruction représentant le code de la propriété
  • remplacer le mot sélectionné par l’instruction générée

#u::                             ;associé au raccourci clavier Win+u
send ^+{left}                    ;ctrl+shift+left
send ^c                          ;copier

sleep 250                        ;attendre que la copie soit effectuée

prefixe := substr(clipboard,1,3) ;récupérer le préfixe
suffixe := substr(clipboard,4)   ;récupérer le nom de la propriété

;déterminer le type de données en fonction du texte dans le préfixe
if prefixe = str
{
varType = string
}
else if prefixe = dat
{
varType = DateTime
}
else if prefixe = int
{
varType = int
}
;envoyer le code de la propriété. Ex : « strMaVariable »
; => « public string MaVariable { get; set; } »
sendraw public %varType% %suffixe% { get; set; }
return

Exemple :

genererproprietes

Actions dans Windows

Pour ceux qui utilisent une résolution d’écran différente de celle des utilisateurs, le script suivant permet de redimensionner une fenêtre à une dimension précise. (1024×700 dans l’exemple ci-dessous)

#w::                                  ;associé au raccourci clavier Win+w
WinGetActiveTitle, TitreFenetre       ;copier le nom de la fenêtre active dans la variable TitreFenetre
WinMove, %TitreFenetre%,,1,1,1024,700 ;déplacer la fenêtre en 1,1 et la redimensionner en 1024x700
return

Lorsque je réalise une présentation dans PowerPoint, je l’accompagne souvent d’un document Word dans lequel je crée une page par diapositive et chaque page contient du texte expliquant le contenu de la diapositive. Cette technique permet d’avoir des commentaires plus complets et au style plus riches que ceux qui peuvent être renseignés dans la zone commentaire dans PowerPoint. Pour copier la diapositive dans Word, j’utilise la fonction qui permet de lier Word à PowerPoint pour cette diapositive en faisant un “copier” dans PowerPoint et un “collage spécial avec liaison” dans Word. Ceci permet à la diapositive dans Word d’être mise à jour si elle est modifiée dans PowerPoint.

Dans ma méthode de travail, je commence par réaliser une grande partie des diapositives et je les copies ensuite toutes dans Word en créant une page par diapositive.
Or, la fonction “collage spécial avec liaison” n’est pas directement accessible dans le ruban de Word et j’ai donc créé le script suivant qui fait une copie dans PowerPoint et colle avec liaison dans Word. Attention, ce script nécessite que Word soit directement accessible via alt+tab à partir de PowerPoint.

#o::             ;Associé au raccourci clavier Win+o
send ^c          ;copier
sleep 250        ;attendre 250ms que la copie soit effectuée
send !{tab}      ;changer d'application avec Alt+Tab
sleep 250        ;attendre 250ms pour le changement d'application
send !l          ;ouvrir le ruban "Accueil" avec Alt+L
sleep 250        ;attendre 250ms pour la réalisation de l'action
send v           ;envoyer v pour ouvrir le menu "Coller"
sleep 250        ;attendre 250ms pour la réalisation de l'action
send g           ;envoyer g pour ouvrir la fenêtre "Collage spécial"
sleep 250        ;attendre 250ms pour la réalisation de l'action
send !s          ;envoyer Alt+S pour passer à la section "Coller avec liaison"
sleep 250        ;attendre 250ms pour la réalisation de l'action
send {enter}     ;Sélectionner la première action de la liste
sleep 250        ;attendre 250ms pour la réalisation de l'action
send !{tab}      ;Alt+Tab pour retourner dans PowerPoint
sleep 250        ;attendre 250ms pour la réalisation de l'action
send {PgDn}      ;Passer à page suivante dans PowerPoint
return

Utilisation plus ésotérique

Il y a quelques années, j’ai participé à la conversion d’une application d’un environnement technologique (central) à un autre (.NET). Un enjeu est vite apparu à propos d’un utilitaire réalisé en assembleur central (voir) qui avait une fonction similaire à soundex mais qui était une implémentation interne.
Le but de cet utilitaire était de prendre 2 valeurs en entrée (Nom et Prénom) et de générer une clé phonétique permettant de faciliter la recherche. Par exemple, Pierre Tremblay et Pierre Tramblay avaient trmblpr comme clé. Ceci permettait à un opérateur au téléphone de trouver une personne facilement.
Cette utilitaire a été utilisé pour générer plusieurs millions de clés et nous étions en phase d’analyse pour la conversion d’assembleur vers .NET… ce qui n’est pas une tâche aisée, surtout de trouver quelqu’un qui soit capable de lire de l’assembleur central de 1984 et de créer du code c# de qualité…

Un miracle est apparu quand un de nos partenaires nous a informé qu’ils avait fait cette conversion 5 ans auparavant sauf que c’était vers VB.NET. Technologiquement, cela ne change pas grand chose pour nous, cela reste du .NET par contre, il fallait s’assurer que depuis 1984, leur code n’avait pas évolué différemment du nôtre.
J’ai utilisé Autohotkey pour communiquer avec le central et générer +- 4000 clés. J’ai ensuite créé un petit programme c# qui a utilisé ces 4000 clés pour tester le code VB.NET.
Résultat : le miracle n’en fut pas, 92% des clés étaient correctes… ce qui extrapolé à plusieurs millions nous a convaincu de convertir le programme assembleur tel quel.

Cette solution rapide (2-3 heures) nous a donné un gros avantage, nous avions déjà 4000 tests qui pouvaient être utilisés pour la conversion !
La logique du script était :

  1. Initialiser le fichier de sortie
  2. lire une entrée dans le fichier source et récupérer la valeur dont la clé doit être générée
  3. se positionner au bon endroit dans l’écran central avec les “flêches”
  4. insérer la valeur de l’entrée
  5. exécuter le programme au central
  6. se positionner au bon endroit dans l’écran central avec les “flêches”
  7. sélectionner la sortie du programme, une zone de 7 de long
  8. récupérer la sortie du programme via un copier
  9. insérer la valeur d’entrée et la valeur de sortie dans un fichier xml qui sera consommé par l’outil de test.
  10. réinitialiser l’écran au central
  11. recommencer à l’étape 1 jusqu’à la fin du fichier source
  12. Finaliser le fichier de sortie

#y::      ;associé au raccourci clavier Win+Y. Doit être appelé directement dans l’écran du central
;initialisation du fichier xml
FileDelete, c:\temp\cles.xml
valeurSortieXml = <?xml version='1.0' ?>
FileAppend, %valeurSortieXml%, c:\temp\cles.xml
valeurSortieXml = <resultats>
FileAppend, %valeurSortieXml%, c:\temp\cles.xml

;lire toutes les lignes du fichier c:\temp\source.txt
Loop, Read, c:\temp\source.txt
{
;lire une entrée dans le fichier source et récupérer la valeur dont la clé doit être générée
valeurAvantCle = %A_LoopReadLine%

;se positionner au bon endroit dans l’écran central avec les “flêches”
send {down}{down}{right}{right}{right}{right}{right}{right}{right}{right}{right}{right}
sleep 150

;insérer la valeur de l’entrée
send %valeurAvantCle%
;exécuter le programme au central
send {F8}
;attendre l’exécution
sleep 350
;se positionner au bon endroit dans l’écran central avec les “flêches”
send {down}{down}{down}{right}{right}{right}{right}{right}{right}{right}{right}{right}{right}
;sélectionner la sortie du programme, une zone de 7 de long
send +{right}+{right}+{right}+{right}+{right}+{right}+{right}
;récupérer la sortie du programme via un copier
send ^c
;insérer la valeur d’entrée et la valeur de sortie dans un fichier xml qui sera consommé par l’outil de test.
valeurSortieXml = <resultat source= »%valeurAvantCle% » cle= »%clipboard% » />
FileAppend, %valeurSortieXml%, c:\temp\cles.xml
}
;finalisation du fichier xml
valeurSortieXml = </resultats>
FileAppend, %valeurSortieXml%, c:\temp\cles.xml
return

Quitter le monde du différé pour rentrer dans le monde du direct ?

Cet article présente une solution d’architecture logicielle pour passer d’une situation où des applications communiquent par lot nocturne à une situation où elles communiquent au besoin. Cette technique permet de rationaliser les responsabilités et de restreindre la portée des impacts d’évolution d’un système. Elle permet aussi de diminuer la charge de surveillance.
Cette solution ne s’applique que dans les situations où les données transférées le sont tel quel et ne subissent aucun traitement avant transfert. (Temps de lecture : 12 minutes)

Situation initiale :

Il est très fréquent d’avoir des lots planifiés pour copier de l’information d’un système vers un autre. Cette situation peut autant trouver son origine dans une volonté d’imposer des règles d’interactions inter systèmes que par des contraintes historiques de problème d’intercommunication entre certaines technologies. Chaque développeur avec un minimum d’expérience a eu à développer un lot. Chaque service informatique a eu à réagir à la panne d’un lot.

Dans l’exemple ci-dessous, le système A gère des données qui doivent être disponibles dans le système B.

image

Un lot, appartenant au système A, a été mis en place pour générer un fichier qui est transféré vers le système B. Le système B possède un lot qui permet de lire les données du système A et de les transférer dans ses données.

Cette solution permet donc à B d’afficher des informations de A même s’il n’est pas de la même technologie ou dans le même environnement technologique.

Habituellement, ce genre de lot est planifié pour être exécuté à intervalle régulier, souvent de nuit, avec un moyen d’alerter des opérateurs du bon (ou du mauvais) déroulement de l’opération. L’opérateur doit donc contrôler l’exécution du lot de manière systématique.

Beaucoup de ces transferts d’informations se font dans des fichiers plats à structure fixe ressemblant à ceci :

     4573579Sébastien                    Patrick                       rue du petit bonhomme en mousse                                       24/b            45785Seine-St-Denis                 France

La plupart des SGBD offrant un export XML, il se peut aussi que le fichier de transfert soit généré par le SGBD et, malheureusement, représente fidèlement la structure interne du système :

<l1>
    <tb_c>
        <i_id>   4573579</i_id>
        <ch_n>Sébastien                     </ch_n>
        <ch_p>Patrick                       </ch_p>
        <ch_a_1>rue du petit bonhomme en mouss</ch_a_1>
        <ch_a_2>e                             </ch_a_2>
        <i_n>          24</i_n>
        <ch_bp>/b     </ch_bp>
        <i_cp>       45785</i_cp>
        <ch_v>Seine-St-Denis                </ch_v>
        <ch_p>France                        </ch_p>
    </tb_c>
</l1>

Ces deux fichiers sont difficilement « digérables » par l’œil humain et nécessitent une documentation claire qui explique la structure du champ et sa raison. Ceci implique que le fichier n’est pas autoporteur pour ses données et ses méta-données et donc, que pour chaque période d’entretien, les personnes en charge de l’entretien devront non seulement prendre connaissance de cette documentation exhaustive mais aussi religieusement entretenir cette documentation.

Cette documentation contient souvent un dictionnaire qui prend la forme suivante :

Nom du champ Type Taille Description
I_id Decimal 12 Identifiant unique
Ch_n Char 30 Contient le nom de naissance du client
Ch_p Char 30 Contient le prénom du client
Ch_a_1 Char 30 Contient la première partie de l’adresse du client
Ch_a_2 Char 30 Contient la deuxième partie de l’adresse du client
I_n Decimal 12 Contient le numéro de l’immeuble du client
Ch_bp Char 7 Contient le numéro d’appartement ou de boîte postale du client
I_cp Decimal 12 Contient le code postal du client.

(Ce code postal est en rapport au système X0124 qui contient la liste des codes postaux en vigueur)

Ch_v Char 30 Contient la ville du client. (Cette ville est en rapport au système x0124 qui contient la liste des villes en vigueur)
Ch_p Char 30 Contient le pays du client. (Ce pays est en rapport au système x0124 qui contient la liste des pays en vigueur)

Le code de l’application récupérer les informations d’un fichier plat ressemble souvent à l’exemple ci-dessous :

class Client
{
    public string Adresse { get; internal set; }
    public string CodePostal { get; internal set; }
    public string Identifiant { get; internal set; }
    public string Nom { get; internal set; }
    public string Pays { get; internal set; }
    public string Prenom { get; internal set; }
    public string Ville { get; internal set; }
}
static void Main(string[] args)
{
    ChargerFichier("entree.txt");
}
 
private static List<Client> ChargerFichier(string nomFichier)
{
    List<Client> clients = new List<Client>();
    using (var fichier = System.IO.File.OpenText(nomFichier))
    {
        clients.Add(ExtraireClient(fichier.ReadLine()));
    }
    return clients;
}
 
private static Client ExtraireClient(string ligneFichier)
{
    if (ligneFichier.Length != LONGUEUR_LIGNE)
        throw new ArgumentOutOfRangeException(ligneFichier, "Longueur incohérente");
    Client cli = new Client();
    int positionActuelle = 0;
    cli.Identifiant = ObtenirSectionLigne(LONGUEUR_CHAMP_I_ID, ligneFichier, ref positionActuelle);
    cli.Nom = ObtenirSectionLigne(LONGUEUR_CHAMP_CH_N, ligneFichier, ref positionActuelle);
    cli.Prenom= ObtenirSectionLigne(LONGUEUR_CHAMP_CH_P, ligneFichier, ref positionActuelle);
    cli.Adresse = ObtenirSectionLigne(LONGUEUR_CHAMP_I_N, ligneFichier, ref positionActuelle) + " " +
                  ObtenirSectionLigne(LONGUEUR_CHAMP_CH_BP, ligneFichier, ref positionActuelle) + " " +
                  ObtenirSectionLigne(LONGUEUR_CHAMP_CH_A_1, ligneFichier, ref positionActuelle) +
                  ObtenirSectionLigne(LONGUEUR_CHAMP_CH_A_2, ligneFichier, ref positionActuelle);
    cli.CodePostal = ObtenirSectionLigne(LONGUEUR_CHAMP_I_CP, ligneFichier, ref positionActuelle);
    cli.Ville = ObtenirSectionLigne(LONGUEUR_CHAMP_CH_V, ligneFichier, ref positionActuelle);
    cli.Pays= ObtenirSectionLigne(LONGUEUR_CHAMP_CH_P, ligneFichier, ref positionActuelle);
 
    return cli;
}
 
private static string ObtenirSectionLigne(int longueurChamp, string ligneFichier, ref int positionActuelle)
{
    var section = ligneFichier.Substring(positionActuelle, longueurChamp);
    positionActuelle += longueurChamp;
    return section.Trim();
}

Dans le cas de certains systèmes contenant des données utilisées par beaucoup d’autres systèmes, il n’est pas rare de voir la topographie suivante :

image

Dans cette situation, le système A est responsable de la qualité et de l’entretien de 3 fichiers pour des systèmes tiers et devrait maintenir plusieurs documentations dont la forme serait :
* Fichier pour B qui ne s’occupe que des informations pour la France pour générer des envois de publicités :

Nom du champ Type Taille Description
Ch_n Char 30
Ch_p Char 30
Ch_a_1 Char 30
Ch_a_2 Char 30
I_n Decimal 12
Ch_bp Char 7
I_cp Decimal 12
Ch_v Char 30

*Fichier pour C qui a besoin d’informations pour générer des statistiques de clients par ville et par Pays :

Nom du champ Type Taille Description
I_cp Decimal 12
Ch_v Char 30
Ch_p Char 30

Et ainsi de suite pour tous les autres systèmes…

Cette situation, bien que 100% fonctionnelle, donnant des données fiables et de qualité entre les différentes applications, est souvent accompagnée des obligations suivantes :

Ceci implique souvent :

  • que les responsables du système A comprennent minimalement l’utilisation faites de ces données par les autres systèmes afin d’effectuer cet entretien.
  • des coûts et des risques plus élevés lors de travaux d’évolution du système A vu que ces évolutions impliquent des essais dans les systèmes liés
  • les systèmes B, C et D ont des dépendances technologiques fortes à A et doivent parfois inclure des composantes inhabituelles pour transférer les données. (Conversion de type de fichiers, problème avec les caractères accentués,…)

La refonte d’un des systèmes impliqué dans cette situation est le moment idéal pour remettre cette solution en question.

Pour envisager une solution plus pragmatique et mieux orientée vers de l’évolution future, il faut répondre à ces 2 questions :

  • Les données du système source (A) sont-elles très volatiles ? Quelle est leur fréquence de modification ?
  • Serait-ce une plus-value pour le système cible (Ex : C) d’utiliser des données fraîches plutôt qu’une copie de la veille ?

Si la réponse est oui à ces deux questions, nous sommes alors face à un système pouvant quitter le monde du différé pour entrer dans le monde du direct.

Mais comment faire évoluer ce système en limitant les impacts ?

Prenons un scénario simple. Le système A contient les informations personnelles de clients d’une entreprise, le système C est utilisé par le service d’aide aux clients lorsqu’ils appellent par téléphone.

Pour des raisons historiques, un lot a été mis en place pour copier certaines informations personnelles des clients toutes les 4 heures de A vers C.

L’entreprise a décidé de refaire au complet le système C pour des raisons de désuétude et voudrait éviter d’avoir une copie des informations de A dans son système pour contrer les situations où les informations du client ne sont pas à jour car elles viennent d’être modifiées dans un délai inférieur à 4 heures.

Malheureusement, le personnel compétent pour travailler dans le système A est fortement occupé et ne peut faire d’évolution actuellement, c’est donc le personnel travaillant sur le système C qui va se charger de la réalisation.

Dans ce cas, il possible de réaliser une situation de transition telle que proposée ci-après :

image

L’équipe C réalise un service web qui prend sa source dans les données générées pour lui et le système C est adapté pour appeler ce service web dès que l’information personnelle d’un client est requise.

Pour cette situation, le système C doit juste garder une clé commune avec A.

Lors de la réalisation du service web, la structure d’export des informations peut être clarifiée pour ressembler à l’exemple suivant :

<clients>
    <client>
        <cleUnique>S_P_4573579</cleUnique>
        <nom>Sébastien</nom>
        <prenom>Patrick</prenom>
        <adresse>rue du petit bonhomme en mousse</adresse>
        <numero>24 /b</numero>
        <codepostal>45785</codepostal>
        <ville>Seine-St-Denis</ville>
        <pays>France</pays>
    </client>
</clients>

En utilisant WebAPI, le service web aurait la structure suivante :

public class ClientController : ApiController
{
    private static ClasseLogiqueAffaire _logiqueAffaire;
    private static ClasseLogiqueAffaire LogiqueAffaire
    {
        get
        {
            if (_logiqueAffaire == null)
                _logiqueAffaire = new ClasseLogiqueAffaire();
            return _logiqueAffaire;
        }
    }
    // GET: api/Client
    public IEnumerable<Client> Get()
    {
        return LogiqueAffaire.ObtenirClients();
    }
    // GET: api/Client/5
    public Client Get(string cleUnique)
    {
        return LogiqueAffaire.ObtenirUnClient(cleUnique);
    }
 }
 
public class ClasseLogiqueAffaire
{
    List<Client> clients;
    public ClasseLogiqueAffaire()
    {
        InitialiserClients();
    }
 
    private void InitialiserClients()
    {
        clients = ChargerFichier("fichierplat");
    }
 
    internal IEnumerable<Client> ObtenirClients()
    {
        return clients;
    }
 
    internal Client ObtenirUnClient(string cleUnique)
    {
        return clients.FirstOrDefault(x => x.CleUnique == cleUnique);
    }
 
    private static List<Client> ChargerFichier(string nomFichier)
    {
        //logique identique à exposé ci-avant
    }
 
}

Lorsqu’il y aura de la disponibilité dans l’équipe A, la section LogiqueAffaire du service web pourra être adaptée afin de prendre ses données directement dans la base de données :
image

Après cette évolution, les responsables du système A doivent moins connaitre C, ils n’ont plus qu’à respecter le contrat du service web et ils peuvent même mettre en place des tests qui le valide.

Et la performance dans tout ça ?

Cette piste de solution ne peut être envisagée qu’après une analyse d’impact sur les performances. Retirer les échanges de fichiers pour simplifier les coûts de maintenance ne doit pas faire diminuer la productivité des utilisateurs parce qu’ils doivent attendre la réponse d’un service web.

Le temps pour faire un appel à un service web est souvent supérieur au temps requis pour faire un appel à une base de données. De plus, si ce service web est devant CICS connecté au central, il pourrait être facilement déstabilisé par la charge supplémentaire.

Enfin, si le fichier contient des données calculées par un traitement lourd, le temps de réponse de chaque calcul peut devenir un vrai problème.

Lorsque le travail de refonte réalisé par l’équipe A est terminé, une phase d’analyse de performance doit être mise en place afin d’optimiser la charge du SGBD et du service web.

L’outil LogParser va permettre d’analyser les journaux d’IIS pour établir fidèlement les périodes de charge.

De plus, l’analyse de volatilité des données effectuées lors de l’orientation vers cette solution devrait permettre de ressortir certaines données pouvant être mises en cache.