<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr"><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://guym.fr/feed.xml" rel="self" type="application/atom+xml" /><link href="http://guym.fr/" rel="alternate" type="text/html" hreflang="fr" /><updated>2026-02-25T12:20:08+01:00</updated><id>http://guym.fr/feed.xml</id><title type="html">Guym code</title><subtitle>Péripéties d&apos;un développeur passionné par le code et la technologie. 
</subtitle><author><name>Guym</name></author><entry><title type="html">Dapr : l’invocation de service en .NET</title><link href="http://guym.fr/2026/02/22/dapr-service-invocation-1.html" rel="alternate" type="text/html" title="Dapr : l’invocation de service en .NET" /><published>2026-02-22T00:00:00+01:00</published><updated>2026-02-22T00:00:00+01:00</updated><id>http://guym.fr/2026/02/22/dapr-service-invocation-1</id><content type="html" xml:base="http://guym.fr/2026/02/22/dapr-service-invocation-1.html"><![CDATA[<p>L’invocation de service (Service Invocation) est l’un des building blocks fondamentaux de Dapr. Il permet à un service d’appeler un autre service par son <strong>app-id</strong>, sans connaître son adresse IP ni son port, avec découverte automatique, load balancing, retry, chiffrement mTLS et traçabilité intégrés. En .NET, le SDK Dapr et <code class="language-plaintext highlighter-rouge">HttpClient</code> offrent plusieurs façons d’exploiter ce mécanisme.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Dapr pour les développeurs .NET</strong> : <strong> 2</strong> sur <strong>3</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/02/21/dapr-presentation.html">Présentation de Dapr : le runtime pour applications distribuées</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2026/02/22/dapr-service-invocation-2-grpc.html">Dapr : invocation de service gRPC en .NET</a>
		
		</li>
	
	
	</ul>
</div>

<h1 id="le-problème">Le problème</h1>

<p>Dans une architecture microservices classique, appeler un autre service implique de gérer :</p>

<ul>
  <li>La <strong>découverte de service</strong> : où se trouve le service cible ? Quelle est son URL ?</li>
  <li>Le <strong>load balancing</strong> : comment répartir les appels entre plusieurs instances ?</li>
  <li>La <strong>résilience</strong> : comment gérer les timeouts, les retries, le circuit breaking ?</li>
  <li>La <strong>sécurité</strong> : comment chiffrer les communications inter-services (mTLS) ?</li>
  <li>L’<strong>observabilité</strong> : comment tracer un appel de bout en bout entre plusieurs services ?</li>
</ul>

<p>Sans Dapr, il faut combiner un service mesh (Istio, Linkerd), un reverse proxy, des bibliothèques de résilience (Polly), un registre de services (Consul, Eureka), etc. Dapr regroupe tout cela dans son sidecar.</p>

<h1 id="fonctionnement-de-linvocation-de-service">Fonctionnement de l’invocation de service</h1>

<p>Lorsqu’un service A veut appeler un service B :</p>

<ol>
  <li>Le service A fait un appel HTTP ou gRPC vers <strong>son propre sidecar</strong> Dapr (sur <code class="language-plaintext highlighter-rouge">localhost</code>).</li>
  <li>Le sidecar de A résout le nom du service B (via le <strong>name resolution component</strong>).</li>
  <li>Le sidecar de A envoie la requête au <strong>sidecar de B</strong> (avec mTLS automatique).</li>
  <li>Le sidecar de B transmet la requête à l’application B sur son port local.</li>
  <li>La réponse fait le chemin inverse.
    <pre><code class="language-mermaid">graph LR
 A["Service A&lt;br/&gt;(localhost)"]
 SA["Sidecar A&lt;br/&gt;(découverte,&lt;br/&gt;retry, tracing)"]
 SB["Sidecar B&lt;br/&gt;(localhost)"]
 B["Service B"]
    
 A --&gt;|localhost| SA
 SA --&gt;|mTLS| SB
 SB --&gt;|localhost| B
</code></pre>
  </li>
</ol>

<p>Tout se passe de manière transparente : le service A ne connaît que le <strong>app-id</strong> du service B et le nom de la méthode à appeler.</p>

<h1 id="lapi-http-dapr">L’API HTTP Dapr</h1>

<p>L’invocation de service passe par l’endpoint suivant du sidecar :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>POST/GET/PUT/DELETE http://localhost:&lt;dapr-port&gt;/v1.0/invoke/&lt;app-id&gt;/method/&lt;method-name&gt;
</code></pre></div></div>

<ul>
  <li><code class="language-plaintext highlighter-rouge">&lt;dapr-port&gt;</code> : port HTTP du sidecar (3500 par défaut).</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;app-id&gt;</code> : identifiant unique du service cible (défini au lancement avec <code class="language-plaintext highlighter-rouge">--app-id</code>).</li>
  <li><code class="language-plaintext highlighter-rouge">&lt;method-name&gt;</code> : route de l’endpoint exposé par le service cible.</li>
</ul>

<h3 id="exemple-avec-curl">Exemple avec curl</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Appeler GET /weatherforecast sur le service "weather-api"</span>
curl http://localhost:3500/v1.0/invoke/weather-api/method/weatherforecast

<span class="c"># Appeler POST /orders sur le service "order-service"</span>
curl <span class="nt">-X</span> POST http://localhost:3500/v1.0/invoke/order-service/method/orders <span class="se">\</span>
  <span class="nt">-H</span> <span class="s2">"Content-Type: application/json"</span> <span class="se">\</span>
  <span class="nt">-d</span> <span class="s1">'{"productId": 42, "quantity": 2}'</span>
</code></pre></div></div>

<h1 id="invocation-de-service-en-net">Invocation de service en .NET</h1>

<p>Le SDK Dapr pour .NET fournit plusieurs approches pour invoquer un service.</p>

<h2 id="installation">Installation</h2>

<pre><code class="language-dotnetcli">dotnet add package Dapr.AspNetCore
</code></pre>

<p>Ce package inclut <code class="language-plaintext highlighter-rouge">DaprClient</code> et les extensions ASP.NET Core.</p>

<h2 id="1-utiliser-daprclient-directement">1. Utiliser <code class="language-plaintext highlighter-rouge">DaprClient</code> directement</h2>

<p><code class="language-plaintext highlighter-rouge">DaprClient</code> est le client principal du SDK Dapr. Il encapsule les appels HTTP/gRPC vers le sidecar.</p>

<h3 id="enregistrement-dans-le-conteneur-di">Enregistrement dans le conteneur DI</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddDaprClient</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="appels-de-service">Appels de service</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">DaprClient</span> <span class="n">_daprClient</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderService</span><span class="p">(</span><span class="n">DaprClient</span> <span class="n">daprClient</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_daprClient</span> <span class="p">=</span> <span class="n">daprClient</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">// Appel POST avec body et réponse typée</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">OrderConfirmation</span><span class="p">&gt;</span> <span class="nf">CreateOrderAsync</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">InvokeMethodAsync</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">,</span> <span class="n">OrderConfirmation</span><span class="p">&gt;(</span>
            <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Post</span><span class="p">,</span>
            <span class="s">"order-service"</span><span class="p">,</span>   <span class="c1">// app-id du service cible</span>
            <span class="s">"orders"</span><span class="p">,</span>          <span class="c1">// route de l'endpoint</span>
            <span class="n">order</span><span class="p">);</span>            <span class="c1">// body de la requête</span>
    <span class="p">}</span>

    <span class="c1">// Appel GET avec réponse typée</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">WeatherForecast</span><span class="p">[</span><span class="k">]&gt;</span> <span class="nf">GetWeatherAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">InvokeMethodAsync</span><span class="p">&lt;</span><span class="n">WeatherForecast</span><span class="p">[</span><span class="k">]&gt;</span><span class="p">(</span>
            <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Get</span><span class="p">,</span>
            <span class="s">"weather-api"</span><span class="p">,</span>
            <span class="s">"weatherforecast"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">// Appel sans réponse (fire and forget)</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">NotifyAsync</span><span class="p">(</span><span class="n">Notification</span> <span class="n">notification</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">InvokeMethodAsync</span><span class="p">(</span>
            <span class="s">"notification-service"</span><span class="p">,</span>
            <span class="s">"notify"</span><span class="p">,</span>
            <span class="n">notification</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="signatures-disponibles">Signatures disponibles</h3>

<p><code class="language-plaintext highlighter-rouge">InvokeMethodAsync</code> propose plusieurs surcharges :</p>

<table>
  <thead>
    <tr>
      <th>Surcharge</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InvokeMethodAsync&lt;TResponse&gt;(method, appId, methodName)</code></td>
      <td>Appel sans body, avec réponse typée</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InvokeMethodAsync&lt;TRequest, TResponse&gt;(method, appId, methodName, data)</code></td>
      <td>Appel avec body et réponse typée</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InvokeMethodAsync(appId, methodName, data)</code></td>
      <td>Appel POST avec body, sans réponse</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InvokeMethodAsync(method, appId, methodName)</code></td>
      <td>Appel sans body ni réponse</td>
    </tr>
  </tbody>
</table>

<h3 id="gestion-des-erreurs">Gestion des erreurs</h3>

<p>En cas d’erreur HTTP (4xx, 5xx), <code class="language-plaintext highlighter-rouge">DaprClient</code> lève une <code class="language-plaintext highlighter-rouge">InvocationException</code> (ou <code class="language-plaintext highlighter-rouge">RpcException</code> en gRPC). On peut la capturer pour récupérer le code de statut :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">InvokeMethodAsync</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">,</span> <span class="n">OrderConfirmation</span><span class="p">&gt;(</span>
        <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Post</span><span class="p">,</span> <span class="s">"order-service"</span><span class="p">,</span> <span class="s">"orders"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">InvocationException</span> <span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Erreur </span><span class="p">{</span><span class="n">ex</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">StatusCode</span><span class="p">}</span><span class="s"> lors de l'appel à order-service"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">body</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ex</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Détail : </span><span class="p">{</span><span class="n">body</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="2-utiliser-httpclient-avec-daprclient">2. Utiliser <code class="language-plaintext highlighter-rouge">HttpClient</code> avec <code class="language-plaintext highlighter-rouge">DaprClient</code></h2>

<p>Pour ceux qui préfèrent travailler avec <code class="language-plaintext highlighter-rouge">HttpClient</code> (par habitude ou pour bénéficier de <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code>), Dapr fournit <code class="language-plaintext highlighter-rouge">DaprClient.CreateInvokeHttpClient()</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddSingleton</span><span class="p">(</span><span class="n">sp</span> <span class="p">=&gt;</span>
    <span class="k">new</span> <span class="nf">DaprClientBuilder</span><span class="p">().</span><span class="nf">Build</span><span class="p">().</span><span class="nf">CreateInvokeHttpClient</span><span class="p">(</span><span class="s">"order-service"</span><span class="p">));</span>
</code></pre></div></div>

<p>On obtient un <code class="language-plaintext highlighter-rouge">HttpClient</code> précconfiguré dont le <code class="language-plaintext highlighter-rouge">BaseAddress</code> pointe vers le sidecar avec le bon app-id. On l’utilise comme n’importe quel <code class="language-plaintext highlighter-rouge">HttpClient</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">HttpClient</span> <span class="n">_httpClient</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderService</span><span class="p">(</span><span class="n">HttpClient</span> <span class="n">httpClient</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_httpClient</span> <span class="p">=</span> <span class="n">httpClient</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">OrderConfirmation</span><span class="p">?&gt;</span> <span class="nf">CreateOrderAsync</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_httpClient</span><span class="p">.</span><span class="nf">PostAsJsonAsync</span><span class="p">(</span><span class="s">"/orders"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
        <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="n">ReadFromJsonAsync</span><span class="p">&lt;</span><span class="n">OrderConfirmation</span><span class="p">&gt;();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">WeatherForecast</span><span class="p">[]?&gt;</span> <span class="nf">GetWeatherAsync</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">_httpClient</span><span class="p">.</span><span class="n">GetFromJsonAsync</span><span class="p">&lt;</span><span class="n">WeatherForecast</span><span class="p">[</span><span class="k">]&gt;</span><span class="p">(</span><span class="s">"/weatherforecast"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Cette approche est intéressante car elle permet de <strong>réutiliser tout l’écosystème <code class="language-plaintext highlighter-rouge">HttpClient</code></strong> (.NET), y compris les <code class="language-plaintext highlighter-rouge">DelegatingHandler</code>, la sérialisation <code class="language-plaintext highlighter-rouge">System.Text.Json</code>, les extensions <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http</code>, etc.</p>

<h2 id="3-utiliser-ihttpclientfactory-approche-recommandée">3. Utiliser <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code> (approche recommandée)</h2>

<p>Pour tirer parti de <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code> et de ses avantages (gestion du pool de connexions, named/typed clients, handlers), on peut combiner Dapr avec un named client :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"order-service"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="c1">// Le base address pointe vers le sidecar Dapr</span>
    <span class="c1">// DAPR_HTTP_PORT est 3500 par défaut</span>
    <span class="kt">var</span> <span class="n">daprPort</span> <span class="p">=</span> <span class="n">Environment</span><span class="p">.</span><span class="nf">GetEnvironmentVariable</span><span class="p">(</span><span class="s">"DAPR_HTTP_PORT"</span><span class="p">)</span> <span class="p">??</span> <span class="s">"3500"</span><span class="p">;</span>
    <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">$"http://localhost:</span><span class="p">{</span><span class="n">daprPort</span><span class="p">}</span><span class="s">/v1.0/invoke/order-service/method/"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">OrderService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">HttpClient</span> <span class="n">_httpClient</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderService</span><span class="p">(</span><span class="n">IHttpClientFactory</span> <span class="n">httpClientFactory</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_httpClient</span> <span class="p">=</span> <span class="n">httpClientFactory</span><span class="p">.</span><span class="nf">CreateClient</span><span class="p">(</span><span class="s">"order-service"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">OrderConfirmation</span><span class="p">?&gt;</span> <span class="nf">CreateOrderAsync</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// L'URL relative est ajoutée au base address</span>
        <span class="c1">// → http://localhost:3500/v1.0/invoke/order-service/method/orders</span>
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_httpClient</span><span class="p">.</span><span class="nf">PostAsJsonAsync</span><span class="p">(</span><span class="s">"orders"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
        <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="n">ReadFromJsonAsync</span><span class="p">&lt;</span><span class="n">OrderConfirmation</span><span class="p">&gt;();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Cette approche est la <strong>plus idiomatique en .NET</strong> et permet d’ajouter facilement des handlers de résilience supplémentaires (via <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code> par exemple).</p>

<h1 id="côté-service-appelé">Côté service appelé</h1>

<p>Le service cible est une API ASP.NET Core standard. Aucune configuration Dapr spécifique n’est nécessaire pour être invocable :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/weatherforecast"</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">forecasts</span> <span class="p">=</span> <span class="n">Enumerable</span><span class="p">.</span><span class="nf">Range</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="m">5</span><span class="p">).</span><span class="nf">Select</span><span class="p">(</span><span class="n">i</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="n">WeatherForecast</span>
    <span class="p">{</span>
        <span class="n">Date</span> <span class="p">=</span> <span class="n">DateOnly</span><span class="p">.</span><span class="nf">FromDateTime</span><span class="p">(</span><span class="n">DateTime</span><span class="p">.</span><span class="n">Now</span><span class="p">.</span><span class="nf">AddDays</span><span class="p">(</span><span class="n">i</span><span class="p">)),</span>
        <span class="n">TemperatureC</span> <span class="p">=</span> <span class="n">Random</span><span class="p">.</span><span class="n">Shared</span><span class="p">.</span><span class="nf">Next</span><span class="p">(-</span><span class="m">20</span><span class="p">,</span> <span class="m">55</span><span class="p">),</span>
        <span class="n">Summary</span> <span class="p">=</span> <span class="s">"Sunny"</span>
    <span class="p">});</span>
    <span class="k">return</span> <span class="n">forecasts</span><span class="p">;</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/orders"</span><span class="p">,</span> <span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="c1">// Traitement de la commande...</span>
    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="k">new</span> <span class="n">OrderConfirmation</span>
    <span class="p">{</span>
        <span class="n">OrderId</span> <span class="p">=</span> <span class="n">Guid</span><span class="p">.</span><span class="nf">NewGuid</span><span class="p">(),</span>
        <span class="n">Status</span> <span class="p">=</span> <span class="s">"Created"</span>
    <span class="p">});</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Le sidecar Dapr intercepte les requêtes entrantes et les transmet à l’application sur le port configuré (<code class="language-plaintext highlighter-rouge">--app-port</code>).</p>

<h1 id="contrôle-daccès">Contrôle d’accès</h1>

<p>Dapr permet de restreindre quels services peuvent invoquer quels autres services, via une <strong>App Policy</strong> :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Configuration</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">daprConfig</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">accessControl</span><span class="pi">:</span>
    <span class="na">defaultAction</span><span class="pi">:</span> <span class="s">deny</span>
    <span class="na">policies</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">appId</span><span class="pi">:</span> <span class="s">frontend</span>
        <span class="na">defaultAction</span><span class="pi">:</span> <span class="s">deny</span>
        <span class="na">trustDomain</span><span class="pi">:</span> <span class="s2">"</span><span class="s">public"</span>
        <span class="na">namespace</span><span class="pi">:</span> <span class="s2">"</span><span class="s">default"</span>
        <span class="na">operations</span><span class="pi">:</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">/orders</span>
            <span class="na">httpVerb</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">POST"</span><span class="pi">]</span>
            <span class="na">action</span><span class="pi">:</span> <span class="s">allow</span>
          <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">/weatherforecast</span>
            <span class="na">httpVerb</span><span class="pi">:</span> <span class="pi">[</span><span class="s2">"</span><span class="s">GET"</span><span class="pi">]</span>
            <span class="na">action</span><span class="pi">:</span> <span class="s">allow</span>
</code></pre></div></div>

<p>Cette configuration autorise uniquement le service <code class="language-plaintext highlighter-rouge">frontend</code> à appeler <code class="language-plaintext highlighter-rouge">POST /orders</code> et <code class="language-plaintext highlighter-rouge">GET /weatherforecast</code>, et refuse tout le reste par défaut.</p>

<h1 id="résilience-intégrée">Résilience intégrée</h1>

<p>Dapr fournit des politiques de résilience configurables via YAML, sans code :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Resiliency</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">resiliency</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">policies</span><span class="pi">:</span>
    <span class="na">retries</span><span class="pi">:</span>
      <span class="na">retryOnError</span><span class="pi">:</span>
        <span class="na">policy</span><span class="pi">:</span> <span class="s">constant</span>
        <span class="na">duration</span><span class="pi">:</span> <span class="s">1s</span>
        <span class="na">maxRetries</span><span class="pi">:</span> <span class="m">3</span>
    <span class="na">circuitBreakers</span><span class="pi">:</span>
      <span class="na">mainBreaker</span><span class="pi">:</span>
        <span class="na">maxRequests</span><span class="pi">:</span> <span class="m">1</span>
        <span class="na">interval</span><span class="pi">:</span> <span class="s">10s</span>
        <span class="na">timeout</span><span class="pi">:</span> <span class="s">30s</span>
        <span class="na">trip</span><span class="pi">:</span> <span class="s">consecutiveFailures &gt; </span><span class="m">5</span>
    <span class="na">timeouts</span><span class="pi">:</span>
      <span class="na">generalTimeout</span><span class="pi">:</span> <span class="s">5s</span>
  <span class="na">targets</span><span class="pi">:</span>
    <span class="na">apps</span><span class="pi">:</span>
      <span class="na">order-service</span><span class="pi">:</span>
        <span class="na">retry</span><span class="pi">:</span> <span class="s">retryOnError</span>
        <span class="na">circuitBreaker</span><span class="pi">:</span> <span class="s">mainBreaker</span>
        <span class="na">timeout</span><span class="pi">:</span> <span class="s">generalTimeout</span>
</code></pre></div></div>

<p>Avec cette configuration, les appels vers <code class="language-plaintext highlighter-rouge">order-service</code> bénéficient automatiquement de :</p>
<ul>
  <li><strong>3 retries</strong> avec un délai de 1 seconde entre chaque tentative,</li>
  <li>un <strong>circuit breaker</strong> qui s’ouvre après 5 échecs consécutifs,</li>
  <li>un <strong>timeout</strong> de 5 secondes par appel.</li>
</ul>

<p>Tout cela sans ajouter une seule ligne de code dans les services.</p>

<h1 id="lancement-en-local">Lancement en local</h1>

<p>Pour tester l’invocation de service en local, on lance chaque service avec son sidecar :</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal 1 : lancer le service météo</span>
dapr run <span class="nt">--app-id</span> weather-api <span class="nt">--app-port</span> 5001 <span class="nt">--</span> dotnet run <span class="nt">--project</span> WeatherApi

<span class="c"># Terminal 2 : lancer le service de commandes</span>
dapr run <span class="nt">--app-id</span> order-service <span class="nt">--app-port</span> 5002 <span class="nt">--</span> dotnet run <span class="nt">--project</span> OrderService

<span class="c"># Terminal 3 : lancer le frontend qui appelle les deux autres</span>
dapr run <span class="nt">--app-id</span> frontend <span class="nt">--app-port</span> 5000 <span class="nt">--</span> dotnet run <span class="nt">--project</span> Frontend
</code></pre></div></div>

<p>Dapr utilise mDNS en mode standalone pour la découverte de services entre sidecars sur la même machine.</p>

<h1 id="lancement-avec-net-aspire">Lancement avec .NET Aspire</h1>

<p>Si vous utilisez <strong>.NET Aspire</strong>, l’orchestration des sidecars est automatique :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">DistributedApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">weatherApi</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="n">AddProject</span><span class="p">&lt;</span><span class="n">Projects</span><span class="p">.</span><span class="n">WeatherApi</span><span class="p">&gt;(</span><span class="s">"weather-api"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithDaprSidecar</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">orderService</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="n">AddProject</span><span class="p">&lt;</span><span class="n">Projects</span><span class="p">.</span><span class="n">OrderService</span><span class="p">&gt;(</span><span class="s">"order-service"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithDaprSidecar</span><span class="p">();</span>

<span class="n">builder</span><span class="p">.</span><span class="n">AddProject</span><span class="p">&lt;</span><span class="n">Projects</span><span class="p">.</span><span class="n">Frontend</span><span class="p">&gt;(</span><span class="s">"frontend"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithDaprSidecar</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithReference</span><span class="p">(</span><span class="n">weatherApi</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithReference</span><span class="p">(</span><span class="n">orderService</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">().</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<h1 id="invocation-grpc">Invocation gRPC</h1>

<p>Par défaut, la communication entre sidecars utilise <strong>gRPC</strong> (plus performant que HTTP). La communication entre l’application et son propre sidecar peut être en HTTP ou en gRPC selon la configuration.</p>

<p>Pour forcer l’utilisation de gRPC côté client SDK :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddDaprClient</span><span class="p">(</span><span class="n">daprBuilder</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">daprBuilder</span><span class="p">.</span><span class="nf">UseGrpcEndpoint</span><span class="p">(</span><span class="s">"http://localhost:50001"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>L’invocation gRPC natif (Protobuf) est aussi possible pour des services qui exposent des endpoints gRPC plutôt que REST, via le proxy gRPC de Dapr.</p>

<h1 id="résumé">Résumé</h1>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>API</strong></td>
      <td><code class="language-plaintext highlighter-rouge">POST/GET/PUT/DELETE http://localhost:3500/v1.0/invoke/{app-id}/method/{method}</code></td>
    </tr>
    <tr>
      <td><strong>Découverte</strong></td>
      <td>Automatique (mDNS en local, Kubernetes DNS en cluster)</td>
    </tr>
    <tr>
      <td><strong>Sécurité</strong></td>
      <td>mTLS automatique entre sidecars + access control policies</td>
    </tr>
    <tr>
      <td><strong>Résilience</strong></td>
      <td>Retry, circuit breaker, timeout configurables en YAML</td>
    </tr>
    <tr>
      <td><strong>Observabilité</strong></td>
      <td>Traces distribuées automatiques (OpenTelemetry)</td>
    </tr>
    <tr>
      <td><strong>SDK .NET</strong></td>
      <td><code class="language-plaintext highlighter-rouge">DaprClient.InvokeMethodAsync</code>, <code class="language-plaintext highlighter-rouge">CreateInvokeHttpClient</code>, ou <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code></td>
    </tr>
    <tr>
      <td><strong>Service cible</strong></td>
      <td>API ASP.NET Core standard, aucune dépendance Dapr requise</td>
    </tr>
  </tbody>
</table>

<p>L’invocation de service Dapr offre une abstraction puissante qui élimine la complexité de la communication inter-services, tout en laissant la liberté d’utiliser des APIs HTTP standard côté application.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="dapr" /><category term="microservices" /><category term="service-invocation" /><summary type="html"><![CDATA[L’invocation de service (Service Invocation) est l’un des building blocks fondamentaux de Dapr. Il permet à un service d’appeler un autre service par son app-id, sans connaître son adresse IP ni son port, avec découverte automatique, load balancing, retry, chiffrement mTLS et traçabilité intégrés. En .NET, le SDK Dapr et HttpClient offrent plusieurs façons d’exploiter ce mécanisme.]]></summary></entry><entry><title type="html">Dapr : invocation de service gRPC en .NET</title><link href="http://guym.fr/2026/02/22/dapr-service-invocation-2-grpc.html" rel="alternate" type="text/html" title="Dapr : invocation de service gRPC en .NET" /><published>2026-02-22T00:00:00+01:00</published><updated>2026-02-22T00:00:00+01:00</updated><id>http://guym.fr/2026/02/22/dapr-service-invocation-2-grpc</id><content type="html" xml:base="http://guym.fr/2026/02/22/dapr-service-invocation-2-grpc.html"><![CDATA[<p>Dans l’article précédent, nous avons vu comment Dapr simplifie l’invocation de service via HTTP. Mais Dapr supporte aussi nativement <strong>gRPC</strong>, à la fois entre les sidecars et entre votre application et son sidecar. gRPC apporte des avantages significatifs : sérialisation binaire (Protobuf), contrats fortement typés, streaming, et de meilleures performances. Dans cet article, on met en place un exemple complet : un service gRPC invoqué via Dapr, et un client qui l’appelle.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Dapr pour les développeurs .NET</strong> : <strong> 3</strong> sur <strong>3</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/02/21/dapr-presentation.html">Présentation de Dapr : le runtime pour applications distribuées</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2026/02/22/dapr-service-invocation-1.html">Dapr : l'invocation de service en .NET</a>
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			Cet article
		
		</li>
	
	
	</ul>
</div>

<h1 id="rappel--http-vs-grpc-dans-dapr">Rappel : HTTP vs gRPC dans Dapr</h1>

<p>Dapr utilise déjà gRPC pour la communication <strong>entre sidecars</strong>. Mais la communication entre votre application et son sidecar peut être :</p>

<ul>
  <li><strong>HTTP</strong> (par défaut) : votre app parle en HTTP au sidecar sur le port 3500.</li>
  <li><strong>gRPC</strong> : votre app parle en gRPC au sidecar sur le port 50001.</li>
</ul>

<p>Indépendamment de cela, le <strong>service cible</strong> lui-même peut exposer :</p>

<ul>
  <li>des endpoints <strong>HTTP/REST</strong> classiques (Minimal API, contrôleurs),</li>
  <li>des endpoints <strong>gRPC natifs</strong> (via <code class="language-plaintext highlighter-rouge">Grpc.AspNetCore</code>).</li>
</ul>

<p>Dapr sait router vers l’un ou l’autre. Dans cet article, on va construire un <strong>service gRPC natif</strong> appelé via Dapr.</p>

<h1 id="architecture-de-lexemple">Architecture de l’exemple</h1>

<p>On va créer deux projets :</p>

<ol>
  <li><strong>ProductService</strong> : un service gRPC qui expose un endpoint pour récupérer un produit par son ID.</li>
  <li><strong>ShopFrontend</strong> : une API REST qui appelle <code class="language-plaintext highlighter-rouge">ProductService</code> via Dapr pour afficher les détails d’un produit.</li>
</ol>

<pre><code class="language-mermaid">graph LR
    SF["ShopFrontend&lt;br/&gt;(REST)"]
    SFS["Sidecar Frontend&lt;br/&gt;(localhost)"]
    SPS["Sidecar Product&lt;br/&gt;(localhost)"]
    PS["ProductService&lt;br/&gt;(gRPC)"]
    
    SF --&gt;|HTTP| SFS
    SFS --&gt;|gRPC/mTLS| SPS
    SPS --&gt;|gRPC| PS
    
    style SF fill:#e1f5ff
    style SFS fill:#fff3e0
    style SPS fill:#fff3e0
    style PS fill:#e1f5ff
</code></pre>

<h1 id="étape-1--créer-le-service-grpc-productservice">Étape 1 : Créer le service gRPC (ProductService)</h1>

<h2 id="créer-le-projet">Créer le projet</h2>

<pre><code class="language-dotnetcli">dotnet new grpc -n ProductService
cd ProductService
</code></pre>

<h2 id="définir-le-contrat-protobuf">Définir le contrat Protobuf</h2>

<p>On remplace le fichier <code class="language-plaintext highlighter-rouge">Protos/greet.proto</code> par notre contrat <code class="language-plaintext highlighter-rouge">Protos/product.proto</code> :</p>

<div class="language-protobuf highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">syntax</span> <span class="o">=</span> <span class="s">"proto3"</span><span class="p">;</span>

<span class="k">option</span> <span class="na">csharp_namespace</span> <span class="o">=</span> <span class="s">"ProductService"</span><span class="p">;</span>

<span class="kn">package</span> <span class="nn">product</span><span class="p">;</span>

<span class="c1">// Le service exposé</span>
<span class="kd">service</span> <span class="n">ProductCatalog</span> <span class="p">{</span>
  <span class="k">rpc</span> <span class="n">GetProduct</span> <span class="p">(</span><span class="n">GetProductRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">ProductReply</span><span class="p">);</span>
  <span class="k">rpc</span> <span class="n">GetProducts</span> <span class="p">(</span><span class="n">GetProductsRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">GetProductsReply</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Requêtes</span>
<span class="kd">message</span> <span class="nc">GetProductRequest</span> <span class="p">{</span>
  <span class="kt">int32</span> <span class="na">product_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">GetProductsRequest</span> <span class="p">{</span>
  <span class="c1">// Pas de filtre pour simplifier</span>
<span class="p">}</span>

<span class="c1">// Réponses</span>
<span class="kd">message</span> <span class="nc">ProductReply</span> <span class="p">{</span>
  <span class="kt">int32</span> <span class="na">product_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">name</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">description</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
  <span class="kt">double</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
  <span class="kt">bool</span> <span class="na">in_stock</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">GetProductsReply</span> <span class="p">{</span>
  <span class="k">repeated</span> <span class="n">ProductReply</span> <span class="na">products</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="référencer-le-proto-dans-le-csproj">Référencer le proto dans le .csproj</h2>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
  <span class="nt">&lt;Protobuf</span> <span class="na">Include=</span><span class="s">"Protos\product.proto"</span> <span class="na">GrpcServices=</span><span class="s">"Server"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<h2 id="implémenter-le-service">Implémenter le service</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Grpc.Core</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ProductService</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">ProductService.Services</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductCatalogService</span> <span class="p">:</span> <span class="n">ProductCatalog</span><span class="p">.</span><span class="n">ProductCatalogBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">ProductReply</span><span class="p">&gt;</span> <span class="n">_products</span> <span class="p">=</span>
    <span class="p">[</span>
        <span class="k">new</span><span class="p">()</span> <span class="p">{</span> <span class="n">ProductId</span> <span class="p">=</span> <span class="m">1</span><span class="p">,</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Clavier mécanique"</span><span class="p">,</span> <span class="n">Description</span> <span class="p">=</span> <span class="s">"Clavier RGB switches red"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">89.99</span><span class="p">,</span> <span class="n">InStock</span> <span class="p">=</span> <span class="k">true</span> <span class="p">},</span>
        <span class="k">new</span><span class="p">()</span> <span class="p">{</span> <span class="n">ProductId</span> <span class="p">=</span> <span class="m">2</span><span class="p">,</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Souris gaming"</span><span class="p">,</span> <span class="n">Description</span> <span class="p">=</span> <span class="s">"Souris sans fil 25000 DPI"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">59.99</span><span class="p">,</span> <span class="n">InStock</span> <span class="p">=</span> <span class="k">true</span> <span class="p">},</span>
        <span class="k">new</span><span class="p">()</span> <span class="p">{</span> <span class="n">ProductId</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Écran 27 pouces"</span><span class="p">,</span> <span class="n">Description</span> <span class="p">=</span> <span class="s">"Écran 4K IPS 144Hz"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">449.99</span><span class="p">,</span> <span class="n">InStock</span> <span class="p">=</span> <span class="k">false</span> <span class="p">}</span>
    <span class="p">];</span>

    <span class="k">public</span> <span class="k">override</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductReply</span><span class="p">&gt;</span> <span class="nf">GetProduct</span><span class="p">(</span><span class="n">GetProductRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">ServerCallContext</span> <span class="n">context</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">product</span> <span class="p">=</span> <span class="n">_products</span><span class="p">.</span><span class="nf">FirstOrDefault</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">ProductId</span> <span class="p">==</span> <span class="n">request</span><span class="p">.</span><span class="n">ProductId</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">product</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">throw</span> <span class="k">new</span> <span class="nf">RpcException</span><span class="p">(</span><span class="k">new</span> <span class="nf">Status</span><span class="p">(</span><span class="n">StatusCode</span><span class="p">.</span><span class="n">NotFound</span><span class="p">,</span>
                <span class="s">$"Produit </span><span class="p">{</span><span class="n">request</span><span class="p">.</span><span class="n">ProductId</span><span class="p">}</span><span class="s"> introuvable"</span><span class="p">));</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span><span class="n">product</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">GetProductsReply</span><span class="p">&gt;</span> <span class="nf">GetProducts</span><span class="p">(</span><span class="n">GetProductsRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">ServerCallContext</span> <span class="n">context</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">reply</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">GetProductsReply</span><span class="p">();</span>
        <span class="n">reply</span><span class="p">.</span><span class="n">Products</span><span class="p">.</span><span class="nf">AddRange</span><span class="p">(</span><span class="n">_products</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="nf">FromResult</span><span class="p">(</span><span class="n">reply</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="configurer-programcs">Configurer <code class="language-plaintext highlighter-rouge">Program.cs</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddGrpc</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="n">MapGrpcService</span><span class="p">&lt;</span><span class="n">ProductCatalogService</span><span class="p">&gt;();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Le service écoute sur un port configuré dans <code class="language-plaintext highlighter-rouge">appsettings.json</code> ou <code class="language-plaintext highlighter-rouge">launchSettings.json</code>. Pour Dapr, on le fait écouter en HTTP/2 (requis par gRPC) :</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"Kestrel"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"Endpoints"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"Grpc"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
        </span><span class="nl">"Url"</span><span class="p">:</span><span class="w"> </span><span class="s2">"http://localhost:5001"</span><span class="p">,</span><span class="w">
        </span><span class="nl">"Protocols"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Http2"</span><span class="w">
      </span><span class="p">}</span><span class="w">
    </span><span class="p">}</span><span class="w">
  </span><span class="p">}</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><strong>Important</strong> : on utilise <code class="language-plaintext highlighter-rouge">http</code> (pas <code class="language-plaintext highlighter-rouge">https</code>) car c’est le sidecar Dapr qui gère le mTLS entre les services. Entre l’application et son propre sidecar, on reste en communication locale non chiffrée.</p>
</blockquote>

<h1 id="étape-2--créer-le-client-shopfrontend">Étape 2 : Créer le client (ShopFrontend)</h1>

<h2 id="créer-le-projet-1">Créer le projet</h2>

<pre><code class="language-dotnetcli">dotnet new web -n ShopFrontend
cd ShopFrontend
dotnet add package Dapr.AspNetCore
dotnet add package Google.Protobuf
dotnet add package Grpc.Net.Client
dotnet add package Grpc.Tools
</code></pre>

<h2 id="ajouter-le-proto-côté-client">Ajouter le proto côté client</h2>

<p>On copie le même fichier <code class="language-plaintext highlighter-rouge">product.proto</code> dans le projet <code class="language-plaintext highlighter-rouge">ShopFrontend/Protos/product.proto</code>, puis on le référence en mode <strong>Client</strong> dans le <code class="language-plaintext highlighter-rouge">.csproj</code> :</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
  <span class="nt">&lt;Protobuf</span> <span class="na">Include=</span><span class="s">"Protos\product.proto"</span> <span class="na">GrpcServices=</span><span class="s">"Client"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<p>Cela génère automatiquement le client gRPC typé <code class="language-plaintext highlighter-rouge">ProductCatalog.ProductCatalogClient</code>.</p>

<h2 id="configurer-programcs-1">Configurer <code class="language-plaintext highlighter-rouge">Program.cs</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Dapr.Client</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ProductService</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddDaprClient</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// GET /products → liste tous les produits via Dapr + gRPC</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/products"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">DaprClient</span> <span class="n">daprClient</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">request</span> <span class="p">=</span> <span class="n">daprClient</span><span class="p">.</span><span class="nf">CreateInvokeMethodRequest</span><span class="p">(</span>
        <span class="n">HttpMethod</span><span class="p">.</span><span class="n">Post</span><span class="p">,</span>
        <span class="s">"product-service"</span><span class="p">,</span>
        <span class="s">"product.ProductCatalog/GetProducts"</span><span class="p">);</span>

    <span class="c1">// Sérialiser le body Protobuf en bytes</span>
    <span class="kt">var</span> <span class="n">protoRequest</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">GetProductsRequest</span><span class="p">();</span>
    <span class="n">request</span><span class="p">.</span><span class="n">Content</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ByteArrayContent</span><span class="p">(</span><span class="n">protoRequest</span><span class="p">.</span><span class="nf">ToByteArray</span><span class="p">());</span>
    <span class="n">request</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">ContentType</span> <span class="p">=</span>
        <span class="k">new</span> <span class="n">System</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">Http</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">MediaTypeHeaderValue</span><span class="p">(</span><span class="s">"application/grpc"</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">daprClient</span><span class="p">.</span><span class="n">InvokeMethodAsync</span><span class="p">&lt;</span><span class="n">GetProductsReply</span><span class="p">&gt;(</span>
        <span class="s">"product-service"</span><span class="p">,</span>
        <span class="s">"product.ProductCatalog/GetProducts"</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">response</span><span class="p">.</span><span class="n">Products</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="k">new</span>
    <span class="p">{</span>
        <span class="n">p</span><span class="p">.</span><span class="n">ProductId</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Description</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Price</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">InStock</span>
    <span class="p">});</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Cependant, cette approche mélange du Protobuf manuel avec le SDK Dapr HTTP. En pratique, l’approche la plus propre est d’utiliser le <strong>proxy gRPC de Dapr</strong>.</p>

<h1 id="lapproche-recommandée--le-proxy-grpc-de-dapr">L’approche recommandée : le proxy gRPC de Dapr</h1>

<p>Le sidecar Dapr expose un <strong>proxy gRPC transparent</strong>. On crée un <code class="language-plaintext highlighter-rouge">GrpcChannel</code> qui pointe vers le sidecar, et on ajoute un header <code class="language-plaintext highlighter-rouge">dapr-app-id</code> pour indiquer le service cible. Le sidecar se charge du routage.</p>

<h2 id="configuration-du-client-grpc-via-le-proxy-dapr">Configuration du client gRPC via le proxy Dapr</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Grpc.Core</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Grpc.Net.Client</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ProductService</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="c1">// Créer le channel gRPC vers le sidecar Dapr</span>
<span class="kt">var</span> <span class="n">daprGrpcPort</span> <span class="p">=</span> <span class="n">Environment</span><span class="p">.</span><span class="nf">GetEnvironmentVariable</span><span class="p">(</span><span class="s">"DAPR_GRPC_PORT"</span><span class="p">)</span> <span class="p">??</span> <span class="s">"50001"</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">channel</span> <span class="p">=</span> <span class="n">GrpcChannel</span><span class="p">.</span><span class="nf">ForAddress</span><span class="p">(</span><span class="s">$"http://localhost:</span><span class="p">{</span><span class="n">daprGrpcPort</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="c1">// Enregistrer le client gRPC typé</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddSingleton</span><span class="p">(</span><span class="k">new</span> <span class="n">ProductCatalog</span><span class="p">.</span><span class="nf">ProductCatalogClient</span><span class="p">(</span><span class="n">channel</span><span class="p">));</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/products/{id:int}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">,</span> <span class="n">ProductCatalog</span><span class="p">.</span><span class="n">ProductCatalogClient</span> <span class="n">client</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="c1">// Ajouter le header dapr-app-id pour cibler le bon service</span>
    <span class="kt">var</span> <span class="n">headers</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Metadata</span>
    <span class="p">{</span>
        <span class="p">{</span> <span class="s">"dapr-app-id"</span><span class="p">,</span> <span class="s">"product-service"</span> <span class="p">}</span>
    <span class="p">};</span>

    <span class="k">try</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">reply</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetProductAsync</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">GetProductRequest</span> <span class="p">{</span> <span class="n">ProductId</span> <span class="p">=</span> <span class="n">id</span> <span class="p">},</span>
            <span class="n">headers</span><span class="p">);</span>

        <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="k">new</span>
        <span class="p">{</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">ProductId</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">Description</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">Price</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">InStock</span>
        <span class="p">});</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">RpcException</span> <span class="n">ex</span><span class="p">)</span> <span class="nf">when</span> <span class="p">(</span><span class="n">ex</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">StatusCode</span><span class="p">.</span><span class="n">NotFound</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">(</span><span class="k">new</span> <span class="p">{</span> <span class="n">Message</span> <span class="p">=</span> <span class="n">ex</span><span class="p">.</span><span class="n">Status</span><span class="p">.</span><span class="n">Detail</span> <span class="p">});</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/products"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">ProductCatalog</span><span class="p">.</span><span class="n">ProductCatalogClient</span> <span class="n">client</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">headers</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Metadata</span>
    <span class="p">{</span>
        <span class="p">{</span> <span class="s">"dapr-app-id"</span><span class="p">,</span> <span class="s">"product-service"</span> <span class="p">}</span>
    <span class="p">};</span>

    <span class="kt">var</span> <span class="n">reply</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetProductsAsync</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">GetProductsRequest</span><span class="p">(),</span>
        <span class="n">headers</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">reply</span><span class="p">.</span><span class="n">Products</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="k">new</span>
    <span class="p">{</span>
        <span class="n">p</span><span class="p">.</span><span class="n">ProductId</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Description</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Price</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">InStock</span>
    <span class="p">});</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>C’est l’approche <strong>la plus propre</strong> : on utilise le client gRPC généré par Protobuf tel quel, avec les contrats fortement typés, et Dapr gère le routage via le header <code class="language-plaintext highlighter-rouge">dapr-app-id</code>.</p>

<h2 id="simplifier-avec-ihttpclientfactory-et-grpcclientfactory">Simplifier avec <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code> et <code class="language-plaintext highlighter-rouge">GrpcClientFactory</code></h2>

<p>Pour une intégration plus idiomatique avec le conteneur DI de .NET :</p>

<pre><code class="language-dotnetcli">dotnet add package Grpc.Net.ClientFactory
</code></pre>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Grpc.Core</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ProductService</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">daprGrpcPort</span> <span class="p">=</span> <span class="n">Environment</span><span class="p">.</span><span class="nf">GetEnvironmentVariable</span><span class="p">(</span><span class="s">"DAPR_GRPC_PORT"</span><span class="p">)</span> <span class="p">??</span> <span class="s">"50001"</span><span class="p">;</span>

<span class="c1">// Enregistrer le client gRPC via la factory</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span>
    <span class="p">.</span><span class="n">AddGrpcClient</span><span class="p">&lt;</span><span class="n">ProductCatalog</span><span class="p">.</span><span class="n">ProductCatalogClient</span><span class="p">&gt;(</span><span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">options</span><span class="p">.</span><span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">$"http://localhost:</span><span class="p">{</span><span class="n">daprGrpcPort</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddCallCredentials</span><span class="p">((</span><span class="n">context</span><span class="p">,</span> <span class="n">metadata</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="c1">// Ajouter le header dapr-app-id automatiquement à chaque appel</span>
        <span class="n">metadata</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"dapr-app-id"</span><span class="p">,</span> <span class="s">"product-service"</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
    <span class="p">});</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// Les endpoints sont plus simples : plus besoin de gérer le header manuellement</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/products/{id:int}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">,</span> <span class="n">ProductCatalog</span><span class="p">.</span><span class="n">ProductCatalogClient</span> <span class="n">client</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">try</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">reply</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetProductAsync</span><span class="p">(</span>
            <span class="k">new</span> <span class="n">GetProductRequest</span> <span class="p">{</span> <span class="n">ProductId</span> <span class="p">=</span> <span class="n">id</span> <span class="p">});</span>

        <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="k">new</span>
        <span class="p">{</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">ProductId</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">Description</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">Price</span><span class="p">,</span>
            <span class="n">reply</span><span class="p">.</span><span class="n">InStock</span>
        <span class="p">});</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">RpcException</span> <span class="n">ex</span><span class="p">)</span> <span class="nf">when</span> <span class="p">(</span><span class="n">ex</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">StatusCode</span><span class="p">.</span><span class="n">NotFound</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">(</span><span class="k">new</span> <span class="p">{</span> <span class="n">Message</span> <span class="p">=</span> <span class="n">ex</span><span class="p">.</span><span class="n">Status</span><span class="p">.</span><span class="n">Detail</span> <span class="p">});</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/products"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">ProductCatalog</span><span class="p">.</span><span class="n">ProductCatalogClient</span> <span class="n">client</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">reply</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetProductsAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">GetProductsRequest</span><span class="p">());</span>

    <span class="k">return</span> <span class="n">reply</span><span class="p">.</span><span class="n">Products</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="k">new</span>
    <span class="p">{</span>
        <span class="n">p</span><span class="p">.</span><span class="n">ProductId</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Description</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">Price</span><span class="p">,</span>
        <span class="n">p</span><span class="p">.</span><span class="n">InStock</span>
    <span class="p">});</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Avec <code class="language-plaintext highlighter-rouge">AddGrpcClient</code> + <code class="language-plaintext highlighter-rouge">AddCallCredentials</code>, le header <code class="language-plaintext highlighter-rouge">dapr-app-id</code> est injecté automatiquement dans chaque appel. Le code des endpoints est propre et ne contient plus aucune référence à Dapr.</p>

<h1 id="lancement-de-lexemple">Lancement de l’exemple</h1>

<h2 id="en-local-avec-le-cli-dapr">En local avec le CLI Dapr</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal 1 : lancer le service gRPC</span>
dapr run <span class="nt">--app-id</span> product-service <span class="se">\</span>
         <span class="nt">--app-port</span> 5001 <span class="se">\</span>
         <span class="nt">--app-protocol</span> grpc <span class="se">\</span>
         <span class="nt">--</span> dotnet run <span class="nt">--project</span> ProductService

<span class="c"># Terminal 2 : lancer le frontend</span>
dapr run <span class="nt">--app-id</span> shop-frontend <span class="se">\</span>
         <span class="nt">--app-port</span> 5000 <span class="se">\</span>
         <span class="nt">--</span> dotnet run <span class="nt">--project</span> ShopFrontend
</code></pre></div></div>

<p>Le flag <code class="language-plaintext highlighter-rouge">--app-protocol grpc</code> est essentiel pour le service cible : il indique au sidecar que l’application derrière lui parle gRPC (et non HTTP).</p>

<h2 id="tester">Tester</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Récupérer tous les produits</span>
curl http://localhost:5000/products

<span class="c"># Récupérer un produit par ID</span>
curl http://localhost:5000/products/1

<span class="c"># Produit inexistant → 404</span>
curl http://localhost:5000/products/99
</code></pre></div></div>

<h2 id="avec-net-aspire">Avec .NET Aspire</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">DistributedApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">productService</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="n">AddProject</span><span class="p">&lt;</span><span class="n">Projects</span><span class="p">.</span><span class="n">ProductService</span><span class="p">&gt;(</span><span class="s">"product-service"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithDaprSidecar</span><span class="p">(</span><span class="k">new</span> <span class="n">DaprSidecarOptions</span>
    <span class="p">{</span>
        <span class="n">AppProtocol</span> <span class="p">=</span> <span class="s">"grpc"</span>
    <span class="p">});</span>

<span class="n">builder</span><span class="p">.</span><span class="n">AddProject</span><span class="p">&lt;</span><span class="n">Projects</span><span class="p">.</span><span class="n">ShopFrontend</span><span class="p">&gt;(</span><span class="s">"shop-frontend"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithDaprSidecar</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithReference</span><span class="p">(</span><span class="n">productService</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">().</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<h1 id="partager-le-fichier-proto-entre-projets">Partager le fichier proto entre projets</h1>

<p>Pour éviter de copier le <code class="language-plaintext highlighter-rouge">.proto</code> manuellement, une bonne pratique est de créer un projet partagé :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Solution/
├── Protos/
│   └── product.proto
├── ProductService/
│   └── ProductService.csproj
├── ShopFrontend/
│   └── ShopFrontend.csproj
</code></pre></div></div>

<p>Dans chaque <code class="language-plaintext highlighter-rouge">.csproj</code>, on référence le proto avec un chemin relatif :</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- ProductService.csproj --&gt;</span>
<span class="nt">&lt;ItemGroup&gt;</span>
  <span class="nt">&lt;Protobuf</span> <span class="na">Include=</span><span class="s">"..\Protos\product.proto"</span> <span class="na">GrpcServices=</span><span class="s">"Server"</span>
            <span class="na">Link=</span><span class="s">"Protos\product.proto"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>

<span class="c">&lt;!-- ShopFrontend.csproj --&gt;</span>
<span class="nt">&lt;ItemGroup&gt;</span>
  <span class="nt">&lt;Protobuf</span> <span class="na">Include=</span><span class="s">"..\Protos\product.proto"</span> <span class="na">GrpcServices=</span><span class="s">"Client"</span>
            <span class="na">Link=</span><span class="s">"Protos\product.proto"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>
</code></pre></div></div>

<p>Ainsi, toute modification du contrat est reflétée des deux côtés à la compilation.</p>

<h1 id="gestion-des-erreurs-grpc">Gestion des erreurs gRPC</h1>

<p>Les erreurs gRPC sont représentées par des <code class="language-plaintext highlighter-rouge">StatusCode</code> (similaires aux codes HTTP). Dapr les propage fidèlement :</p>

<table>
  <thead>
    <tr>
      <th>StatusCode gRPC</th>
      <th>Signification</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">OK</code></td>
      <td>Succès</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">NotFound</code></td>
      <td>Ressource introuvable</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">InvalidArgument</code></td>
      <td>Paramètre invalide</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">PermissionDenied</code></td>
      <td>Accès refusé</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Internal</code></td>
      <td>Erreur interne du service</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Unavailable</code></td>
      <td>Service indisponible</td>
    </tr>
  </tbody>
</table>

<p>Côté service, on lève une <code class="language-plaintext highlighter-rouge">RpcException</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">throw</span> <span class="k">new</span> <span class="nf">RpcException</span><span class="p">(</span><span class="k">new</span> <span class="nf">Status</span><span class="p">(</span><span class="n">StatusCode</span><span class="p">.</span><span class="n">NotFound</span><span class="p">,</span> <span class="s">"Produit introuvable"</span><span class="p">));</span>
</code></pre></div></div>

<p>Côté client, on la capte :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">try</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">reply</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetProductAsync</span><span class="p">(</span><span class="n">request</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">RpcException</span> <span class="n">ex</span><span class="p">)</span> <span class="nf">when</span> <span class="p">(</span><span class="n">ex</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">StatusCode</span><span class="p">.</span><span class="n">NotFound</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Gérer le 404</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">RpcException</span> <span class="n">ex</span><span class="p">)</span> <span class="nf">when</span> <span class="p">(</span><span class="n">ex</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">StatusCode</span><span class="p">.</span><span class="n">Unavailable</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Le service est down</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="résilience">Résilience</h1>

<p>Les mêmes politiques de résilience Dapr (retry, circuit breaker, timeout) s’appliquent aux appels gRPC :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Resiliency</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">resiliency</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">policies</span><span class="pi">:</span>
    <span class="na">retries</span><span class="pi">:</span>
      <span class="na">grpcRetry</span><span class="pi">:</span>
        <span class="na">policy</span><span class="pi">:</span> <span class="s">constant</span>
        <span class="na">duration</span><span class="pi">:</span> <span class="s">2s</span>
        <span class="na">maxRetries</span><span class="pi">:</span> <span class="m">3</span>
    <span class="na">timeouts</span><span class="pi">:</span>
      <span class="na">grpcTimeout</span><span class="pi">:</span> <span class="s">10s</span>
  <span class="na">targets</span><span class="pi">:</span>
    <span class="na">apps</span><span class="pi">:</span>
      <span class="na">product-service</span><span class="pi">:</span>
        <span class="na">retry</span><span class="pi">:</span> <span class="s">grpcRetry</span>
        <span class="na">timeout</span><span class="pi">:</span> <span class="s">grpcTimeout</span>
</code></pre></div></div>

<h1 id="http-vs-grpc--quand-choisir-quoi-">HTTP vs gRPC : quand choisir quoi ?</h1>

<table>
  <thead>
    <tr>
      <th>Critère</th>
      <th>HTTP/REST</th>
      <th>gRPC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Contrat</strong></td>
      <td>Loose (JSON, OpenAPI optionnel)</td>
      <td>Strict (Protobuf, <code class="language-plaintext highlighter-rouge">.proto</code> obligatoire)</td>
    </tr>
    <tr>
      <td><strong>Performance</strong></td>
      <td>Bon (JSON texte)</td>
      <td>Excellent (binaire, HTTP/2)</td>
    </tr>
    <tr>
      <td><strong>Streaming</strong></td>
      <td>Limité (SSE, WebSocket)</td>
      <td>Natif (unary, server, client, bidirectionnel)</td>
    </tr>
    <tr>
      <td><strong>Outillage navigateur</strong></td>
      <td>curl, Postman, fetch</td>
      <td>Nécessite grpcurl ou grpc-web</td>
    </tr>
    <tr>
      <td><strong>Interopérabilité</strong></td>
      <td>Universelle</td>
      <td>Nécessite un client Protobuf</td>
    </tr>
    <tr>
      <td><strong>Cas d’usage</strong></td>
      <td>APIs publiques, frontend</td>
      <td>Communication inter-services</td>
    </tr>
  </tbody>
</table>

<p>En règle générale :</p>
<ul>
  <li><strong>HTTP/REST</strong> pour les APIs exposées aux clients externes (navigateurs, apps mobiles).</li>
  <li><strong>gRPC</strong> pour la <strong>communication inter-services</strong> interne où la performance et les contrats typés comptent.</li>
</ul>

<p>Dapr permet de mixer les deux dans la même architecture, chaque service choisissant son protocole indépendamment.</p>

<h1 id="résumé">Résumé</h1>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Protocole</strong></td>
      <td>gRPC natif via le proxy du sidecar Dapr</td>
    </tr>
    <tr>
      <td><strong>Routage</strong></td>
      <td>Header <code class="language-plaintext highlighter-rouge">dapr-app-id</code> sur le channel gRPC</td>
    </tr>
    <tr>
      <td><strong>Flag service</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--app-protocol grpc</code> au lancement du service cible</td>
    </tr>
    <tr>
      <td><strong>Client .NET</strong></td>
      <td><code class="language-plaintext highlighter-rouge">GrpcChannel</code> + client Protobuf généré, ou <code class="language-plaintext highlighter-rouge">AddGrpcClient</code> avec factory</td>
    </tr>
    <tr>
      <td><strong>Contrat</strong></td>
      <td>Fichier <code class="language-plaintext highlighter-rouge">.proto</code> partagé entre client et serveur</td>
    </tr>
    <tr>
      <td><strong>Résilience</strong></td>
      <td>Retry, circuit breaker, timeout via config YAML Dapr</td>
    </tr>
    <tr>
      <td><strong>Sécurité</strong></td>
      <td>mTLS automatique entre sidecars</td>
    </tr>
  </tbody>
</table>

<p>L’invocation gRPC via Dapr combine le meilleur des deux mondes : les <strong>contrats typés et les performances de gRPC</strong> avec la <strong>simplicité opérationnelle de Dapr</strong> (découverte, résilience, sécurité, observabilité), le tout sans couplage direct entre les services.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="dapr" /><category term="microservices" /><category term="grpc" /><category term="service-invocation" /><summary type="html"><![CDATA[Dans l’article précédent, nous avons vu comment Dapr simplifie l’invocation de service via HTTP. Mais Dapr supporte aussi nativement gRPC, à la fois entre les sidecars et entre votre application et son sidecar. gRPC apporte des avantages significatifs : sérialisation binaire (Protobuf), contrats fortement typés, streaming, et de meilleures performances. Dans cet article, on met en place un exemple complet : un service gRPC invoqué via Dapr, et un client qui l’appelle.]]></summary></entry><entry><title type="html">Présentation de Dapr : le runtime pour applications distribuées</title><link href="http://guym.fr/2026/02/21/dapr-presentation.html" rel="alternate" type="text/html" title="Présentation de Dapr : le runtime pour applications distribuées" /><published>2026-02-21T00:00:00+01:00</published><updated>2026-02-21T00:00:00+01:00</updated><id>http://guym.fr/2026/02/21/dapr-presentation</id><content type="html" xml:base="http://guym.fr/2026/02/21/dapr-presentation.html"><![CDATA[<p>Dapr (Distributed Application Runtime) est un runtime open source, porté par Microsoft et incubé à la CNCF, qui simplifie le développement d’applications distribuées (microservices, applications cloud-native). Il fournit un ensemble de <strong>building blocks</strong> — invocation de services, gestion d’état, pub/sub, bindings, secrets, etc. — accessibles via des API HTTP ou gRPC, quel que soit le langage ou le framework utilisé. L’objectif : permettre aux développeurs de se concentrer sur la logique métier, sans se noyer dans la plomberie de l’infrastructure.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Dapr pour les développeurs .NET</strong> : <strong> 1</strong> sur <strong>3</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2026/02/22/dapr-service-invocation-1.html">Dapr : l'invocation de service en .NET</a>
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2026/02/22/dapr-service-invocation-2-grpc.html">Dapr : invocation de service gRPC en .NET</a>
		
		</li>
	
	
	</ul>
</div>

<h1 id="pourquoi-dapr-">Pourquoi Dapr ?</h1>

<p>Quand on développe des microservices, on se retrouve rapidement face à des problèmes récurrents :</p>

<ul>
  <li><strong>Communication inter-services</strong> : comment appeler un autre service de façon fiable, avec retry, circuit breaker, etc. ?</li>
  <li><strong>Gestion d’état</strong> : comment stocker et retrouver l’état d’un service de manière distribuée ?</li>
  <li><strong>Pub/Sub</strong> : comment publier et consommer des événements entre services ?</li>
  <li><strong>Secrets</strong> : comment accéder aux secrets de manière sécurisée ?</li>
  <li><strong>Observabilité</strong> : comment tracer les appels entre services ?</li>
</ul>

<p>Chaque problème a ses propres bibliothèques, SDK, et configurations spécifiques selon le broker, la base de données, le fournisseur de secrets, etc. Dapr propose une <strong>couche d’abstraction unifiée</strong> qui résout tous ces problèmes via des API standardisées, indépendantes de l’implémentation sous-jacente.</p>

<h1 id="architecture-de-dapr">Architecture de Dapr</h1>

<p>Dapr fonctionne en <strong>sidecar</strong> : un processus léger (<code class="language-plaintext highlighter-rouge">daprd</code>) tourne à côté de chaque instance de votre application. Votre code communique avec le sidecar via HTTP ou gRPC sur <code class="language-plaintext highlighter-rouge">localhost</code>, et c’est le sidecar qui gère toute la complexité (réseau, retry, sérialisation, connexion au broker, etc.).</p>

<pre><code class="language-mermaid">graph LR
    App["Application&lt;br/&gt;(.NET, Go, Python...)"]
    Sidecar["Sidecar Dapr&lt;br/&gt;(daprd)"]
    Components["Composants externes&lt;br/&gt;(Redis, Kafka, Azure&lt;br/&gt;Service Bus, etc.)"]
    
    App --&gt;|HTTP/gRPC&lt;br/&gt;localhost| Sidecar
    Sidecar --&gt;|Gère la communication| Components
    
    style App fill:#4A90E2
    style Sidecar fill:#F5A623
    style Components fill:#7ED321
</code></pre>

<h3 id="avantages-du-modèle-sidecar">Avantages du modèle sidecar</h3>

<ul>
  <li><strong>Agnostique au langage</strong> : peu importe que votre service soit en C#, Go, Python ou Java, l’API est la même.</li>
  <li><strong>Découplage</strong> : votre code ne dépend pas directement des SDK de Redis, Kafka, RabbitMQ, etc.</li>
  <li><strong>Portabilité</strong> : passez d’un composant à l’autre (ex. : Redis → Azure Cosmos DB) en changeant simplement un fichier de configuration YAML.</li>
  <li><strong>Kubernetes-native</strong> : Dapr s’intègre naturellement dans Kubernetes, mais fonctionne aussi en local (standalone).</li>
</ul>

<h1 id="les-building-blocks">Les Building Blocks</h1>

<p>Dapr expose ses fonctionnalités sous forme de <strong>building blocks</strong>, chacun résolvant un problème courant des systèmes distribués :</p>

<h2 id="1-invocation-de-service-service-invocation">1. Invocation de service (Service Invocation)</h2>

<p>Appeler un autre service par son nom, avec découverte automatique, load balancing, retry et mTLS intégrés.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST http://localhost:3500/v1.0/invoke/order-service/method/create
Content-Type: application/json

{
  "productId": 42,
  "quantity": 2
}
</span></code></pre></div></div>

<p>En .NET avec le SDK Dapr :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">DaprClientBuilder</span><span class="p">().</span><span class="nf">Build</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Order</span> <span class="p">{</span> <span class="n">ProductId</span> <span class="p">=</span> <span class="m">42</span><span class="p">,</span> <span class="n">Quantity</span> <span class="p">=</span> <span class="m">2</span> <span class="p">};</span>
<span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="n">InvokeMethodAsync</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">,</span> <span class="n">OrderConfirmation</span><span class="p">&gt;(</span>
    <span class="s">"order-service"</span><span class="p">,</span> <span class="s">"create"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="2-gestion-détat-state-management">2. Gestion d’état (State Management)</h2>

<p>Stocker et lire de l’état clé/valeur avec des garanties de concurrence optimiste (ETags).</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Sauvegarder un état</span>
<span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">SaveStateAsync</span><span class="p">(</span><span class="s">"statestore"</span><span class="p">,</span> <span class="s">"order-42"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>

<span class="c1">// Lire un état</span>
<span class="kt">var</span> <span class="n">saved</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;(</span><span class="s">"statestore"</span><span class="p">,</span> <span class="s">"order-42"</span><span class="p">);</span>

<span class="c1">// Supprimer un état</span>
<span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">DeleteStateAsync</span><span class="p">(</span><span class="s">"statestore"</span><span class="p">,</span> <span class="s">"order-42"</span><span class="p">);</span>
</code></pre></div></div>

<p>Le composant <code class="language-plaintext highlighter-rouge">statestore</code> est configuré dans un fichier YAML. On peut utiliser Redis, PostgreSQL, Azure Cosmos DB, etc., sans changer une ligne de code :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Component</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">statestore</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">state.redis</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">redisHost</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">localhost:6379"</span>
</code></pre></div></div>

<h2 id="3-pubsub-publish--subscribe">3. Pub/Sub (Publish &amp; Subscribe)</h2>

<p>Publier et consommer des événements de manière asynchrone, avec garantie de livraison (at-least-once).</p>

<h3 id="publier-un-événement">Publier un événement</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">PublishEventAsync</span><span class="p">(</span><span class="s">"pubsub"</span><span class="p">,</span> <span class="s">"orders"</span><span class="p">,</span> <span class="k">new</span> <span class="n">OrderCreated</span>
<span class="p">{</span>
    <span class="n">OrderId</span> <span class="p">=</span> <span class="m">42</span><span class="p">,</span>
    <span class="n">CreatedAt</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="sabonner-à-un-topic">S’abonner à un topic</h3>

<p>En ASP.NET Core, il suffit d’annoter un endpoint :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/orders"</span><span class="p">,</span> <span class="p">[</span><span class="nf">Topic</span><span class="p">(</span><span class="s">"pubsub"</span><span class="p">,</span> <span class="s">"orders"</span><span class="p">)]</span> <span class="p">(</span><span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Commande reçue : </span><span class="p">{</span><span class="n">evt</span><span class="p">.</span><span class="n">OrderId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Le broker sous-jacent (RabbitMQ, Kafka, Azure Service Bus, Redis Streams…) est interchangeable via la configuration :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Component</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">pubsub</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">type</span><span class="pi">:</span> <span class="s">pubsub.rabbitmq</span>
  <span class="na">version</span><span class="pi">:</span> <span class="s">v1</span>
  <span class="na">metadata</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">connectionString</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">amqp://guest:guest@localhost:5672"</span>
</code></pre></div></div>

<h2 id="4-bindings-inputoutput-bindings">4. Bindings (Input/Output Bindings)</h2>

<p>Permet de connecter votre application à des systèmes externes (files de messages, CRON, bases de données, services cloud) en tant que source d’événements (input) ou destination (output), sans SDK spécifique.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Output binding : envoyer un email via SendGrid</span>
<span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">InvokeBindingAsync</span><span class="p">(</span><span class="s">"sendgrid"</span><span class="p">,</span> <span class="s">"create"</span><span class="p">,</span> <span class="k">new</span>
<span class="p">{</span>
    <span class="n">To</span> <span class="p">=</span> <span class="s">"user@example.com"</span><span class="p">,</span>
    <span class="n">Subject</span> <span class="p">=</span> <span class="s">"Confirmation"</span><span class="p">,</span>
    <span class="n">Body</span> <span class="p">=</span> <span class="s">"Votre commande a été validée."</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="5-gestion-des-secrets-secrets-management">5. Gestion des secrets (Secrets Management)</h2>

<p>Accéder à des secrets stockés dans un coffre-fort (Azure Key Vault, HashiCorp Vault, Kubernetes Secrets, fichier local, etc.) via une API unifiée.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">secret</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetSecretAsync</span><span class="p">(</span><span class="s">"vault"</span><span class="p">,</span> <span class="s">"db-connection-string"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">connectionString</span> <span class="p">=</span> <span class="n">secret</span><span class="p">[</span><span class="s">"db-connection-string"</span><span class="p">];</span>
</code></pre></div></div>

<h2 id="6-observabilité">6. Observabilité</h2>

<p>Dapr génère automatiquement des traces distribuées (OpenTelemetry), des métriques et des logs pour chaque appel inter-services, sans instrumentation manuelle. Il suffit de configurer un exporteur :</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">apiVersion</span><span class="pi">:</span> <span class="s">dapr.io/v1alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Configuration</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">daprConfig</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">tracing</span><span class="pi">:</span>
    <span class="na">samplingRate</span><span class="pi">:</span> <span class="s2">"</span><span class="s">1"</span>
    <span class="na">otel</span><span class="pi">:</span>
      <span class="na">endpointAddress</span><span class="pi">:</span> <span class="s2">"</span><span class="s">http://otel-collector:4317"</span>
      <span class="na">isSecure</span><span class="pi">:</span> <span class="no">false</span>
      <span class="na">protocol</span><span class="pi">:</span> <span class="s">grpc</span>
</code></pre></div></div>

<h2 id="7-acteurs-actors">7. Acteurs (Actors)</h2>

<p>Dapr implémente le modèle d’acteur virtuel (inspiré d’Orléans) : chaque acteur est un objet mono-thread avec état intégré, activé à la demande.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IOrderActor</span> <span class="p">:</span> <span class="n">IActor</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="nf">SubmitOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">);</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">OrderStatus</span><span class="p">&gt;</span> <span class="nf">GetStatus</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">OrderActor</span> <span class="p">:</span> <span class="n">Actor</span><span class="p">,</span> <span class="n">IOrderActor</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="nf">OrderActor</span><span class="p">(</span><span class="n">ActorHost</span> <span class="n">host</span><span class="p">)</span> <span class="p">:</span> <span class="k">base</span><span class="p">(</span><span class="n">host</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">SubmitOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">StateManager</span><span class="p">.</span><span class="nf">SetStateAsync</span><span class="p">(</span><span class="s">"order"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">OrderStatus</span><span class="p">&gt;</span> <span class="nf">GetStatus</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">StateManager</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">OrderStatus</span><span class="p">&gt;(</span><span class="s">"order"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="mise-en-place-en-net">Mise en place en .NET</h1>

<h2 id="installation-du-cli-dapr">Installation du CLI Dapr</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Windows (PowerShell)</span>
powershell <span class="nt">-Command</span> <span class="s2">"iwr -useb https://raw.githubusercontent.com/dapr/cli/master/install/install.ps1 | iex"</span>

<span class="c"># Initialisation (lance les conteneurs Redis, Zipkin, etc.)</span>
dapr init
</code></pre></div></div>

<h2 id="ajouter-le-sdk-dapr-à-un-projet-aspnet-core">Ajouter le SDK Dapr à un projet ASP.NET Core</h2>

<pre><code class="language-dotnetcli">dotnet add package Dapr.AspNetCore
</code></pre>

<h2 id="configurer-le-service">Configurer le service</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">WebApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="c1">// Ajouter les services Dapr</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddDaprClient</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">app</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// Activer le middleware Dapr (CloudEvents, Pub/Sub)</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseCloudEvents</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapSubscribeHandler</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/orders"</span><span class="p">,</span> <span class="p">[</span><span class="nf">Topic</span><span class="p">(</span><span class="s">"pubsub"</span><span class="p">,</span> <span class="s">"orders"</span><span class="p">)]</span> <span class="p">(</span><span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Commande reçue : </span><span class="p">{</span><span class="n">evt</span><span class="p">.</span><span class="n">OrderId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">();</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="lancer-lapplication-avec-dapr">Lancer l’application avec Dapr</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dapr run <span class="nt">--app-id</span> my-service <span class="nt">--app-port</span> 5000 <span class="nt">--</span> dotnet run
</code></pre></div></div>

<p>Cette commande lance le sidecar Dapr à côté de votre application, lui attribuant l’identifiant <code class="language-plaintext highlighter-rouge">my-service</code> et écoutant sur le port 5000.</p>

<h1 id="dapr-et-aspire">Dapr et Aspire</h1>

<p>Pour ceux qui utilisent <strong>.NET Aspire</strong> (l’orchestrateur de développement local pour .NET), Dapr s’intègre facilement via le package <code class="language-plaintext highlighter-rouge">Aspire.Hosting.Dapr</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">builder</span> <span class="p">=</span> <span class="n">DistributedApplication</span><span class="p">.</span><span class="nf">CreateBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">stateStore</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">AddDaprStateStore</span><span class="p">(</span><span class="s">"statestore"</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">pubSub</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="nf">AddDaprPubSub</span><span class="p">(</span><span class="s">"pubsub"</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="n">AddProject</span><span class="p">&lt;</span><span class="n">Projects</span><span class="p">.</span><span class="n">OrderService</span><span class="p">&gt;(</span><span class="s">"order-service"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithDaprSidecar</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithReference</span><span class="p">(</span><span class="n">stateStore</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithReference</span><span class="p">(</span><span class="n">pubSub</span><span class="p">);</span>

<span class="n">builder</span><span class="p">.</span><span class="nf">Build</span><span class="p">().</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<p>Aspire gère automatiquement le lancement des sidecars Dapr et des composants associés dans l’environnement de développement.</p>

<h1 id="composants-disponibles">Composants disponibles</h1>

<p>L’un des grands atouts de Dapr est l’écosystème de <strong>composants</strong> interchangeables. Voici quelques exemples :</p>

<table>
  <thead>
    <tr>
      <th>Building Block</th>
      <th>Composants disponibles</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>State Store</td>
      <td>Redis, PostgreSQL, Azure Cosmos DB, MongoDB, MySQL, DynamoDB…</td>
    </tr>
    <tr>
      <td>Pub/Sub</td>
      <td>RabbitMQ, Kafka, Azure Service Bus, Redis Streams, Pulsar…</td>
    </tr>
    <tr>
      <td>Secrets</td>
      <td>Azure Key Vault, HashiCorp Vault, Kubernetes Secrets, Local…</td>
    </tr>
    <tr>
      <td>Bindings</td>
      <td>Cron, SendGrid, Twilio, Azure Blob Storage, S3, SMTP…</td>
    </tr>
  </tbody>
</table>

<p>Pour passer d’un composant à un autre, il suffit de modifier le fichier YAML de configuration, <strong>sans toucher au code applicatif</strong>.</p>

<h1 id="quand-utiliser-dapr-">Quand utiliser Dapr ?</h1>

<p>Dapr est particulièrement pertinent quand :</p>

<ul>
  <li>Vous développez une <strong>architecture microservices</strong> et souhaitez standardiser la communication inter-services.</li>
  <li>Vous voulez <strong>découpler votre code</strong> des implémentations d’infrastructure (brokers, bases de données, secrets).</li>
  <li>Vous cherchez une solution <strong>portable</strong> entre environnements (local, Kubernetes, cloud).</li>
  <li>Vous voulez bénéficier de <strong>résilience intégrée</strong> (retry, circuit breaker, timeout) sans implémenter ces patterns manuellement.</li>
  <li>Vous avez des services écrits dans <strong>différents langages</strong> qui doivent communiquer ensemble.</li>
</ul>

<p>Dapr n’est en revanche <strong>pas nécessaire</strong> pour :</p>

<ul>
  <li>Une application monolithique simple.</li>
  <li>Un projet où vous n’avez qu’un seul service sans communication externe complexe.</li>
  <li>Des cas où vous maîtrisez parfaitement vos dépendances d’infrastructure et ne souhaitez pas de couche d’abstraction supplémentaire.</li>
</ul>

<h1 id="résumé">Résumé</h1>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Modèle</strong></td>
      <td>Sidecar (processus à côté de l’application)</td>
    </tr>
    <tr>
      <td><strong>API</strong></td>
      <td>HTTP et gRPC sur localhost</td>
    </tr>
    <tr>
      <td><strong>Building blocks</strong></td>
      <td>Invocation, state, pub/sub, bindings, secrets, acteurs, config…</td>
    </tr>
    <tr>
      <td><strong>Portabilité</strong></td>
      <td>Composants interchangeables via configuration YAML</td>
    </tr>
    <tr>
      <td><strong>Environnements</strong></td>
      <td>Local (standalone), Kubernetes, cloud</td>
    </tr>
    <tr>
      <td><strong>Observabilité</strong></td>
      <td>Traces distribuées, métriques et logs via OpenTelemetry</td>
    </tr>
    <tr>
      <td><strong>Intégration .NET</strong></td>
      <td>SDK Dapr.AspNetCore + support Aspire</td>
    </tr>
  </tbody>
</table>

<p>Dapr simplifie considérablement le développement d’applications distribuées en fournissant des abstractions prêtes à l’emploi, tout en laissant la liberté de choisir et de changer les composants d’infrastructure sous-jacents.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="dapr" /><category term="microservices" /><category term="architecture" /><summary type="html"><![CDATA[Dapr (Distributed Application Runtime) est un runtime open source, porté par Microsoft et incubé à la CNCF, qui simplifie le développement d’applications distribuées (microservices, applications cloud-native). Il fournit un ensemble de building blocks — invocation de services, gestion d’état, pub/sub, bindings, secrets, etc. — accessibles via des API HTTP ou gRPC, quel que soit le langage ou le framework utilisé. L’objectif : permettre aux développeurs de se concentrer sur la logique métier, sans se noyer dans la plomberie de l’infrastructure.]]></summary></entry><entry><title type="html">Polly en .NET : résilience et tolérance aux pannes</title><link href="http://guym.fr/2026/02/15/polly.html" rel="alternate" type="text/html" title="Polly en .NET : résilience et tolérance aux pannes" /><published>2026-02-15T00:00:00+01:00</published><updated>2026-02-15T00:00:00+01:00</updated><id>http://guym.fr/2026/02/15/polly</id><content type="html" xml:base="http://guym.fr/2026/02/15/polly.html"><![CDATA[<p>En production, les appels réseau échouent : timeouts, erreurs 503, connexions refusées. Plutôt que de laisser ces erreurs transitoires remonter jusqu’à l’utilisateur, <strong>Polly</strong> permet de définir des stratégies de résilience — retry, circuit breaker, timeout, fallback — de manière déclarative. Depuis .NET 8, Polly v8 et <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code> s’intègrent nativement dans l’écosystème .NET.</p>

<!--more-->

<h1 id="pourquoi-la-résilience-">Pourquoi la résilience ?</h1>

<h2 id="le-problème--les-erreurs-transitoires">Le problème : les erreurs transitoires</h2>

<p>Dans une architecture distribuée (microservices, APIs externes, bases de données), les erreurs <strong>transitoires</strong> sont inévitables :</p>

<ul>
  <li>Un service redémarre → erreur 503 pendant 2 secondes.</li>
  <li>Le réseau est saturé → timeout.</li>
  <li>La base de données est surchargée → connexion refusée.</li>
</ul>

<p>Sans stratégie de résilience, chaque erreur transitoire se propage en cascade :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Sans résilience : une erreur réseau = une erreur utilisateur</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">$"/api/clients/</span><span class="p">{</span><span class="n">id</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span> <span class="c1">// lève une exception si 5xx</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="n">ReadFromJsonAsync</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;();</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="la-solution--des-stratégies-de-résilience">La solution : des stratégies de résilience</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Avec Polly : retry automatique sur erreurs transitoires</span>
<span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ResiliencePipelineBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
        <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromMilliseconds</span><span class="p">(</span><span class="m">500</span><span class="p">),</span>
        <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span>
        <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">ServiceUnavailable</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h1 id="installation">Installation</h1>

<p>Polly v8 est distribué via deux packages principaux :</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Package de base (stratégies de résilience)</span><span class="w">
</span><span class="n">dotnet</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">package</span><span class="w"> </span><span class="nx">Polly.Core</span><span class="w">

</span><span class="c"># Intégration avec HttpClientFactory (recommandé pour les appels HTTP)</span><span class="w">
</span><span class="n">dotnet</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">package</span><span class="w"> </span><span class="nx">Microsoft.Extensions.Http.Resilience</span><span class="w">

</span><span class="c"># Intégration avec l'injection de dépendances</span><span class="w">
</span><span class="n">dotnet</span><span class="w"> </span><span class="nx">add</span><span class="w"> </span><span class="nx">package</span><span class="w"> </span><span class="nx">Microsoft.Extensions.Resilience</span><span class="w">
</span></code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code> est le package officiel de Microsoft qui intègre Polly v8 avec <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code>. C’est le point d’entrée recommandé pour les appels HTTP.</p>
</blockquote>

<h1 id="les-stratégies-de-résilience">Les stratégies de résilience</h1>

<h2 id="1-retry--réessayer-automatiquement">1. Retry — Réessayer automatiquement</h2>

<p>La stratégie la plus courante : réessayer l’opération après un délai.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>               <span class="c1">// 3 tentatives max</span>
        <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">),</span>    <span class="c1">// délai initial</span>
        <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span> <span class="c1">// 1s, 2s, 4s</span>
        <span class="n">UseJitter</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>                   <span class="c1">// ajoute un aléa pour éviter les "thundering herds"</span>
        <span class="n">OnRetry</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Retry #</span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">AttemptNumber</span><span class="p">}</span><span class="s"> après </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">RetryDelay</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// Utilisation</span>
<span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">pipeline</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">(</span><span class="k">async</span> <span class="n">ct</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="s">"https://api.example.com/data"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="types-de-backoff">Types de backoff</h3>

<table>
  <thead>
    <tr>
      <th>Type</th>
      <th>Délais (base = 1s)</th>
      <th>Usage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Constant</code></td>
      <td>1s, 1s, 1s</td>
      <td>Délai fixe entre chaque retry</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Linear</code></td>
      <td>1s, 2s, 3s</td>
      <td>Augmentation linéaire</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Exponential</code></td>
      <td>1s, 2s, 4s</td>
      <td>Doublement du délai (le plus courant)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Exponential</code> + jitter</td>
      <td>~1.1s, ~2.3s, ~3.8s</td>
      <td>Exponentiel avec variation aléatoire</td>
    </tr>
  </tbody>
</table>

<p>Le <strong>jitter</strong> (<code class="language-plaintext highlighter-rouge">UseJitter = true</code>) est important : sans lui, tous les clients réessaient exactement au même moment après une panne, ce qui peut surcharger le service au moment de la reprise (“thundering herd”).</p>

<h2 id="2-circuit-breaker--couper-le-circuit">2. Circuit Breaker — Couper le circuit</h2>

<p>Le circuit breaker surveille le taux d’échec et <strong>coupe</strong> les appels quand le service distant est manifestement en panne. Cela évite de saturer un service déjà surchargé.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddCircuitBreaker</span><span class="p">(</span><span class="k">new</span> <span class="n">CircuitBreakerStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">FailureRatio</span> <span class="p">=</span> <span class="m">0.5</span><span class="p">,</span>                          <span class="c1">// 50% d'échecs déclenche l'ouverture</span>
        <span class="n">MinimumThroughput</span> <span class="p">=</span> <span class="m">10</span><span class="p">,</span>                       <span class="c1">// au moins 10 appels avant d'évaluer</span>
        <span class="n">SamplingDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">),</span>  <span class="c1">// fenêtre d'évaluation</span>
        <span class="n">BreakDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">15</span><span class="p">),</span>     <span class="c1">// durée du circuit ouvert</span>
        <span class="n">OnOpened</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Circuit OUVERT pour </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">BreakDuration</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">},</span>
        <span class="n">OnClosed</span> <span class="p">=</span> <span class="n">_</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Circuit FERMÉ"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="les-trois-états-du-circuit">Les trois états du circuit</h3>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    ┌──────────────────┐
    │     FERMÉ        │ ← état normal, les appels passent
    │  (Closed)        │
    └───────┬──────────┘
            │ taux d'échec &gt; seuil
            ▼
    ┌──────────────────┐
    │     OUVERT       │ ← tous les appels sont rejetés immédiatement
    │  (Open)          │   (BrokenCircuitException)
    └───────┬──────────┘
            │ après BreakDuration
            ▼
    ┌──────────────────┐
    │   SEMI-OUVERT    │ ← un seul appel-test est autorisé
    │  (Half-Open)     │
    └───────┬──────────┘
            │ succès → FERMÉ
            │ échec  → OUVERT
</code></pre></div></div>

<h2 id="3-timeout--limiter-le-temps-dattente">3. Timeout — Limiter le temps d’attente</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="k">new</span> <span class="n">TimeoutStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">Timeout</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">),</span>
        <span class="n">OnTimeout</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Timeout après </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Timeout</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>Il existe deux usages de timeout :</p>

<ul>
  <li><strong>Timeout global</strong> (outer) : enveloppe tout le pipeline, y compris les retries.</li>
  <li><strong>Timeout par tentative</strong> (inner) : limite chaque tentative individuelle.</li>
</ul>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="c1">// Timeout global : 30s pour l'ensemble (retries inclus)</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="k">new</span> <span class="n">TimeoutStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">Timeout</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
        <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">),</span>
        <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span>
    <span class="p">})</span>
    <span class="c1">// Timeout par tentative : 5s max par appel individuel</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="k">new</span> <span class="n">TimeoutStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">Timeout</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="4-fallback--valeur-de-repli">4. Fallback — Valeur de repli</h2>

<p>Retourne une valeur par défaut quand l’opération échoue, au lieu de lever une exception.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ResiliencePipelineBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">AddFallback</span><span class="p">(</span><span class="k">new</span> <span class="n">FallbackStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="n">FallbackAction</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">fallbackResponse</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpResponseMessage</span><span class="p">(</span><span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">OK</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="n">Content</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringContent</span><span class="p">(</span><span class="s">"[]"</span><span class="p">)</span> <span class="c1">// liste vide comme fallback</span>
            <span class="p">};</span>
            <span class="k">return</span> <span class="n">Outcome</span><span class="p">.</span><span class="nf">FromResultAsValueTask</span><span class="p">(</span><span class="n">fallbackResponse</span><span class="p">);</span>
        <span class="p">},</span>
        <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">HttpRequestException</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">TimeoutRejectedException</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="p">!</span><span class="n">r</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="5-rate-limiter--limiter-le-débit">5. Rate Limiter — Limiter le débit</h2>

<p>Contrôle le nombre d’appels simultanés ou par seconde pour ne pas surcharger un service.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddRateLimiter</span><span class="p">(</span><span class="k">new</span> <span class="nf">SlidingWindowRateLimiter</span><span class="p">(</span>
        <span class="k">new</span> <span class="n">SlidingWindowRateLimiterOptions</span>
        <span class="p">{</span>
            <span class="n">PermitLimit</span> <span class="p">=</span> <span class="m">100</span><span class="p">,</span>                           <span class="c1">// 100 appels max</span>
            <span class="n">Window</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromMinutes</span><span class="p">(</span><span class="m">1</span><span class="p">),</span>            <span class="c1">// par minute</span>
            <span class="n">SegmentsPerWindow</span> <span class="p">=</span> <span class="m">6</span><span class="p">,</span>                       <span class="c1">// 6 segments de 10s</span>
            <span class="n">QueueProcessingOrder</span> <span class="p">=</span> <span class="n">QueueProcessingOrder</span><span class="p">.</span><span class="n">OldestFirst</span><span class="p">,</span>
            <span class="n">QueueLimit</span> <span class="p">=</span> <span class="m">10</span>                              <span class="c1">// 10 appels en file d'attente max</span>
        <span class="p">}))</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h1 id="composer-les-stratégies">Composer les stratégies</h1>

<p>La puissance de Polly réside dans la <strong>composition</strong>. On combine les stratégies dans un pipeline, et l’ordre compte (l’exécution va du premier ajouté vers le dernier) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ResiliencePipelineBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
    <span class="c1">// 1. Fallback (le plus externe) : valeur de repli si tout échoue</span>
    <span class="p">.</span><span class="nf">AddFallback</span><span class="p">(</span><span class="k">new</span> <span class="n">FallbackStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="n">FallbackAction</span> <span class="p">=</span> <span class="n">_</span> <span class="p">=&gt;</span> <span class="n">Outcome</span><span class="p">.</span><span class="nf">FromResultAsValueTask</span><span class="p">(</span>
            <span class="k">new</span> <span class="nf">HttpResponseMessage</span><span class="p">(</span><span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">OK</span><span class="p">)</span>
            <span class="p">{</span> <span class="n">Content</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringContent</span><span class="p">(</span><span class="s">"{\"cache\": true}"</span><span class="p">)</span> <span class="p">}),</span>
        <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">Exception</span><span class="p">&gt;()</span>
    <span class="p">})</span>
    <span class="c1">// 2. Timeout global : 30s pour l'ensemble</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">))</span>
    <span class="c1">// 3. Retry : 3 tentatives avec backoff</span>
    <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
        <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">),</span>
        <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span>
        <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">HttpRequestException</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">&gt;=</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">InternalServerError</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="c1">// 4. Circuit breaker : coupe si trop d'échecs</span>
    <span class="p">.</span><span class="nf">AddCircuitBreaker</span><span class="p">(</span><span class="k">new</span> <span class="n">CircuitBreakerStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="n">FailureRatio</span> <span class="p">=</span> <span class="m">0.5</span><span class="p">,</span>
        <span class="n">MinimumThroughput</span> <span class="p">=</span> <span class="m">10</span><span class="p">,</span>
        <span class="n">SamplingDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">),</span>
        <span class="n">BreakDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">15</span><span class="p">),</span>
        <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">HttpRequestException</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">&gt;=</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">InternalServerError</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="c1">// 5. Timeout par tentative : 5s max par appel</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>L’ordre d’exécution pour un appel :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Fallback → Timeout global (30s) → Retry → Circuit Breaker → Timeout (5s) → appel HTTP
</code></pre></div></div>

<h1 id="intégration-avec-ihttpclientfactory-recommandé">Intégration avec <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code> (recommandé)</h1>

<p>La manière la plus idiomatique d’utiliser Polly en .NET est de l’intégrer avec <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code> via <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code>.</p>

<h2 id="configuration-dans-programcs">Configuration dans <code class="language-plaintext highlighter-rouge">Program.cs</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"CatalogueApi"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://api.catalogue.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span> <span class="c1">// pipeline de résilience standard</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler()</code> configure automatiquement un pipeline complet :</p>

<table>
  <thead>
    <tr>
      <th>Stratégie</th>
      <th>Configuration par défaut</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Rate limiter</td>
      <td>1000 requêtes simultanées max</td>
    </tr>
    <tr>
      <td>Timeout global</td>
      <td>30 secondes</td>
    </tr>
    <tr>
      <td>Retry</td>
      <td>3 tentatives, backoff exponentiel + jitter</td>
    </tr>
    <tr>
      <td>Circuit breaker</td>
      <td>Coupe à 10% d’échecs sur 30s, pause de 5s</td>
    </tr>
    <tr>
      <td>Timeout par tentative</td>
      <td>10 secondes</td>
    </tr>
  </tbody>
</table>

<h2 id="personnaliser-le-pipeline-standard">Personnaliser le pipeline standard</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">builder</span><span class="p">.</span><span class="n">Services</span>
    <span class="p">.</span><span class="nf">AddHttpClient</span><span class="p">(</span><span class="s">"CatalogueApi"</span><span class="p">,</span> <span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://api.catalogue.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">options</span><span class="p">.</span><span class="n">Retry</span><span class="p">.</span><span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">5</span><span class="p">;</span>
        <span class="n">options</span><span class="p">.</span><span class="n">Retry</span><span class="p">.</span><span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromMilliseconds</span><span class="p">(</span><span class="m">500</span><span class="p">);</span>
        <span class="n">options</span><span class="p">.</span><span class="n">CircuitBreaker</span><span class="p">.</span><span class="n">BreakDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">);</span>
        <span class="n">options</span><span class="p">.</span><span class="n">AttemptTimeout</span><span class="p">.</span><span class="n">Timeout</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">3</span><span class="p">);</span>
        <span class="n">options</span><span class="p">.</span><span class="n">TotalRequestTimeout</span><span class="p">.</span><span class="n">Timeout</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">60</span><span class="p">);</span>
    <span class="p">});</span>
</code></pre></div></div>

<h2 id="utilisation-dans-un-service">Utilisation dans un service</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">CatalogueService</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">HttpClient</span> <span class="n">_httpClient</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">CatalogueService</span><span class="p">(</span><span class="n">IHttpClientFactory</span> <span class="n">factory</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_httpClient</span> <span class="p">=</span> <span class="n">factory</span><span class="p">.</span><span class="nf">CreateClient</span><span class="p">(</span><span class="s">"CatalogueApi"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;</span><span class="n">Produit</span><span class="p">&gt;&gt;</span> <span class="nf">GetProduitsAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">ct</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Les stratégies de résilience sont appliquées automatiquement</span>
        <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"/api/produits"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="n">response</span><span class="p">.</span><span class="nf">EnsureSuccessStatusCode</span><span class="p">();</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="n">ReadFromJsonAsync</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;</span><span class="n">Produit</span><span class="p">&gt;&gt;(</span><span class="n">ct</span><span class="p">)</span> <span class="p">??</span> <span class="p">[];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="avec-un-client-typé">Avec un client typé</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Enregistrement</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span>
    <span class="p">.</span><span class="n">AddHttpClient</span><span class="p">&lt;</span><span class="n">CatalogueService</span><span class="p">&gt;(</span><span class="n">client</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">client</span><span class="p">.</span><span class="n">BaseAddress</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Uri</span><span class="p">(</span><span class="s">"https://api.catalogue.com"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span>

<span class="c1">// Le service reçoit directement un HttpClient configuré</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">CatalogueService</span><span class="p">(</span><span class="n">HttpClient</span> <span class="n">httpClient</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;</span><span class="n">Produit</span><span class="p">&gt;&gt;</span> <span class="nf">GetProduitsAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">ct</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="n">GetFromJsonAsync</span><span class="p">&lt;</span><span class="n">List</span><span class="p">&lt;</span><span class="n">Produit</span><span class="p">&gt;&gt;(</span><span class="s">"/api/produits"</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">??</span> <span class="p">[];</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="intégration-avec-linjection-de-dépendances">Intégration avec l’injection de dépendances</h1>

<p>Pour des opérations non-HTTP (base de données, file de messages, etc.), on utilise <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Resilience</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Enregistrement d'un pipeline nommé</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddResiliencePipeline</span><span class="p">(</span><span class="s">"database"</span><span class="p">,</span> <span class="n">builder</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">builder</span>
        <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
        <span class="p">{</span>
            <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
            <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromMilliseconds</span><span class="p">(</span><span class="m">200</span><span class="p">),</span>
            <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span>
            <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PredicateBuilder</span><span class="p">()</span>
                <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">SqlException</span><span class="p">&gt;(</span><span class="n">ex</span> <span class="p">=&gt;</span> <span class="n">ex</span><span class="p">.</span><span class="n">IsTransient</span><span class="p">)</span>
        <span class="p">})</span>
        <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">10</span><span class="p">));</span>
<span class="p">});</span>

<span class="c1">// Utilisation via injection</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">CommandeRepository</span><span class="p">(</span><span class="n">ResiliencePipelineProvider</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">pipelineProvider</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Commande</span><span class="p">?&gt;</span> <span class="nf">GetByIdAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">pipeline</span> <span class="p">=</span> <span class="n">pipelineProvider</span><span class="p">.</span><span class="nf">GetPipeline</span><span class="p">(</span><span class="s">"database"</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">await</span> <span class="n">pipeline</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">(</span><span class="k">async</span> <span class="n">token</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="k">await</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Commandes</span><span class="p">.</span><span class="nf">FindAsync</span><span class="p">([</span><span class="n">id</span><span class="p">],</span> <span class="n">token</span><span class="p">);</span>
        <span class="p">},</span> <span class="n">ct</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="observabilité">Observabilité</h1>

<p>Polly v8 s’intègre nativement avec les métriques .NET et OpenTelemetry :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Les métriques sont émises automatiquement</span>
<span class="c1">// Compteurs disponibles :</span>
<span class="c1">// - polly.strategy.attempt.duration   (durée de chaque tentative)</span>
<span class="c1">// - polly.strategy.pipeline.duration  (durée totale du pipeline)</span>
<span class="c1">// - polly.strategy.attempt.count      (nombre de tentatives)</span>

<span class="c1">// Pour activer les métriques dans OpenTelemetry :</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddOpenTelemetry</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithMetrics</span><span class="p">(</span><span class="n">metrics</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">metrics</span><span class="p">.</span><span class="nf">AddMeter</span><span class="p">(</span><span class="s">"Polly"</span><span class="p">);</span> <span class="c1">// active les métriques Polly</span>
    <span class="p">});</span>
</code></pre></div></div>

<h1 id="bonnes-pratiques">Bonnes pratiques</h1>

<h2 id="1-toujours-utiliser-un-jitter-sur-les-retries">1. Toujours utiliser un jitter sur les retries</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Sans jitter : tous les clients réessaient au même instant</span>
<span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
<span class="p">{</span>
    <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">2</span><span class="p">),</span>
    <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span>
    <span class="n">UseJitter</span> <span class="p">=</span> <span class="k">false</span> <span class="c1">// ← thundering herd</span>
<span class="p">})</span>

<span class="c1">// ✅ Avec jitter : les retries sont désynchronisés</span>
<span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
<span class="p">{</span>
    <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">2</span><span class="p">),</span>
    <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span>
    <span class="n">UseJitter</span> <span class="p">=</span> <span class="k">true</span> <span class="c1">// ← les délais varient aléatoirement</span>
<span class="p">})</span>
</code></pre></div></div>

<h2 id="2-ne-pas-retrier-les-erreurs-non-transitoires">2. Ne pas retrier les erreurs non transitoires</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Retrier une erreur 400 Bad Request n'a aucun sens</span>
<span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
        <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="p">!</span><span class="n">r</span><span class="p">.</span><span class="n">IsSuccessStatusCode</span><span class="p">)</span> <span class="c1">// ← inclut les 400, 401, 404...</span>
<span class="p">})</span>

<span class="c1">// ✅ Retrier uniquement les erreurs transitoires (5xx, timeout)</span>
<span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="n">ShouldHandle</span> <span class="p">=</span> <span class="k">new</span> <span class="n">PredicateBuilder</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;()</span>
        <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">HttpRequestException</span><span class="p">&gt;()</span>
        <span class="p">.</span><span class="n">Handle</span><span class="p">&lt;</span><span class="n">TimeoutRejectedException</span><span class="p">&gt;()</span>
        <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">&gt;=</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">InternalServerError</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">RequestTimeout</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">HandleResult</span><span class="p">(</span><span class="n">r</span> <span class="p">=&gt;</span> <span class="n">r</span><span class="p">.</span><span class="n">StatusCode</span> <span class="p">==</span> <span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">TooManyRequests</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<h2 id="3-propager-le-cancellationtoken">3. Propager le <code class="language-plaintext highlighter-rouge">CancellationToken</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Toujours passer le CancellationToken à travers le pipeline</span>
<span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">pipeline</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">(</span>
    <span class="k">async</span> <span class="n">ct</span> <span class="p">=&gt;</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">ct</span><span class="p">),</span>
    <span class="n">cancellationToken</span>  <span class="c1">// ← permet l'annulation depuis l'appelant</span>
<span class="p">);</span>
</code></pre></div></div>

<h2 id="4-préférer-addstandardresiliencehandler-pour-les-appels-http">4. Préférer <code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler</code> pour les appels HTTP</h2>

<p>Le pipeline standard couvre la majorité des cas d’usage. Ne créer un pipeline personnalisé que si les valeurs par défaut ne conviennent pas.</p>

<h2 id="5-combiner-retry-et-circuit-breaker">5. Combiner retry et circuit breaker</h2>

<p>Le retry seul ne suffit pas : si le service est complètement en panne, les retries saturent la file d’attente. Le circuit breaker coupe les appels immédiatement.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Le circuit breaker protège le service en panne</span>
<span class="c1">// Le retry gère les erreurs ponctuelles</span>
<span class="p">.</span><span class="nf">AddRetry</span><span class="p">(...)</span>
<span class="p">.</span><span class="nf">AddCircuitBreaker</span><span class="p">(...)</span>
</code></pre></div></div>

<h1 id="résumé">Résumé</h1>

<table>
  <thead>
    <tr>
      <th>Stratégie</th>
      <th>Quand l’utiliser</th>
      <th>Ce qu’elle fait</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Retry</strong></td>
      <td>Erreurs transitoires ponctuelles</td>
      <td>Réessaie l’opération après un délai</td>
    </tr>
    <tr>
      <td><strong>Circuit Breaker</strong></td>
      <td>Service en panne prolongée</td>
      <td>Coupe les appels pour protéger le service</td>
    </tr>
    <tr>
      <td><strong>Timeout</strong></td>
      <td>Appels qui traînent</td>
      <td>Limite le temps d’attente</td>
    </tr>
    <tr>
      <td><strong>Fallback</strong></td>
      <td>Dégradation gracieuse</td>
      <td>Retourne une valeur par défaut</td>
    </tr>
    <tr>
      <td><strong>Rate Limiter</strong></td>
      <td>Protection du service distant</td>
      <td>Limite le débit d’appels</td>
    </tr>
    <tr>
      <td><strong>Composition</strong></td>
      <td>Cas réel</td>
      <td>Combine les stratégies dans un pipeline</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler</code></strong></td>
      <td>Appels HTTP standard</td>
      <td>Pipeline complet prêt à l’emploi</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="polly" /><category term="resilience" /><category term="http" /><category term="retry" /><category term="circuit-breaker" /><summary type="html"><![CDATA[En production, les appels réseau échouent : timeouts, erreurs 503, connexions refusées. Plutôt que de laisser ces erreurs transitoires remonter jusqu’à l’utilisateur, Polly permet de définir des stratégies de résilience — retry, circuit breaker, timeout, fallback — de manière déclarative. Depuis .NET 8, Polly v8 et Microsoft.Extensions.Http.Resilience s’intègrent nativement dans l’écosystème .NET.]]></summary></entry><entry><title type="html">Async/Await en C# : comprendre la programmation asynchrone</title><link href="http://guym.fr/2026/02/14/async-await.html" rel="alternate" type="text/html" title="Async/Await en C# : comprendre la programmation asynchrone" /><published>2026-02-14T00:00:00+01:00</published><updated>2026-02-14T00:00:00+01:00</updated><id>http://guym.fr/2026/02/14/async-await</id><content type="html" xml:base="http://guym.fr/2026/02/14/async-await.html"><![CDATA[<p>La programmation asynchrone avec <code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code> est au cœur du développement .NET moderne. Elle permet de libérer des threads pendant les opérations d’entrée/sortie (réseau, fichiers, base de données) au lieu de les bloquer. Mais derrière cette syntaxe simple se cachent une machine à états, un <code class="language-plaintext highlighter-rouge">SynchronizationContext</code>, et de nombreux pièges. Cet article détaille le fonctionnement réel d’<code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code>, de la théorie aux bonnes pratiques.</p>

<!--more-->

<h1 id="pourquoi-lasynchrone-">Pourquoi l’asynchrone ?</h1>

<h2 id="le-problème--le-thread-bloqué">Le problème : le thread bloqué</h2>

<p>Dans un serveur web, chaque requête HTTP est traitée par un thread du pool. Si ce thread effectue un appel réseau <strong>synchrone</strong>, il reste bloqué en attente de la réponse :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Synchrone : le thread est bloqué pendant toute la durée de l'appel HTTP</span>
<span class="k">public</span> <span class="n">IActionResult</span> <span class="nf">GetData</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="n">client</span><span class="p">.</span><span class="nf">Send</span><span class="p">(</span><span class="k">new</span> <span class="nf">HttpRequestMessage</span><span class="p">(</span><span class="n">HttpMethod</span><span class="p">.</span><span class="n">Get</span><span class="p">,</span> <span class="s">"https://api.example.com/data"</span><span class="p">));</span>
    <span class="kt">var</span> <span class="n">content</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StreamReader</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStream</span><span class="p">()).</span><span class="nf">ReadToEnd</span><span class="p">();</span>
    <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">content</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Avec 100 requêtes simultanées et un pool de 100 threads, le serveur est <strong>saturé</strong> : plus aucun thread disponible pour traiter de nouvelles requêtes, alors que chaque thread ne fait que <strong>attendre</strong>.</p>

<h2 id="la-solution--libérer-le-thread-pendant-lattente">La solution : libérer le thread pendant l’attente</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Asynchrone : le thread est libéré pendant l'appel HTTP</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">IActionResult</span><span class="p">&gt;</span> <span class="nf">GetData</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">HttpClient</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"https://api.example.com/data"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">content</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
    <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">content</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Avec <code class="language-plaintext highlighter-rouge">await</code>, le thread est <strong>rendu au pool</strong> pendant l’attente réseau. Il peut traiter d’autres requêtes. Quand la réponse arrive, un thread du pool reprend l’exécution là où elle s’était arrêtée.</p>

<h2 id="ce-que-lasynchrone-nest-pas">Ce que l’asynchrone n’est PAS</h2>

<ul>
  <li><strong>Ce n’est PAS du parallélisme</strong> : <code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code> ne crée pas de thread supplémentaire. Il libère le thread courant.</li>
  <li><strong>Ce n’est PAS plus rapide</strong> pour une seule opération : l’appel HTTP prend toujours le même temps. Le gain est en <strong>scalabilité</strong> (plus de requêtes simultanées avec moins de threads).</li>
  <li><strong>Ce n’est PAS utile pour du calcul CPU</strong> : si le travail est du calcul pur (boucle, algorithme), <code class="language-plaintext highlighter-rouge">await</code> n’apporte rien — le thread est occupé à calculer, pas à attendre.</li>
</ul>

<h1 id="task-et-taskt--la-promesse-dun-résultat"><code class="language-plaintext highlighter-rouge">Task</code> et <code class="language-plaintext highlighter-rouge">Task&lt;T&gt;</code> : la promesse d’un résultat</h1>

<h2 id="task--une-opération-sans-résultat"><code class="language-plaintext highlighter-rouge">Task</code> : une opération sans résultat</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">EnvoyerEmailAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">destinataire</span><span class="p">,</span> <span class="kt">string</span> <span class="n">message</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">smtpClient</span><span class="p">.</span><span class="nf">SendMailAsync</span><span class="p">(</span><span class="n">destinataire</span><span class="p">,</span> <span class="n">message</span><span class="p">);</span>
    <span class="c1">// Pas de valeur de retour</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="taskt--une-opération-avec-résultat"><code class="language-plaintext highlighter-rouge">Task&lt;T&gt;</code> : une opération avec résultat</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span><span class="p">.</span><span class="nf">FindAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">client</span><span class="p">;</span> <span class="c1">// retourne un Client, encapsulé dans Task&lt;Client&gt;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Un <code class="language-plaintext highlighter-rouge">Task</code> représente une <strong>opération en cours</strong> (ou déjà terminée). C’est l’équivalent d’une “promesse” (Promise en JavaScript). On peut :</p>
<ul>
  <li><strong>Attendre</strong> son résultat avec <code class="language-plaintext highlighter-rouge">await</code>.</li>
  <li>Vérifier son état : <code class="language-plaintext highlighter-rouge">IsCompleted</code>, <code class="language-plaintext highlighter-rouge">IsFaulted</code>, <code class="language-plaintext highlighter-rouge">IsCanceled</code>.</li>
  <li>Accéder au résultat (si terminé) via <code class="language-plaintext highlighter-rouge">.Result</code> — mais <strong>attention aux deadlocks</strong> (voir plus bas).</li>
</ul>

<h2 id="valuetaskt--lalternative-légère"><code class="language-plaintext highlighter-rouge">ValueTask&lt;T&gt;</code> : l’alternative légère</h2>

<p><code class="language-plaintext highlighter-rouge">ValueTask&lt;T&gt;</code> évite l’allocation d’un objet <code class="language-plaintext highlighter-rouge">Task</code> sur le tas quand le résultat est <strong>souvent déjà disponible</strong> (cache, valeur par défaut) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Si le cache contient la valeur, pas besoin d'allouer un Task</span>
<span class="k">public</span> <span class="n">ValueTask</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">_cache</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="n">id</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">client</span><span class="p">))</span>
        <span class="k">return</span> <span class="k">new</span> <span class="n">ValueTask</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;(</span><span class="n">client</span><span class="p">);</span> <span class="c1">// pas d'allocation</span>

    <span class="k">return</span> <span class="k">new</span> <span class="n">ValueTask</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;(</span><span class="nf">GetClientFromDbAsync</span><span class="p">(</span><span class="n">id</span><span class="p">));</span> <span class="c1">// allocation Task si nécessaire</span>
<span class="p">}</span>

<span class="k">private</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientFromDbAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span><span class="p">.</span><span class="nf">FindAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Règle</strong> : utiliser <code class="language-plaintext highlighter-rouge">ValueTask&lt;T&gt;</code> quand la méthode retourne souvent un résultat <strong>synchrone</strong> (cache hit, valeur par défaut). Sinon, <code class="language-plaintext highlighter-rouge">Task&lt;T&gt;</code> suffit.</p>

<h1 id="ce-que-le-compilateur-génère">Ce que le compilateur génère</h1>

<h2 id="la-machine-à-états">La machine à états</h2>

<p>Quand le compilateur rencontre <code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code>, il transforme la méthode en une <strong>machine à états</strong> (<code class="language-plaintext highlighter-rouge">IAsyncStateMachine</code>). Chaque <code class="language-plaintext highlighter-rouge">await</code> correspond à un <strong>point de suspension</strong>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Ce qu'on écrit</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"https://api.example.com"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">content</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">content</span><span class="p">.</span><span class="nf">ToUpper</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Le compilateur génère (version très simplifiée) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Ce que le compilateur produit</span>
<span class="k">private</span> <span class="k">struct</span> <span class="nc">GetDataAsyncStateMachine</span> <span class="p">:</span> <span class="n">IAsyncStateMachine</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">_state</span><span class="p">;</span>  <span class="c1">// -1 = début, 0 = après 1er await, 1 = après 2ème await</span>
    <span class="k">public</span> <span class="n">AsyncTaskMethodBuilder</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">_builder</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">HttpClient</span> <span class="n">httpClient</span><span class="p">;</span>

    <span class="c1">// Variables locales "promues" en champs</span>
    <span class="k">private</span> <span class="n">HttpResponseMessage</span> <span class="n">_response</span><span class="p">;</span>
    <span class="k">private</span> <span class="kt">string</span> <span class="n">_content</span><span class="p">;</span>

    <span class="c1">// Awaiters pour chaque await</span>
    <span class="k">private</span> <span class="n">TaskAwaiter</span><span class="p">&lt;</span><span class="n">HttpResponseMessage</span><span class="p">&gt;</span> <span class="n">_awaiter1</span><span class="p">;</span>
    <span class="k">private</span> <span class="n">TaskAwaiter</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">_awaiter2</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">MoveNext</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">try</span>
        <span class="p">{</span>
            <span class="k">switch</span> <span class="p">(</span><span class="n">_state</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="k">case</span> <span class="p">-</span><span class="m">1</span><span class="p">:</span> <span class="c1">// début de la méthode</span>
                    <span class="n">_awaiter1</span> <span class="p">=</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"https://api.example.com"</span><span class="p">).</span><span class="nf">GetAwaiter</span><span class="p">();</span>
                    <span class="k">if</span> <span class="p">(!</span><span class="n">_awaiter1</span><span class="p">.</span><span class="n">IsCompleted</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="n">_state</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>
                        <span class="n">_builder</span><span class="p">.</span><span class="nf">AwaitUnsafeOnCompleted</span><span class="p">(</span><span class="k">ref</span> <span class="n">_awaiter1</span><span class="p">,</span> <span class="k">ref</span> <span class="k">this</span><span class="p">);</span>
                        <span class="k">return</span><span class="p">;</span> <span class="c1">// le thread est libéré ICI</span>
                    <span class="p">}</span>
                    <span class="k">goto</span> <span class="k">case</span> <span class="m">0</span><span class="p">;</span>

                <span class="k">case</span> <span class="m">0</span><span class="p">:</span> <span class="c1">// reprise après le 1er await</span>
                    <span class="n">_response</span> <span class="p">=</span> <span class="n">_awaiter1</span><span class="p">.</span><span class="nf">GetResult</span><span class="p">();</span>
                    <span class="n">_awaiter2</span> <span class="p">=</span> <span class="n">_response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">().</span><span class="nf">GetAwaiter</span><span class="p">();</span>
                    <span class="k">if</span> <span class="p">(!</span><span class="n">_awaiter2</span><span class="p">.</span><span class="n">IsCompleted</span><span class="p">)</span>
                    <span class="p">{</span>
                        <span class="n">_state</span> <span class="p">=</span> <span class="m">1</span><span class="p">;</span>
                        <span class="n">_builder</span><span class="p">.</span><span class="nf">AwaitUnsafeOnCompleted</span><span class="p">(</span><span class="k">ref</span> <span class="n">_awaiter2</span><span class="p">,</span> <span class="k">ref</span> <span class="k">this</span><span class="p">);</span>
                        <span class="k">return</span><span class="p">;</span> <span class="c1">// le thread est libéré ICI</span>
                    <span class="p">}</span>
                    <span class="k">goto</span> <span class="k">case</span> <span class="m">1</span><span class="p">;</span>

                <span class="k">case</span> <span class="m">1</span><span class="p">:</span> <span class="c1">// reprise après le 2ème await</span>
                    <span class="n">_content</span> <span class="p">=</span> <span class="n">_awaiter2</span><span class="p">.</span><span class="nf">GetResult</span><span class="p">();</span>
                    <span class="n">_builder</span><span class="p">.</span><span class="nf">SetResult</span><span class="p">(</span><span class="n">_content</span><span class="p">.</span><span class="nf">ToUpper</span><span class="p">());</span>
                    <span class="k">return</span><span class="p">;</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">_builder</span><span class="p">.</span><span class="nf">SetException</span><span class="p">(</span><span class="n">ex</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="points-clés">Points clés</h3>

<ol>
  <li><strong>Les variables locales</strong> (<code class="language-plaintext highlighter-rouge">response</code>, <code class="language-plaintext highlighter-rouge">content</code>) deviennent des <strong>champs</strong> de la struct, car elles doivent survivre entre les reprises.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">IsCompleted</code></strong> est vérifié d’abord : si le <code class="language-plaintext highlighter-rouge">Task</code> est déjà terminé, on continue <strong>sans suspendre</strong> (optimisation synchrone).</li>
  <li><strong><code class="language-plaintext highlighter-rouge">return</code></strong> dans le <code class="language-plaintext highlighter-rouge">MoveNext()</code> libère le thread. Quand l’opération asynchrone se termine, <code class="language-plaintext highlighter-rouge">MoveNext()</code> est rappelé avec le bon <code class="language-plaintext highlighter-rouge">_state</code>.</li>
  <li><strong>Les exceptions</strong> sont capturées et stockées dans le <code class="language-plaintext highlighter-rouge">Task</code> via <code class="language-plaintext highlighter-rouge">SetException</code>.</li>
</ol>

<h1 id="le-synchronizationcontext">Le <code class="language-plaintext highlighter-rouge">SynchronizationContext</code></h1>

<h2 id="le-problème-de-la-reprise">Le problème de la reprise</h2>

<p>Quand un <code class="language-plaintext highlighter-rouge">await</code> se termine, sur <strong>quel thread</strong> reprend l’exécution ? C’est le rôle du <code class="language-plaintext highlighter-rouge">SynchronizationContext</code>.</p>

<table>
  <thead>
    <tr>
      <th>Environnement</th>
      <th><code class="language-plaintext highlighter-rouge">SynchronizationContext</code></th>
      <th>Reprise sur</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>ASP.NET Core</td>
      <td>Aucun (<code class="language-plaintext highlighter-rouge">null</code>)</td>
      <td>N’importe quel thread du pool</td>
    </tr>
    <tr>
      <td>WPF / WinForms</td>
      <td>UI context</td>
      <td>Le thread UI</td>
    </tr>
    <tr>
      <td>Application console</td>
      <td>Aucun (<code class="language-plaintext highlighter-rouge">null</code>)</td>
      <td>N’importe quel thread du pool</td>
    </tr>
    <tr>
      <td>ASP.NET classique (.NET Framework)</td>
      <td><code class="language-plaintext highlighter-rouge">AspNetSynchronizationContext</code></td>
      <td>Le même thread de requête</td>
    </tr>
  </tbody>
</table>

<h2 id="pourquoi-cest-important">Pourquoi c’est important</h2>

<p>En <strong>WPF/WinForms</strong>, après un <code class="language-plaintext highlighter-rouge">await</code>, l’exécution reprend automatiquement sur le <strong>thread UI</strong> (pour pouvoir mettre à jour les contrôles) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// WPF : après await, on est de retour sur le thread UI</span>
<span class="k">private</span> <span class="k">async</span> <span class="k">void</span> <span class="nf">Button_Click</span><span class="p">(</span><span class="kt">object</span> <span class="n">sender</span><span class="p">,</span> <span class="n">RoutedEventArgs</span> <span class="n">e</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="s">"https://api.example.com"</span><span class="p">);</span>
    <span class="n">TextBlock</span><span class="p">.</span><span class="n">Text</span> <span class="p">=</span> <span class="n">data</span><span class="p">;</span> <span class="c1">// ✅ OK : on est sur le thread UI</span>
<span class="p">}</span>
</code></pre></div></div>

<p>En <strong>ASP.NET Core</strong>, il n’y a pas de <code class="language-plaintext highlighter-rouge">SynchronizationContext</code>, donc la reprise se fait sur n’importe quel thread du pool — ce qui est plus performant.</p>

<h2 id="configureawaitfalse"><code class="language-plaintext highlighter-rouge">ConfigureAwait(false)</code></h2>

<p><code class="language-plaintext highlighter-rouge">ConfigureAwait(false)</code> dit : “je n’ai pas besoin de revenir sur le contexte d’origine”.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Dans du code de bibliothèque (pas de UI, pas de contexte HTTP)</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="n">url</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
    <span class="c1">// La reprise se fait sur n'importe quel thread du pool</span>
    <span class="kt">var</span> <span class="n">content</span> <span class="p">=</span> <span class="k">await</span> <span class="n">response</span><span class="p">.</span><span class="n">Content</span><span class="p">.</span><span class="nf">ReadAsStringAsync</span><span class="p">().</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">content</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Règles</strong> :</p>
<ul>
  <li><strong>Code applicatif ASP.NET Core</strong> : <code class="language-plaintext highlighter-rouge">ConfigureAwait(false)</code> est inutile (pas de <code class="language-plaintext highlighter-rouge">SynchronizationContext</code>), mais ne fait pas de mal.</li>
  <li><strong>Bibliothèques</strong> : toujours mettre <code class="language-plaintext highlighter-rouge">ConfigureAwait(false)</code> pour éviter les deadlocks quand la bibliothèque est utilisée dans un contexte UI ou ASP.NET classique.</li>
  <li><strong>Code UI (WPF/WinForms)</strong> : ne pas mettre <code class="language-plaintext highlighter-rouge">ConfigureAwait(false)</code> si on accède à des contrôles UI après le <code class="language-plaintext highlighter-rouge">await</code>.</li>
</ul>

<h1 id="pièges-courants">Pièges courants</h1>

<h2 id="1-async-void--à-éviter-absolument">1. <code class="language-plaintext highlighter-rouge">async void</code> — à éviter absolument</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ async void : les exceptions ne sont pas capturables</span>
<span class="k">public</span> <span class="k">async</span> <span class="k">void</span> <span class="nf">EnvoyerNotification</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">PostAsync</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">content</span><span class="p">);</span>
    <span class="c1">// Si une exception est levée, elle crashe l'application</span>
    <span class="c1">// car personne ne peut await cette méthode (pas de Task)</span>
<span class="p">}</span>

<span class="c1">// ✅ async Task : les exceptions sont propagées via le Task</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">EnvoyerNotificationAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">PostAsync</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">content</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Exception unique</strong> : les event handlers UI (WPF, WinForms) <strong>imposent</strong> <code class="language-plaintext highlighter-rouge">async void</code> car la signature de l’événement est <code class="language-plaintext highlighter-rouge">void</code>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Seul cas acceptable pour async void</span>
<span class="k">private</span> <span class="k">async</span> <span class="k">void</span> <span class="nf">Button_Click</span><span class="p">(</span><span class="kt">object</span> <span class="n">sender</span><span class="p">,</span> <span class="n">RoutedEventArgs</span> <span class="n">e</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">try</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="nf">DoWorkAsync</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">Exception</span> <span class="n">ex</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Toujours un try-catch dans un async void !</span>
        <span class="n">MessageBox</span><span class="p">.</span><span class="nf">Show</span><span class="p">(</span><span class="n">ex</span><span class="p">.</span><span class="n">Message</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="2-deadlock-avec-result-ou-wait">2. Deadlock avec <code class="language-plaintext highlighter-rouge">.Result</code> ou <code class="language-plaintext highlighter-rouge">.Wait()</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ DEADLOCK en WPF / ASP.NET classique</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">GetData</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="nf">GetDataAsync</span><span class="p">().</span><span class="n">Result</span><span class="p">;</span> <span class="c1">// bloque le thread UI</span>
    <span class="c1">// GetDataAsync attend de revenir sur le thread UI (SynchronizationContext)</span>
    <span class="c1">// Mais le thread UI est bloqué par .Result → DEADLOCK</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>
    <span class="c1">// await veut revenir sur le thread UI, mais il est bloqué</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Le scénario du deadlock :</p>
<ol>
  <li><code class="language-plaintext highlighter-rouge">.Result</code> bloque le thread courant (UI) en attendant le <code class="language-plaintext highlighter-rouge">Task</code>.</li>
  <li>Le <code class="language-plaintext highlighter-rouge">await</code> dans <code class="language-plaintext highlighter-rouge">GetDataAsync</code> veut reprendre sur le thread UI (via <code class="language-plaintext highlighter-rouge">SynchronizationContext</code>).</li>
  <li>Le thread UI est bloqué → <strong>deadlock</strong>.</li>
</ol>

<p><strong>Solutions</strong> :</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Solution 1 : utiliser await partout (async all the way)</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetData</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="nf">GetDataAsync</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">// ✅ Solution 2 : ConfigureAwait(false) dans la méthode appelée</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="n">url</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="3-async-inutile-élision-dawait">3. <code class="language-plaintext highlighter-rouge">async</code> inutile (élision d’await)</h2>

<p>Quand une méthode ne fait que <strong>transmettre</strong> un <code class="language-plaintext highlighter-rouge">Task</code> sans rien faire après, <code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code> est superflu :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ async/await inutile : crée une machine à états pour rien</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetByIdAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// ✅ Retourner directement le Task</span>
<span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetByIdAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="documentation">Documentation</h1>

<h2 id="attention-concernant-lélision-asyncawait">Attention concernant l’élision async/await</h2>

<p>Ce commentaire avertit que <strong>l’élision de <code class="language-plaintext highlighter-rouge">async</code></strong> (c’est-à-dire la suppression du mot-clé <code class="language-plaintext highlighter-rouge">async</code> d’une méthode qui retourne directement une <code class="language-plaintext highlighter-rouge">Task</code>) modifie le comportement du code de deux façons importantes :</p>

<h3 id="1-sémantique-des-exceptions">1. <strong>Sémantique des exceptions</strong></h3>
<ul>
  <li>Avec <code class="language-plaintext highlighter-rouge">async</code> : les exceptions sont capturées et enveloppées dans la <code class="language-plaintext highlighter-rouge">Task</code> retournée</li>
  <li>Sans <code class="language-plaintext highlighter-rouge">async</code> : les exceptions sont levées directement lors de l’appel</li>
</ul>

<h3 id="2-stack-trace">2. <strong>Stack trace</strong></h3>
<ul>
  <li>Avec <code class="language-plaintext highlighter-rouge">async</code> : la pile d’appels peut être modifiée/raccourcie par le compilateur</li>
  <li>Sans <code class="language-plaintext highlighter-rouge">async</code> : la pile d’appels originale est préservée</li>
</ul>

<h3 id="recommandation">Recommandation</h3>

<p>Conserver le mot-clé <code class="language-plaintext highlighter-rouge">async</code> si la méthode contient de la validation ou de la logique <strong>avant le <code class="language-plaintext highlighter-rouge">return</code></strong>, car cela garantit que :</p>
<ul>
  <li>Toutes les exceptions sont gérées de manière cohérente</li>
  <li>La stack trace reste complète et utile pour le déboggage
<strong>Attention</strong> : l’élision change la sémantique des exceptions et du stack trace. Si la méthode contient de la validation avant le <code class="language-plaintext highlighter-rouge">return</code>, garder <code class="language-plaintext highlighter-rouge">async</code> :</li>
</ul>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Garder async car il y a de la logique avant le await</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="kt">int</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">id</span> <span class="p">&lt;=</span> <span class="m">0</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentException</span><span class="p">(</span><span class="s">"Id invalide"</span><span class="p">);</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">repository</span><span class="p">.</span><span class="nf">GetByIdAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
    <span class="c1">// Sans async, l'exception serait levée à l'appel, pas encapsulée dans le Task</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="4-oublier-dawait-un-task">4. Oublier d’<code class="language-plaintext highlighter-rouge">await</code> un <code class="language-plaintext highlighter-rouge">Task</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Le Task est créé mais jamais attendu : fire-and-forget silencieux</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">TraiterCommandeAsync</span><span class="p">(</span><span class="n">Commande</span> <span class="n">cmd</span><span class="p">)</span>
<span class="p">{</span>
    <span class="nf">EnvoyerConfirmationAsync</span><span class="p">(</span><span class="n">cmd</span><span class="p">.</span><span class="n">Email</span><span class="p">);</span> <span class="c1">// ⚠️ pas de await !</span>
    <span class="c1">// L'exécution continue immédiatement</span>
    <span class="c1">// Si EnvoyerConfirmationAsync lève une exception, elle est perdue</span>
<span class="p">}</span>

<span class="c1">// ✅ Toujours await les Task</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">TraiterCommandeAsync</span><span class="p">(</span><span class="n">Commande</span> <span class="n">cmd</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="nf">EnvoyerConfirmationAsync</span><span class="p">(</span><span class="n">cmd</span><span class="p">.</span><span class="n">Email</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Le compilateur émet un <strong>warning CS4014</strong> pour ce cas — ne pas l’ignorer.</p>

<h2 id="5-lancer-des-tâches-en-séquentiel-au-lieu-de-parallèle">5. Lancer des tâches en séquentiel au lieu de parallèle</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Séquentiel : 3 secondes au total (1 + 1 + 1)</span>
<span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>          <span class="c1">// attend 1s</span>
<span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetCommandesAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>    <span class="c1">// attend 1s</span>
<span class="kt">var</span> <span class="n">factures</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">GetFacturesAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>      <span class="c1">// attend 1s</span>

<span class="c1">// ✅ Parallèle : ~1 seconde au total (les 3 en même temps)</span>
<span class="kt">var</span> <span class="n">clientTask</span> <span class="p">=</span> <span class="nf">GetClientAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">commandesTask</span> <span class="p">=</span> <span class="nf">GetCommandesAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">facturesTask</span> <span class="p">=</span> <span class="nf">GetFacturesAsync</span><span class="p">(</span><span class="n">id</span><span class="p">);</span>

<span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">WhenAll</span><span class="p">(</span><span class="n">clientTask</span><span class="p">,</span> <span class="n">commandesTask</span><span class="p">,</span> <span class="n">facturesTask</span><span class="p">);</span>

<span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="n">clientTask</span><span class="p">.</span><span class="n">Result</span><span class="p">;</span>      <span class="c1">// déjà terminé, pas de blocage</span>
<span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="n">commandesTask</span><span class="p">.</span><span class="n">Result</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">factures</span> <span class="p">=</span> <span class="n">facturesTask</span><span class="p">.</span><span class="n">Result</span><span class="p">;</span>

<span class="c1">// OU en C# plus concis :</span>
<span class="kt">var</span> <span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">commandes</span><span class="p">,</span> <span class="n">factures</span><span class="p">)</span> <span class="p">=</span> <span class="p">(</span>
    <span class="k">await</span> <span class="n">clientTask</span><span class="p">,</span>
    <span class="k">await</span> <span class="n">commandesTask</span><span class="p">,</span>
    <span class="k">await</span> <span class="n">facturesTask</span>
<span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Task.WhenAll</code> lance toutes les opérations en parallèle et attend qu’elles soient <strong>toutes</strong> terminées.</p>

<h1 id="patterns-utiles">Patterns utiles</h1>

<h2 id="timeout-avec-cancellationtoken">Timeout avec <code class="language-plaintext highlighter-rouge">CancellationToken</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAvecTimeoutAsync</span><span class="p">(</span><span class="n">CancellationToken</span> <span class="n">cancellationToken</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">using</span> <span class="nn">var</span> <span class="n">cts</span> <span class="p">=</span> <span class="n">CancellationTokenSource</span><span class="p">.</span><span class="nf">CreateLinkedTokenSource</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">);</span>
    <span class="n">cts</span><span class="p">.</span><span class="nf">CancelAfter</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">));</span> <span class="c1">// timeout de 5 secondes</span>

    <span class="k">try</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="n">url</span><span class="p">,</span> <span class="n">cts</span><span class="p">.</span><span class="n">Token</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">OperationCanceledException</span><span class="p">)</span> <span class="nf">when</span> <span class="p">(!</span><span class="n">cancellationToken</span><span class="p">.</span><span class="n">IsCancellationRequested</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">TimeoutException</span><span class="p">(</span><span class="s">"L'appel a dépassé le délai de 5 secondes."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="retry-simple">Retry simple</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">AvecRetryAsync</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">Func</span><span class="p">&lt;</span><span class="n">Task</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;&gt;</span> <span class="n">operation</span><span class="p">,</span> <span class="kt">int</span> <span class="n">maxRetries</span> <span class="p">=</span> <span class="m">3</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">i</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">i</span> <span class="p">&lt;</span> <span class="n">maxRetries</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="k">try</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="k">await</span> <span class="nf">operation</span><span class="p">();</span>
        <span class="p">}</span>
        <span class="k">catch</span> <span class="p">(</span><span class="n">HttpRequestException</span><span class="p">)</span> <span class="nf">when</span> <span class="p">(</span><span class="n">i</span> <span class="p">&lt;</span> <span class="n">maxRetries</span> <span class="p">-</span> <span class="m">1</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="n">Math</span><span class="p">.</span><span class="nf">Pow</span><span class="p">(</span><span class="m">2</span><span class="p">,</span> <span class="n">i</span><span class="p">)));</span> <span class="c1">// backoff exponentiel</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span><span class="s">"Ne devrait pas arriver"</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Utilisation</span>
<span class="kt">var</span> <span class="n">data</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">AvecRetryAsync</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="n">url</span><span class="p">));</span>
</code></pre></div></div>

<h2 id="iasyncenumerablet--streaming-asynchrone"><code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code> — streaming asynchrone</h2>

<p>Introduit en C# 8, <code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code> permet de produire des éléments <strong>un par un de manière asynchrone</strong> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Producteur : chaque élément est produit de façon asynchrone</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetClientsEnStreamAsync</span><span class="p">(</span>
    <span class="p">[</span><span class="n">EnumeratorCancellation</span><span class="p">]</span> <span class="n">CancellationToken</span> <span class="n">ct</span> <span class="p">=</span> <span class="k">default</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">page</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(</span><span class="k">true</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">batch</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span>
            <span class="p">.</span><span class="nf">OrderBy</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Id</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Skip</span><span class="p">(</span><span class="n">page</span> <span class="p">*</span> <span class="m">100</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Take</span><span class="p">(</span><span class="m">100</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">ToListAsync</span><span class="p">(</span><span class="n">ct</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">batch</span><span class="p">.</span><span class="n">Count</span> <span class="p">==</span> <span class="m">0</span><span class="p">)</span> <span class="k">yield</span> <span class="k">break</span><span class="p">;</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">client</span> <span class="k">in</span> <span class="n">batch</span><span class="p">)</span>
            <span class="k">yield</span> <span class="k">return</span> <span class="n">client</span><span class="p">;</span>

        <span class="n">page</span><span class="p">++;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Consommateur : itère de manière asynchrone</span>
<span class="k">await</span> <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">client</span> <span class="k">in</span> <span class="nf">GetClientsEnStreamAsync</span><span class="p">())</span>
<span class="p">{</span>
    <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">client</span><span class="p">.</span><span class="n">Nom</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="channelt--producteurconsommateur-asynchrone"><code class="language-plaintext highlighter-rouge">Channel&lt;T&gt;</code> — producteur/consommateur asynchrone</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">channel</span> <span class="p">=</span> <span class="n">Channel</span><span class="p">.</span><span class="n">CreateBounded</span><span class="p">&lt;</span><span class="n">Commande</span><span class="p">&gt;(</span><span class="m">100</span><span class="p">);</span>

<span class="c1">// Producteur</span>
<span class="n">_</span> <span class="p">=</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">cmd</span> <span class="k">in</span> <span class="nf">GetCommandesEnStreamAsync</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">channel</span><span class="p">.</span><span class="n">Writer</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="n">channel</span><span class="p">.</span><span class="n">Writer</span><span class="p">.</span><span class="nf">Complete</span><span class="p">();</span>
<span class="p">});</span>

<span class="c1">// Consommateur</span>
<span class="k">await</span> <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">cmd</span> <span class="k">in</span> <span class="n">channel</span><span class="p">.</span><span class="n">Reader</span><span class="p">.</span><span class="nf">ReadAllAsync</span><span class="p">())</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="nf">TraiterCommandeAsync</span><span class="p">(</span><span class="n">cmd</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="taskrun--quand-et-quand-ne-pas-lutiliser"><code class="language-plaintext highlighter-rouge">Task.Run</code> : quand et quand ne pas l’utiliser</h1>

<h2 id="ce-que-taskrun-fait-vraiment">Ce que <code class="language-plaintext highlighter-rouge">Task.Run</code> fait vraiment</h2>

<p><code class="language-plaintext highlighter-rouge">Task.Run</code> planifie du travail sur le <strong>ThreadPool</strong>. C’est utile uniquement pour décharger du <strong>calcul CPU</strong> du thread courant.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ✅ Décharger un calcul lourd du thread UI</span>
<span class="k">private</span> <span class="k">async</span> <span class="k">void</span> <span class="nf">CalculerButton_Click</span><span class="p">(</span><span class="kt">object</span> <span class="n">sender</span><span class="p">,</span> <span class="n">RoutedEventArgs</span> <span class="n">e</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">resultat</span> <span class="p">=</span> <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Run</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nf">CalculComplexe</span><span class="p">(</span><span class="n">donnees</span><span class="p">));</span>
    <span class="n">ResultatTextBlock</span><span class="p">.</span><span class="n">Text</span> <span class="p">=</span> <span class="n">resultat</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span> <span class="c1">// retour sur le thread UI</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="quand-ne-pas-utiliser-taskrun">Quand ne PAS utiliser <code class="language-plaintext highlighter-rouge">Task.Run</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Ne pas wrapper un appel déjà asynchrone dans Task.Run</span>
<span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">await</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">}</span>
<span class="c1">// httpClient.GetStringAsync est déjà asynchrone, Task.Run gaspille un thread</span>

<span class="c1">// ✅ Appeler directement</span>
<span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="nf">GetDataAsync</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">httpClient</span><span class="p">.</span><span class="nf">GetStringAsync</span><span class="p">(</span><span class="n">url</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Règle</strong> : <code class="language-plaintext highlighter-rouge">Task.Run</code> pour le <strong>calcul CPU</strong> en contexte UI. Jamais dans du code serveur (ASP.NET Core), où les threads sont précieux.</p>

<h1 id="résumé">Résumé</h1>

<table>
  <thead>
    <tr>
      <th>Concept</th>
      <th>À retenir</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code></strong></td>
      <td>Libère le thread pendant les opérations I/O, ne crée pas de thread</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">Task&lt;T&gt;</code></strong></td>
      <td>Représente une opération en cours avec un résultat futur</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">ValueTask&lt;T&gt;</code></strong></td>
      <td>Alternative légère quand le résultat est souvent déjà disponible</td>
    </tr>
    <tr>
      <td><strong>Machine à états</strong></td>
      <td>Le compilateur transforme la méthode en <code class="language-plaintext highlighter-rouge">IAsyncStateMachine</code> avec un <code class="language-plaintext highlighter-rouge">switch</code> sur <code class="language-plaintext highlighter-rouge">_state</code></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">SynchronizationContext</code></strong></td>
      <td>Détermine sur quel thread reprendre après un <code class="language-plaintext highlighter-rouge">await</code></td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">ConfigureAwait(false)</code></strong></td>
      <td>Indispensable dans les bibliothèques, inutile en ASP.NET Core</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">async void</code></strong></td>
      <td>À éviter sauf pour les event handlers UI — les exceptions sont non capturables</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">.Result</code> / <code class="language-plaintext highlighter-rouge">.Wait()</code></strong></td>
      <td>Risque de deadlock — préférer <code class="language-plaintext highlighter-rouge">await</code> partout</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">Task.WhenAll</code></strong></td>
      <td>Paralléliser des opérations indépendantes</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">CancellationToken</code></strong></td>
      <td>Toujours propager pour permettre l’annulation</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code></strong></td>
      <td>Streaming asynchrone élément par élément</td>
    </tr>
    <tr>
      <td><strong><code class="language-plaintext highlighter-rouge">Task.Run</code></strong></td>
      <td>Uniquement pour décharger du calcul CPU, jamais pour wrapper de l’async</td>
    </tr>
  </tbody>
</table>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="basics" /><category term="async" /><category term="await" /><category term="csharp" /><summary type="html"><![CDATA[La programmation asynchrone avec async/await est au cœur du développement .NET moderne. Elle permet de libérer des threads pendant les opérations d’entrée/sortie (réseau, fichiers, base de données) au lieu de les bloquer. Mais derrière cette syntaxe simple se cachent une machine à états, un SynchronizationContext, et de nombreux pièges. Cet article détaille le fonctionnement réel d’async/await, de la théorie aux bonnes pratiques.]]></summary></entry><entry><title type="html">LINQ : bonnes pratiques et pièges à éviter</title><link href="http://guym.fr/2026/02/08/linq-best-parctices.html" rel="alternate" type="text/html" title="LINQ : bonnes pratiques et pièges à éviter" /><published>2026-02-08T00:00:00+01:00</published><updated>2026-02-08T00:00:00+01:00</updated><id>http://guym.fr/2026/02/08/linq-best-parctices</id><content type="html" xml:base="http://guym.fr/2026/02/08/linq-best-parctices.html"><![CDATA[<p>LINQ rend le code expressif et concis, mais il est aussi facile de tomber dans des pièges qui dégradent les performances, provoquent des bugs subtils ou rendent le code difficile à maintenir. Cet article recense les bonnes pratiques à adopter et les erreurs les plus courantes à éviter quand on utilise LINQ au quotidien.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Linq</strong> : <strong> 4</strong> sur <strong>4</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/01/31/linq-principe.html">LINQ en C# : principes et fonctionnement</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2026/02/01/linq-implementation.html">Comprendre LINQ par l'implémentation : recoder les opérateurs</a>
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2026/02/07/linq-IEnumerable-vs-IQueryable.html">IEnumerable<T> vs IQueryable<T> : deux mondes, un même LINQ&lt;/a&gt;
		
		&lt;/li&gt;
	
	
	
		
		<li>Part 4 - 
		
			Cet article
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	&lt;/ul&gt;
&lt;/div&gt;


# Performances

## 1. Préférer `Any()` à `Count() &gt; 0`

`Count()` doit parcourir **toute** la séquence pour compter les éléments. `Any()` s'arrête au **premier élément trouvé**.

```csharp
// ❌ Parcourt toute la collection
if (commandes.Count() &gt; 0) { /* ... */ }

// ✅ S'arrête dès le premier élément
if (commandes.Any()) { /* ... */ }

// Idem avec prédicat
// ❌
if (commandes.Count(c =&gt; c.Montant &gt; 100) &gt; 0) { /* ... */ }
// ✅
if (commandes.Any(c =&gt; c.Montant &gt; 100)) { /* ... */ }
```

**Exception** : si la source est un `ICollection<T>` (comme `List<T>`), la propriété `.Count` (pas la méthode LINQ `.Count()`) est en O(1). Mais `Any()` reste plus lisible pour exprimer l'intention "y a-t-il au moins un élément ?".

## 2. Ne pas itérer plusieurs fois sur un `IEnumerable<T>`

Chaque appel à `foreach`, `Count()`, `ToList()`, etc. sur un `IEnumerable<T>` **ré-exécute** tout le pipeline. Si la source est une requête Entity Framework, cela signifie **plusieurs aller-retours SQL**.

```csharp
// ❌ Deux itérations = deux exécutions complètes
IEnumerable<Client> actifs = clients.Where(c =&gt; c.IsActif);
var nombre = actifs.Count();        // 1ère itération
var liste  = actifs.ToList();       // 2ème itération

// ✅ Matérialiser une seule fois
var actifs = clients.Where(c =&gt; c.IsActif).ToList();
var nombre = actifs.Count;          // propriété O(1)
```

ReSharper et Roslyn émettent d'ailleurs un avertissement `CA1851` / "Possible multiple enumerations of IEnumerable" pour ce cas.

## 3. Filtrer le plus tôt possible dans le pipeline

L'ordre des opérateurs a un impact direct sur les performances. Placez les `Where` **avant** les `Select`, `OrderBy` ou `GroupBy` pour réduire le volume de données traitées.

```csharp
// ❌ Trie TOUS les éléments, puis filtre
var resultat = commandes
    .OrderBy(c =&gt; c.Date)
    .Where(c =&gt; c.Montant &gt; 1000)
    .ToList();

// ✅ Filtre d'abord (moins d'éléments à trier)
var resultat = commandes
    .Where(c =&gt; c.Montant &gt; 1000)
    .OrderBy(c =&gt; c.Date)
    .ToList();
```

## 4. Chaîner les opérateurs : coût réel de deux `Where` consécutifs

Une question fréquente : est-ce que `.Where(A).Where(B)` est moins performant que `.Where(x =&gt; A(x) &amp;&amp; B(x))` ?

### Ce qui se passe sous le capot

Chaque appel à `Where` crée un **nouvel itérateur**. Deux `Where` consécutifs produisent donc **deux objets** et **deux niveaux d'imbrication** dans le pipeline :

```csharp
// Deux itérateurs : WhereIterator2 → WhereIterator1 → source
var resultat = personnes
    .Where(p =&gt; p.Age &gt;= 18)       // crée WhereIterator1
    .Where(p =&gt; p.Nom.Length &gt; 3)  // crée WhereIterator2
    .ToList();

// Un seul itérateur : WhereIterator → source
var resultat = personnes
    .Where(p =&gt; p.Age &gt;= 18 &amp;&amp; p.Nom.Length &gt; 3) // crée 1 seul WhereIterator
    .ToList();
```

Avec deux `Where`, chaque élément de la source passe par :
1. `WhereIterator1.MoveNext()` → appelle `source.MoveNext()` + évalue le 1er prédicat
2. `WhereIterator2.MoveNext()` → appelle `WhereIterator1.MoveNext()` + évalue le 2ème prédicat

Avec un seul `Where`, chaque élément passe par :
1. `WhereIterator.MoveNext()` → appelle `source.MoveNext()` + évalue les 2 prédicats

### Le coût concret

| Aspect | `.Where(A).Where(B)` | `.Where(A &amp;&amp; B)` |
|---|---|---|
| Allocations | 2 itérateurs + 2 delegates | 1 itérateur + 1 delegate |
| Appels `MoveNext()` par élément | 2 | 1 |
| Appels de delegates par élément | 1 ou 2 (court-circuit si A = false) | 1 (court-circuit interne via `&amp;&amp;`) |
| Overhead mémoire | ~2× plus d'objets sur le tas | Minimal |

### L'optimisation de .NET : la fusion d'itérateurs

Bonne nouvelle : l'implémentation officielle de LINQ dans .NET **optimise** ce cas. Quand on enchaîne `.Where().Where()` sur la même source, le runtime fusionne les deux prédicats en un seul itérateur (`CombinedPredicates`) :

```csharp
// .NET détecte l'enchaînement et produit en interne quelque chose comme :
// WhereIterator(source, x =&gt; predicate1(x) &amp;&amp; predicate2(x))
```

Cette optimisation existe depuis .NET Core, mais **uniquement pour `Where().Where()`**. Elle ne s'applique pas aux combinaisons mixtes comme `Where().Select().Where()`.

### En pratique : quand s'en préoccuper ?

```csharp
// ✅ Parfaitement acceptable pour la lisibilité courante
var commandesValides = commandes
    .Where(c =&gt; c.Statut != Statut.Annulee)
    .Where(c =&gt; c.Montant &gt; 0)
    .Where(c =&gt; c.Date &gt;= dateDebut);
// → fusionné par .NET, coût quasi identique à un seul Where

// ✅ Un seul Where si le prédicat reste lisible
var commandesValides = commandes
    .Where(c =&gt; c.Statut != Statut.Annulee &amp;&amp; c.Montant &gt; 0 &amp;&amp; c.Date &gt;= dateDebut);

// ⚠️ Dans un hot path critique (millions d'itérations par seconde),
//     préférer un seul Where ou une boucle foreach
for (int i = 0; i &lt; data.Length; i++)
{
    if (data[i].A &amp;&amp; data[i].B)
        resultats.Add(data[i]);
}
```

### Impact avec `Select` entre deux `Where`

Attention : si un `Select` sépare deux `Where`, la fusion ne s'applique **pas**, et le coût est réel :

```csharp
// ❌ Pas de fusion possible : 3 itérateurs distincts
var resultat = personnes
    .Where(p =&gt; p.Age &gt;= 18)          // WhereIterator
    .Select(p =&gt; new { p.Nom, p.Age }) // SelectIterator
    .Where(x =&gt; x.Nom.Length &gt; 3)     // WhereIterator (sur type anonyme)
    .ToList();

// ✅ Regrouper les filtres avant la projection
var resultat = personnes
    .Where(p =&gt; p.Age &gt;= 18 &amp;&amp; p.Nom.Length &gt; 3) // 1 seul WhereIterator
    .Select(p =&gt; new { p.Nom, p.Age })            // 1 SelectIterator
    .ToList();
```

### Résumé

&gt; Pour du code courant, **la lisibilité prime** : séparer les `Where` en plusieurs lignes est parfaitement acceptable grâce à la fusion automatique. Mais dans les **chemins critiques** ou quand un `Select` s'intercale, regrouper les conditions dans un seul `Where` évite les allocations et les niveaux d'indirection supplémentaires.

## 5. Attention aux allocations cachées

Chaque opérateur LINQ crée un **itérateur** (un objet sur le tas). Dans du code appelé très fréquemment (boucle serrée, hot path), cela peut peser.

```csharp
// ❌ Dans un hot path : allocations à chaque appel
bool Contient(List<int> liste, int valeur)
    =&gt; liste.Where(x =&gt; x == valeur).Any();

// ✅ Utiliser une boucle classique dans les chemins critiques
bool Contient(List<int> liste, int valeur)
{
    foreach (var x in liste)
        if (x == valeur) return true;
    return false;
}

// ✅ Ou simplement utiliser Contains
bool Contient(List<int> liste, int valeur)
    =&gt; liste.Contains(valeur);
```

## 6. Utiliser les bonnes structures de données

LINQ fonctionne sur `IEnumerable<T>`, mais les performances dépendent de la **structure sous-jacente**.

```csharp
// ❌ Recherche dans une List<T> : O(n)
var existe = listeClients.Any(c =&gt; c.Id == 42);

// ✅ Utiliser un HashSet ou Dictionary quand on fait beaucoup de recherches
var index = new HashSet<int>(listeClients.Select(c =&gt; c.Id));
var existe = index.Contains(42); // O(1)
```

# Pièges avec `IQueryable<T>` (Entity Framework)

## 7. Ne pas faire basculer en mémoire trop tôt

Appeler `ToList()`, `AsEnumerable()` ou `ToArray()` trop tôt dans le pipeline force le chargement **de toutes les données** en mémoire. Les opérateurs suivants s'exécutent alors côté client.

```csharp
// ❌ Charge TOUTE la table, puis filtre en mémoire
var grosClients = dbContext.Clients
    .ToList()                               // SELECT * FROM Clients
    .Where(c =&gt; c.ChiffreAffaires &gt; 100000) // filtre en mémoire
    .OrderBy(c =&gt; c.Nom)                    // tri en mémoire
    .ToList();

// ✅ Tout est traduit en SQL
var grosClients = dbContext.Clients
    .Where(c =&gt; c.ChiffreAffaires &gt; 100000) // WHERE ChiffreAffaires &gt; 100000
    .OrderBy(c =&gt; c.Nom)                    // ORDER BY Nom
    .ToList();                              // une seule requête SQL optimisée
```

## 8. Éviter les méthodes C# non traduisibles en SQL

Les providers `IQueryable` (Entity Framework) ne peuvent traduire que certaines expressions. Appeler une méthode C# arbitraire dans un `Where` ou `Select` provoque soit une erreur, soit une évaluation côté client inattendue.

```csharp
// ❌ EF ne peut pas traduire FormatNom() en SQL
var noms = dbContext.Clients
    .Where(c =&gt; FormatNom(c.Nom).StartsWith("A"))
    .ToList();

// ✅ Option 1 : utiliser les expressions que EF sait traduire
var noms = dbContext.Clients
    .Where(c =&gt; c.Nom.StartsWith("A"))
    .ToList();

// ✅ Option 2 : basculer en mémoire APRÈS le filtre SQL
var noms = dbContext.Clients
    .Where(c =&gt; c.Nom.StartsWith("A"))  // filtre SQL
    .AsEnumerable()                      // bascule en mémoire
    .Where(c =&gt; FormatNom(c.Nom).Length &gt; 5) // filtre C#
    .ToList();
```

## 9. Projeter uniquement les colonnes nécessaires

Sans `Select`, Entity Framework charge **toutes les colonnes** de l'entité.

```csharp
// ❌ Charge toutes les colonnes de Client (y compris les BLOBs, etc.)
var noms = dbContext.Clients
    .Where(c =&gt; c.IsActif)
    .ToList()
    .Select(c =&gt; c.Nom);

// ✅ Ne charge que la colonne Nom (SELECT Nom FROM ...)
var noms = dbContext.Clients
    .Where(c =&gt; c.IsActif)
    .Select(c =&gt; c.Nom)
    .ToList();
```

# Pièges avec l'exécution différée

## 10. La source peut changer entre la définition et l'exécution

L'exécution différée signifie que la requête n'est évaluée que quand on itère. Si la source change entre-temps, les résultats reflètent l'état **au moment de l'itération**, pas de la définition.

```csharp
var liste = new List<int> { 1, 2, 3 };
var grands = liste.Where(n =&gt; n &gt; 1);

liste.Add(10);     // modification APRÈS la définition de la requête
liste.Remove(2);   // suppression d'un élément

var resultat = grands.ToList(); // [3, 10] — reflète l'état actuel
```

Cela peut être **voulu** (la requête est toujours "fraîche") ou être un **bug** (on s'attendait à un snapshot).

## 11. Les closures capturent la variable, pas la valeur

```csharp
// ❌ Piège classique : toutes les actions utilisent la valeur finale de i
var actions = new List&lt;Func<int>&gt;();
for (int i = 0; i &lt; 5; i++)
{
    actions.Add(() =&gt; i * 10);
}
// actions[0]() == 50, actions[1]() == 50, ..., actions[4]() == 50
// Car i vaut 5 après la boucle

// ✅ Capturer une copie locale
for (int i = 0; i &lt; 5; i++)
{
    var copie = i;
    actions.Add(() =&gt; copie * 10);
}
// actions[0]() == 0, actions[1]() == 10, ..., actions[4]() == 40
```

&gt; Avec `foreach`, ce piège n'existe plus depuis C# 5 : la variable d'itération est capturée correctement à chaque itération.

## 12. `SingleOrDefault` vs `FirstOrDefault`

Ces deux méthodes ne sont **pas interchangeables** :

| Méthode | 0 élément | 1 élément | 2+ éléments |
|---|---|---|---|
| `First()` | ❌ Exception | ✅ Retourne l'élément | ✅ Retourne le premier |
| `FirstOrDefault()` | ✅ Retourne `default` | ✅ Retourne l'élément | ✅ Retourne le premier |
| `Single()` | ❌ Exception | ✅ Retourne l'élément | ❌ Exception |
| `SingleOrDefault()` | ✅ Retourne `default` | ✅ Retourne l'élément | ❌ Exception |

```csharp
// Quand on s'attend à 0 ou 1 résultat (et que &gt;1 est une erreur)
var client = dbContext.Clients.SingleOrDefault(c =&gt; c.Email == email);

// Quand on veut juste le premier, peu importe s'il y en a plusieurs
var derniereCommande = dbContext.Commandes
    .OrderByDescending(c =&gt; c.Date)
    .FirstOrDefault();
```

**Côté SQL** : `SingleOrDefault` doit vérifier qu'il n'y a pas de second résultat, ce qui peut être marginalement plus coûteux.

# Lisibilité et maintenabilité

## 13. Éviter les pipelines trop longs

Un pipeline LINQ de 15 opérateurs enchaînés devient illisible. Découper en étapes nommées.

```csharp
// ❌ Pipeline illisible
var resultat = commandes
    .Where(c =&gt; c.Date &gt;= debut &amp;&amp; c.Date &lt;= fin)
    .Where(c =&gt; c.Statut != Statut.Annulee)
    .GroupBy(c =&gt; c.ClientId)
    .Select(g =&gt; new { ClientId = g.Key, Total = g.Sum(c =&gt; c.Montant) })
    .Where(x =&gt; x.Total &gt; 10000)
    .OrderByDescending(x =&gt; x.Total)
    .Take(10)
    .ToList();

// ✅ Étapes nommées qui documentent l'intention
var commandesPeriode = commandes
    .Where(c =&gt; c.Date &gt;= debut &amp;&amp; c.Date &lt;= fin)
    .Where(c =&gt; c.Statut != Statut.Annulee);

var topClients = commandesPeriode
    .GroupBy(c =&gt; c.ClientId)
    .Select(g =&gt; new { ClientId = g.Key, Total = g.Sum(c =&gt; c.Montant) })
    .Where(x =&gt; x.Total &gt; 10000)
    .OrderByDescending(x =&gt; x.Total)
    .Take(10)
    .ToList();
```

Comme les variables intermédiaires sont des `IEnumerable`/`IQueryable`, il n'y a **aucun coût** supplémentaire : le pipeline final est identique.

## 14. Nommer les types anonymes quand ils sont réutilisés

```csharp
// ❌ Type anonyme difficile à passer en paramètre ou retourner
var stats = commandes
    .GroupBy(c =&gt; c.ClientId)
    .Select(g =&gt; new { ClientId = g.Key, Total = g.Sum(c =&gt; c.Montant), Nombre = g.Count() });

// ✅ Utiliser un record pour un type nommé, réutilisable, testable
public record ClientStats(int ClientId, decimal Total, int Nombre);

var stats = commandes
    .GroupBy(c =&gt; c.ClientId)
    .Select(g =&gt; new ClientStats(g.Key, g.Sum(c =&gt; c.Montant), g.Count()));
```

## 15. Préférer les opérateurs LINQ aux réinventions manuelles

```csharp
// ❌ Réinventer la roue
var max = int.MinValue;
foreach (var c in commandes)
    if (c.Montant &gt; max) max = c.Montant;

// ✅ Utiliser l'opérateur dédié
var max = commandes.Max(c =&gt; c.Montant);

// ❌ Construire un dictionnaire manuellement
var dict = new Dictionary&lt;int, Client&gt;();
foreach (var c in clients)
    dict[c.Id] = c;

// ✅
var dict = clients.ToDictionary(c =&gt; c.Id);
```

# Pièges spécifiques

## 16. `ToDictionary` lance une exception si les clés ne sont pas uniques

```csharp
// ❌ Exception si deux clients ont le même email
var parEmail = clients.ToDictionary(c =&gt; c.Email);

// ✅ Si les doublons sont possibles, utiliser ToLookup ou GroupBy
var parEmail = clients.ToLookup(c =&gt; c.Email); // ILookup&lt;string, Client&gt;

// ✅ Ou gérer explicitement les doublons
var parEmail = clients
    .GroupBy(c =&gt; c.Email)
    .ToDictionary(g =&gt; g.Key, g =&gt; g.First());
```

## 17. `Distinct()` utilise l'égalité par défaut du type

Pour les types référence (classes), `Distinct()` compare par **référence** sauf si `Equals`/`GetHashCode` sont redéfinis.

```csharp
// ❌ Ne supprime PAS les doublons logiques (comparaison par référence)
var villes = clients.Select(c =&gt; new Ville(c.CodePostal, c.NomVille)).Distinct();

// ✅ Option 1 : utiliser un record (égalité structurelle automatique)
public record Ville(string CodePostal, string Nom);
var villes = clients.Select(c =&gt; new Ville(c.CodePostal, c.NomVille)).Distinct();

// ✅ Option 2 : utiliser DistinctBy (.NET 6+)
var villes = clients.DistinctBy(c =&gt; c.CodePostal);

// ✅ Option 3 : fournir un IEqualityComparer<T>
var villes = clients
    .Select(c =&gt; new Ville(c.CodePostal, c.NomVille))
    .Distinct(new VilleComparer());
```

## 18. `OrderBy` n'est pas stable avec `IQueryable`

En LINQ to Objects, `OrderBy` est un tri **stable** (les éléments égaux conservent leur ordre relatif). Mais en SQL, l'ordre des lignes avec la même clé de tri est **indéterminé**.

```csharp
// ❌ L'ordre des clients ayant le même nom est imprévisible en SQL
var clients = dbContext.Clients.OrderBy(c =&gt; c.Nom).ToList();

// ✅ Toujours ajouter un critère de départage déterministe
var clients = dbContext.Clients
    .OrderBy(c =&gt; c.Nom)
    .ThenBy(c =&gt; c.Id)  // Id unique = ordre déterministe
    .ToList();
```

## 19. `string.Contains` est sensible à la casse en C# mais pas toujours en SQL

```csharp
// En LINQ to Objects : sensible à la casse
var resultat = noms.Where(n =&gt; n.Contains("alice")); // ne trouve pas "Alice"

// En EF Core avec SQL Server : insensible à la casse (collation par défaut)
var resultat = dbContext.Clients
    .Where(c =&gt; c.Nom.Contains("alice")); // TROUVE "Alice" (dépend de la collation)

// ✅ Être explicite sur la comparaison
// En mémoire :
var resultat = noms
    .Where(n =&gt; n.Contains("alice", StringComparison.OrdinalIgnoreCase));

// En EF Core (.NET 8+) :
var resultat = dbContext.Clients
    .Where(c =&gt; EF.Functions.Like(c.Nom, "%alice%"));
```

# Aide-mémoire

| Règle | Pourquoi |
|---|---|
| `Any()` plutôt que `Count() &gt; 0` | Court-circuit, performance |
| Matérialiser une seule fois | Éviter les itérations multiples et les requêtes SQL dupliquées |
| Filtrer avant de trier/projeter | Réduire le volume de données à traiter |
| `ToList()` le plus tard possible (EF) | Garder l'exécution côté serveur |
| `Select` pour projeter les colonnes (EF) | Ne charger que ce qui est nécessaire |
| Nommer les étapes longues | Lisibilité et maintenabilité |
| `SingleOrDefault` vs `FirstOrDefault` | Sémantique différente, choisir avec intention |
| `ThenBy` pour un tri déterministe | Éviter les ordres imprévisibles en SQL |
| `DistinctBy` ou records pour `Distinct` | L'égalité par défaut des classes est par référence |
| Boucle classique dans les hot paths | Éviter les allocations des itérateurs LINQ |
</T></int></int></T></int></T></T></int></int></int></Client></T></T></T></T></T></T></a></li></ul></div>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="basics" /><category term="linq" /><category term="csharp" /><category term="best-practices" /><summary type="html"><![CDATA[LINQ rend le code expressif et concis, mais il est aussi facile de tomber dans des pièges qui dégradent les performances, provoquent des bugs subtils ou rendent le code difficile à maintenir. Cet article recense les bonnes pratiques à adopter et les erreurs les plus courantes à éviter quand on utilise LINQ au quotidien.]]></summary></entry><entry><title type="html">IEnumerable vs IQueryable : deux mondes, un même LINQ</title><link href="http://guym.fr/2026/02/07/linq-IEnumerable-vs-IQueryable.html" rel="alternate" type="text/html" title="IEnumerable vs IQueryable : deux mondes, un même LINQ" /><published>2026-02-07T00:00:00+01:00</published><updated>2026-02-07T00:00:00+01:00</updated><id>http://guym.fr/2026/02/07/linq-IEnumerable-vs-IQueryable</id><content type="html" xml:base="http://guym.fr/2026/02/07/linq-IEnumerable-vs-IQueryable.html"><![CDATA[<p>Quand on écrit <code class="language-plaintext highlighter-rouge">.Where(x =&gt; x.IsActive)</code>, la syntaxe est identique que l’on travaille sur une <code class="language-plaintext highlighter-rouge">List&lt;T&gt;</code> ou sur un <code class="language-plaintext highlighter-rouge">DbSet&lt;T&gt;</code> Entity Framework. Pourtant le comportement est radicalement différent : d’un côté le filtre s’exécute en mémoire, de l’autre il est traduit en SQL. Comprendre cette dualité <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> / <code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> est essentiel pour éviter les problèmes de performance et les bugs subtils.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Linq</strong> : <strong> 3</strong> sur <strong>4</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/01/31/linq-principe.html">LINQ en C# : principes et fonctionnement</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2026/02/01/linq-implementation.html">Comprendre LINQ par l'implémentation : recoder les opérateurs</a>
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2026/02/08/linq-best-parctices.html">LINQ : bonnes pratiques et pièges à éviter</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	</ul>
</div>

<h1 id="deux-interfaces-deux-stratégies">Deux interfaces, deux stratégies</h1>

<h2 id="ienumerablet--exécution-en-mémoire"><code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> — exécution en mémoire</h2>

<p><code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> est l’interface de base de toute séquence en .NET. Les opérateurs LINQ définis dans <code class="language-plaintext highlighter-rouge">System.Linq.Enumerable</code> travaillent sur cette interface.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Les opérateurs reçoivent des delegates (Func&lt;T, bool&gt;)</span>
<span class="k">public</span> <span class="k">static</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">Where</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span>
    <span class="k">this</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">source</span><span class="p">,</span>
    <span class="n">Func</span><span class="p">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;</span> <span class="n">predicate</span><span class="p">);</span>
</code></pre></div></div>

<p>Le prédicat est un <strong>delegate</strong> : du code compilé en IL, exécuté directement par le CLR en mémoire.</p>

<h2 id="iqueryablet--traduction-par-un-provider"><code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> — traduction par un provider</h2>

<p><code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> hérite de <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> mais les opérateurs LINQ sont définis dans <code class="language-plaintext highlighter-rouge">System.Linq.Queryable</code> et reçoivent des <strong>arbres d’expressions</strong>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Les opérateurs reçoivent des arbres d'expressions (Expression&lt;Func&lt;T, bool&gt;&gt;)</span>
<span class="k">public</span> <span class="k">static</span> <span class="n">IQueryable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">Where</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span>
    <span class="k">this</span> <span class="n">IQueryable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">source</span><span class="p">,</span>
    <span class="n">Expression</span><span class="p">&lt;</span><span class="n">Func</span><span class="p">&lt;</span><span class="n">T</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;&gt;</span> <span class="n">predicate</span><span class="p">);</span>
</code></pre></div></div>

<p>Le prédicat est un <strong>arbre d’expression</strong> (<code class="language-plaintext highlighter-rouge">Expression&lt;Func&lt;T, bool&gt;&gt;</code>) : une structure de données qui <strong>décrit</strong> le code sans l’exécuter. Un provider (Entity Framework, LINQ to SQL, etc.) peut l’analyser et le traduire dans un autre langage (SQL, HTTP, etc.).</p>

<h1 id="funct-bool-vs-expressionfunct-bool"><code class="language-plaintext highlighter-rouge">Func&lt;T, bool&gt;</code> vs <code class="language-plaintext highlighter-rouge">Expression&lt;Func&lt;T, bool&gt;&gt;</code></h1>

<p>C’est <strong>la</strong> différence fondamentale.</p>

<h2 id="funct-bool--du-code-exécutable"><code class="language-plaintext highlighter-rouge">Func&lt;T, bool&gt;</code> — du code exécutable</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;</span> <span class="n">estPair</span> <span class="p">=</span> <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span> <span class="p">%</span> <span class="m">2</span> <span class="p">==</span> <span class="m">0</span><span class="p">;</span>

<span class="c1">// C'est un delegate : on peut l'appeler</span>
<span class="kt">bool</span> <span class="n">resultat</span> <span class="p">=</span> <span class="nf">estPair</span><span class="p">(</span><span class="m">42</span><span class="p">);</span> <span class="c1">// true</span>
</code></pre></div></div>

<p>Le compilateur transforme la lambda en <strong>méthode anonyme</strong> compilée en IL. C’est une boîte noire : on peut l’exécuter, mais pas l’inspecter.</p>

<h2 id="expressionfuncint-bool--une-description-du-code"><code class="language-plaintext highlighter-rouge">Expression&lt;Func&lt;int, bool&gt;&gt;</code> — une description du code</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Expression</span><span class="p">&lt;</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;&gt;</span> <span class="n">estPair</span> <span class="p">=</span> <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span> <span class="p">%</span> <span class="m">2</span> <span class="p">==</span> <span class="m">0</span><span class="p">;</span>

<span class="c1">// C'est un arbre : on peut l'analyser</span>
<span class="c1">// estPair.Body → (x % 2) == 0</span>
<span class="c1">// estPair.Body.NodeType → Equal</span>
<span class="c1">// estPair.Body.Left → (x % 2)</span>
<span class="c1">// estPair.Body.Left.Left → x</span>
<span class="c1">// estPair.Body.Left.Right → 2</span>
</code></pre></div></div>

<p>Le compilateur génère un <strong>arbre de syntaxe</strong> qu’on peut parcourir, transformer et traduire. C’est ce qu’Entity Framework fait pour produire du SQL.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>         Equal (==)
        /          \
    Modulo (%)      Constant (0)
    /        \
Parameter (x)  Constant (2)
</code></pre></div></div>

<h2 id="démonstration-concrète">Démonstration concrète</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Expression</span><span class="p">&lt;</span><span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;&gt;</span> <span class="n">expr</span> <span class="p">=</span> <span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span> <span class="p">&gt;</span> <span class="m">5</span> <span class="p">&amp;&amp;</span> <span class="n">x</span> <span class="p">&lt;</span> <span class="m">100</span><span class="p">;</span>

<span class="c1">// On peut "visiter" l'arbre</span>
<span class="kt">var</span> <span class="n">body</span> <span class="p">=</span> <span class="p">(</span><span class="n">BinaryExpression</span><span class="p">)</span><span class="n">expr</span><span class="p">.</span><span class="n">Body</span><span class="p">;</span>          <span class="c1">// &amp;&amp;</span>
<span class="kt">var</span> <span class="n">left</span> <span class="p">=</span> <span class="p">(</span><span class="n">BinaryExpression</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">Left</span><span class="p">;</span>          <span class="c1">// x &gt; 5</span>
<span class="kt">var</span> <span class="n">right</span> <span class="p">=</span> <span class="p">(</span><span class="n">BinaryExpression</span><span class="p">)</span><span class="n">body</span><span class="p">.</span><span class="n">Right</span><span class="p">;</span>        <span class="c1">// x &lt; 100</span>

<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">body</span><span class="p">.</span><span class="n">NodeType</span><span class="p">);</span>   <span class="c1">// AndAlso</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">left</span><span class="p">.</span><span class="n">NodeType</span><span class="p">);</span>   <span class="c1">// GreaterThan</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">right</span><span class="p">.</span><span class="n">NodeType</span><span class="p">);</span>  <span class="c1">// LessThan</span>

<span class="c1">// Et on peut aussi le compiler en delegate pour l'exécuter</span>
<span class="n">Func</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">,</span> <span class="kt">bool</span><span class="p">&gt;</span> <span class="n">func</span> <span class="p">=</span> <span class="n">expr</span><span class="p">.</span><span class="nf">Compile</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="nf">func</span><span class="p">(</span><span class="m">42</span><span class="p">));</span>  <span class="c1">// true</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="nf">func</span><span class="p">(</span><span class="m">3</span><span class="p">));</span>   <span class="c1">// false</span>
</code></pre></div></div>

<h1 id="comportement-côté-exécution">Comportement côté exécution</h1>

<h2 id="la-même-requête-deux-exécutions-différentes">La même requête, deux exécutions différentes</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Source IEnumerable&lt;T&gt; (liste en mémoire)</span>
<span class="n">List</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="n">clientsEnMemoire</span> <span class="p">=</span> <span class="nf">GetClients</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">actifs1</span> <span class="p">=</span> <span class="n">clientsEnMemoire</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">IsActif</span><span class="p">)</span>           <span class="c1">// Func&lt;Client, bool&gt; → filtre en mémoire</span>
    <span class="p">.</span><span class="nf">OrderBy</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Nom</span><span class="p">)</span>             <span class="c1">// tri en mémoire</span>
    <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Nom</span><span class="p">)</span>              <span class="c1">// projection en mémoire</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>

<span class="c1">// Source IQueryable&lt;T&gt; (DbSet Entity Framework)</span>
<span class="n">IQueryable</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="n">clientsDb</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">actifs2</span> <span class="p">=</span> <span class="n">clientsDb</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">IsActif</span><span class="p">)</span>           <span class="c1">// Expression → traduit en WHERE IsActif = 1</span>
    <span class="p">.</span><span class="nf">OrderBy</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Nom</span><span class="p">)</span>             <span class="c1">// Expression → traduit en ORDER BY Nom</span>
    <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Nom</span><span class="p">)</span>              <span class="c1">// Expression → traduit en SELECT Nom</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>                        <span class="c1">// exécute la requête SQL</span>
</code></pre></div></div>

<p>Pour <code class="language-plaintext highlighter-rouge">clientsEnMemoire</code> :</p>
<ol>
  <li><strong>Toutes les données</strong> sont déjà en mémoire.</li>
  <li><code class="language-plaintext highlighter-rouge">Where</code> itère sur chaque élément et appelle le delegate.</li>
  <li><code class="language-plaintext highlighter-rouge">OrderBy</code> charge les résultats filtrés dans un buffer et trie.</li>
  <li><code class="language-plaintext highlighter-rouge">Select</code> projette chaque élément trié.</li>
</ol>

<p>Pour <code class="language-plaintext highlighter-rouge">clientsDb</code> :</p>
<ol>
  <li>Chaque opérateur <strong>enrichit un arbre d’expression</strong>.</li>
  <li>Rien ne s’exécute tant qu’on n’appelle pas <code class="language-plaintext highlighter-rouge">ToList()</code>.</li>
  <li>Au moment du <code class="language-plaintext highlighter-rouge">ToList()</code>, EF traduit tout l’arbre en <strong>une seule requête SQL</strong> :</li>
</ol>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="p">[</span><span class="k">c</span><span class="p">].[</span><span class="n">Nom</span><span class="p">]</span>
<span class="k">FROM</span> <span class="p">[</span><span class="n">Clients</span><span class="p">]</span> <span class="k">AS</span> <span class="p">[</span><span class="k">c</span><span class="p">]</span>
<span class="k">WHERE</span> <span class="p">[</span><span class="k">c</span><span class="p">].[</span><span class="n">IsActif</span><span class="p">]</span> <span class="o">=</span> <span class="mi">1</span>
<span class="k">ORDER</span> <span class="k">BY</span> <span class="p">[</span><span class="k">c</span><span class="p">].[</span><span class="n">Nom</span><span class="p">]</span>
</code></pre></div></div>

<h1 id="le-piège-de-la-bascule-implicite">Le piège de la bascule implicite</h1>

<h2 id="asenumerable-et-tolist-changent-le-monde-dexécution"><code class="language-plaintext highlighter-rouge">AsEnumerable()</code> et <code class="language-plaintext highlighter-rouge">ToList()</code> changent le monde d’exécution</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">resultat</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">IsActif</span><span class="p">)</span>        <span class="c1">// ← IQueryable : traduit en SQL</span>
    <span class="p">.</span><span class="nf">AsEnumerable</span><span class="p">()</span>                <span class="c1">// ← bascule vers IEnumerable !</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Nom</span><span class="p">.</span><span class="n">Length</span> <span class="p">&gt;</span> <span class="m">5</span><span class="p">)</span> <span class="c1">// ← IEnumerable : exécuté en mémoire</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<p>La requête SQL générée est :</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">Clients</span> <span class="k">WHERE</span> <span class="n">IsActif</span> <span class="o">=</span> <span class="mi">1</span>
<span class="c1">-- Le filtre sur Nom.Length est fait EN MÉMOIRE après chargement</span>
</code></pre></div></div>

<p>Le second <code class="language-plaintext highlighter-rouge">Where</code> n’est <strong>pas</strong> dans le SQL. Toutes les lignes actives sont chargées, puis filtrées en C#.</p>

<h2 id="comment-détecter-sur-quel-monde-on-est">Comment détecter sur quel “monde” on est</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IQueryable</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="n">query</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">IsActif</span><span class="p">);</span>
<span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="n">query</span> <span class="k">is</span> <span class="n">IQueryable</span><span class="p">);</span>    <span class="c1">// True → monde SQL</span>

<span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="n">enumerable</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">AsEnumerable</span><span class="p">();</span>
<span class="c1">// enumerable est toujours un IQueryable sous le capot,</span>
<span class="c1">// mais les opérateurs LINQ utilisés seront ceux de Enumerable (pas Queryable)</span>
</code></pre></div></div>

<p>La règle de résolution des méthodes d’extension en C# fait que :</p>
<ul>
  <li>Sur une variable typée <code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> → les méthodes de <code class="language-plaintext highlighter-rouge">Queryable</code> sont choisies (arbres d’expressions).</li>
  <li>Sur une variable typée <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> → les méthodes de <code class="language-plaintext highlighter-rouge">Enumerable</code> sont choisies (delegates).</li>
</ul>

<h1 id="ce-que-iqueryable-ne-peut-pas-faire">Ce que <code class="language-plaintext highlighter-rouge">IQueryable</code> ne peut PAS faire</h1>

<h2 id="méthodes-c-non-traduisibles">Méthodes C# non traduisibles</h2>

<p>Un provider <code class="language-plaintext highlighter-rouge">IQueryable</code> ne peut traduire que les expressions qu’il connaît. Les méthodes C# arbitraires ne sont pas traduisibles.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ EF ne sait pas traduire une méthode perso</span>
<span class="kt">var</span> <span class="n">resultat</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">MonFormateur</span><span class="p">.</span><span class="nf">Formater</span><span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">Nom</span><span class="p">)</span> <span class="p">==</span> <span class="s">"ALICE"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
<span class="c1">// → Exception : "The LINQ expression could not be translated"</span>

<span class="c1">// ❌ Certaines méthodes .NET ne sont pas supportées non plus</span>
<span class="kt">var</span> <span class="n">resultat</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Clients</span>
    <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">DateCreation</span><span class="p">.</span><span class="nf">ToString</span><span class="p">(</span><span class="s">"yyyy-MM-dd"</span><span class="p">)</span> <span class="p">==</span> <span class="s">"2025-01-01"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
<span class="c1">// → Exception ou évaluation côté client selon la version d'EF</span>
</code></pre></div></div>

<h3 id="méthodes-couramment-supportées-par-ef-core">Méthodes couramment supportées par EF Core</h3>

<table>
  <thead>
    <tr>
      <th>Catégorie</th>
      <th>Supporté</th>
      <th>Non supporté</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Comparaisons</td>
      <td><code class="language-plaintext highlighter-rouge">==</code>, <code class="language-plaintext highlighter-rouge">!=</code>, <code class="language-plaintext highlighter-rouge">&gt;</code>, <code class="language-plaintext highlighter-rouge">&lt;</code>, <code class="language-plaintext highlighter-rouge">&gt;=</code>, <code class="language-plaintext highlighter-rouge">&lt;=</code></td>
      <td> </td>
    </tr>
    <tr>
      <td>String</td>
      <td><code class="language-plaintext highlighter-rouge">Contains</code>, <code class="language-plaintext highlighter-rouge">StartsWith</code>, <code class="language-plaintext highlighter-rouge">EndsWith</code>, <code class="language-plaintext highlighter-rouge">ToUpper</code>, <code class="language-plaintext highlighter-rouge">ToLower</code>, <code class="language-plaintext highlighter-rouge">Trim</code>, <code class="language-plaintext highlighter-rouge">Length</code></td>
      <td><code class="language-plaintext highlighter-rouge">Format</code>, <code class="language-plaintext highlighter-rouge">Split</code>, regex</td>
    </tr>
    <tr>
      <td>Math</td>
      <td><code class="language-plaintext highlighter-rouge">Math.Abs</code>, <code class="language-plaintext highlighter-rouge">Math.Round</code>, <code class="language-plaintext highlighter-rouge">Math.Floor</code></td>
      <td><code class="language-plaintext highlighter-rouge">Math.Log</code>, <code class="language-plaintext highlighter-rouge">Math.Pow</code> (variable)</td>
    </tr>
    <tr>
      <td>Nullable</td>
      <td><code class="language-plaintext highlighter-rouge">HasValue</code>, <code class="language-plaintext highlighter-rouge">Value</code>, <code class="language-plaintext highlighter-rouge">GetValueOrDefault</code></td>
      <td> </td>
    </tr>
    <tr>
      <td>Collections</td>
      <td><code class="language-plaintext highlighter-rouge">Contains</code> (traduit en <code class="language-plaintext highlighter-rouge">IN</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">Intersect</code>, <code class="language-plaintext highlighter-rouge">Union</code> sur des listes C#</td>
    </tr>
    <tr>
      <td>Date</td>
      <td><code class="language-plaintext highlighter-rouge">DateTime.Now</code>, <code class="language-plaintext highlighter-rouge">.Year</code>, <code class="language-plaintext highlighter-rouge">.Month</code>, <code class="language-plaintext highlighter-rouge">.Day</code>, <code class="language-plaintext highlighter-rouge">.AddDays</code></td>
      <td>Formatage arbitraire</td>
    </tr>
    <tr>
      <td>EF Functions</td>
      <td><code class="language-plaintext highlighter-rouge">EF.Functions.Like</code>, <code class="language-plaintext highlighter-rouge">EF.Functions.DateDiffDay</code></td>
      <td> </td>
    </tr>
  </tbody>
</table>

<h2 id="pas-de-code-impératif-dans-les-expressions">Pas de code impératif dans les expressions</h2>

<p>Les arbres d’expressions ne supportent pas les instructions (if/else, boucles, try/catch, assignations…). Uniquement des <strong>expressions</strong>.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ Pas possible dans un arbre d'expression</span>
<span class="n">Expression</span><span class="p">&lt;</span><span class="n">Func</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;&gt;</span> <span class="n">expr</span> <span class="p">=</span> <span class="n">c</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">c</span><span class="p">.</span><span class="n">IsVip</span><span class="p">)</span> <span class="k">return</span> <span class="s">"VIP"</span><span class="p">;</span>  <span class="c1">// instruction → erreur de compilation</span>
    <span class="k">return</span> <span class="s">"Standard"</span><span class="p">;</span>
<span class="p">};</span>

<span class="c1">// ✅ Utiliser l'opérateur ternaire (c'est une expression)</span>
<span class="n">Expression</span><span class="p">&lt;</span><span class="n">Func</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;&gt;</span> <span class="n">expr</span> <span class="p">=</span> <span class="n">c</span> <span class="p">=&gt;</span>
    <span class="n">c</span><span class="p">.</span><span class="n">IsVip</span> <span class="p">?</span> <span class="s">"VIP"</span> <span class="p">:</span> <span class="s">"Standard"</span><span class="p">;</span>

<span class="c1">// EF traduit en : CASE WHEN IsVip = 1 THEN 'VIP' ELSE 'Standard' END</span>
</code></pre></div></div>

<h1 id="composition-de-requêtes">Composition de requêtes</h1>

<h2 id="iqueryable-permet-la-composition-dynamique"><code class="language-plaintext highlighter-rouge">IQueryable</code> permet la composition dynamique</h2>

<p>Un des grands avantages de <code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> est qu’on peut <strong>composer</strong> la requête progressivement sans l’exécuter :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IQueryable</span><span class="p">&lt;</span><span class="n">Commande</span><span class="p">&gt;</span> <span class="n">query</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Commandes</span><span class="p">;</span>

<span class="c1">// Ajout conditionnel de filtres</span>
<span class="k">if</span> <span class="p">(</span><span class="n">dateDebut</span><span class="p">.</span><span class="n">HasValue</span><span class="p">)</span>
    <span class="n">query</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Date</span> <span class="p">&gt;=</span> <span class="n">dateDebut</span><span class="p">.</span><span class="n">Value</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(</span><span class="n">dateFin</span><span class="p">.</span><span class="n">HasValue</span><span class="p">)</span>
    <span class="n">query</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Date</span> <span class="p">&lt;=</span> <span class="n">dateFin</span><span class="p">.</span><span class="n">Value</span><span class="p">);</span>

<span class="k">if</span> <span class="p">(!</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrEmpty</span><span class="p">(</span><span class="n">recherche</span><span class="p">))</span>
    <span class="n">query</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Reference</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="n">recherche</span><span class="p">));</span>

<span class="c1">// Le SQL final ne contient QUE les filtres ajoutés</span>
<span class="kt">var</span> <span class="n">resultats</span> <span class="p">=</span> <span class="k">await</span> <span class="n">query</span>
    <span class="p">.</span><span class="nf">OrderByDescending</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Date</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Take</span><span class="p">(</span><span class="m">50</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ToListAsync</span><span class="p">();</span>
</code></pre></div></div>

<p>Si seul <code class="language-plaintext highlighter-rouge">dateDebut</code> est renseigné, le SQL sera :</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="n">TOP</span><span class="p">(</span><span class="mi">50</span><span class="p">)</span> <span class="o">*</span> <span class="k">FROM</span> <span class="n">Commandes</span> <span class="k">WHERE</span> <span class="nb">Date</span> <span class="o">&gt;=</span> <span class="o">@</span><span class="n">p0</span> <span class="k">ORDER</span> <span class="k">BY</span> <span class="nb">Date</span> <span class="k">DESC</span>
</code></pre></div></div>

<h2 id="ienumerable-aussi-mais-tout-est-en-mémoire"><code class="language-plaintext highlighter-rouge">IEnumerable</code> aussi, mais tout est en mémoire</h2>

<p>On peut faire pareil avec <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code>, mais toute la source doit déjà être en mémoire :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">Commande</span><span class="p">&gt;</span> <span class="n">query</span> <span class="p">=</span> <span class="n">commandesEnMemoire</span><span class="p">;</span>

<span class="k">if</span> <span class="p">(</span><span class="n">dateDebut</span><span class="p">.</span><span class="n">HasValue</span><span class="p">)</span>
    <span class="n">query</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Date</span> <span class="p">&gt;=</span> <span class="n">dateDebut</span><span class="p">.</span><span class="n">Value</span><span class="p">);</span>

<span class="c1">// Fonctionne, mais filtre en mémoire sur des données déjà chargées</span>
<span class="kt">var</span> <span class="n">resultats</span> <span class="p">=</span> <span class="n">query</span><span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h1 id="conversions-entre-les-deux-mondes">Conversions entre les deux mondes</h1>

<table>
  <thead>
    <tr>
      <th>Conversion</th>
      <th>Méthode</th>
      <th>Effet</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IQueryable</code> → <code class="language-plaintext highlighter-rouge">IEnumerable</code></td>
      <td><code class="language-plaintext highlighter-rouge">.AsEnumerable()</code></td>
      <td>Bascule la résolution des opérateurs, <strong>ne matérialise pas</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IQueryable</code> → <code class="language-plaintext highlighter-rouge">List&lt;T&gt;</code></td>
      <td><code class="language-plaintext highlighter-rouge">.ToList()</code></td>
      <td>Exécute la requête SQL et charge en mémoire</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">IEnumerable</code> → <code class="language-plaintext highlighter-rouge">IQueryable</code></td>
      <td><code class="language-plaintext highlighter-rouge">.AsQueryable()</code></td>
      <td>Wrapping, <strong>ne recrée pas</strong> un vrai provider SQL</td>
    </tr>
  </tbody>
</table>

<h3 id="attention-à-asqueryable">Attention à <code class="language-plaintext highlighter-rouge">AsQueryable()</code></h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// ❌ AsQueryable() ne transforme PAS une liste en requête SQL</span>
<span class="kt">var</span> <span class="n">listeEnMemoire</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="p">{</span> <span class="m">1</span><span class="p">,</span> <span class="m">2</span><span class="p">,</span> <span class="m">3</span><span class="p">,</span> <span class="m">4</span><span class="p">,</span> <span class="m">5</span> <span class="p">};</span>
<span class="kt">var</span> <span class="n">queryable</span> <span class="p">=</span> <span class="n">listeEnMemoire</span><span class="p">.</span><span class="nf">AsQueryable</span><span class="p">();</span>

<span class="c1">// queryable est un IQueryable, mais le "provider" est EnumerableQuery</span>
<span class="c1">// → le Where est toujours exécuté en mémoire !</span>
<span class="kt">var</span> <span class="n">resultat</span> <span class="p">=</span> <span class="n">queryable</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">x</span> <span class="p">=&gt;</span> <span class="n">x</span> <span class="p">&gt;</span> <span class="m">3</span><span class="p">).</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AsQueryable()</code> est utile pour le <strong>testing</strong> (mocker un <code class="language-plaintext highlighter-rouge">IQueryable</code>) ou pour écrire du code générique, mais il ne crée pas de vraie traduction SQL.</p>

<h1 id="quand-utiliser-quoi-">Quand utiliser quoi ?</h1>

<table>
  <thead>
    <tr>
      <th>Scénario</th>
      <th>Interface</th>
      <th>Raison</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Données déjà en mémoire (listes, tableaux)</td>
      <td><code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code></td>
      <td>Pas de traduction à faire</td>
    </tr>
    <tr>
      <td>Requêter une base de données (EF)</td>
      <td><code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code></td>
      <td>Traduction SQL, filtrage côté serveur</td>
    </tr>
    <tr>
      <td>API publique d’un repository</td>
      <td><code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> ou <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code></td>
      <td>Voir ci-dessous</td>
    </tr>
    <tr>
      <td>Méthode utilitaire qui manipule des séquences</td>
      <td><code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code></td>
      <td>Plus générique, fonctionne partout</td>
    </tr>
    <tr>
      <td>Tests unitaires</td>
      <td><code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> ou <code class="language-plaintext highlighter-rouge">List&lt;T&gt;</code></td>
      <td>Pas besoin de provider</td>
    </tr>
  </tbody>
</table>

<h3 id="le-débat-iqueryable-dans-les-repositories">Le débat <code class="language-plaintext highlighter-rouge">IQueryable</code> dans les repositories</h3>

<p>Exposer <code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> dans un repository donne de la flexibilité aux appelants (ils peuvent composer des filtres), mais <strong>couple</strong> le code appelant à la sémantique de la base de données et rend les tests plus complexes.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Option 1 : exposer IQueryable (flexible mais couplé)</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IClientRepository</span>
<span class="p">{</span>
    <span class="n">IQueryable</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;</span> <span class="nf">GetAll</span><span class="p">();</span> <span class="c1">// l'appelant ajoute ses filtres</span>
<span class="p">}</span>

<span class="c1">// Option 2 : exposer IEnumerable avec des paramètres explicites (découplé)</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IClientRepository</span>
<span class="p">{</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Client</span><span class="p">&gt;&gt;</span> <span class="nf">GetActifs</span><span class="p">(</span><span class="kt">string</span><span class="p">?</span> <span class="n">recherche</span> <span class="p">=</span> <span class="k">null</span><span class="p">,</span> <span class="kt">int</span><span class="p">?</span> <span class="n">limit</span> <span class="p">=</span> <span class="k">null</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Il n’y a pas de réponse universelle. L’option 2 est préférable dans une <strong>Clean Architecture</strong> où on veut isoler la couche domaine de l’infrastructure.</p>

<h1 id="résumé">Résumé</h1>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th><code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code></th>
      <th><code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Namespace</strong></td>
      <td><code class="language-plaintext highlighter-rouge">System.Linq.Enumerable</code></td>
      <td><code class="language-plaintext highlighter-rouge">System.Linq.Queryable</code></td>
    </tr>
    <tr>
      <td><strong>Paramètre du filtre</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Func&lt;T, bool&gt;</code> (delegate)</td>
      <td><code class="language-plaintext highlighter-rouge">Expression&lt;Func&lt;T, bool&gt;&gt;</code> (arbre)</td>
    </tr>
    <tr>
      <td><strong>Exécution</strong></td>
      <td>En mémoire (CLR)</td>
      <td>Traduite par un provider (SQL, etc.)</td>
    </tr>
    <tr>
      <td><strong>Ce qui est envoyé</strong></td>
      <td>Code IL compilé</td>
      <td>Arbre d’expression analysable</td>
    </tr>
    <tr>
      <td><strong>Composition</strong></td>
      <td>Chaîne d’itérateurs en mémoire</td>
      <td>Construction incrémentale d’un arbre</td>
    </tr>
    <tr>
      <td><strong>Méthodes C# perso</strong></td>
      <td>✅ Toutes supportées</td>
      <td>❌ Seulement celles traduisibles</td>
    </tr>
    <tr>
      <td><strong>Performance gros volumes</strong></td>
      <td>Tout doit être en mémoire</td>
      <td>Filtrage côté serveur</td>
    </tr>
    <tr>
      <td><strong>Bascule</strong></td>
      <td><code class="language-plaintext highlighter-rouge">AsEnumerable()</code> depuis IQueryable</td>
      <td><code class="language-plaintext highlighter-rouge">AsQueryable()</code> (wrapping seul)</td>
    </tr>
  </tbody>
</table>

<h1 id="pour-aller-plus-loin--entity-framework-core">Pour aller plus loin : Entity Framework Core</h1>

<p>Cet article parle beaucoup d’<code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> et de traduction SQL. Pour approfondir le sujet côté Entity Framework Core, consultez la série dédiée :</p>

<p><a href="/2025/06/01/EntityPart1.html">Comprendre la “magie” derrière Entity Framework</a></p>

<p><a href="/2025/07/14/EntityPart4.html">Bonnes pratiques pour les requêtes (EF Core)</a></p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="basics" /><category term="linq" /><category term="csharp" /><category term="ienumerable" /><category term="iqueryable" /><summary type="html"><![CDATA[Quand on écrit .Where(x =&gt; x.IsActive), la syntaxe est identique que l’on travaille sur une List&lt;T&gt; ou sur un DbSet&lt;T&gt; Entity Framework. Pourtant le comportement est radicalement différent : d’un côté le filtre s’exécute en mémoire, de l’autre il est traduit en SQL. Comprendre cette dualité IEnumerable&lt;T&gt; / IQueryable&lt;T&gt; est essentiel pour éviter les problèmes de performance et les bugs subtils.]]></summary></entry><entry><title type="html">Comprendre LINQ par l’implémentation : recoder les opérateurs</title><link href="http://guym.fr/2026/02/01/linq-implementation.html" rel="alternate" type="text/html" title="Comprendre LINQ par l’implémentation : recoder les opérateurs" /><published>2026-02-01T00:00:00+01:00</published><updated>2026-02-01T00:00:00+01:00</updated><id>http://guym.fr/2026/02/01/linq-implementation</id><content type="html" xml:base="http://guym.fr/2026/02/01/linq-implementation.html"><![CDATA[<p>LINQ semble magique quand on l’utilise, mais sous le capot, ce sont des <strong>méthodes d’extension</strong>, des <strong>itérateurs</strong> (<code class="language-plaintext highlighter-rouge">yield return</code>) et des <strong>delegates</strong> qui font tout le travail. La meilleure façon de comprendre LINQ en profondeur est de <strong>recoder soi-même</strong> ses opérateurs principaux. Cet article propose une implémentation simplifiée de LINQ to Objects, opérateur par opérateur.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Linq</strong> : <strong> 2</strong> sur <strong>4</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/01/31/linq-principe.html">LINQ en C# : principes et fonctionnement</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2026/02/07/linq-IEnumerable-vs-IQueryable.html">IEnumerable<T> vs IQueryable<T> : deux mondes, un même LINQ&lt;/a&gt;
		
		&lt;/li&gt;
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2026/02/08/linq-best-parctices.html">LINQ : bonnes pratiques et pièges à éviter</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	&lt;/ul&gt;
&lt;/div&gt;

# Les fondations : `IEnumerable<T>` et `yield return`

Toute l'implémentation de LINQ repose sur deux mécanismes du langage C# :

1. **`IEnumerable<T>`** : l'interface que toute séquence doit implémenter.
2. **`yield return`** : le mot-clé qui permet au compilateur de générer un **itérateur** (une machine à états) automatiquement.

## Rappel sur `IEnumerable<T>`

```csharp
public interface IEnumerable<out T=""> : IEnumerable
{
    IEnumerator<T> GetEnumerator();
}

public interface IEnumerator<out T=""> : IDisposable, IEnumerator
{
    T Current { get; }
    bool MoveNext();
    void Reset();
}
```

Quand on écrit `foreach (var item in collection)`, le compilateur appelle `GetEnumerator()` puis boucle sur `MoveNext()` / `Current`.

## Le rôle de `yield return`

Sans `yield return`, il faudrait écrire manuellement une classe qui implémente `IEnumerator<T>` avec une machine à états. C'est exactement ce que le compilateur fait pour nous :

```csharp
// Ce qu'on écrit
public static IEnumerable<int> Naturels(int max)
{
    for (int i = 0; i &lt; max; i++)
        yield return i;
}

// Ce que le compilateur génère (simplifié)
private sealed class NaturelsIterator : IEnumerable<int>, IEnumerator<int>
{
    private int _state = 0;
    private int _current;
    private int _max;
    private int _i;

    public NaturelsIterator(int max) =&gt; _max = max;

    public int Current =&gt; _current;

    public bool MoveNext()
    {
        switch (_state)
        {
            case 0:
                _i = 0;
                _state = 1;
                goto case 1;
            case 1:
                if (_i &lt; _max)
                {
                    _current = _i;
                    _i++;
                    return true;
                }
                _state = -1;
                return false;
            default:
                return false;
        }
    }

    // ... GetEnumerator, Dispose, Reset omis
}
```

Le `yield return` produit naturellement une **exécution différée** : le code du corps de la méthode ne s'exécute que quand l'appelant appelle `MoveNext()`.

# La classe de base : `MyEnumerable`

On va créer une classe statique `MyEnumerable` qui contiendra nos méthodes d'extension, exactement comme le fait `System.Linq.Enumerable` dans le framework.

```csharp
public static class MyEnumerable
{
    // Nos opérateurs iront ici
}
```

Chaque opérateur suit le même schéma :
1. C'est une **méthode d'extension** sur `IEnumerable<T>`.
2. Elle **valide les paramètres** immédiatement (pas de `yield` dans la méthode publique).
3. Elle délègue à une **méthode privée** qui utilise `yield return` pour l'exécution différée.

## Pourquoi séparer validation et itération ?

Parce que `yield return` rend l'exécution différée. Si on valide les paramètres dans la même méthode, l'exception ne sera levée que quand on itère, pas quand on appelle la méthode :

```csharp
// ❌ L'exception n'est levée qu'au foreach, pas à l'appel
public static IEnumerable<T> MauvaisWhere<T>(
    this IEnumerable<T> source, Func&lt;T, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    // avec yield, le code ci-dessus ne s'exécute qu'au MoveNext() !
    foreach (var item in source)
        if (predicate(item))
            yield return item;
}

// ✅ L'exception est levée immédiatement à l'appel
public static IEnumerable<T> BonWhere<T>(
    this IEnumerable<T> source, Func&lt;T, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));
    return BonWhereIterator(source, predicate);
}

private static IEnumerable<T> BonWhereIterator<T>(
    IEnumerable<T> source, Func&lt;T, bool&gt; predicate)
{
    foreach (var item in source)
        if (predicate(item))
            yield return item;
}
```

C'est exactement ce que fait l'implémentation officielle de .NET.

# Implémentation des opérateurs

## `Where` — Filtrage

Le plus simple des opérateurs. On parcourt la source et on ne retourne que les éléments qui satisfont le prédicat.

```csharp
public static IEnumerable<TSource> Where<TSource>(
    this IEnumerable<TSource> source,
    Func&lt;TSource, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));
    return WhereIterator(source, predicate);
}

private static IEnumerable<TSource> WhereIterator<TSource>(
    IEnumerable<TSource> source,
    Func&lt;TSource, bool&gt; predicate)
{
    foreach (var element in source)
    {
        if (predicate(element))
            yield return element;
    }
}
```

**Ce qu'il faut retenir** : grâce à `yield return`, chaque élément est produit **un par un**. Si l'appelant arrête d'itérer (par exemple avec `First()`), le reste de la source n'est jamais parcouru.

## `Select` — Projection

Transforme chaque élément via une fonction de projection.

```csharp
public static IEnumerable<TResult> Select&lt;TSource, TResult&gt;(
    this IEnumerable<TSource> source,
    Func&lt;TSource, TResult&gt; selector)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));
    return SelectIterator(source, selector);
}

private static IEnumerable<TResult> SelectIterator&lt;TSource, TResult&gt;(
    IEnumerable<TSource> source,
    Func&lt;TSource, TResult&gt; selector)
{
    foreach (var element in source)
    {
        yield return selector(element);
    }
}
```

## `SelectMany` — Aplatissement

C'est le `flatMap` de LINQ. Chaque élément produit une sous-séquence, et on aplatit le tout.

```csharp
public static IEnumerable<TResult> SelectMany&lt;TSource, TResult&gt;(
    this IEnumerable<TSource> source,
    Func&lt;TSource, IEnumerable<TResult>&gt; selector)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (selector == null) throw new ArgumentNullException(nameof(selector));
    return SelectManyIterator(source, selector);
}

private static IEnumerable<TResult> SelectManyIterator&lt;TSource, TResult&gt;(
    IEnumerable<TSource> source,
    Func&lt;TSource, IEnumerable<TResult>&gt; selector)
{
    foreach (var element in source)
    {
        foreach (var subElement in selector(element))
        {
            yield return subElement;
        }
    }
}
```

## `Any` — Existence (opérateur immédiat)

Premier exemple d'opérateur **immédiat** : il consomme la séquence et retourne un résultat tout de suite. Pas de `yield return` ici.

```csharp
public static bool Any<TSource>(
    this IEnumerable<TSource> source,
    Func&lt;TSource, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    foreach (var element in source)
    {
        if (predicate(element))
            return true;  // court-circuit : on s'arrête au premier match
    }
    return false;
}

// Surcharge sans prédicat : vérifie si la séquence contient au moins un élément
public static bool Any<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));

    using var enumerator = source.GetEnumerator();
    return enumerator.MoveNext();
}
```

## `First` et `FirstOrDefault` — Accès au premier élément

```csharp
public static TSource First<TSource>(
    this IEnumerable<TSource> source,
    Func&lt;TSource, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    foreach (var element in source)
    {
        if (predicate(element))
            return element;
    }

    throw new InvalidOperationException("La séquence ne contient aucun élément correspondant.");
}

public static TSource? FirstOrDefault<TSource>(
    this IEnumerable<TSource> source,
    Func&lt;TSource, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    foreach (var element in source)
    {
        if (predicate(element))
            return element;
    }

    return default;
}
```

## `Count` — Comptage

```csharp
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));

    // Optimisation : si c'est déjà une collection, on connaît la taille
    if (source is ICollection<TSource> collection)
        return collection.Count;

    int count = 0;
    using var enumerator = source.GetEnumerator();
    while (enumerator.MoveNext())
        count++;

    return count;
}

public static int Count<TSource>(
    this IEnumerable<TSource> source,
    Func&lt;TSource, bool&gt; predicate)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (predicate == null) throw new ArgumentNullException(nameof(predicate));

    int count = 0;
    foreach (var element in source)
    {
        if (predicate(element))
            count++;
    }
    return count;
}
```

**Point intéressant** : l'optimisation `ICollection<T>` est un pattern récurrent dans l'implémentation officielle. LINQ vérifie souvent si la source implémente une interface plus spécifique pour court-circuiter l'itération. Par expemple, `Count` peut retourner immédiatement la taille d'une liste ou d'un tableau sans parcourir les éléments :

```csharp
public static int Count<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));

    // Optimisation : si c'est une collection, accès direct à la propriété Count
    if (source is ICollection<TSource> collection)
        return collection.Count;

    // Sinon, on itère et on compte
    int count = 0;
    using var enumerator = source.GetEnumerator();
    while (enumerator.MoveNext())
        count++;

    return count;
}
```


## `ToList` — Matérialisation

```csharp
public static List<TSource> ToList<TSource>(this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    return new List<TSource>(source);
}
```

C'est tout. Le constructeur de `List<T>` fait déjà le travail d'itération et de copie.

## `OrderBy` — Tri (opérateur bufferisé)

Le tri est un cas spécial : c'est un opérateur **différé** mais **bufferisé**. Il doit lire toute la source avant de produire le premier résultat.

```csharp
public static IOrderedEnumerable<TSource> OrderBy&lt;TSource, TKey&gt;(
    this IEnumerable<TSource> source,
    Func&lt;TSource, TKey&gt; keySelector)
    where TKey : IComparable<TKey>
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
    return new OrderedEnumerable&lt;TSource, TKey&gt;(source, keySelector, descending: false);
}

// Implémentation simplifiée de IOrderedEnumerable
public class OrderedEnumerable&lt;TSource, TKey&gt; : IOrderedEnumerable<TSource>
    where TKey : IComparable<TKey>
{
    private readonly IEnumerable<TSource> _source;
    private readonly Func&lt;TSource, TKey&gt; _keySelector;
    private readonly bool _descending;

    public OrderedEnumerable(
        IEnumerable<TSource> source,
        Func&lt;TSource, TKey&gt; keySelector,
        bool descending)
    {
        _source = source;
        _keySelector = keySelector;
        _descending = descending;
    }

    public IEnumerator<TSource> GetEnumerator()
    {
        // Bufferisation : on charge tout en mémoire pour trier
        var buffer = _source.ToList();
        
        buffer.Sort((a, b) =&gt;
        {
            int cmp = _keySelector(a).CompareTo(_keySelector(b));
            return _descending ? -cmp : cmp;
        });

        foreach (var element in buffer)
            yield return element;
    }

    IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator();

    // Nécessaire pour supporter ThenBy
    public IOrderedEnumerable<TSource> CreateOrderedEnumerable<TNextKey>(
        Func&lt;TSource, TNextKey&gt; keySelector,
        IComparer<TNextKey>? comparer,
        bool descending)
    {
        throw new NotImplementedException("Simplifié pour l'article");
    }
}
```

**Point clé** : même si `OrderBy` est différé (le tri ne se fait pas à l'appel), il est **bufferisé** — quand on commence à itérer, toute la source doit être chargée en mémoire pour pouvoir trier. C'est une différence importante avec `Where` ou `Select` qui sont de vrais opérateurs *streaming*.

## `Skip` et `Take` — Partitionnement

```csharp
public static IEnumerable<TSource> Skip<TSource>(
    this IEnumerable<TSource> source, int count)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    return SkipIterator(source, count);
}

private static IEnumerable<TSource> SkipIterator<TSource>(
    IEnumerable<TSource> source, int count)
{
    int skipped = 0;
    foreach (var element in source)
    {
        if (skipped &lt; count)
        {
            skipped++;
            continue;
        }
        yield return element;
    }
}

public static IEnumerable<TSource> Take<TSource>(
    this IEnumerable<TSource> source, int count)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    return TakeIterator(source, count);
}

private static IEnumerable<TSource> TakeIterator<TSource>(
    IEnumerable<TSource> source, int count)
{
    int taken = 0;
    foreach (var element in source)
    {
        if (taken &gt;= count)
            yield break;  // on arrête complètement l'itération

        yield return element;
        taken++;
    }
}
```

**`yield break`** est l'équivalent de `return` dans un itérateur : il termine la séquence.

## `Distinct` — Suppression des doublons

```csharp
public static IEnumerable<TSource> Distinct<TSource>(
    this IEnumerable<TSource> source)
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    return DistinctIterator(source);
}

private static IEnumerable<TSource> DistinctIterator<TSource>(
    IEnumerable<TSource> source)
{
    var seen = new HashSet<TSource>();
    foreach (var element in source)
    {
        if (seen.Add(element))  // Add retourne false si déjà présent
            yield return element;
    }
}
```

Cet opérateur est **semi-bufferisé** : il maintient un `HashSet` en mémoire, mais produit les éléments en streaming (dès qu'un élément nouveau est rencontré, il est émis).

## `GroupBy` — Regroupement

```csharp
public static IEnumerable&lt;IGrouping&lt;TKey, TSource&gt;&gt; GroupBy&lt;TSource, TKey&gt;(
    this IEnumerable<TSource> source,
    Func&lt;TSource, TKey&gt; keySelector)
    where TKey : notnull
{
    if (source == null) throw new ArgumentNullException(nameof(source));
    if (keySelector == null) throw new ArgumentNullException(nameof(keySelector));
    return GroupByIterator(source, keySelector);
}

private static IEnumerable&lt;IGrouping&lt;TKey, TSource&gt;&gt; GroupByIterator&lt;TSource, TKey&gt;(
    IEnumerable<TSource> source,
    Func&lt;TSource, TKey&gt; keySelector)
    where TKey : notnull
{
    // Bufferisation : on doit tout lire pour connaître tous les groupes
    var groups = new Dictionary&lt;TKey, List<TSource>&gt;();
    var orderedKeys = new List<TKey>();

    foreach (var element in source)
    {
        var key = keySelector(element);
        if (!groups.TryGetValue(key, out var list))
        {
            list = new List<TSource>();
            groups[key] = list;
            orderedKeys.Add(key); // préserver l'ordre d'apparition
        }
        list.Add(element);
    }

    foreach (var key in orderedKeys)
    {
        yield return new Grouping&lt;TKey, TSource&gt;(key, groups[key]);
    }
}

// Implémentation simplifiée de IGrouping
public class Grouping&lt;TKey, TElement&gt; : IGrouping&lt;TKey, TElement&gt;
{
    public TKey Key { get; }
    private readonly List<TElement> _elements;

    public Grouping(TKey key, List<TElement> elements)
    {
        Key = key;
        _elements = elements;
    }

    public IEnumerator<TElement> GetEnumerator() =&gt; _elements.GetEnumerator();
    IEnumerator IEnumerable.GetEnumerator() =&gt; GetEnumerator();
}
```

# Comment le chaînage fonctionne

Quand on écrit :

```csharp
var result = nombres
    .Where(n =&gt; n &gt; 5)
    .Select(n =&gt; n * 2)
    .Take(3)
    .ToList();
```

On construit un **pipeline d'itérateurs imbriqués**. Voici ce qui se passe concrètement :

```
ToList() demande des éléments à →
  TakeIterator qui demande des éléments à →
    SelectIterator qui demande des éléments à →
      WhereIterator qui demande des éléments à →
        nombres (la source)
```

Chaque itérateur "tire" les éléments depuis l'itérateur précédent via `MoveNext()`. C'est un modèle **pull-based** (tiré par le consommateur).

```
nombres: [1, 8, 3, 12, 7, 15, 2, 9]

Étape par étape (ce que fait MoveNext à chaque appel) :
─────────────────────────────────────────────────────
1. ToList appelle Take.MoveNext()
2. Take appelle Select.MoveNext()
3. Select appelle Where.MoveNext()
4. Where lit 1 → 1 &gt; 5 ? Non, continue
5. Where lit 8 → 8 &gt; 5 ? Oui → yield return 8
6. Select reçoit 8 → yield return 8 * 2 = 16
7. Take reçoit 16 → taken=1 &lt; 3 → yield return 16
8. ToList ajoute 16

9.  Take appelle Select.MoveNext()
10. Select appelle Where.MoveNext()
11. Where lit 3 → 3 &gt; 5 ? Non, continue
12. Where lit 12 → 12 &gt; 5 ? Oui → yield return 12
13. Select reçoit 12 → yield return 24
14. Take reçoit 24 → taken=2 &lt; 3 → yield return 24
15. ToList ajoute 24

16. Take appelle Select.MoveNext()
17. Select appelle Where.MoveNext()
18. Where lit 7 → 7 &gt; 5 ? Oui → yield return 7
19. Select reçoit 7 → yield return 14
20. Take reçoit 14 → taken=3 → yield return 14, puis yield break
21. ToList ajoute 14

Résultat: [16, 24, 14]
Les éléments 15, 2, 9 n'ont JAMAIS été lus de la source.
```

C'est la beauté de l'exécution différée : on ne parcourt **que ce qui est nécessaire**.

# Catégorisation des opérateurs

| Catégorie | Comportement | Exemples |
|---|---|---|
| **Streaming différé** | Produit les éléments un par un sans tout charger | `Where`, `Select`, `SelectMany`, `Skip`, `Take` |
| **Bufferisé différé** | Doit charger toute la source avant de produire | `OrderBy`, `GroupBy`, `Reverse`, `Join` |
| **Semi-bufferisé** | Maintient un état partiel en mémoire | `Distinct`, `Union`, `Intersect`, `Except` |
| **Immédiat** | Consomme la séquence et retourne un résultat | `ToList`, `Count`, `First`, `Any`, `Sum` |

Comprendre cette catégorisation est essentiel pour estimer la **consommation mémoire** d'un pipeline LINQ.

# Ce qui change dans le vrai .NET

Notre implémentation est fonctionnelle mais simplifiée. L'implémentation officielle dans `System.Linq` ajoute :

1. **Optimisations pour `IList<T>`** : si la source est une liste, `ElementAt`, `Count`, `Last` accèdent par index au lieu d'itérer.

2. **Fusion d'itérateurs** : enchaîner `.Where().Where()` sur le même source crée un seul itérateur combiné au lieu de deux imbriqués.

3. **`Span<T>` et vectorisation** : depuis .NET 8+, certains opérateurs comme `Sum` ou `Min` utilisent SIMD quand la source est un tableau.

4. **`TryGetNonEnumeratedCount`** (.NET 6+) : permet de connaître la taille sans itérer quand la source est une collection.

5. **Arbres d'expressions pour `IQueryable<T>`** : les opérateurs de `Queryable` ne reçoivent pas des `Func&lt;&gt;` mais des `Expression&lt;Func&lt;&gt;&gt;`, ce qui permet de les **analyser** et de les **traduire** (en SQL par exemple) au lieu de les exécuter.

# Résumé

| Concept | Ce qu'on a appris |
|---|---|
| **`yield return`** | Crée un itérateur avec exécution différée, le compilateur génère une machine à états |
| **Méthodes d'extension** | Chaque opérateur LINQ est une méthode d'extension sur `IEnumerable<T>` |
| **Validation séparée** | La méthode publique valide les arguments, une méthode privée contient le `yield` |
| **Pipeline pull-based** | Chaque itérateur "tire" les éléments de l'itérateur précédent via `MoveNext()` |
| **Streaming vs bufferisé** | `Where`/`Select` sont streaming, `OrderBy`/`GroupBy` doivent tout charger |
| **Court-circuit** | `Take`, `First`, `Any` arrêtent l'itération dès que possible |
| **Optimisations** | L'implémentation officielle détecte `ICollection<T>`, fusionne les itérateurs, utilise SIMD |
</T></T></T></T></T></TElement></TElement></TElement></TSource></TKey></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TNextKey></TNextKey></TSource></TSource></TSource></TSource></TKey></TSource></TKey></TSource></TSource></T></TSource></TSource></TSource></TSource></TSource></TSource></TSource></T></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TSource></TResult></TSource></TResult></TResult></TSource></TResult></TSource></TResult></TSource></TResult></TSource></TSource></TSource></TSource></TSource></TSource></T></T></T></T></T></T></T></T></T></T></int></int></int></T></out></T></out></T></T></T></T></T></a></li></ul></div>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="basics" /><category term="linq" /><category term="csharp" /><category term="implementation" /><summary type="html"><![CDATA[LINQ semble magique quand on l’utilise, mais sous le capot, ce sont des méthodes d’extension, des itérateurs (yield return) et des delegates qui font tout le travail. La meilleure façon de comprendre LINQ en profondeur est de recoder soi-même ses opérateurs principaux. Cet article propose une implémentation simplifiée de LINQ to Objects, opérateur par opérateur.]]></summary></entry><entry><title type="html">LINQ en C# : principes et fonctionnement</title><link href="http://guym.fr/2026/01/31/linq-principe.html" rel="alternate" type="text/html" title="LINQ en C# : principes et fonctionnement" /><published>2026-01-31T00:00:00+01:00</published><updated>2026-01-31T00:00:00+01:00</updated><id>http://guym.fr/2026/01/31/linq-principe</id><content type="html" xml:base="http://guym.fr/2026/01/31/linq-principe.html"><![CDATA[<p>LINQ (Language Integrated Query) est l’une des fonctionnalités les plus puissantes de C#. Il permet d’interroger et de transformer des collections, des bases de données, du XML ou tout autre source de données avec une syntaxe unifiée, directement intégrée au langage. Comprendre ses principes — exécution différée, composition d’opérateurs, distinction <code class="language-plaintext highlighter-rouge">IEnumerable&lt;T&gt;</code> / <code class="language-plaintext highlighter-rouge">IQueryable&lt;T&gt;</code> — est indispensable pour écrire du code .NET lisible, maintenable et performant.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Linq</strong> : <strong> 1</strong> sur <strong>4</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2026/02/01/linq-implementation.html">Comprendre LINQ par l'implémentation : recoder les opérateurs</a>
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2026/02/07/linq-IEnumerable-vs-IQueryable.html">IEnumerable<T> vs IQueryable<T> : deux mondes, un même LINQ&lt;/a&gt;
		
		&lt;/li&gt;
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2026/02/08/linq-best-parctices.html">LINQ : bonnes pratiques et pièges à éviter</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	&lt;/ul&gt;
&lt;/div&gt;

# Qu'est-ce que LINQ ?

**LINQ** (Language Integrated Query) est un ensemble de fonctionnalités introduites dans **C# 3.0** (.NET Framework 3.5, 2007) qui permet d'écrire des **requêtes sur des données** directement dans le langage C#, avec un typage fort et l'aide de l'IntelliSense.

Avant LINQ, interroger une base de données passait par des chaînes SQL en dur, interroger du XML par des chemins XPath, et parcourir des collections par des boucles `foreach` manuelles. LINQ unifie tout cela.

```csharp
// Avant LINQ
var resultats = new List<string>();
foreach (var p in personnes)
{
    if (p.Age &gt;= 18)
        resultats.Add(p.Nom);
}
resultats.Sort();

// Avec LINQ
var resultats = personnes
    .Where(p =&gt; p.Age &gt;= 18)
    .OrderBy(p =&gt; p.Nom)
    .Select(p =&gt; p.Nom)
    .ToList();
```

L'avantage est immédiat : le code est **déclaratif** (on décrit *ce qu'on veut*) plutôt qu'**impératif** (on décrit *comment le faire*).

# Les deux syntaxes

LINQ offre deux façons d'écrire les requêtes. Elles sont **fonctionnellement équivalentes** ; le compilateur transforme la syntaxe de requête en appels de méthodes.

## Syntaxe de requête (query syntax)

Elle ressemble à du SQL, mais attention : elle commence par `from` et finit par `select` (ou `group`).

```csharp
var adultes = from p in personnes
              where p.Age &gt;= 18
              orderby p.Nom
              select p.Nom;
```

## Syntaxe de méthodes (method syntax / fluent syntax)

Elle utilise des **méthodes d'extension** chaînées, combinées avec des **expressions lambda**.

```csharp
var adultes = personnes
    .Where(p =&gt; p.Age &gt;= 18)
    .OrderBy(p =&gt; p.Nom)
    .Select(p =&gt; p.Nom);
```

### Quelle syntaxe choisir ?

| Critère | Query syntax | Method syntax |
|---|---|---|
| Lisibilité pour les jointures | ✅ Plus lisible | Parfois verbeux |
| Accès à tous les opérateurs | ❌ Pas tous disponibles | ✅ Tous les opérateurs |
| Cohérence avec le reste du code | Variable | ✅ Style fluent classique |
| Convention la plus courante | Peu utilisée seule | ✅ Standard de facto |

En pratique, la **syntaxe de méthodes** est la plus utilisée. La syntaxe de requête reste utile pour les jointures complexes ou quand elle améliore la lisibilité.

# Les briques fondamentales

## Les expressions lambda

Les lambdas sont au cœur de LINQ. Ce sont des **fonctions anonymes** passées en paramètre aux opérateurs LINQ.

```csharp
// Lambda simple : un paramètre, une expression
Func&lt;int, bool&gt; estPair = x =&gt; x % 2 == 0;

// Lambda avec corps
Func&lt;int, string&gt; decrire = x =&gt;
{
    if (x &gt; 0) return "positif";
    if (x &lt; 0) return "négatif";
    return "zéro";
};
```

## Les méthodes d'extension

Les opérateurs LINQ (`Where`, `Select`, `OrderBy`…) sont des **méthodes d'extension** définies dans la classe `System.Linq.Enumerable` (pour `IEnumerable<T>`) et `System.Linq.Queryable` (pour `IQueryable<T>`).

```csharp
// Ce qu'on écrit :
personnes.Where(p =&gt; p.Age &gt;= 18);

// Ce que le compilateur voit :
Enumerable.Where(personnes, p =&gt; p.Age &gt;= 18);
```

C'est pour cela qu'il faut toujours avoir le `using System.Linq;` (implicite depuis .NET 6 avec les *global usings*).

## Les types anonymes

LINQ utilise souvent des **types anonymes** pour projeter les données :

```csharp
var resultats = personnes
    .Select(p =&gt; new { p.Nom, p.Age })
    .ToList();

// Chaque élément a des propriétés Nom et Age, 
// typées automatiquement par le compilateur
```

# `IEnumerable<T>` vs `IQueryable<T>`

C'est **la distinction la plus importante** à comprendre pour bien utiliser LINQ.

## `IEnumerable<T>` — LINQ to Objects

- Travaille **en mémoire**, sur des collections déjà chargées.
- Les lambdas sont compilées en **delegates** (`Func&lt;T, bool&gt;`).
- Chaque opérateur itère sur les éléments un par un.

```csharp
List<Personne> personnes = GetPersonnes(); // données en mémoire

var adultes = personnes
    .Where(p =&gt; p.Age &gt;= 18)   // filtre en mémoire
    .ToList();
```

## `IQueryable<T>` — LINQ to Entities (et autres providers)

- Travaille avec un **provider** externe (Entity Framework, base de données…).
- Les lambdas sont compilées en **arbres d'expressions** (`Expression&lt;Func&lt;T, bool&gt;&gt;`).
- La requête est **traduite** (par exemple en SQL) et exécutée **côté serveur**.

```csharp
IQueryable<Personne> personnes = dbContext.Personnes; // DbSet<T>

var adultes = personnes
    .Where(p =&gt; p.Age &gt;= 18)   // traduit en SQL : WHERE Age &gt;= 18
    .ToList();                  // exécution SQL à ce moment-là
```

### Pourquoi c'est crucial ?

| Aspect | `IEnumerable<T>` | `IQueryable<T>` |
|---|---|---|
| Exécution | En mémoire (client) | Côté serveur (base de données) |
| Filtre | Après avoir tout chargé | Avant chargement (SQL WHERE) |
| Performance | Peut être coûteux sur gros volumes | Optimisé par le provider |
| Expression | `Func&lt;T, bool&gt;` (delegate) | `Expression&lt;Func&lt;T, bool&gt;&gt;` (arbre) |

**Piège classique** : appeler `.AsEnumerable()` ou `.ToList()` trop tôt force le chargement de toutes les données en mémoire, puis les filtres suivants s'exécutent côté client.

```csharp
// ❌ Mauvais : charge TOUT puis filtre en mémoire
var resultat = dbContext.Commandes
    .ToList()                          // SELECT * FROM Commandes
    .Where(c =&gt; c.Montant &gt; 1000);     // filtre en mémoire

// ✅ Bon : filtre côté SQL
var resultat = dbContext.Commandes
    .Where(c =&gt; c.Montant &gt; 1000)      // WHERE Montant &gt; 1000
    .ToList();                          // exécution SQL filtrée
```

# L'exécution différée (deferred execution)

C'est le concept **le plus important** de LINQ.

## Principe

Quand on écrit une requête LINQ, **rien ne s'exécute immédiatement**. On construit un **pipeline de transformations**. L'exécution ne se déclenche que lorsqu'on **consomme** les résultats.

```csharp
var query = nombres.Where(n =&gt; n &gt; 5); // rien ne se passe encore

foreach (var n in query)  // l'exécution se déclenche ICI
{
    Console.WriteLine(n);
}
```

## Ce qui déclenche l'exécution

Les opérateurs LINQ se divisent en deux catégories :

### Opérateurs différés (ne déclenchent PAS l'exécution)

Ils retournent un `IEnumerable<T>` ou `IQueryable<T>` et ne font que **composer** le pipeline :

- `Where`, `Select`, `SelectMany`
- `OrderBy`, `ThenBy`, `OrderByDescending`
- `Skip`, `Take`
- `Distinct`, `GroupBy`, `Join`
- `Concat`, `Union`, `Intersect`, `Except`

### Opérateurs immédiats (DÉCLENCHENT l'exécution)

Ils consomment la séquence et retournent un résultat concret :

- **Matérialisation** : `ToList()`, `ToArray()`, `ToDictionary()`, `ToHashSet()`
- **Élément unique** : `First()`, `FirstOrDefault()`, `Single()`, `Last()`, `ElementAt()`
- **Agrégation** : `Count()`, `Sum()`, `Average()`, `Min()`, `Max()`, `Aggregate()`
- **Test** : `Any()`, `All()`, `Contains()`
- **Itération** : `foreach`

## Conséquences pratiques

### La source peut changer entre la définition et l'exécution

```csharp
var liste = new List<int> { 1, 2, 3 };
var query = liste.Where(n =&gt; n &gt; 1);

liste.Add(4); // on modifie la source AVANT d'itérer

var resultat = query.ToList(); // [2, 3, 4] — le 4 est inclus !
```

### Chaque itération ré-exécute la requête

```csharp
var query = personnes.Where(p =&gt; p.Age &gt;= 18);

// Deux itérations = deux exécutions du filtre
var count = query.Count();       // 1ère exécution
var list  = query.ToList();      // 2ème exécution
```

Si c'est un `IQueryable`, cela signifie **deux requêtes SQL** envoyées à la base. Pour éviter cela, matérialisez une seule fois avec `ToList()` :

```csharp
var adultes = personnes.Where(p =&gt; p.Age &gt;= 18).ToList(); // 1 seule exécution
var count = adultes.Count;   // propriété List<T>, pas de ré-exécution
```

# Les opérateurs LINQ essentiels

## Filtrage

```csharp
// Where : filtre selon un prédicat
var majeurs = personnes.Where(p =&gt; p.Age &gt;= 18);

// OfType : filtre par type
var chaines = objets.OfType<string>();
```

## Projection

```csharp
// Select : transforme chaque élément
var noms = personnes.Select(p =&gt; p.Nom);

// SelectMany : "aplatit" les collections imbriquées
var tousLesEmails = clients.SelectMany(c =&gt; c.Emails);
// Si chaque client a plusieurs emails, on obtient une seule séquence plate
```

## Tri

```csharp
var triParNom = personnes
    .OrderBy(p =&gt; p.Nom)
    .ThenByDescending(p =&gt; p.Age);
```

## Agrégation

```csharp
var total       = commandes.Sum(c =&gt; c.Montant);
var moyenne     = commandes.Average(c =&gt; c.Montant);
var nbAdultes   = personnes.Count(p =&gt; p.Age &gt;= 18);
var plusVieux    = personnes.Max(p =&gt; p.Age);
```

## Regroupement

```csharp
var parVille = personnes
    .GroupBy(p =&gt; p.Ville)
    .Select(g =&gt; new 
    { 
        Ville = g.Key, 
        Nombre = g.Count() 
    });
```

## Jointures

```csharp
// Join : jointure interne (comme INNER JOIN en SQL)
var commandes = clients
    .Join(commandes,
        client =&gt; client.Id,
        commande =&gt; commande.ClientId,
        (client, commande) =&gt; new { client.Nom, commande.Montant });

// GroupJoin : jointure groupée (comme LEFT JOIN avec regroupement)
var clientsAvecCommandes = clients
    .GroupJoin(commandes,
        c =&gt; c.Id,
        cmd =&gt; cmd.ClientId,
        (client, cmds) =&gt; new { client.Nom, Commandes = cmds.ToList() });
```

En syntaxe de requête, les jointures sont plus lisibles :

```csharp
var resultat = from c in clients
               join cmd in commandes on c.Id equals cmd.ClientId
               select new { c.Nom, cmd.Montant };
```

## Partitionnement

```csharp
var page = personnes
    .OrderBy(p =&gt; p.Nom)
    .Skip(20)    // sauter les 20 premiers
    .Take(10);   // prendre les 10 suivants (page 3 de taille 10)
```

## Ensembles

```csharp
var union     = liste1.Union(liste2);        // éléments des deux, sans doublons
var inter     = liste1.Intersect(liste2);    // éléments communs
var diff      = liste1.Except(liste2);       // dans liste1 mais pas dans liste2
var distinct  = liste1.Distinct();           // supprime les doublons
```

## Vérification d'existence

```csharp
bool auMoinsUnAdulte = personnes.Any(p =&gt; p.Age &gt;= 18);
bool tousMajeurs     = personnes.All(p =&gt; p.Age &gt;= 18);
bool contientAlice   = personnes.Any(p =&gt; p.Nom == "Alice");
```

# Créer sa propre méthode LINQ

LINQ est extensible. On peut écrire ses propres opérateurs sous forme de méthodes d'extension sur `IEnumerable<T>` :

```csharp
public static class EnumerableExtensions
{
    /// <summary>
    /// Filtre les éléments non null d'une séquence.
    /// </summary>
    public static IEnumerable<T> WhereNotNull<T>(this IEnumerable&lt;T?&gt; source)
        where T : class
    {
        foreach (var item in source)
        {
            if (item is not null)
                yield return item;
        }
    }
}

// Utilisation
var noms = personnes
    .Select(p =&gt; p.Nom)
    .WhereNotNull()
    .ToList();
```

Le mot-clé `yield return` génère un **itérateur** qui respecte naturellement l'exécution différée.

# Bonnes pratiques et pièges courants

## 1. Ne pas mélanger `IQueryable` et opérations non traduisibles

```csharp
// ❌ Erreur à l'exécution : EF ne sait pas traduire MaMethode() en SQL
var resultat = dbContext.Personnes
    .Where(p =&gt; MaMethodeCustom(p.Nom))
    .ToList();

// ✅ Charger d'abord, puis filtrer en mémoire
var resultat = dbContext.Personnes
    .AsEnumerable()              // bascule en IEnumerable
    .Where(p =&gt; MaMethodeCustom(p.Nom))
    .ToList();
```

## 2. Matérialiser quand on itère plusieurs fois

```csharp
// ❌ Deux passages sur la requête (deux requêtes SQL si IQueryable)
var query = GetData().Where(x =&gt; x.IsActive);
var count = query.Count();
var items = query.ToList();

// ✅ Un seul passage
var items = GetData().Where(x =&gt; x.IsActive).ToList();
var count = items.Count;
```

## 3. Préférer `Any()` à `Count() &gt; 0`

```csharp
// ❌ Count() parcourt toute la collection
if (personnes.Count() &gt; 0) { ... }

// ✅ Any() s'arrête au premier élément trouvé
if (personnes.Any()) { ... }
```

## 4. Attention à la capture de variables dans les closures

```csharp
var filtres = new List&lt;Func&lt;int, bool&gt;&gt;();
for (int i = 0; i &lt; 5; i++)
{
    filtres.Add(x =&gt; x == i); // ⚠️ capture de la variable i, pas de la valeur
}
// Tous les filtres testent x == 5 (valeur finale de i)

// ✅ Capturer une copie locale
for (int i = 0; i &lt; 5; i++)
{
    var copie = i;
    filtres.Add(x =&gt; x == copie);
}
```

## 5. Utiliser les surcharges avec index quand c'est utile

```csharp
var indexés = noms
    .Select((nom, index) =&gt; $"{index + 1}. {nom}")
    .ToList();
// ["1. Alice", "2. Bob", "3. Charlie"]
```

# Résumé

| Concept | À retenir |
|---|---|
| **LINQ** | Requêtes intégrées au langage, typées, composables |
| **Deux syntaxes** | Query (`from ... select`) et Method (`.Where().Select()`) — équivalentes |
| **Exécution différée** | La requête ne s'exécute que quand on consomme les résultats |
| **`IEnumerable<T>`** | Exécution en mémoire (LINQ to Objects) |
| **`IQueryable<T>`** | Traduction en requête externe (SQL via EF, etc.) |
| **Opérateurs immédiats** | `ToList()`, `Count()`, `First()`, `Any()`… déclenchent l'exécution |
| **Extensibilité** | On peut créer ses propres opérateurs avec des méthodes d'extension |
| **Performance** | Filtrer avant de matérialiser, `Any()` plutôt que `Count() &gt; 0`, matérialiser une seule fois |
</T></T></T></T></T></string></T></int></T></T></T></T></T></Personne></T></Personne></T></T></T></T></T></string></T></T></a></li></ul></div>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="basics" /><category term="linq" /><category term="csharp" /><summary type="html"><![CDATA[LINQ (Language Integrated Query) est l’une des fonctionnalités les plus puissantes de C#. Il permet d’interroger et de transformer des collections, des bases de données, du XML ou tout autre source de données avec une syntaxe unifiée, directement intégrée au langage. Comprendre ses principes — exécution différée, composition d’opérateurs, distinction IEnumerable&lt;T&gt; / IQueryable&lt;T&gt; — est indispensable pour écrire du code .NET lisible, maintenable et performant.]]></summary></entry><entry><title type="html">Semantic API Roslyn pour les Source Generators (Code Gen)</title><link href="http://guym.fr/2026/01/26/semantic-api.html" rel="alternate" type="text/html" title="Semantic API Roslyn pour les Source Generators (Code Gen)" /><published>2026-01-26T00:00:00+01:00</published><updated>2026-01-26T00:00:00+01:00</updated><id>http://guym.fr/2026/01/26/semantic-api</id><content type="html" xml:base="http://guym.fr/2026/01/26/semantic-api.html"><![CDATA[<p>La <strong>Semantic API</strong> de Roslyn complète la Syntax API en donnant accès à la compréhension “sémantique” du code : types réels, symboles, héritage, interfaces implémentées, attributs résolus, etc. Dans un Source Generator, c’est elle qui permet de passer d’un simple nœud de syntaxe à un modèle riche sur lequel baser la génération de code.</p>

<p>Dans cet article, on va voir :</p>

<ul>
  <li>ce qu’est un <code class="language-plaintext highlighter-rouge">SemanticModel</code> ;</li>
  <li>comment obtenir des <code class="language-plaintext highlighter-rouge">ISymbol</code> (types, méthodes, propriétés) à partir de la syntaxe ;</li>
  <li>comment lire les attributs et types effectifs ;</li>
  <li>comment utiliser efficacement la Semantic API dans un générateur incrémental.</li>
</ul>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Source Generators en .NET</strong> : <strong> 3</strong> sur <strong>3</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/01/17/sources-generators.html">Source Generators en .NET (Code Gen)</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2026/01/25/syntax-api.html">Syntax API Roslyn pour les Source Generators (Code Gen)</a>
		
		</li>
	
	
	
		
		<li>Part 3 - 
		
			Cet article
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	</ul>
</div>

<h2 id="1-semanticmodel-le-lien-entre-syntaxe-et-symboles">1. SemanticModel : le lien entre syntaxe et symboles</h2>

<p>La <code class="language-plaintext highlighter-rouge">Compilation</code> fournie à un source generator contient toute la connaissance du compilateur sur le projet. À partir d’elle, on peut récupérer un <code class="language-plaintext highlighter-rouge">SemanticModel</code> pour un <code class="language-plaintext highlighter-rouge">SyntaxTree</code> donné :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">semanticModel</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Compilation</span><span class="p">.</span><span class="nf">GetSemanticModel</span><span class="p">(</span><span class="n">classSyntax</span><span class="p">.</span><span class="n">SyntaxTree</span><span class="p">);</span>
</code></pre></div></div>

<p>Le <code class="language-plaintext highlighter-rouge">SemanticModel</code> permet par exemple de :</p>

<ul>
  <li>obtenir le symbole déclaré par un nœud (<code class="language-plaintext highlighter-rouge">GetDeclaredSymbol</code>) ;</li>
  <li>interroger le type d’une expression (<code class="language-plaintext highlighter-rouge">GetTypeInfo</code>) ;</li>
  <li>résoudre le symbole référencé par un identifiant (<code class="language-plaintext highlighter-rouge">GetSymbolInfo</code>).</li>
</ul>

<p>Dans un Source Generator, on l’utilise généralement pour transformer un <code class="language-plaintext highlighter-rouge">SyntaxNode</code> filtré (via la Syntax API) en <code class="language-plaintext highlighter-rouge">ISymbol</code> exploitable.</p>

<h2 id="2-récupérer-les-symboles-depuis-la-syntaxe">2. Récupérer les symboles depuis la syntaxe</h2>

<p>Partons du receveur syntaxique de l’article précédent, qui collecte des <code class="language-plaintext highlighter-rouge">ClassDeclarationSyntax</code>. Dans <code class="language-plaintext highlighter-rouge">Execute</code>, on veut maintenant récupérer des <code class="language-plaintext highlighter-rouge">INamedTypeSymbol</code> pour ces classes.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">void</span> <span class="nf">Execute</span><span class="p">(</span><span class="n">GeneratorExecutionContext</span> <span class="n">context</span><span class="p">)</span>
<span class="p">{</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">SyntaxReceiver</span> <span class="k">is</span> <span class="n">not</span> <span class="n">DemoSyntaxReceiver</span> <span class="n">receiver</span><span class="p">)</span>
		<span class="k">return</span><span class="p">;</span>

	<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">classSyntax</span> <span class="k">in</span> <span class="n">receiver</span><span class="p">.</span><span class="n">CandidateClasses</span><span class="p">)</span>
	<span class="p">{</span>
		<span class="kt">var</span> <span class="n">semanticModel</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Compilation</span><span class="p">.</span><span class="nf">GetSemanticModel</span><span class="p">(</span><span class="n">classSyntax</span><span class="p">.</span><span class="n">SyntaxTree</span><span class="p">);</span>
		<span class="k">if</span> <span class="p">(</span><span class="n">semanticModel</span><span class="p">.</span><span class="nf">GetDeclaredSymbol</span><span class="p">(</span><span class="n">classSyntax</span><span class="p">)</span> <span class="k">is</span> <span class="n">not</span> <span class="n">INamedTypeSymbol</span> <span class="n">typeSymbol</span><span class="p">)</span>
			<span class="k">continue</span><span class="p">;</span>

		<span class="c1">// À partir d'ici, on travaille avec le symbole, pas seulement avec la syntaxe</span>
		<span class="kt">var</span> <span class="n">className</span> <span class="p">=</span> <span class="n">typeSymbol</span><span class="p">.</span><span class="n">Name</span><span class="p">;</span>                 <span class="c1">// nom simple</span>
		<span class="kt">var</span> <span class="n">fullName</span> <span class="p">=</span> <span class="n">typeSymbol</span><span class="p">.</span><span class="nf">ToDisplayString</span><span class="p">();</span>    <span class="c1">// nom complet avec namespace</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Le symbole <code class="language-plaintext highlighter-rouge">INamedTypeSymbol</code> offre une vue riche :</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ContainingNamespace</code>, <code class="language-plaintext highlighter-rouge">BaseType</code>, <code class="language-plaintext highlighter-rouge">AllInterfaces</code> ;</li>
  <li><code class="language-plaintext highlighter-rouge">GetMembers()</code> pour accéder aux propriétés, méthodes, champs, etc.</li>
</ul>

<h2 id="3-lire-les-propriétés-et-leurs-types">3. Lire les propriétés et leurs types</h2>

<p>Reprenons le cas du générateur de mapper : on veut projeter toutes les propriétés “mappables” d’un type.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">properties</span> <span class="p">=</span> <span class="n">typeSymbol</span><span class="p">.</span><span class="nf">GetMembers</span><span class="p">()</span>
	<span class="p">.</span><span class="n">OfType</span><span class="p">&lt;</span><span class="n">IPropertySymbol</span><span class="p">&gt;()</span>
	<span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="n">p</span><span class="p">.</span><span class="n">SetMethod</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span><span class="p">);</span> <span class="c1">// seulement les propriétés avec setter</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">prop</span> <span class="k">in</span> <span class="n">properties</span><span class="p">)</span>
<span class="p">{</span>
	<span class="kt">var</span> <span class="n">propName</span> <span class="p">=</span> <span class="n">prop</span><span class="p">.</span><span class="n">Name</span><span class="p">;</span>
	<span class="kt">var</span> <span class="n">propType</span> <span class="p">=</span> <span class="n">prop</span><span class="p">.</span><span class="n">Type</span><span class="p">.</span><span class="nf">ToDisplayString</span><span class="p">();</span>
	<span class="kt">var</span> <span class="n">isNullable</span> <span class="p">=</span> <span class="n">prop</span><span class="p">.</span><span class="n">NullableAnnotation</span> <span class="p">==</span> <span class="n">NullableAnnotation</span><span class="p">.</span><span class="n">Annotated</span><span class="p">;</span>

	<span class="c1">// Utiliser ces infos pour générer du code</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Grâce à la Semantic API, <code class="language-plaintext highlighter-rouge">prop.Type</code> est un vrai symbole de type (<code class="language-plaintext highlighter-rouge">ITypeSymbol</code>), pas juste une chaîne de caractères issue de la syntaxe.</p>

<h2 id="4-lire-et-interpréter-les-attributs">4. Lire et interpréter les attributs</h2>

<p>Les attributs sont une des principales sources de configuration pour un Source Generator. Avec la Semantic API, on peut :</p>

<ul>
  <li>vérifier la présence d’un attribut donné ;</li>
  <li>lire ses arguments (nommés et positionnels) ;</li>
  <li>récupérer les types passés en paramètres.</li>
</ul>

<p>Exemple avec un attribut <code class="language-plaintext highlighter-rouge">[GenerateMapper(typeof(CustomerDto))]</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">mapperAttributes</span> <span class="p">=</span> <span class="n">typeSymbol</span><span class="p">.</span><span class="nf">GetAttributes</span><span class="p">()</span>
	<span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">a</span> <span class="p">=&gt;</span> <span class="n">a</span><span class="p">.</span><span class="n">AttributeClass</span><span class="p">?.</span><span class="n">Name</span> <span class="p">==</span> <span class="s">"GenerateMapperAttribute"</span><span class="p">);</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">attr</span> <span class="k">in</span> <span class="n">mapperAttributes</span><span class="p">)</span>
<span class="p">{</span>
	<span class="c1">// Argument positionnel 0 : typeof(CustomerDto)</span>
	<span class="k">if</span> <span class="p">(</span><span class="n">attr</span><span class="p">.</span><span class="n">ConstructorArguments</span><span class="p">.</span><span class="nf">FirstOrDefault</span><span class="p">().</span><span class="n">Value</span> <span class="k">is</span> <span class="n">INamedTypeSymbol</span> <span class="n">targetType</span><span class="p">)</span>
	<span class="p">{</span>
		<span class="kt">var</span> <span class="n">targetTypeName</span> <span class="p">=</span> <span class="n">targetType</span><span class="p">.</span><span class="nf">ToDisplayString</span><span class="p">();</span>
		<span class="c1">// targetType fournit aussi ses propres propriétés, namespace, etc.</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>C’est ce mécanisme qui permet de passer de simples annotations dans le code utilisateur à une configuration riche côté générateur.</p>

<h2 id="5-semantic-api-dans-un-générateur-incrémental">5. Semantic API dans un générateur incrémental</h2>

<p>Avec <code class="language-plaintext highlighter-rouge">IIncrementalGenerator</code>, on évite généralement d’appeler <code class="language-plaintext highlighter-rouge">GetSemanticModel</code> directement dans la boucle principale. À la place, on enrichit les données dès la phase <code class="language-plaintext highlighter-rouge">transform</code> du <code class="language-plaintext highlighter-rouge">SyntaxProvider</code>.</p>

<p>Schéma simplifié :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Generator</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">IncrementalSemanticDemo</span> <span class="p">:</span> <span class="n">IIncrementalGenerator</span>
<span class="p">{</span>
	<span class="k">public</span> <span class="k">void</span> <span class="nf">Initialize</span><span class="p">(</span><span class="n">IncrementalGeneratorInitializationContext</span> <span class="n">context</span><span class="p">)</span>
	<span class="p">{</span>
		<span class="kt">var</span> <span class="n">candidates</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">SyntaxProvider</span>
			<span class="p">.</span><span class="nf">CreateSyntaxProvider</span><span class="p">(</span>
				<span class="n">predicate</span><span class="p">:</span> <span class="k">static</span> <span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">node</span> <span class="k">is</span> <span class="n">ClassDeclarationSyntax</span> <span class="p">{</span> <span class="n">AttributeLists</span><span class="p">.</span><span class="n">Count</span><span class="p">:</span> <span class="p">&gt;</span> <span class="m">0</span> <span class="p">},</span>
				<span class="n">transform</span><span class="p">:</span> <span class="k">static</span> <span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span>
				<span class="p">{</span>
					<span class="kt">var</span> <span class="n">classSyntax</span> <span class="p">=</span> <span class="p">(</span><span class="n">ClassDeclarationSyntax</span><span class="p">)</span><span class="n">ctx</span><span class="p">.</span><span class="n">Node</span><span class="p">;</span>
					<span class="kt">var</span> <span class="n">symbol</span> <span class="p">=</span> <span class="p">(</span><span class="n">INamedTypeSymbol</span><span class="p">?)</span><span class="n">ctx</span><span class="p">.</span><span class="n">SemanticModel</span><span class="p">.</span><span class="nf">GetDeclaredSymbol</span><span class="p">(</span><span class="n">classSyntax</span><span class="p">);</span>
					<span class="k">return</span> <span class="n">symbol</span><span class="p">;</span>
				<span class="p">})</span>
			<span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">symbol</span> <span class="p">=&gt;</span> <span class="n">symbol</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span><span class="p">)!;</span>

		<span class="n">context</span><span class="p">.</span><span class="nf">RegisterSourceOutput</span><span class="p">(</span><span class="n">candidates</span><span class="p">,</span> <span class="p">(</span><span class="n">spc</span><span class="p">,</span> <span class="n">typeSymbol</span><span class="p">)</span> <span class="p">=&gt;</span>
		<span class="p">{</span>
			<span class="c1">// Ici, on a déjà l'INamedTypeSymbol : plus besoin de SemanticModel</span>
			<span class="c1">// Génération de code à partir du symbole…</span>
		<span class="p">});</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>L’idée :</p>

<ul>
  <li>utiliser la Syntax API pour filtrer vite (dans <code class="language-plaintext highlighter-rouge">predicate</code>) ;</li>
  <li>utiliser la Semantic API dans <code class="language-plaintext highlighter-rouge">transform</code> pour produire directement des symboles ;</li>
  <li>ne conserver dans le pipeline que les données dont on a besoin pour la génération.</li>
</ul>

<h2 id="6-bonnes-pratiques-avec-la-semantic-api">6. Bonnes pratiques avec la Semantic API</h2>

<ul>
  <li><strong>Limiter les appels à SemanticModel</strong> : c’est une opération coûteuse. Regroupez si possible les requêtes par <code class="language-plaintext highlighter-rouge">SyntaxTree</code> et privilégiez l’utilisation de <code class="language-plaintext highlighter-rouge">ctx.SemanticModel</code> dans les générateurs incrémentaux.</li>
  <li><strong>Travailler le plus possible avec les symboles</strong> : une fois les symboles obtenus, faites vos filtrages/transformations dessus plutôt que de revenir à la syntaxe.</li>
  <li><strong>Utiliser <code class="language-plaintext highlighter-rouge">ToDisplayString</code> avec un format contrôlé</strong> (<code class="language-plaintext highlighter-rouge">SymbolDisplayFormat</code>) si vous avez besoin de noms stables (pour des clés de cache, des diagnostics, etc.).</li>
  <li><strong>Gérer le cas du code invalide</strong> : certains symboles peuvent être partiels ou manquants pendant l’édition, prévoyez des <code class="language-plaintext highlighter-rouge">null</code> et des <code class="language-plaintext highlighter-rouge">continue</code> défensifs.</li>
</ul>

<p>La Semantic API est le cœur “intelligent” d’un Source Generator : c’est elle qui transforme des nœuds de syntaxe filtrés en une vue riche du modèle .NET (types, membres, attributs). Combinée à la Syntax API pour cibler efficacement ce qui vous intéresse, elle permet de générer un code précis, fiable et aligné sur la réalité du projet compilé.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="roslyn" /><category term="source-generators" /><category term="semantic-api" /><summary type="html"><![CDATA[La Semantic API de Roslyn complète la Syntax API en donnant accès à la compréhension “sémantique” du code : types réels, symboles, héritage, interfaces implémentées, attributs résolus, etc. Dans un Source Generator, c’est elle qui permet de passer d’un simple nœud de syntaxe à un modèle riche sur lequel baser la génération de code. Dans cet article, on va voir : ce qu’est un SemanticModel ; comment obtenir des ISymbol (types, méthodes, propriétés) à partir de la syntaxe ; comment lire les attributs et types effectifs ; comment utiliser efficacement la Semantic API dans un générateur incrémental.]]></summary></entry></feed>