Transformer les templates T4 pendant la build, et passer des variables du projet

T4 (Text Template Transformation Toolkit) est un excellent outil pour générer du code ; on peut, par exemple, créer des classes POCO à partir des tables d’une base de données, générer du code répétitif, etc. Dans Visual Studio, les fichiers T4 (extension .tt) sont associés au custom tool TextTemplatingFileGenerator, qui transforme un template pour générer un fichier de sortie à chaque fois qu’on enregistre le template. Mais il arrive que ce ne soit pas suffisant, et qu’on souhaite regénérer les sorties des templates à chaque build. C’est assez facile à mettre en œuvre, mais il y a quelques écueils à éviter.

Transformer les templates lors du build

Si votre projet est un csproj ou vbproj "classique" (c’est-à-dire pas un projet .NET Core "SDK-style"), les choses sont assez simples et bien documentées sur cette page.

Déchargez votre projet, et ouvrez le dans l’éditeur. Ajoutez le PropertyGroup suivant vers le début du fichier :

<PropertyGroup>
    <!-- 15.0 is for VS2017, adjust if necessary -->
    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
    <!-- This is what will cause the templates to be transformed when the project is built (default is false) -->
    <TransformOnBuild>true</TransformOnBuild>
    <!-- Set to true to force overwriting of read-only output files, e.g. if they're not checked out (default is false) -->
    <OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles>
    <!-- Set to false to transform files even if the output appears to be up-to-date (default is true)  -->
    <TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>

Et ajoutez l’Import suivant à la fin, après l’import de Microsoft.CSharp.targets ou Microsoft.VisualBasic.targets :

<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />

Rechargez votre projet, et le tour est joué. Générer le projet devrait maintenant transformer les templates et regénérer leur sortie.

Projets "SDK-style"

Si vous utilisez le nouveau format de projet du SDK .NET Core (souvent appelé de façon informelle projet "SDK-style"), l’approche décrite ci-dessus nécessite un petit changement pour fonctionner. C’est parce que le fichier de cibles par défaut (Sdk.targets dans le SDK .NET Core) est maintenant importé implicitement à la toute fin du projet, il n’est donc pas possible d’importer les cibles T4 après les cibles par défaut. Du coup la variable BuildDependsOn, qui est modifiée par les cibles T4, est écrasée par les cibles par défaut, et la cible TransformAll ne s’exécute plus avant la cible Build.

Heureusement, il y a un workaround : vous pouvez importer les cibles par défaut explicitement, et importer les cibles T4 après celles-ci :

<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />

Notez que cela causera un avertissement MSBuild dans la sortie de la build (MSB4011), parce que Sdk.targets est importé deux fois ; cet avertissement peut être ignoré sans risque.

Passer des variables MSBuild aux templates

À un moment donné, il est possible que la logique de génération de code devienne trop complexe pour rester entièrement dans le template T4. Vous pourriez vouloir en extraire une partie dans un assembly utilitaire, qui sera référencé depuis le template comme ceci :

<#@ assembly name="../CodeGenHelper/bin/Release/net462/CodeGenHelper.dll" #>

Mais spécifier le chemin de l’assembly de cette façon n’est pas très pratique… par exemple, si vous êtes actuellement sur la configuration Debug, la version Release de CodeGenHelper.dll ne sera pas forcément à jour. Heureusement, le custom tool TextTemplatingFileGenerator de Visual Studio supporte l’utilisation de variables MSBuild du projet, il est donc possible de faire ceci :

<#@ assembly name="$(SolutionDir)/CodeGenHelper/bin/$(Configuration)/net462/CodeGenHelper.dll" #>

Les variables $(SolutionDir) et $(Configuration) seront remplacées par leurs valeurs respectives. Si vous enregistrez le template, il sera bien transformé en utilisant l’assembly CodeGenHelper.dll. Pratique !

Mais il y a un hic… Si vous avez configuré votre projet pour transformer les templates lors de la build, comme décrit plus haut, la build va maintenant échouer, avec une erreur comme celle-ci :

System.IO.FileNotFoundException: Could not find a part of the path ‘C:\Path\To\The\Project\$(SolutionDir)\CodeGenHelper\bin\$(Configuration)\net462\CodeGenHelper.dll’.

Vous avez remarqué les variables $(SolutionDir) et $(Configuration) dans le chemin ? Elles n’ont pas été remplacées ! C’est parce que la cible MSBuild qui transforme les templates et le custom tool TextTemplatingFileGenerator n’utilisent pas le même moteur de transformation de texte. Et malheureusement, celui utilisé par MSBuild ne supporte pas nativement les variables MSBuild… un comble !

Cependant tout n’est pas perdu. Il suffit de spécifier explicitement les variables que vous voulez passer en temps que paramètres T4. Éditez à nouveau votre fichier projet, et créez un nouveau ItemGroup avec les éléments suivants :

<ItemGroup>
    <T4ParameterValues Include="SolutionDir">
        <Value>$(SolutionDir)</Value>
        <Visible>False</Visible>
    </T4ParameterValues>
    <T4ParameterValues Include="Configuration">
        <Value>$(Configuration)</Value>
        <Visible>False</Visible>
    </T4ParameterValues>
</ItemGroup>

L’attribut Include est le nom du paramètre tel qu’il sera passé au moteur de transformation de texte. L’élément Value, comme son nom l’indique, est la valeur du paramètre. Et l’élément Visible permet d’éviter que l’élément T4ParameterValues n’apparaisse comme élément du projet dans l’explorateur de solution.

Avec ce changement, la build devrait à nouveau transformer les templates correctement.

Pour résumer, gardez à l’esprit que le custom tool TextTemplatingFileGenerator et la cible MSBuild de transformation de texte ont des mécanismes différents pour passer des variables aux templates :

  • TextTemplatingFileGenerator supporte uniquement les variables MSBuild du projet
  • MSBuild supporte uniquement T4ParameterValues

Donc si vous utilisez des variables dans votre template, et que vous voulez pouvoir transformer le template quand vous l’enregistrez dans Visual Studio et quand vous générez le projet, les variables doivent être définies à la fois comme variables MSBuild et comme T4ParameterValues.

Propriétés et éléments MSBuild partagés avec Directory.Build.props

Pour être honnête, je n’ai jamais vraiment aimé MSBuild jusqu’à récemment. Les fichiers de projet générés par Visual Studio étaient immondes, l’essentiel de leur contenu était redondant, il fallait décharger les projets pour les éditer, c’était mal documenté… Mais avec l’avènement de .NET Core et du nouveau format de projet, plus léger, MSBuild est devenu un bien meilleur outil.

MSBuild 15 a introduit une nouvelle fonctionnalité assez sympa : les imports implicites (je ne sais pas si c’est le nom officiel, mais c’est celui que j’utiliserai). En gros, vous pouvez créer un fichier nommmé Directory.Build.props n’importe où dans votre solution, et il sera automatiquement importé par tous les projets sous le répertoire qui contient ce fichier. Cela permet de partager très facilement des propriétés et éléments communs entre les projets. Cette fonctionnalité est décrite en détail sur cette page.

Par exemple, si vous voulez partager certaines métadonnées entre plusieurs projets, créer simplement un fichier Directory.Build.props dans le dossier parent de vos projets :

<Project>

  <PropertyGroup>
    <Version>1.2.3</Version>
    <Authors>John Doe</Authors>
  </PropertyGroup>

</Project>

On peut aussi faire des choses plus intéressantes, comme activer et configurer StyleCop pour tous les projets :

<Project>

  <PropertyGroup>
    <!-- Common ruleset shared by all projects -->
    <CodeAnalysisRuleset>$(MSBuildThisFileDirectory)MyRules.ruleset</CodeAnalysisRuleset>
  </PropertyGroup>

  <ItemGroup>
    <!-- Add reference to StyleCop analyzers to all projects  -->
    <PackageReference Include="StyleCop.Analyzers" Version="1.0.2" />
    
    <!-- Common StyleCop configuration -->
    <AdditionalFiles Include="$(MSBuildThisFileDirectory)stylecop.json" />
  </ItemGroup>

</Project>

Notez que la variable $(MSBuildThisFileDirectory) fait référence au répertoire contenant le fichier MSBuild courant. Une autre variable utile est $(MSBuildProjectDirectory), qui fait référence au répertoire du projet en cours de génération.

MSBuild cherche le fichier Directory.Build.props en partant du répertoire du projet et en remontant les dossiers jusqu’à ce qu’il trouve un fichier correspondant, puis s’arrête de chercher. Dans certains cas, il peut être utile de définir des propriétés communes à tous les projets, et d’en ajouter d’autres qui ne s’appliquent qu’à un sous-répertoire. Pour faire cela, il faut que le fichier Directory.Build.props le plus "profond" importe explicitement celui du répertoire parent :

  • (rootDir)/Directory.build.props:
<Project>

  <!-- Properties common to all projects -->
  <!-- ... -->
  
</Project>
  • (rootDir)/tests/Directory.build.props:
<Project>

  <!-- Import parent Directory.build.props -->
  <Import Project="../Directory.Build.props" />

  <!-- Properties common to all test projects -->
  <!-- ... -->
  
</Project>

La documentation mentionne une autre approche, utilisant la fonction GetPathOfFileAbove, mais cela ne semblait pas fonctionner quand j’ai essayé… De toute façon, je pense qu’il est plus simple d’utiliser un chemin relatif, on risque moins de se tromper.

Utiliser les imports implicites apporte quelques avantages :

  • des fichiers de projet plus petits, puisque les propriétés et éléments identiques peuvent être factorisés dans des fichiers communs
  • un seul point de référence : si tous les projets référencent le même package NuGet, la version à référencer est définie à un seul endroit; il n’est plus possible d’avoir des incohérences.

Cette approche a cependant un inconvénient : Visual Studio n’a pas la notion de l’origine d’une variable ou d’un élément, donc si vous changez une propriété ou une référence de package dans l’IDE (via les pages de propriétés du projet ou le gestionnaire de packages NuGet), elle sera modifiée dans le fichier de projet lui-même, et non dans le fichier Directory.Build.props. De mon point de vue, ce n’est pas un gros problème, parce que j’ai pris l’habitude d’éditer les projets manuellement plutôt que d’utiliser l’IDE, mais ça peut être gênant pour certaines personnes.

Si vous voulez un exemple réel de l’utilisation de cette technique, jetez un oeil au repository de FakeItEasy, où nous utilisons plusieurs fichiers Directory.Build.props pour garder les projets propres et concis.

Notez que vous pouvez également créer un fichier Directory.Build.targets, suivant les mêmes principes, pour définir des cibles communes.

Tester et déboguer une bibliothèque depuis LINQPad

Cela faisait bien longtemps que je voulais bloguer à propos de LINQPad. Au cas où vous ne connaissez pas, LINQPad est un outil qui permet d’écrire et de tester du code très rapidement sans avoir besoin de créer un projet complet dans Visual Studio. Il supporte C#, VB.NET, F# et SQL. Il était initialement conçu comme un outil éducatif pour expérimenter avec LINQ (son auteur, Joe Albahari, l’avait développé pour accompagner son livre C# in a Nutshell), mais il est aussi extrêmement utile comme outil générique pour tester du code .NET.

J’utilise fréquemment LINQPad pour tester rapidement une bibliothèque sur laquelle je travaille. C’est très facile, il suffit de référencer l’assembly à tester et de l’utiliser normalement. Mais quand la bibliothèque ne se comporte pas comme prévu, il est souvent utile de pouvoir la déboguer pas à pas… Il s’avère que c’est assez simple à faire à partir de LINQPad !

La version premium de LINQPad a un débogueur intégré, qui n’est pas aussi puissant que celui de Visual Studio, mais quand même utile pour déboguer les scripts LINQPad. Cependant, il ne permet pas de rentrer dans le code de la bibliothèque… Heureusement, il y a une astuce qui permet d’utiliser le débogueur de Visual Studio pour déboguer le script qui s’exécuter dans LINQPad.

Tout d’abord, ouvrez dans Visual Studio la bibliothèque à déboguer, si ce n’est pas déjà fait. Compilez le projet, et ajoutez une référence à l’assembly dans votre script LINQPad:

Add reference to the library

Ecrivez du code qui utilise votre bibliothèque:

Use your library from LINQPad

Et ajoutez cette ligne au début du script LINQPad:

Debugger.Launch();

Quand vous exécutez le script, un dialogue va s’affichez pour vous demander de choisir un débogueur:

Choose a debugger

Sélectionnez l’instance de Visual Studio dans laquelle votre solution est ouverte et cliquez OK. Cela va attacher le débogueur de Visual Studio au processus qui exécute le script LINQPad, et suspendre l’exécution au niveau de l’appel à Debugger.Launch():

Debugging in Visual Studio

Vous pouvez maintenant déboguer votre script LINQPad et votre bibliothèque. Vous pouvez mettre des points d’arrêt, rentrer dans les méthodes, ajouter des espions, etc, exactement comme quand vous déboguez une application normale !

Amélioration des performances de Linq dans .NET Core

Depuis le temps qu’on en parle, vous êtes sans doute au courant que Microsoft a publié une version open-source et multiplateforme de .NET : .NET Core. Cela signifie que vous pouvez maintenant créer et exécuter des applications .NET sous Linux ou macOS. C’est déjà assez cool en soi, mais ça ne s’arrête pas là : .NET Core apporte aussi beaucoup d’améliorations à la Base Class Library.

Par exemple, Linq est plus rapide dans .NET Core. J’ai fait un petit benchmark pour comparer les performances de certaines méthodes couramment utilisées de Linq, et les résultats sont assez impressionnants :


Le code complet de ce benchmark est disponible ici. Comme pour tous les microbenchmarks, les résultats ne sont pas à prendre pour argent comptant, mais ça donne quand même une idée des améliorations.

Certaines lignes de ce tableau sont assez surprenantes. Comment Select peut-il s’exécuter 5000 fois presque instantanément ? D’abord, il faut garder à l’esprit que la plupart des opérateurs Linq ont une exécution différée : ils ne font rien tant que qu’on n’énumère pas le résultat, donc quelque chose comme array.Select(i => i * i) s’exécute en temps constant (ça renvoie juste une séquence "lazy", sans consommer les éléments de array). C’est pourquoi j’ai ajouté un appel à Count() dans mon benchmark, pour m’assurer que le résultat est bien énuméré.

Pourtant, ce test s’exécute 5000 fois en 413µs… Cela est possible grâce à une optimisation dans l’implémentation .NET Core de Select et Count. Une propriété utile de Select est qu’il produit une séquence avec le même nombre d’éléments que la séquence source. Dans .NET Core, Select tire parti de cette propriété. Si la source est une ICollection<T> ou un tableau, il renvoie un objet énumérable qui garde la trace du nombre d’élément. Count peut ensuite récupérer directement la valeur et la renvoyer sans énumérer la séquence, ce qui donne un résultat en temps constant. L’implémentation de .NET 4.6.2, en revanche, énumère naïvement la séquence produite par Select, ce qui prend beaucoup plus longtemps.

Il est intéressant de noter que dans cette situation, .NET Core ne va pas exécuter la projection spécifiée dans Select, c’est donc un breaking change par rapport à .NET 4.6.2 pour du code qui dépend des effets de bord de la projection. Cela a été identifié comme un problème, qui a déjà été corrigé sur la branche master, donc la prochaine version n’aura plus cette optimisation et exécutera bien la projection sur chaque élément.

OrderBy suivi de Count() s’exécute aussi presque instantanément… Les développeurs de Microsoft auraient-ils inventé un algorithme de tri en O(1) ? Malheureusement, non… L’explication est la même que pour Select : puisque OrderBy préserve le nombre d’éléments, l’information est conservée pour pouvoir être utilisée par Count, et il n’est pas nécessaire de vraiment trier les éléments avant d’obtenir leur nombre.

Bon, ces cas étaient des améliorations assez évidentes (qui ne vont d’ailleurs pas rester, comment mentionné précédemment). Mais que dire du cas de SelectAndToArray ? Dans ce test, j’appelle ToArray() sur le résultat de Select, pour être certain que la projection soit bien exécutée sur chaque élément : cette fois, on ne triche pas. Pourtant, l’implémentation de .NET Core arrive encore à être 68% plus rapide que celle du framework .NET classique dans ce scénario. La raison est liée aux allocations : puisque l’implémentation .NET Core sait combien il y a d’éléments dans le résultat de Select, elle peut directement allouer un tableau de la bonne taille. Dans .NET 4.6.2, cette information n’est pas disponible, donc il faut commencer par allouer un petit tableau, y copier des éléments jusqu’à ce qu’il soit plein, puis allouer un tableau plus grand, y copier les données du premier tableau, y copier les éléments suivants de la séquence jusqu’à ce qu’il soit plein, etc. Cela cause de nombreuses allocations et copies, d’où la performance dégradée. Il y a quelques années, j’avais suggéré des versions optimisées de ToList et ToArray, auxquelles on passait le nombre d’éléments. L’implémentation de .NET Core fait grosso modo la même chose, sauf qu’il n’y a pas besoin de passer la taille manuellement, elle est transmise à travers la chaine de méthodes Linq.

Where et WhereAndToArray sont tous les deux environ 8% plus rapides sur .NET Core 1.1. En examinant le code, (.NET 4.6.2, .NET Core), je ne vois pas de différences évidentes qui pourraient expliquer les meilleures performances, donc je soupçonne qu’elles sont dues à des améliorations du runtime. Dans ce cas, ToArray ne connait pas le nombre d’éléments dans la séquence (puisqu’on ne peut pas prédire le nombre d’éléments que Where va laisser passer), il ne peut donc pas utiliser la même optimisation qu’avec Select, et doit construire le tableau en utilisant l’approche plus lente.

On a déjà parlé du cas de OrderBy + Count(), qui n’était pas une comparaison équitable puisque l’implémentation de .NET Core ne triait pas réellement la séquence. Le cas de OrderByAndToArray est plus intéressant, car le tri ne peut pas être évité. Et dans ce cas, l’implémentation de .NET Core est un peu plus lente que celle de .NET 4.6.2. Je ne sais pas très bien pourquoi; là aussi, l’implémentation est très similaire, à part quelques refactorisations dans la version .NET Core.

Au final, Linq a l’air globalement plus rapide dans .NET Core que dans 4.6.2, ce qui est une très bonne nouvelle. Bien sûr, je n’ai benchmarké qu’un nombre limité de scénarios, mais cela montre quand même que l’équipe .NET Core travaille dur pour optimiser tout ce qu’ils peuvent.

Parser du texte facilement en C# avec Sprache

Il y a quelques jours, j’ai découvert un petit bijou : Sprache. Le nom signifie "langage" en allemand. C’est une librairie très élégante et facile à utiliser pour créer des analyseurs de texte, à l’aide de parser combinators, qui sont une technique très courante en programmation fonctionnelle. Le concept théorique peut sembler un peu effrayant, mais comme vous allez le voir dans un instant, Sprache rend ça très accessible.

Analyse syntaxique

L’analyse syntaxique (parsing) est une tâche très courante, mais qui peut être laborieuse et où il est facile de faire des erreurs. Il existe de nombreuses approches :

  • analyse manuelle basée sur Split, IndexOf, Substring etc.
  • expressions régulières
  • parser codé à la main qui scanne les tokens dans une chaine
  • parser généré avec ANTLR ou un outil similaire
  • et certainement beaucoup d’autres qui m’échappent…

Aucune de ces options n’est très séduisante. Pour les cas simples, découper la chaine ou utiliser une regex peut suffire, mais ça devient vite ingérable pour des grammaires plus complexes. Construire un vrai parser à la main pour une grammaire non-triviale est loin d’être facile. ANTLR nécessite Java, un peu de connaissance, et se base sur de la génération de code, ce qui complique le process de build.

Heureusement, Sprache offre une alternative très intéressante. Il fournit de nombreux parsers et combinateurs prédéfinis qu’on peut utiliser pour définir une grammaire. Examinons pas à pas un cas concret : analyser le challenge dans le header WWW-Authenticate d’une réponse HTTP (j’ai dû faire un parser à la main pour ça récemment, et j’aurais aimé connaître Sprache à ce moment-là).

La grammaire

Le header WWW-Authenticate est envoyé par un serveur HTTP dans une réponse 401 (Non autorisé) pour indiquer au client comment s’authentifier :

# Challenge Basic
WWW-Authenticate: Basic realm="FooCorp"

# Challenge OAuth 2.0 après l'envoi d'un token expiré
WWW-Authenticate: Bearer realm="FooCorp", error=invalid_token, error_description="The access token has expired"

Ce qu’on veut parser est le challenge, c’est-à-dire la valeur du header. Nous avons donc un mécanisme d’authentification ou "scheme" (Basic, Bearer), suivi d’un ou plusieurs paramètres (paires nom-valeur). Ça semble assez simple, on pourrait sans doute juste découper selon les ',', puis selon les '=' pour obtenir les valeurs… mais les guillemets compliquent les choses, car une chaine entre guillemets peut contenir les caractères ',' ou '='. De plus, les guillemets sont optionnels si la valeur du paramètre est un simple token, donc on ne peut pas compter sur le fait que les guillemets seront présents (ou pas). Clairement, si on veut parser ça de façon fiable, il va falloir regarder les specs de plus près…

Le header WWW-Authenticate est décrit en détails dans la RFC-2617. La grammaire ressemble à ceci, sous une forme que la spec appelle "forme Backus-Naur augmentée" (voir RFC 2616 §2.1) :

# from RFC-2617 (HTTP Basic and Digest authentication)

challenge      = auth-scheme 1*SP 1#auth-param
auth-scheme    = token
auth-param     = token "=" ( token | quoted-string )

# from RFC2616 (HTTP/1.1)

token          = 1*<any CHAR except CTLs or separators>
separators     = "(" | ")" | "<" | ">" | "@"
               | "," | ";" | ":" | "\" | <">
               | "/" | "[" | "]" | "?" | "="
               | "{" | "}" | SP | HT
quoted-string  = ( <"> *(qdtext | quoted-pair ) <"> )
qdtext         = <any TEXT except <">>
quoted-pair    = "\" CHAR

Nous avons donc quelques règles de grammaire, voyons comment on peut les encoder en C# à l’aide de Sprache, et les utiliser pour analyser le challenge.

Parser les tokens

Commençons par les parties les plus simples de la grammaire : les tokens ("jetons"). Un token est défini comme un ou plusieurs caractères qui ne sont ni des caractères de contrôle, ni des séparateurs.

Nous allons définir nos règles dans une classe Grammar. Commençons pour définir certaines classes de caractères :

static class Grammar
{
    private static readonly Parser<char> SeparatorChar =
        Parse.Chars("()<>@,;:\\\"/[]?={} \t");

    private static readonly Parser<char> ControlChar =
        Parse.Char(Char.IsControl, "Control character");

}
  • Chaque règle est déclarée comme un Parser<T> ; puisque ces règles valident des caractères seuls, elles sont de type Parser<char>.
  • La classe Parse de Sprache expose des primitives d’analyse et des combinateurs.
  • Parse.Chars valide n’importe quel caractère de la chaine spécifiée, on l’utilise pour spécifier la liste des caractères de séparation.
  • La surcharge de Parse.Char qu’on utilise ici prend un prédicat qui sera appelé pour valider un caractère, et une description de cette classe de caractères. Ici on utilise la méthode System.Char.IsControl comme prédicat pour identifier les caractères de contrôle.

Définissons maintenant une classe de caractères TokenChar, qui correspond aux caractères qui peuvent être inclus dans un token. Selon la RFC, il s’agit de n’importe quel caractère qui n’appartient pas aux deux classes précédemment définies :

    private static readonly Parser<char> TokenChar =
        Parse.AnyChar
            .Except(SeparatorChar)
            .Except(ControlChar);
  • Parse.AnyChar, comme son nom l’indique, valide n’importe quel caractère.
  • Except permet de spécifier des exceptions, c’est à dire des règles qui ne doivent pas valider le caractère.

Enfin, un token est une séquence d’un ou plusieurs de ces caractères :

    private static readonly Parser<string> Token =
        TokenChar.AtLeastOnce().Text();
  • Un token est une chaine, donc la règle pour un token est de type Parser<string>.
  • AtLeastOnce() signifie une ou plusieurs répétitions, et puisque TokenChar est un Parser<char>, AtLeastOnce() renvoie un Parser<IEnumerable<char>>.
  • Text() combine la séquence de caractères en une chaine, et renvoie donc un Parser<string>

Nous voilà donc capables de parser un token. Mais ce n’est qu’un premier pas, il nous reste encore pas mal de travail…

Parser les chaines entre guillemets

La RFC définit une chaine entre guillemets (quoted string) comme une séquence de :

  • un guillemet qui ouvre la chaine
  • n’importe quel nombre de l’un de ces éléments :
    • un "qdtext", c’est-à-dire n’importe quel caractère sauf un guillemet
    • un "quoted pair", c’est-à-dire n’importe quel caractère précédé d’un backslash (c’est utilisé pour échapper les guillemets à l’intérieur d’une chaine)
  • un guillemet qui ferme la chaine

Écrivons donc les règles pour "qdtext" et "quoted pair" :

    private static readonly Parser<char> DoubleQuote = Parse.Char('"');
    private static readonly Parser<char> Backslash = Parse.Char('\\');

    private static readonly Parser<char> QdText =
        Parse.AnyChar.Except(DoubleQuote);

    private static readonly Parser<char> QuotedPair =
        from _ in Backslash
        from c in Parse.AnyChar
        select c;

La règle QdText se passe d’explication, mais QuotedPair est plus intéressante… Comme vous pouvez le voir, ça ressemble à une requête Linq : c’est comme ça qu’on spécifie une séquence avec Sprache. Cette requête-là signifie : prendre un backslash (qu’on nomme _ parce qu’on va l’ignorer) suivi de n’importe quel caractère nommé c, et renvoyer juste c (les "quoted pairs" ne sont pas vraiment des séquences d’échappement comme en C, Java ou C#, donc "\n" n’est pas interprété comme "nouvelle ligne", mais simplement comme "n").

On peut donc maintenant écrire la règle pour une chaine entre guillemets :

    private static readonly Parser<string> QuotedString =
        from open in DoubleQuote
        from text in QuotedPair.Or(QdText).Many().Text()
        from close in DoubleQuote
        select text;
  • La méthode Or indique un choix entre deux parsers. QuotedPair.Or(QdText) essaie de valider une "quoted pair", et si cela échoue, il essaie de valider un "qdtext" à la place.
  • Many() indique un nombre quelconque de répétitions.
  • Text() combine les caractères en une chaine.
  • on sélectionne juste text, car on n’a plus besoin des guillemets (ils ne servaient qu’à délimiter la chaine).

Nous avons maintenant toutes les briques de bases, on va donc pouvoir passer à des règles de plus haut niveau.

Parser les paramètres du challenge

Un challenge est constitué d’un scheme d’authentification suivi d’un ou plusieurs paramètres. Le scheme est trivial (c’est juste un token), donc commençons par parser les paramètres.

Bien que la RFC n’ait pas de règle nommée pour ça, définissons-nous une règle pour les valeurs des paramètres. La valeur peut être soit un token, soit une chaine entre guillemets :

    private static readonly Parser<string> ParameterValue =
        Token.Or(QuotedString);

Puisqu’un paramètre est une élément composite (nom et valeur), déclarons une classe pour le représenter :

class Parameter
{
    public Parameter(string name, string value)
    {
        Name = name;
        Value = value;
    }
    
    public string Name { get; }
    public string Value { get; }
}

Le T de Parser<T> n’est pas limité aux caractères ou aux chaines, ça peut être n’importe quel type. La règle pour parser les paramètres sera donc de type Parser<Parameter> :

    private static readonly Parser<char> EqualSign = Parse.Char('=');

    private static readonly Parser<Parameter> Parameter =
        from name in Token
        from _ in EqualSign
        from value in ParameterValue
        select new Parameter(name, value);

Ici on prend un token (le nom du paramètre), suivi du signe '=', suivi d’une valeur de paramètre, et on combine le nom et la valeur en une instance de Parameter.

Analysons maintenant une séquence d’un ou plusieurs caractères. Les paramètres sont séparés par des virgules, avec des caractères d’espacement optionnels avant et après la virgule (chercher "#rule" dans la RFC 2616 §2.1). La grammaire pour les listes autorise plusieurs virgules successives sans éléments entre elles, par exemple item1 ,, item2,item3, ,item4, donc la règle pour le séparateur de liste peut être écrite comme ceci :

    private static readonly Parser<char> Comma = Parse.Char(',');

    private static readonly Parser<char> ListDelimiter =
        from leading in Parse.WhiteSpace.Many()
        from c in Comma
        from trailing in Parse.WhiteSpace.Or(Comma).Many()
        select c;

On valide juste la première virgule, le reste peut être n’importe quel nombre de virgules et de caractères d’espacement. On renvoie la virgule parce qu’il faut bien renvoyer quelque chose, mais on ne l’utilisera pas (dans un langage fonctionnel on aurait pu renvoyer le type unit à la place).

On peut maintenant analyser une séquence de paramètres comme ceci :

    private static readonly Parser<Parameter[]> Parameters =
        from first in Parameter.Once()
        from others in (
            from _ in ListDelimiter
            from p in Parameter
            select p).Many()
        select first.Concat(others).ToArray();

Mais c’est un peu alambiqué… heureusement Sprache fournit une approche plus facile avec la méthode DelimitedBy :

    private static readonly Parser<Parameter[]> Parameters =
        from p in Parameter.DelimitedBy(ListDelimiter)
        select p.ToArray();

Parser le challenge

On y est presque. On a maintenant tout ce qu’il nous faut pour parser le challenge complet. Déclarons d’abord une classe pour le représenter :

class Challenge
{
    public Challenge(string scheme, Parameter[] parameters)
    {
        Scheme = scheme;
        Parameters = parameters;
    }
    public string Scheme { get; }
    public Parameter[] Parameters { get; }
}

Et on peut enfin écrire la règle globale :

    public static readonly Parser<Challenge> Challenge =
        from scheme in Token
        from _ in Parse.WhiteSpace.AtLeastOnce()
        from parameters in Parameters
        select new Challenge(scheme, parameters);

Remarquez que j’ai déclaré cette règle comme publique, contrairement aux autres : c’est la seule qu’on a besoin d’exposer.

Utiliser le parseur

Notre parseur est terminé, il n’y a plus qu’à l’utiliser, ce qui est assez simple :

void ParseAndPrintChallenge(string input)
{
    var challenge = Grammar.Challenge.Parse(input);
    Console.WriteLine($"Scheme: {challenge.Scheme}");
    Console.WriteLine($"Parameters:");
    foreach (var p in challenge.Parameters)
    {
        Console.WriteLine($"- {p.Name} = {p.Value}");
    }
}

Avec le challenge OAuth 2.0 de l’exemple précédent, ce code produit la sortie suivante :

Scheme: Bearer
Parameters:
- realm = FooCorp
- error = invalid_token
- error_description = The access token has expired

S’il y a une erreur de syntaxe dans le texte en entrée, la méthode Parse lancera une ParseException avec un message décrivant où et pourquoi l’analyse a échoué. Par exemple, si j’enlève l’espace entre "Bearer" et "realm", j’obtiens l’erreur suivante :

Parsing failure: unexpected ‘=’; expected whitespace (Line 1, Column 12); recently consumed: earerrealm

Vous trouverez le code complet de cet article ici.

Conclusion

Comme vous le voyez, il est très facile avec Sprache de parser un texte complexe. Le code n’est pas particulièrement concis, mais il est complètement déclaratif ; il n’y pas de boucles, pas de conditions, pas de variables temporaires, pas d’état… Cela rend le code très facile à comprendre, et il peut facilement être comparé à la définition originale de la grammaire pour s’assurer de sa conformité. Sprache fournit aussi de bons retours en cas d’erreur, ce qui est assez difficile à implémenter dans un parseur écrit à la main.

Quoi de neuf dans FakeItEasy 3.0.0 ?

FakeItEasy est un framework de mocking populaire pour .NET, avec une API intuitive et facile à utiliser. Depuis environ un an, je suis un des principaux développeurs de FakeItEasy, avec Adam Ralph and Blair Conrad. Ça a été un vrai plaisir de travailler avec eux, et je me suis éclaté !

Aujourd’hui j’ai le plaisir d’annoncer la sortie de FakeItEasy 3.0.0, avec le support de .NET Core et quelques fonctionnalités utiles.

Voyons ce que cette nouvelle version apporte !

Support de .NET Core

En plus de .NET 4+, FakeItEasy supporte maintenant .NET Standard 1.6, vous pouvez donc l’utiliser dans vos projets .NET Core.

Notez qu’en raison de certaines limitations de .NET Standard 1.x, il y a quelques différences mineures avec la version .NET 4 de FakeItEasy :

  • Les fakes ne sont pas sérialisables avec la sérialisation binaire
  • Les fakes "auto-initialisés" (self-initializing fakes) ne sont pas supportés (c’est-à-dire fakeService = A.Fake<IService>(options => options.Wrapping(realService).RecordedBy(recorder))).

Un immense merci aux personnes qui ont rendu possible le support de .NET Core :

  • Jonathon Rossi, qui maintient le projet Castle.Core. FakeItEasy s’appuie beaucoup sur Castle.Core, il n’aurait donc pas été possible de supporter .NET Core sans que Castle.Core le supporte aussi.
  • Jeremy Meng de Microsoft, qui a fait l’essentiel du gros-œuvre pour porter FakeItEasy et Castle.Core vers .NET Core.

Analyseur

Support de VB.NET

L’analyseur FakeItEasy, qui avertit en cas d’usage incorrect de la librairie, supporte maintenant VB.NET en plus de C#.

Nouvelles fonctionnalités et améliorations

Syntaxe améliorée pour configurer des appels successifs au même membre

Quand on configure les appels vers un fake, cela crée des règles qui sont "empilées" les unes sur les autres, ce qui fait qu’on peut écraser une règle précédemment configurée. Combiné avec la possibilité de spécifier le nombre de fois qu’une règle doit s’appliquer, cela permet de configurer des choses comme "renvoie 42 deux fois, puis lance une exception". Jusqu’ici, pour faire cela il fallait configurer les appels dans l’ordre inverse, ce qui n’était pas très intuitif et obligeait à répéter la spécification de l’appel :

A.CallTo(() => foo.Bar()).Throws(new Exception("oops"));
A.CallTo(() => foo.Bar()).Returns(42).Twice();

FakeItEasy 3.0.0 introduit une nouvelle syntaxe pour rendre cela plus simple :

A.CallTo(() => foo.Bar()).Returns(42).Twice()
    .Then.Throws(new Exception("oops"));

Notez que si vous ne spécifiez pas le nombre de fois que la règle doit s’appliquer, elle s’appliquera indéfiniment jusqu’à ce qu’elle soit écrasée. Par conséquent, on ne peut utiliser Then qu’après Once(), Twice() ou NumberOfTimes(...).

C’est un breaking change au niveau de l’API, dans la mesure où la forme des interfaces de configuration a changé, mais à moins que vous ne manipuliez explicitement ces interfaces, vous ne devriez pas être affecté.

Support automatique pour l’annulation

Quand une méthode accepte un paramètre de type CancellationToken, elle doit généralement lancer une exception si on lui passe un token qui est déjà annulé. Jusqu’à maintenant, ce comportement devait être configuré manuellement. Dans FakeItEasy 3.0.0, les méthodes d’un fake lanceront une OperationCanceledException par défaut si on les appelle avec un token déjà annulé. Les méthodes asynchrones renverront une tâche annulée.

Techniquement, c’est également un breaking change, mais la plupart des utilisateurs ne devraient pas être affectés.

Lancer une exceptionde façon asynchrone

FakeItEasy permet de configurer une méthode pour qu’elle lance une exception grâce à la méthode Throws. Mais pour les méthodes asynchrones, il y a en fait deux façons de signaler une erreur :

  • lancer une exception de façon synchrone, avant même de renvoyer une tâche (c’est ce que fait Throws)
  • renvoyer une tâche échouée (cela devait être fait manuellement jusqu’à maintenant)

Dans certains cas la différence peut être importante pour l’appelant, s’il n’await pas directement la méthode asynchrone. FakeItEasy introduit une méthode ThrowsAsync pour configurer une méthode pour qu’elle renvoie une tâche échouée :

A.CallTo(() => foo.BarAsync()).ThrowsAsync(new Exception("foo"));

Configuration des setters de propriétés sur les fakes non-naturels

Les fakes non-naturels (c’est-à-dire Fake<T>) ont maintenant une méthode CallsToSet, qui fait la même chose que A.CallToSet sur les fakes naturels :

var fake = new Fake<IFoo>();
fake.CallsToSet(foo => foo.Bar).To(0).Throws(new Exception("The value of Bar can't be 0"));

API améliorée pour spécifier des attributs supplémentaires

L’API pour spécifier des attributs supplémentaires sur les fakes n’était pas très pratique; il fallait créer une collection de CustomAttributeBuilders, qui eux-mêmes devaient être créés en spécifiant le constructeur et les valeurs des arguments. La méthode WithAdditionalAttributes a été supprimée dans FakeItEasy 3.0.0 et remplacée par une méthode plus simple WithAttributes qui accepte des expressions :

var foo = A.Fake<IFoo>(x => x.WithAttributes(() => new FooAttribute()));

C’est un breaking change.

Autres changements notables

Dépréciation des fakes auto-initialisés (self-initializing fakes)

Les fakes "auto-initialisés" permettent d’enregistrer les appels faits sur un vrai objet, et de faire rejouer les résultats obtenus par un fake (voir l’article de Martin Fowler à ce sujet). Cette fonctionnalité était utilisée par très peu de gens, et n’avait pas vraiment sa place dans la librairie principale de FakeItEasy. Elle est donc dépréciée et sera retirée dans une version future. Nous envisageons de fournir une fonctionnalité équivalente en tant que package distinct.

Corrections de bugs

  • Créer plusieurs fakes d’un même type avec des attributs supplémentaires différents génère maintenant plusieurs types de fake distincts. (#436)
  • Tous les appels non-void tentent maintenant de renvoyer un Dummy par défaut, même après avoir été reconfigurés par Invokes ou DoesNothing (#830)

La liste complète des changements est disponible dans les notes de versions.

Les autres personnes ayant contribué à cette version sont :

Un grand merci à eux !

Méthodes C# dans les en-têtes de diff git

Si vous utilisez git en ligne de commande, vous aurez peut-être remarqué que les diffs indiquent souvent la signature de la méthode dans l’en-tête du bloc (la ligne qui commence par @@), comme ceci :

diff --git a/Program.cs b/Program.cs
index 655a213..5ae1016 100644
--- a/Program.cs
+++ b/Program.cs
@@ -13,6 +13,7 @@ static void Main(string[] args)
         Console.WriteLine("Hello World!");
         Console.WriteLine("Hello World!");
         Console.WriteLine("Hello World!");
+        Console.WriteLine("blah");
     }

C’est très pratique pour savoir où vous vous trouvez quand vous regardez un diff.

Git a quelques patterns d’expressions régulières prédéfinis pour détecter les méthodes dans quelques langages, y compris C#; ils sont définis dans userdiff.c. Mais par défaut, ces patterns ne sont pas utilisés… il faut dire à git quelles extensions de fichier sont associées à quel langage. Cela peut être spécifié dans un fichier .gitattributes à la racine de votre repo :

*.cs    diff=csharp

Cela fait, git diff devrait afficher une sortie similaire à l’exemple ci-dessus.

Est-ce que ça suffit ? Presque. En fait, les patterns pour C# ont été ajoutés à git il y a longtemps, et C# a pas mal évolué depuis. Certains nouveaux mots-clés qui peuvent maintenant apparaître dans une signature de méthode ne sont pas reconnus par le pattern prédéfini, par exemple async ou partial. C’est assez agaçant, parce que quand du code a changé dans une méthode asynchrone, l’en-tête du bloc dans le diff indique la signature de la précédente méthode non-asynchrone, ou la ligne où la classe est déclarée, ce qui prête à confusion.

Mon premier réflexe a été de soumettre une pull request sur Github pour ajouter les mots-clés manquants ; cependant j’ai vite réalisé que le repo de git sur Github est juste un miroir et n’accepte pas de pull requests… Le processus de contribution consiste à envoyer un patch à la mailing list de git, avec une longue et ennuyeuse liste de règles à respecter. Ce processus m’a semblé si laborieux que j’ai abandonné l’idée. Franchement, je ne sais pas pourquoi git utilise un processus de contribution aussi obsolète et compliqué, ça ne fait que décourager les contributeurs occasionnels. Mais c’est un peu hors-sujet, alors passons et voyons si on peut résoudre le problème autrement.

Heureusement, les patterns prédéfinis peuvent être redéfinis dans la configuration de git. Pour définir le pattern de signature de fonction pour C#, il faut définir le paramètre diff.csharp.xfuncname dans votre fichier de configuration git :

[diff "csharp"]
  xfuncname = ^[ \\t]*(((static|public|internal|private|protected|new|virtual|sealed|override|unsafe|async|partial)[ \\t]+)*[][<>@.~_[:alnum:]]+[ \\t]+[<>@._[:alnum:]]+[ \\t]*\\(.*\\))[ \\t]*$

Comme vous pouvez le voir, c’est le même pattern que dans userdiff.c, avec les backslashes échappés et les mots-clés manquants ajoutés. Avec ce pattern, git diff affiche maintenant la signature correcte pour les méthodes asynchrones :

diff --git a/Program.cs b/Program.cs
index 655a213..5ae1016 100644
--- a/Program.cs
+++ b/Program.cs
@@ -31,5 +32,6 @@ static async Task FooAsync()
         Console.WriteLine("Hello world");
         Console.WriteLine("Hello world");
         Console.WriteLine("Hello world");
+        await Task.Delay(100);
     }
 }

Ça m’a pris un moment de trouver comment le faire marcher, alors j’espère que ça vous sera utile !

Tout faire ou presque avec le pipeline de HttpClient

Il y a quelques années, Microsoft a introduit la classe HttpClient comme alternative moderne à HttpWebRequest pour faire des requêtes web depuis des applications .NET. Non seulement cette nouvelle API est beaucoup plus facile à utiliser, plus propre, et asynchrone, mais elle est aussi facilement extensible.

Vous avez peut-être remarqué que HttpClient a un constructeur qui accepte un HttpMessageHandler. De quoi s’agit-il ? Un HttpMessageHandler est un objet qui accepte une requête (HttpRequestMessage) et renvoie une réponse (HttpResponseMessage) ; la façon dont il le fait dépend complètement de l’implémentation. Par défaut, HttpClient utilise HttpClientHandler, un handler qui envoie une requête vers un serveur sur le réseau et renvoie la réponse du serveur. L’autre implémentation fournie de HttpMessageHandler est une classe abstraite nommée DelegatingHandler, et c’est de celle là que je voudrais parler.

Le pipeline

DelegatingHandler est un handler qui est conçu pour être chaîné à un autre handler, ce qui donne un pipeline à travers lequel les requêtes et réponses vont passer, comme illustré par ce schéma :

Schéma du pipeline de HttpClient

(Image tirée du site officiel ASP.NET)

Chaque handler a la possibilité d’examiner et/ou de modifier la requête avant de la passer au handler suivant, et d’examiner et/ou de modifier la réponse reçue du handler suivant. Habituellement, le dernier handler dans le pipeline est le HttpClientHandler, qui communique directement avec le réseau.

La chaîne de handlers peut être configurée comme ceci :

var pipeline = new MyHandler1()
{
    InnerHandler = new MyHandler2()
    {
        InnerHandler = new HttpClientHandler()
    }
};
var client = new HttpClient(pipeline);

Mais si vous préférez les interfaces “fluent”, il est facile de créer une méthode d’extension qui permet de le faire comme ceci :

var pipeline = new HttpClientHandler()
    .DecorateWith(new MyHandler2())
    .DecorateWith(new MyHandler1());
var client = new HttpClient(pipeline);

Tout ça semble peut-être un peu abstrait pour l’instant, mais cette architecture à base de pipeline rend possible plein de scénarios intéressants. En effet, ces handlers de messages HTTP peuvent être utilisés pour ajouter des comportements personnalisés au traitement des requêtes et réponses. Je vais en donner quelques exemples.

Remarque : Je présente cette fonctionnalité d’un point de vue client (vu que je développe essentiellement des applis clientes), mais le même système de handlers est également utilisé côté serveur dans ASP.NET Web API.

Tests unitaires

Le premier cas d’utilisation qui vient à l’esprit, et le premier que j’ai mis en oeuvre, c’est les tests unitaires. Si vous testez une classe qui fait des paiements en ligne via HTTP, vous ne voulez pas que vos tests envoient réellement des requêtes au vrai serveur… Vous voulez juste vous assurer que les requêtes envoyées sont correctes, et que le code réagit correctement à des réponses spécifiques. Une solution simple à ce problème est de créer un handler “stub”, et de l’injecter dans votre classe à la place de HttpClientHandler. Voici une implémentation possible :

class StubHandler : HttpMessageHandler
{
    // Responses to return
    private readonly Queue<HttpResponseMessage> _responses =
        new Queue<System.Net.Http.HttpResponseMessage>();

    // Requests that were sent via the handler
    private readonly List<HttpRequestMessage> _requests =
        new List<System.Net.Http.HttpRequestMessage>();

    protected override Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (_responses.Count == 0)
            throw new InvalidOperationException("No response configured");

        _requests.Add(request);
        var response = _responses.Dequeue();
        return Task.FromResult(response);
    }

    public void QueueResponse(HttpResponseMessage response) =>
        _responses.Enqueue(response);

    public IEnumerable<HttpRequestMessage> GetRequests() =>
        _requests;
}

Cette classe permet d’enregister les requêtes qui sont envoyées via le handler et de spécifier les réponses qui doivent être renvoyées. Par exemple, on pourrait écrire un test comme celui-ci :

// Arrange
var handler = new StubHandler();
handler.EnqueueResponse(new HttpResponseMessage(HttpStatusCode.Unauthorized));
var processor = new PaymentProcessor(handler);

// Act
var paymentResult = await processor.ProcessPayment(new Payment());

// Assert
Assert.AreEqual(PaymentStatus.Failed, paymentResult.Status);

Bien sûr, plutôt que de créer un stub manuellement, il est possible d’utiliser un framework de mock pour générer un faux handler. Le fait que la méthode SendAsync soit protégée rend cette approche un peu moins facile qu’elle devrait l’être, mais on peut facilement contourner le problème en créant une classe dérivée qui expose une méthode publique virtuelle, et en faisant un mock de cette classe :

public abstract class MockableMessageHandler : HttpMessageHandler
{
    protected override sealed Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        return DoSendAsync(request);
    }

    public abstract Task<HttpResponseMessage> DoSendAsync(HttpRequestMessage request);
}

Exemple d’utilisation avec FakeItEasy :

// Arrange
var handler = A.Fake<MockableMessageHandler>();
A.CallTo(() => handler.DoSendAsync(A<HttpRequestMessage>._))
    .Returns(new HttpResponseMessage(HttpStatusCode.Unauthorized));
var processor = new PaymentProcessor(handler);
...

Logging

Écrire dans le log les requêtes envoyées et les réponses reçues peut aider à diagnostiquer certains problèmes. C’est très facile à mettre en oeuvre avec un un DelegatingHandler personnalisé :

public class LoggingHandler : DelegatingHandler
{
    private readonly ILogger _logger;

    public LoggingHandler(ILogger logger)
    {
        _logger = logger;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        _logger.Trace($"Request: {request}");
        try
        {
            // base.SendAsync calls the inner handler
            var response = await base.SendAsync(request, cancellationToken);
            _logger.Trace($"Response: {response}");
            return response;
        }
        catch (Exception ex)
        {
            _logger.Error($"Failed to get response: {ex}");
            throw;
        }
    }
}

Réessayer les requêtes échouées

Un autre cas d’utilisation intéressant des handlers de messages HTTP est de réessayer automatiquement les requêtes qui ont échouées. Par exemple, le serveur auquel on s’adresse peut être temporairement indisponible (503), il peut limiter nos requêtes (429), ou on peut tout simplement avoir perdu l’accès à internet. Dans toutes ces situations, réessayer la même requête plus tard a de bonnes chances de fonctionner (le serveur peut avoir redémarré, on peut avoir retrouvé du wifi…). Gérer la retentative au niveau du code applicatif est laborieux, car ça peut se produire pratiquement n’importe où. Avoir cette logique au plus bas niveau possible et implémentée d’une façon complètement transparente pour l’appelant rend les choses beaucoup plus simples.

Voici une implémentation possible d’un handler qui réessaie les requêtes échouées :

public class RetryHandler : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        while (true)
        {
            try
            {
                // base.SendAsync calls the inner handler
                var response = await base.SendAsync(request, cancellationToken);

                if (response.StatusCode == HttpStatusCode.ServiceUnavailable)
                {
                    // 503 Service Unavailable
                    // Wait a bit and try again later
                    await Task.Delay(5000, cancellationToken);
                    continue;
                }

                if (response.StatusCode == (HttpStatusCode)429)
                {
                    // 429 Too many requests
                    // Wait a bit and try again later
                    await Task.Delay(1000, cancellationToken);
                    continue;
                }

                // Not something we can retry, return the response as is
                return response;
            }
            catch (Exception ex) when(IsNetworkError(ex))
            {
                // Network error
                // Wait a bit and try again later
                await Task.Delay(2000, cancellationToken);
                continue;
            }
        }
    }

    private static bool IsNetworkError(Exception ex)
    {
        // Check if it's a network error
        if (ex is SocketException)
            return true;
        if (ex.InnerException != null)
            return IsNetworkError(ex.InnerException);
        return false;
    }
}

Remarquez que cette implémentation est un peu simpliste ; pour l’utiliser dans du code de production, il faudra sans doute ajouter un “exponential backoff” (attendre de plus en plus longtemps entre chaque tentative), prendre en compte l’en-tête Retry-After pour déterminer combien de temps il faut attendre, ou encore déterminer de façon un peu plus subtile si une exception correspond à une erreur réseau. De plus, en l’état actuel, ce handler réessaiera indéfiniment jusqu’à ce que la requête réussisse ; assurez-vous donc de passer un jeton d’annulation (CancellationToken) pour pouvoir l’arrêter si besoin.

Autres cas d’utilisations

Je ne peux pas donner d’exemples pour chaque scénario possible, mais voilà quelques autres cas d’utilisation possible des handlers de messages HTTP :

  • Gestion personnalisée des cookies (quelque chose que j’ai réellement fait, pour contourner un bug dans CookieContainer)
  • Authentification personnalisée (également quelque chose que j’ai fait, pour implémenter l’authentification OAuth2)
  • Utiliser l’en-tête X-HTTP-Method-Override pour passer les proxy qui refusent certaines méthode HTTP (voir l’article de Scott Hanselman pour plus de détails)
  • Chiffrement ou encodage personnalisé
  • Mise en cache

Comme vous le voyez, il y a tout un monde de possibilités. Si vous avez d’autres idées, indiquez les dans les commentaires !

Déconstruction de tuples en C# 7

Dans mon précedent billet, j’ai parlé d’une nouvelle fonctionnalité de C# 7 : les tuples. Dans Visual Studio 15 Preview 3, cette feature n’était pas tout à fait terminée ; il lui manquait 2 aspects importants :

  • la génération de métadonnées pour les noms des éléments des tuples, pour que les noms soient préservés entre les assemblies
  • la déconstruction des tuples en variables distinctes

Eh bien, il semble que l’équipe du langage C# n’a pas chômé au cours du mois écoulé, car ces deux éléments sont maintenant implémentés dans VS 15 Preview 4, qui a été publié hier ! Ils ont aussi rédigé des guides sur l’utilisation des tuples et de la déconstruction.

Il est maintenant possible d’écrire des choses comme ça :

var values = ...
var (count, sum) = Tally(values);
Console.WriteLine($"There are {count} values and their sum is {sum}");

(la méthode Tally est celle du précédent billet)

Remarquez que la variable t du billet précédent a disparu ; on affecte maintenant directement les variables count et sum à partir du résultat de la méthode, ce qui est à mon sens beaucoup plus élégant. Il ne semble pas y avoir de moyen d’ignorer une partie du tuple (c’est-à-dire ne pas l’affecter à une variable), mais peut-être cette possibilité viendra-t-elle plus tard.

Un aspect intéressant de la déconstruction est qu’elle n’est pas limitée aux tuples ; n’importe quel type peut être déconstruit, à condition d’avoir une méthode Deconstruct avec les paramètres out adéquats :

class Point
{
    public int X { get; }
    public int Y { get; }

    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }

    public void Deconstruct(out int x, out int y)
    {
        x = X;
        y = Y;
    }
}

...

var (x, y) = point;
Console.WriteLine($"Coordinates: ({x}, {y})");

La méthode Deconstruct peut également être une méthode d’extension, ce qui peut être utile si vous voulez déconstruire un type dont vous ne contrôlez pas le code. Les vieilles classes Sytem.Tuple, par exemple, peuvent être déconstruites à l’aide de méthodes d’extension comme celle-ci :

public static void Deconstruct<T1, T2>(this Tuple<T1, T2> tuple, out T1 item1, out T2 item2)
{
    item1 = tuple.Item1;
    item2 = tuple.Item2;
}

...

var tuple = Tuple.Create("foo", 42);
var (name, value) = tuple;
Console.WriteLine($"Name: {name}, Value = {value}");

Pour finir, les méthodes qui renvoient des tuples sont maintenant décorées d’un attribut [TupleElementNames] qui indique les noms des éléments du tuple :

// Code décompilé
[return: TupleElementNames(new[] { "count", "sum" })]
public static ValueTuple<int, double> Tally(IEnumerable<double> values)
{
   ...
}

(l’attribut est généré par le compilateur, vous n’avez pas besoin de l’écrire vous-même)

Cela permet de partager les noms des éléments du tuple entre les assemblies, et permet aux outils comme Intellisense de fournir des informations utiles sur la méthode.

L’implémentation des tuples en C# 7 semble donc à peu près terminée ; gardez cependant à l’esprit qu’il s’agit encore d’une preview, et que les choses peuvent encore changer d’ici la release finale.

Tuples en C# 7

Un tuple est une liste finie et ordonnée de valeurs, éventuellement de types différents, et est utilisé pour regrouper des valeurs liées entre elles sans avoir à créer une type spécifique pour les contenir.

.NET 4.0 a introduit un ensemble de classes Tuple , qui s’utilisent de la façon suivante

private static Tuple<int, double> Tally(IEnumerable<double> values)
{
	int count = 0;
	double sum = 0.0;
	foreach (var value in values)
	{
	    count++;
	    sum += value;
	}
	return Tuple.Create(count, sum);
}

...

var values = ...
var t = Tally(values);
Console.WriteLine($"There are {t.Item1} values and their sum is {t.Item2}");

Les classes Tuple ont deux principaux inconvénients:

  • Ce sont des classes, c’est-à-dire des types référence. Cela implique qu’elles doivent être allouées sur le tas, et collectées par le garbage collector quand elles ne sont plus utilisées. Pour les applications où les performances sont critiques, cela peut être problématique. De plus, le fait qu’elles puissent être nulles n’est souvent pas souhaitable.
  • Les éléments du tuple n’ont pas de noms, ou plutôt, ils ont toujours les mêmes noms (Item1, Item2, etc), qui ne sont pas du tout significatifs. Le type Tuple<T1, T2> ne communique absolument aucune information sur ce que le tuple représente réellement, ce qui en fait un mauvais candidat pour les APIs publiques.

En C# 7, une nouvelle fonctionnalité sera introduite pour améliorer le support des tuples : il sera possible de déclarer des types tuple “inline”, un peu comme des types anonymes, à part qu’ils ne sont pas limités à la méthode courante. En utilisant cette feature, le code précédent devient beaucoup plus clair:

static (int count, double sum) Tally(IEnumerable<double> values)
{
	int count = 0;
	double sum = 0.0;
	foreach (var value in values)
	{
	    count++;
	    sum += value;
	}
	return (count, sum);
}

...

var values = ...
var t = Tally(values);
Console.WriteLine($"There are {t.count} values and their sum is {t.sum}");

Notez comment le type de retour de Tally est déclaré, et comment le résultat est utilisé. C’est beaucoup mieux ! Les éléments du tuple ont maintenant des noms significatifs, et la syntaxe est plus agréable. Cette fonctionnalité repose sur un nouveau type ValueTuple<T1, T2>, qui est une structure et ne nécessite donc pas d’allocation sur le tas.

Vous pouvez essayer cette feature dès maintenant dans Visual Studio 15 Preview 3. Cependant, le type ValueTuple<T1, T2> ne fait pas (encore) partie du .NET Framework; pour faire fonctionner cet exemple, il faudra installer le package NuGet System.ValueTuple.

Enfin, une dernière remarque concernant les noms des membres du tuple : comme beaucoup d’autres fonctionnalités du langage, c’est juste du sucre syntaxique. Dans le code compilé, les membres du tuple sont référencés en tant que Item1 et Item2, et non count et sum. La méthode Tally renvoie en fait un ValueTuple<int, double>, et non un type spécialement généré.

Notez que le compilateur qui est livré avec VS 15 Preview 3 ne génère aucune métadonnée concernant les noms des membres du tuple. Cette partie de la feature n’est pas encore implémentée, mais devrait être incluse dans la version finale. Cela signifie qu’en attendant, on ne peut pas utiliser de tuples entre différents assemblies (enfin, on peut, mais en perdant les noms des membres, il faudra donc utiliser Item1 et Item2 pour y faire référence).

css.php