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

Piège: utiliser var et async ensemble

Il y a quelques jours au bureau, je suis tombé sur un bug assez sournois dans notre application principale. Le code semblait assez innocent, et à première vue je ne voyais vraiment pas ce qui n’allait pas… Le code était similaire à ceci:

public async Task<bool> BookExistsAsync(int id)
{
    var store = await GetBookStoreAsync();
    var book = store.GetBookByIdAsync(id);
    return book != null;
}

// Pour donner le contexte, voici les types et méthodes utilisés dans BookExistsAsync:

private Task<IBookStore> GetBookStoreAsync()
{
    // ...
}


public interface IBookStore
{
    Task<Book> GetBookByIdAsync(int id);
    // ...
}

public class Book
{
    public int Id { get; set; }
    // ...
}

La méthode BookExistsAsync renvoie toujours true. Voyez-vous pourquoi ?

Regardez cette ligne :

var book = store.GetBookByIdAsync(id);

Vite, sans réfléchir, quel est le type de book ? Si vous avez répondu Book, regardez de plus près : c’est Task<Book>. Il manque le await ! Et une méthode async renvoie toujours une tâche non nulle, donc book n’est jamais nul.

Quand vous avez une méthode async sans await, le compilateur vous avertit, mais en l’occurrence il y a un await sur la ligne précédente. La seule chose qu’on fait avec book est de vérifier qu’il n’est pas null ; puisque Task<T> est un type référence, il n’y rien de suspect dans le fait de le comparer à null. Donc le compilateur ne voit rien d’anormal ; l’analyseur statique de code (ReSharper dans mon cas) ne voit rien d’anormal ; et le pauvre cerveau humain qui fait la revue de code ne voit rien d’anormal non plus… Évidemment, le problème aurait facilement pu être détecté avec une couverture de tests adéquate, mais malheureusement cette méthode n’était pas couverte.

Alors, comment éviter ce type d’erreur ? En arrêtant d’utiliser var et en spécifiant toujours les types explicitement ? Mais j’aime var, je l’utilise presque partout ! D’ailleurs, je crois que c’est la première fois que je vois un bug lié à l’utilisation de var. Je ne suis vraiment pas prêt à l’abandonner…

Idéalement, j’aurais aimé que ReSharper détecte le problème; peut-être qu’il devrait considérer que toutes les méthodes qui renvoient une Task sont implicitement [NotNull], sauf mention contraire. En attendant, je n’ai pas de solution miracle pour ce problème ; faites juste attention quand vous appelez une méthode asynchrone, et écrivez des tests unitaires !

Publier un package sur NuGet.org depuis AppVeyor

Depuis quelques mois, j’utilise AppVeyor CI pour certains de mes projets open-source (avec Cake pour les scripts de build). J’en suis très content, mais il y avait un point qui m’ennuyait : je ne trouvais pas comment publier des packages sur NuGet.org directement depuis AppVeyor. Il fallait que je télécharge le package en local, puis que je l’uploade manuellement depuis ma machine (soit avec nuget push en ligne de commande, soit via le formulaire web sur NuGet.org), ce qui augmente considérablement les risques de mauvaise manipulation.

La solution s’est avérée assez simple en fin de compte, une fois que j’ai su où chercher. Je vais en décrire ici les étapes.

Configuration initiale

1. Configurer NuGet.org comme environnement de déploiement

Avant de pouvoir pousser des packages vers la galerie NuGet, il faut configurer NuGet.org comme environnement de déploiement (c’est une opération à faire une seule fois, à moins de vouloir publier des packages sous différentes identités).

Une fois connecté sur AppVeyor, allez sur la page Environments, cliquez sur New Environment, et sélectionnez NuGet comme fournisseur de déploiement :

appveyor_new_environment

Choisissez un nom pour l’environnement (par exemple NuGet.org), et saisissez votre clé d’API (non, ce n’est pas ma vraie clé sur la copie d’écran…). Vous pouvez trouver votre clé d’API NuGet sur votre page de compte quand vous êtes connecté sur NuGet.org. Laissez l’URL vide si vous voulez poussez sur la galerie NuGet officielle. Quand vous avez terminé, cliquez sur Add Environment.

2. Configurer les artéfacts dans votre projet

Dans les paramètres de votre projet, assurez-vous que les packages NuGet générés par votre processus de build sont configurés comme des artéfacts. Il suffit pour cela d’ajouter le chemin vers les fichiers .nupkg (le nom de déploiement est optionnel) :

appveyor_configure_artifacts

C’est tout ! Vous êtes maintenant prêt à publier.

Publier des packages

Lancez une nouvelle build, et quand il est terminé, cliquez sur le bouton Deploy pour cette build :

appveyor_deploy_button

Sélectionnez NuGet.org comme environnement de déploiement :

appveyor_deploy_to_nuget

Cliquez sur Deploy, et laissez la magie opérer. Si tout se passe bien, vous devriez voir quelque chose comme ça :

appveyor_deploy_result

Félicitations, votre package est maintenant sur la galerie NuGet. Si si, vous pouvez allez vérifier !

Essai des fonctionnalités de C# 7 dans Visual Studio “15” Preview

Il y a environ deux semaines, Microsoft a publié la première version préliminaire de la prochaine mouture de Visual Studio. Vous pourrez découvrir toutes les nouveautés qu’elle contient dans les notes de version. Il y a quelques nouveautés vraiment sympa (j’aime particulièrement le nouvel “installeur léger”), mais le plus intéressant pour moi est que le compilateur C# livré avec inclut quelques unes des fonctionnalités prévues pour C# 7. Regardons ça de plus près !

Activer les nouvelles fonctionnalités

Les nouvelles fonctionnalités ne sont pas activées par défaut. On peut les activer individuellement avec l’option /feature: en ligne de commande, mais le plus simple est de toutes les activer en ajoutant __DEMO__ et __DEMO_EXPERIMENTAL__ dans les symboles de compilation conditionnelle (Propriétés du projet, onglet Build).

Fonctions locales

La plupart des langages fonctionnels permettent de déclarer des fonctions dans le corps d’autres fonctions. Il est maintenant possible de faire la même chose en C# 7 ! La syntaxe pour déclarer une méthode dans une autre est sans grande surprise :

long Factorial(int n)
{
    long Fact(int i, long acc)
    {
        return i == 0 ? acc : Fact(i - 1, acc * i);
    }
    return Fact(n, 1);
}

Ici, la méthode Fact est locale à la méthode Factorial (au cas où vous vous posez la question, c’est une implémentation avec récursion terminale de la factorielle — ce qui n’a pas beaucoup de sens, vu que C# ne supporte pas la récursion terminale, mais bon, c’est juste un exemple…).

Bien sûr, il était déjà possible de simuler une fonction locale à l’aide d’une expression lambda, mais cela avait plusieurs inconvénients :

  • c’est moins lisible, car il faut déclarer explicitement le type du delegate ;
  • c’est plus lent, à cause du coût de création d’une instance de delegate, et de l’appel via le delegate ;
  • l’écriture d’expressions lambda récursives est peu intuitive.

Voici les principaux avantages des fonctions locales :

  • quand une méthode est utilisée seulement comme auxiliaire d’une autre méthode, la rendre locale permet de rendre cette relation plus explicite;
  • comme les lambdas, une fonction locale peut capturer les variables et paramètres de la méthode qui la contient;
  • les fonctions locales supportent la récursion comme n’importe quelle autre méthode

Pour en savoir plus sur les fonctions locales, allez voir sur le dépôt Github de Roslyn.

Valeurs de retour et variables locales par référence

Il est possible, depuis la première version de C#, de passer des paramètres par référence, ce qui est conceptuellement similaire au fait de passer un pointeur dans un langage comme C. Jusqu’ici, cette possibilité était limitée aux paramètres, mais C# 7 l’étend aux valeurs de retour et aux variables locales. Voici un exemple :

static void TestRefReturn()
{
    var foo = new Foo();
    Console.WriteLine(foo); // 0, 0
     
    foo.GetByRef("x") = 42;
 
    ref int y = ref foo.GetByRef("y");
    y = 99;
 
    Console.WriteLine(foo); // 42, 99
}
 
class Foo
{
    private int x;
    private int y;
 
    public ref int GetByRef(string name)
    {
        if (name == "x")
            return ref x;
        if (name == "y")
            return ref y;
        throw new ArgumentException(nameof(name));
    }
 
    public override string ToString() => $"{x},{y}";
}

Examinons ce code d’un peu plus près.

  • A la ligne 6, on dirait que j’affecte une valeur à une méthode. Qu’est-ce que ça peut bien vouloir dire ? Eh bien, la méthode GetByRef renvoie un champ de la classe Foo par référence (notez le type de retour ref int). Si je passe "x" en argument, elle renvoie le champ x par référence (et non une copie de la valeur du champ). Donc si j’affecte une valeur à cela, la valeur du champ x est également modifiée.
  • A la ligne 8, au lieu de simplement affecter une valeur directement au champ renvoyé par GetByRef, je passe par une variable locale y. Cette variable partage maintenant le même emplacement mémoire que le champ foo.y. Si j’affecte une valeur à y, cela modifie donc aussi foo.y.

Notez qu’il est également possible de renvoyer par référence un emplacement dans un tableau :

private MyBigStruct[] array = new MyBigStruct[10];
private int current;
 
public ref MyBigStruct GetCurrentItem()
{
    return ref array[current];
}

Il est probable que la plupart des développeurs C# n’auront jamais besoin de cette fonctionnalité; elle est assez bas-niveau, et ce n’est pas le genre de chose dont on a habituellement besoin quand on écrit des applications métier. Cependant, elle est très utile pour du code dont les performances sont critiques : copier une structure de grande taille est assez couteux, donc avoir la possibilité de la renvoyer plutôt par référence peut représenter un gain de performance non négligeable.

Pour en savoir plus sur cette fonctionnalité, rendez-vous sur Github.

Pattern matching

Le pattern matching (parfois traduit en “filtrage par motif”, mais je trouve cette traduction un peu bancale) est une fonctionnalité très courante dans les langages fonctionnels. C# 7 introduit certains aspects du pattern matching, sous forme d’extensions de l’opérateur is. Par exemple, quand on teste le type d’une variable, on peut maintenant introduire une nouvelle variable après le type, de façon à ce que cette variable reçoive la valeur de l’opérande de gauche, mais avec le type indiqué par l’opérande de droite (ce sera plus clair dans un instant avec un exemple).

Par exemple, si vous devez tester qu’une valeur est de type DateTime, puis faire quelque chose avec ce DateTime, il faut actuellement tester le type, puis convertir la valeur vers ce type :

object o = GetValue();
if (o is DateTime)
{
    var d = (DateTime)o;
    // Do something with d
}

En C# 7, on peut simplifier un peu :

object o = GetValue();
if (o is DateTime d)
{
    // Do something with d
}

d est maintenant déclarée directement dans l’expression o is DateTime.

Cette fonctionnalité peut aussi être utilisée dans un block switch :

object v = GetValue();
switch (v)
{
    case string s:
        Console.WriteLine($"{v} is a string of length {s.Length}");
        break;
    case int i:
        Console.WriteLine($"{v} is an {(i % 2 == 0 ? "even" : "odd")} int");
        break;
    default:
        Console.WriteLine($"{v} is something else");
        break;
}

Dans ce code, chaque case introduit une variable du type approprié, qu’on peut ensuite utiliser dans le corps du case.

Pour l’instant, je n’ai couvert que le pattern matching avec un simple test du type, mais il existe aussi des formes plus sophistiquées. Par exemple :

switch (DateTime.Today)
{
    case DateTime(*, 10, 31):
        Console.WriteLine("Happy Halloween!");
        break;
    case DateTime(var year, 7, 4) when year > 1776:
        Console.WriteLine("Happy Independance Day!");
        break;
    case DateTime { DayOfWeek is DayOfWeek.Saturday }:
    case DateTime { DayOfWeek is DayOfWeek.Sunday }:
        Console.WriteLine("Have a nice week-end!");
        break;
    default:
        break;
}

Plutôt cool, non ?

Le pattern matching existe aussi sous une autre forme (encore expérimentale), avec un nouveau mot-clé match :

object o = GetValue();
string description = o match
    (
        case string s : $"{o} is a string of length {s.Length}"
        case int i : $"{o} is an {(i % 2 == 0 ? "even" : "odd")} int"
        case * : $"{o} is something else"
    );

Cela ressemble beaucoup à un switch, sauf que c’est une expression et non une instruction.

Il y a encore beaucoup à dire sur le pattern matching, mais ça devient un peu long pour un article de blog, je vous invite donc à consulter la spec sur Github pour plus de détails.

Littéraux binaires et séparateurs de chiffres

Ces fonctionnalités n’étaient pas explicitement mentionnées dans les notes de version, mais j’ai remarqué qu’elles étaient inclues aussi. Elles étaient initialement prévues pour C# 6, mais avaient finalement été retirées avant la release ; elles sont de retour en C# 7.

Il est maintenant possible d’écrire des littéraux numériques sous forme binaire, en plus du décimal et de l’hexadécimal :

int x = 0b11001010;

Pratique pour définir des masques de bits !

De plus, pour rendre les grands nombres plus lisibles, on peut désormais grouper les chiffres en introduisant des séparateurs. Cela fonctionne avec les littéraux décimaux, hexadécimaux, et binaires :

int oneBillion = 1_000_000_000;
int foo = 0x7FFF_1234;
int bar = 0b1001_0110_1010_0101;

Conclusion

Visual Studio “15” Preview vous permet donc de commencer à expérimenter avec les nouvelles fonctionnalités de C# 7 ; n’hésitez pas à faire part de vos retours sur Github ! Et gardez à l’esprit que ce n’est qu’une version préliminaire, beaucoup de choses peuvent encore changer avant la version finale.

css.php