Comment Dispose un HttpResponseMessage (et gérer l'erreur CA2000 de FxCop)

icon Tags de l'article : ,

Mars 29, 2018
Yo,

Petit souci sur lequel je suis tombé aujourd'hui : j'utilisais un using autour de mon HttpResponseMessage :

using(var response = new HttpResponseMessage(HttpStatusCode.OK);
{
     //...
     return response;
}

Evidemment... ça ne marchait pas. Le Dispose était fait avant que la requête ne parte chez l'utilisateur, et donc... ben j'essayais d'accéder à un objet disposed.

Sauf que si je retire mon using, j'ai une magnifique erreur FxCop CA2000 qui apparait...

Du coup comment faire ?

Après quelques recherches, j'ai trouvé plusieurs informations intéressantes :
  • Ce n'est, évidemment, pas obligatoire de dispose le HttpResponseMessage dans le cas où tout se passe bien. La requête sera dispose, et par conséquent l'objet HttpResponseMessage également. Le GarbageCollector est de plus là pour les restes ;)
  • Si jamais on veut planifier le Dispose pour s'assurer qu'il soit fait (sans attendre le GC), alors on peut utiliser Request.RegisterForDispose(response);
  • Dans tous les cas, l'erreur CA2000 apparaitra car dans le cas d'une exception, ben... notre HttpResponseMessage ne sera pas disposed.

Ce warning FxCop est un peu inutile quand on manipule des HttpResponseMessage dans un contexte de requêtes d'API.
Ce warning est là, normalement, pour forcer à utiliser des usings, mais là... on est dans une situation où un using ne marche pas, donc inadapté.

Ma suggestion ?
Désactiver l'erreur CA2000 pour cette méthode.

Et si vraiment vous ne pouvez/voulez pas... Vous pouvez toujours vous assurer qu'un Dispose sera fait, comme ceci :

var response = new HttpResponseMessage(HttpStatusCode.OK);
try
{
     Request.RegisterForDispose(response);
     //...
     return response;
}
catch
{
     response.Dispose();
     
     throw;
}

Bon dev et bonne journée tout le monde !

Ranger les using "System" avant les autres, dans Visual Studio

icon Tags de l'article : , ,

Mars 27, 2018
Salut,

Si vous êtes comme moi, vous aimez bien faire les choses.

Or, en C#, on sait qu'il faut toujours mettre les using "System" avant les autres. C'est une bonne pratique, ça permet de s'y retrouver plus facilement, et de gagner en clarté/lisibilité.

Sauf que voilà, sans resharper... c'est chiant d'avoir à déplacer les using pour les trier automatiquement.

Sauf que voilà... y'a pas à le faire à la main.

Visual Studio le gère automatiquement aujourd'hui. Il faut juste l'activer.

Allez donc dans Tools => Settings, puis dans C# => Advanced.

Là, vous n'avez plus qu'à cocher la case "Place 'System' directives first when sorting usings".



Et c'est fait !

Bon dev et bonne journée tout le monde !

Installer Git Extensions et Git sur Windows pour cloner un repository GitHub

icon Tags de l'article : ,

Mars 26, 2018
Bonjour à tous,

Un petit article pour bien installer Git Extensions sur son PC, afin de pouvoir l’utiliser avec un repo git (ici, Github).

Tout d’abord, vous devez télécharger Git for Windows et l’installer en administrateur.

A noter que dans les options, je choisis :
  • Notepad++ au lieu de Vim par défaut
  • Use Git and optional Unix tools from the Windows Command Prompt
  • Use OpenSSH
  • Use the OpenSSL library
  • Checkout as-is, commit as-is (évite les conversions intempestives)
  • Use Min-TTY (mieux qu’une simple console windows)
  • Enable file system caching & Enable Git Credential Manager

Ensuite, téléchargez Git Extensions et installez-le :
  • Install for all users of this machine
  • Décochez la case pour installer Git for Windows
  • Cochez l’installation de KDiff3 que si vous n’avez pas de logiciel de diff/merge d’installé
  • PuTTY comme client SSH

Lancez-ensuite Git Extensions.
Par défaut, il doit vous ouvrir la fenêtre de configuration. Si ce n’est pas le cas, ouvrez là.
Dedans, cliquez sur chaque ligne rouge et configurez là pour qu’elle passe verte (username, git path, etc.)

Une fois ça fait, vous n’avez plus qu’à récupérer le chemin de votre repository à cloner, et à l’ajouter dans votre Git Extensions pour le cloner.

S’il s’agit d’un chemin BitBucket ou Github, pensez à ajouter dans l’url votre username avant le nom de domaine, comme ceci :
https://my_account_name_or_email@github.com/HowTommy/ mycryptochat.git

Ainsi, quand vous essaierez de cloner/push un repository, une popup s'ouvrira pour vérifier votre identité.

And voilà!

En espérant que ça vous soit utile !

Bon dev et bonne semaine à tous/toutes !

Refactoring : la règle des trois

icon Tags de l'article : ,

Mars 16, 2018
Salut tout le monde,

Aujourd'hui on va parler Refactoring.

Je pense que vous vous êtes déjà tou·tes retrouvé·es dans une situation où vous avez besoin de reprendre du code... mais ça vous obligerait à faire un copier coller.

Donc, en tant que bon·ne dev, vous vous dites : "hors de question, je vais refacto pour réutiliser cette méthode/vue ailleurs". Et vous vous lancez dans votre refacto.

Sauf que voilà. En général une refacto de ce type prendra au moins 3x le temps qu'il aurait fallu pour faire un copier/coller... Et rien ne vous assure qu'elle sera utile plus tard.

Ca se trouve, vous perdrez des heures à refacto du code qui ne sera jamais plus utilisé.

Face à cette interrogation, mes collègues m'ont fait découvrir une règle que je ne connaissais pas : la règle de trois.

Elle est très simple : quand vous avez besoin de réutiliser un morceau de code, faites un copier/coller. Si, en revanche, vous en avez à nouveau besoin plus tard, là vous pourrez vous lancer dans une refacto.

En résumé : on peut copier/coller du code, mais une seule fois. Au delà, il faut refacto notre code.

Cette "autorisation de copier/coller" peut permettre d'éviter de se lancer dans d'énormes refacto couteuses qui n'auront aucune utilité. Et le cout sera quasi-nul, car un simple copier/coller c'est souvent l'affaire de quelques minutes...

Bon dev à tou·tes !

S'authentifier en POST avec un WebClient

icon Tags de l'article : ,

Aout 09, 2016
Hello,

Lorsqu'on est comme moi, on a tendance à aimer faire des tools qui vont scraper des pages web à l'aide d'un WebClient C#.

Sauf que voilà, il faut parfois pouvoir s'authentifier pour accéder à des pages qu'on aimerait parser... Et pour ça. Il y a une solution ! :)

Déjà, il va vous falloir une classe dérivée de WebClient qui gère les cookies et l'authentification :

// source : http://stackoverflow.com/questions/11118712/webclient-accessing-page-with-credentials
public class CookieAwareWebClient : WebClient
{
    public CookieAwareWebClient()
    {
        CookieContainer = new CookieContainer();
    }
    public CookieContainer CookieContainer { get; private set; }

    protected override WebRequest GetWebRequest(Uri address)
    {
        var request = (HttpWebRequest)base.GetWebRequest(address);
        request.CookieContainer = CookieContainer;
        return request;
    }
}

Maintenant, vous n'avez plus qu'à utiliser cette classe pour vous authentifier avec un UploadValues (en lui passant vos paramètres POST) :

using (var client = new CookieAwareWebClient())
{                
    // on spécifie que c'est du POST
    client.Headers.Add("Content-Type", "application/x-www-form-urlencoded");

    // cette ligne est ajoutée car parfois les authentifications vérifient le referer
    client.Headers.Add("Referer", "https://referer/if/needed");

    // on crée notre table clef / valeur
    var values = new NameValueCollection
    {
        { "username", "us3rn4me" },
        { "password", "p4ssw0rd" }
    };

    // on s'authentifie
    client.UploadValues("https://url/de/la/page/d/authentification", values);

    // et voilà, on n'a plus qu'à appeler les pages qui nous intéressent, accessibles désormais vu qu'on s'est authentifiés avec le UploadValues !
    var pageContent = client.DownloadString(url);
}

Et le tour est joué !

Bon dev à tous !

Limiter l'usage d'une API avec ASP.Net Web Api

icon Tags de l'article :

Juillet 04, 2016
Hello,

Petite classe perso qui permet de limiter très facilement une API (externe dans mon cas) à 20 appels par 20 secondes :

using System;
using System.Net;
using System.Net.Http;
using System.Web;
using System.Web.Caching;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;

// source: http://stackoverflow.com/questions/20817300/how-to-throttle-requests-in-a-web-api

[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public class ExternalApiThrottleAttribute : ActionFilterAttribute
{
    public const int DefaultDurationOfApiCallingPeriod = 20;
    public const int DefaultNumberOfCallsAllowedPerApiCallingPeriod = 20;

    public int CustomNumberOfCallsPerWindow { get; set; }

    public override void OnActionExecuting(HttpActionContext c)
    {
        if (CustomNumberOfCallsPerWindow == 0)
            CustomNumberOfCallsPerWindow = DefaultNumberOfCallsAllowedPerApiCallingPeriod;

        var allowExecute = false;

        var key = HttpContext.Current.User.Identity.Name;

        if (HttpRuntime.Cache[key] == null)
        {
            HttpRuntime.Cache.Add(key,
                1, // is this the smallest data we can have?
                null, // no dependencies
                DateTime.Now.AddSeconds(DefaultDurationOfApiCallingPeriod), // absolute expiration
                Cache.NoSlidingExpiration,
                CacheItemPriority.Low,
                null); // no callback

            allowExecute = true;
        }
        else
        {
            var previousNumberOfCalls = (int)HttpRuntime.Cache[key];
            if (previousNumberOfCalls < CustomNumberOfCallsPerWindow)
            {
                HttpRuntime.Cache[key] = previousNumberOfCalls + 1;

                allowExecute = true;
            }
        }

        if (!allowExecute)
        {
            c.Response = c.Request.CreateResponse(HttpStatusCode.Conflict, string.Format("You can't request that API so often. Your limit is {0} calls in {1} seconds.",
                CustomNumberOfCallsPerWindow,
                DefaultDurationOfApiCallingPeriod));
        }
    }
}

Et à l'usage, rien de plus simple :

[ExternalApiThrottle]
public IHttpActionResult GetCollection(ODataQueryOptions<Profiles> queryOptions)

Ou encore :

[ExternalApiThrottle(CustomNumberOfCallsPerWindow = 200)]
public IHttpActionResult GetCollection(ODataQueryOptions<Profiles> queryOptions)

A noter : ce code est loin d'être ce qu'il y a de mieux, et je recommande, si vous avez le temps, d'utiliser plutôt le package Nuget WebApiThrottle, bien plus complet et efficace. Je ne partage ce morceau de code que parce qu'il peut éventuellement aider des gens à créer par eux-même un ActionFilterAttribute pour limiter de façon perso l'usage de leur API.

Envoyer un fichier en POST à ASP.Net MVC

icon Tags de l'article :

Juin 27, 2016
Hello,

Petit article qui peut dépanner : comment envoyer un fichier en POST à une route ASP.Net MVC.

Pour ça, côté HTML, on va d'abord créer un input de type file, et un bouton pour uploader notre fichier (appeler notre route en POST) :

<input type="file" name="fileUpload" id="fileUpload" /><br />
<input type="button" value="Upload" id="buttonUpload" class="btn btn-default" />

Côté JS, on va créer une collection de données de formulaire avec FormData, dans laquelle on va poser notre fichier, et ensuite appeler notre route en POST avec $.ajax :

$('#buttonUpload').on('click', function () {
    var data = new FormData();
    var files = $('#fileUpload').get(0).files;
    // ici on teste si on n'a bien qu'un fichier de sélectionné
    if (1 === files.length) {
        // ici on ajoute notre fichier à notre FormData
        data.append("UploadedFile", files[0]);
        // ensuite on appelle notre route en POST
        $.ajax({
            type: "POST",
            url: 'url/to/call',
            contentType: false,
            processData: false,
            data: data,
            success: function (result) {
                alert('success !');
            }
        });
    } else {
        alert('Error: you must choose a file');
    }
});

Et enfin, côté C#, on va récupérer le fichier à l'aide de Request.Files[]. On pourra ensuite récupérer son stream à l'aide de file.InputStream :

[HttpPost]
public ActionResult Import(int? creatorId, int? userId)
{
    var model = new VM();

    // ici on vérifie bien qu'on aie qu'un seul fichier, pas 0 ou 2
    if (Request.Files.Count != 1)
    {
        model.ErrorMessages.Add("You must upload a file to import profiles");
        return Json(model);
    }

    // ici on récupère bien notre fichier
    var uploadedFile = Request.Files[0];

    // ici on teste si notre fichier termine bien par CSV
    if (!uploadedFile.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
    {
        model.ErrorMessages.Add("Invalid extension: only CSV file can be used for import");
        return Json(model);
    }

    var fileStream = uploadedFile.InputStream;

    // ...
    // ici on peut appeler notre code métier
    // ...

    return Json(model);
}

Et voilà !

Bon dev à tous !

KnockoutJS, JavaScript et JQuery : notes et exemples perso

icon Tags de l'article : , ,

Juin 24, 2016
Hello,

Une petite liste de notes perso, de morceaux de code et de comment faire 2/3 choses, pour moi, mais qui peut vous servir.

Enjoy !

<!-- quelques databind par défaut -->

<input type="text" data-bind="value: searchText" />
<td data-bind="text: 1 === Role() ? 'User' : 'Admin'"></td>
<p data-bind="visible: currentlyWorking">

<!-- A noter : si on est dans un cas de visible "si pas ce booléen", il faut mettre les parenthèses : -->
<div data-bind="visible: !currentlyWorking()">
<p data-bind="visible: !showSearchResults() && firstSearchDone()">

<!-- Plusieurs paramètres -->
<input class="btn" type="button" value="Add" data-bind="attr: { 'data-value': Id }, visible: !model.IsMethod(Id)" /> 

<!-- Appeler le modèle parent de l'objet KO courant : $parent -->
<span data-bind="visible: null !== ItemId && $parent.apiConsumerId() !== ApiConsumerId">

  • Dans l'idéal il vaut mieux éviter d'avoir de la logique côté vue, il vaut mieux faire des computed :
self.showSearchResults = ko.computed(function () {
    return self.firstSearchDone() && self.searchedUsers().length > 0;
});

// Attraper la touche entrée sur un champ texte
$('#txtSearch').on('keyup', function (e) {
    if (e.which !== 13) {
        return;
    }

    $('#btSearchUser').click();
});

// Passer des informations depuis la vue ASP.Net MVC vers le fichier JavaScript 
<script type="text/javascript">
    var urls = {
        'promoteUser': '@Url.Action("Promote", "User")',
    };
    var initialData = @MvcHtmlString.Create(Newtonsoft.Json.JsonConvert.SerializeObject(Model.Users));
    var currentItemId = @Model.Item.Id;
</script>

// Demander confirmation pour une action dans certaines conditions ?
// Créer une méthode JS globale "mustConfirm()" qui sera appelée avant chaque action critique comme ça :
if (confirm('Are you sure that you want to DELETE this user account? This action CANNOT BE UNDONE.') && mustConfirm()) {

}

// Un appel en direct dans le HTML :
<input type="submit" value="Purge" class="btn btn-default" onclick="return mustConfirm(); " />

// Et cette méthode dans la Layout CSHTML :
var mustConfirm = function () {
    @{
        if (UserHelper.GetCurrentEnvironment().AskForConfirmation)
        {
            <text>return confirm('Attention: This is a production environment with sensitive personal and customer data. Are you sure?');</text>
        }
        else
        {
            <text>return true;</text>
        }
    }
};

// Recharger la page courante
location.reload();

// Récupérer une collection d'observables à partir d'un JSON
// Pour ça on utilise la lib JS ko.mapping :
self.Users = ko.mapping.fromJS(initialData);

// Envoyer un fichier en POST à un contrôleur ASP.Net MVC
var data = new FormData();
var files = $('#fileUpload').get(0).files;
if (files.length === 1) {
    data.append("UploadedFile", files[0]);
    data.append("CreatorId", $('#creatorId').val());
    $.ajax({
        type: "POST",
        url: urls.import,
        contentType: false,
        processData: false,
        data: data,
        success: function (result) {
            if (0 === result.ErrorMessages.length) {
                setTimeout(function() { document.location = urls.importsList; }, 1000);
            } else {
                model.currentlyWorking(false);
                model.LoadResult(result, false);
            }
        }
    });
} else {
    alert('Error: you must choose a file');
}

// et côté ASP.Net MVC
public ActionResult Import(int? creatorId, int? userId)
{
    var context = this.CreateContext();

    var model = new ImportVM();

    if (Request.Files.Count != 1)
    {
        model.ErrorMessages.Add("You must upload a file to import profiles");
        return Json(model);
    }

    var uploadedFile = Request.Files[0];

    if (!uploadedFile.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase))
    {
        model.ErrorMessages.Add("Invalid extension: only CSV file can be used for import");
        return Json(model);
    }

    _importBusiness.Import(uploadedFile.InputStream, context);

    return Json(model);
}

// Exemple complet côté JS 
var model;

(function () {
    var Users = function () {
        var self = this;

        self.firstSearchDone = ko.observable(false);
        self.searchText = ko.observable("");
        self.searchedUsers = ko.observableArray();

        self.currentlyWorking = ko.observable(false);

        self.showSearchResults = ko.computed(function () {
            return self.firstSearchDone() && self.searchedUsers().length > 0;
        });

        self.cleanSearch = function () {
            self.searchedUsers.removeAll();
            self.searchText('');
            self.firstSearchDone(false);
        }
    }

    $('#txtSearch').on('keyup', function (e) {
        if (e.which !== 13) {
            return;
        }

        $('#btSearchUser').click();
    });

    $('#btSearchUser').on('click', function () {
        $('#txtSearch').val('');
        model.currentlyWorking(true);

        $.post(urls.searchUserByName, { text: model.searchText() }, function (response) {
            if (!response.ok) {
                alert(response.errorMessage);
                model.cleanSearch();
            } else {
                model.firstSearchDone(true);
                model.searchedUsers(response.searchedUsers);
            }
            model.currentlyWorking(false);
        });
    });

    // we initialize the model
    model = new Users();

    ko.applyBindings(model);
})();

Modifier ses AppSettings et ConnectionStrings via les fichiers de config en .Net

icon Tags de l'article : , , ,

Juin 24, 2016
Hello,

Petit article, ça faisait longtemps :)

Aujourd'hui on va parler .Net, AppSettings et ConnectionStrings.

Lorsqu'on développe, la bonne pratique est d'utiliser les fichiers de configuration pour stocker nos paramètres d'application et nos chaines de connexion. Ce fichier de configuration s'appelle, pour ceux qui ne le savent pas, Web.Config ou App.Config. Exemple :

<appSettings>
     <add key="MyValue" value="42"/>
     <add key="MyIPAddress" value="::1"/>
</appSettings>
<connectionStrings>
     <add name="MyConnectionString" connectionString="Data Source=.\;Initial Catalog=MyDB;Persist Security Info=True;User ID=MyLogin;Password=MyPassword;MultipleActiveResultSets=True" />
</connectionStrings>

Classique vous me direz. Maintenant, savez vous comment faire pour que les valeurs de vos AppSettings et ConnectionStrings changent lorsque vous publiez en mode Release ?

Et oui, il faut modifier le fichier Web.Release.Config (ou App.Release.Config).

Dans ce fichier, vous pouvez ajouter de nouveaux AppSettings ou ConnectionStrings, mais vous pouvez aussi modifier les valeurs par défaut (du Web.config ou App.config)

Voici un exemple qui va :
  • Supprimer l'AppSettings MyValue
  • Modifier la valeur de l'AppSettings MyIPAddress
  • Modifier la valeur de la ConnectionString MyConnectionString

<appSettings>
     <add key="MyValue" xdt:Transform="Remove" xdt:Locator="Match(key)"/>
     <add key="MyIPAddress" value="1.2.3.4" xdt:Transform="SetAttributes" xdt:Locator="Match(key)" />
</appSettings>
<connectionStrings>
     <add name="MyConnectionString" connectionString="Server=tcp:myserver.database.windows.net,1433;Database=mydb;User ID=mylogin@mydb;Password=mypassword;Trusted_Connection=False;Encrypt=True;Connection Timeout=30;" xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
</connectionStrings>

Simple non ?

En résumé, lors de la publication dans une config sélectionnée (debug, release ou autre) :
  • Supprimer une ligne: xdt:Transform="Remove" xdt:Locator="Match(key)"
  • Modifier la valeur d'une ligne : xdt:Transform="SetAttributes" xdt:Locator="Match(key)"
  • Ajouter une ligne : il suffit de l'ajouter dans le fichier concerné

Une dernière chose, non négligeable : ces modifications / suppressions n'impacteront pas votre application lorsque vous la lancez en local, même dans la configuration sélectionnée ! Pour ça, il faut faire les modifications à la main dans le web.config ou app.config.

Bon dev à tous !

Le célèbre bug du cookie ASP.Net qui disparait

icon Tags de l'article : , ,

Septembre 18, 2015
Tout le monde se fait avoir, et ça vient de m'arriver.

Alors que je développais tranquillement ma solution, je me suis rendu compte que ma connexion à l'application cassait juste après que je me sois connecté.

Après vérification, il s'avérait que mon cookie disparaissait sans raison dès que je faisais la moindre action après ma connexion.

J'ai tout vérifié, mon code de création de cookie était bon :

Response.Cookies["UserSettings"]["IdUser"] = user.Id.ToString();
Response.Cookies["UserSettings"]["Token"] = user.Token;
Response.Cookies["UserSettings"]["Name"] = user.FirstName + " " + user.LastName;

Response.Cookies["UserSettings"].Expires = DateTime.UtcNow.AddDays(WebAppConstants.COOKIE_LIFETIME);

D'où ça vient ? C'est très simple : je devais probablement faire un Response.Cookies à un endroit au lieu d'un Request.Cookies pour lire le contenu de mon cookie.

Et effectivement c'était le cas, à un endroit où j'essayais de lire mon cookie :
if(Response.Cookies["UserSettings"] != null && Response.Cookies["UserSettings"]["Name"] != null) 
{
     //...
}

En effet, en ASP.Net, dès qu'on essaie de lire un cookie avec Response.Cookies, un cookie qui sera automatiquement renvoyé au navigateur est créé.

Or comme on ne spécifie pas la date d'expiration du cookie... il s'agit du 01/01/0001. Et du coup, quand le navigateur recevra ce cookie, il se dira que son cookie existant est obsolète... et le supprimera.

Donc rappelez-vous en : dès qu'un cookie disparait sans raison après avoir été créé, il y a de fortes chances que vous ayez essayé d'y accéder en passant par Response.Cookies au lieu de Request.Cookies !