<?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-07-01T15:07:36+02: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">AsSplitQuery — maîtriser le chargement de plusieurs relations (EFcore)</title><link href="http://guym.fr/2026/03/22/EntityPart6.html" rel="alternate" type="text/html" title="AsSplitQuery — maîtriser le chargement de plusieurs relations (EFcore)" /><published>2026-03-22T00:00:00+01:00</published><updated>2026-03-22T00:00:00+01:00</updated><id>http://guym.fr/2026/03/22/EntityPart6</id><content type="html" xml:base="http://guym.fr/2026/03/22/EntityPart6.html"><![CDATA[<p>Entity Framework Core charge par défaut toutes les relations avec une seule requête SQL. Cette approche peut provoquer une <strong>explosion cartésienne</strong> lorsque plusieurs collections sont incluses. <code class="language-plaintext highlighter-rouge">AsSplitQuery</code> résout ce problème en découpant la requête en plusieurs instructions SQL distinctes.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Entity Framework</strong> : <strong> 8</strong> sur <strong>8</strong>.</p>
	<ul>
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2025/06/01/EntityPart1.html">Comprendre la "magie" derriere Entity Framework</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2025/06/14/EntityPart2.html">Le Database Context  (EFcore)</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2025/07/01/EntityPart3.html">Le Change Tracker (EFcore)</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2025/07/14/EntityPart4.html">Bonnes pratiques pour les requêtes (EFcore)</a>
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			<a href="/2025/08/09/EntityPart5.html">Manipuler le Change Tracker (EFcore)</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 6 - 
		
			<a href="/2026/03/15/EntityPart6.html">Optimiser les performances avec EF Core</a>
		
		</li>
	
	
	
		
		<li>Part 7 - 
		
			<a href="/2026/03/21/EntityPart7.html">Mesurer et diagnostiquer les performances EF Core</a>
		
		</li>
	
	
	
		
		<li>Part 8 - 
		
			Cet article
		
		</li>
	
	
	</ul>
</div>

<h1 id="1-le-problème--lexplosion-cartésienne">1. Le problème : l’explosion cartésienne</h1>

<p>Lorsqu’on inclut plusieurs collections avec <code class="language-plaintext highlighter-rouge">.Include()</code>, EF Core génère par défaut une unique requête SQL qui effectue des jointures entre toutes les tables.</p>

<h2 id="exemple">Exemple</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">CdeClients</span>
    <span class="p">.</span><span class="nf">Include</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">Lignes</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Include</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">Paiements</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<p>EF Core génère une requête de ce type :</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">SELECT</span> <span class="k">c</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">l</span><span class="p">.</span><span class="o">*</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">CdeClients</span> <span class="k">c</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">LigCdeClients</span> <span class="n">l</span> <span class="k">ON</span> <span class="n">l</span><span class="p">.</span><span class="n">CdeClientId</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">Id</span>
<span class="k">LEFT</span> <span class="k">JOIN</span> <span class="n">Paiements</span> <span class="n">p</span>     <span class="k">ON</span> <span class="n">p</span><span class="p">.</span><span class="n">CdeClientId</span> <span class="o">=</span> <span class="k">c</span><span class="p">.</span><span class="n">Id</span>
</code></pre></div></div>

<h2 id="le-piège-du-produit-cartésien">Le piège du produit cartésien</h2>

<p>Supposons qu’une commande ait <strong>3 lignes</strong> et <strong>2 paiements</strong>. La jointure produit <strong>3 × 2 = 6 lignes</strong> dans le jeu de résultats, même si les données n’en contiennent que 5.</p>

<table>
  <thead>
    <tr>
      <th>CdeClientId</th>
      <th>LigneId</th>
      <th>PaiementId</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>L1</td>
      <td>P1</td>
    </tr>
    <tr>
      <td>1</td>
      <td>L1</td>
      <td>P2</td>
    </tr>
    <tr>
      <td>1</td>
      <td>L2</td>
      <td>P1</td>
    </tr>
    <tr>
      <td>1</td>
      <td>L2</td>
      <td>P2</td>
    </tr>
    <tr>
      <td>1</td>
      <td>L3</td>
      <td>P1</td>
    </tr>
    <tr>
      <td>1</td>
      <td>L3</td>
      <td>P2</td>
    </tr>
  </tbody>
</table>

<p>EF Core déduplique correctement ces données, mais le volume <strong>transféré sur le réseau</strong> est multiplié. Avec de grandes collections, cela peut sérieusement dégrader les performances.</p>

<blockquote>
  <p>Le terme <strong>explosion cartésienne</strong> vient du produit cartésien mathématique : le nombre de lignes résultantes est le <strong>produit</strong> des cardinalités de chaque collection incluse.</p>
</blockquote>

<hr />

<h1 id="2-la-solution--assplitquery">2. La solution : <code class="language-plaintext highlighter-rouge">AsSplitQuery()</code></h1>

<p><code class="language-plaintext highlighter-rouge">AsSplitQuery()</code> indique à EF Core de découper la requête en <strong>plusieurs requêtes SQL distinctes</strong>, une par collection incluse. EF Core recompose ensuite les données en mémoire.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">CdeClients</span>
    <span class="p">.</span><span class="nf">Include</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">Lignes</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Include</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">Paiements</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsSplitQuery</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<p>EF Core génère alors trois requêtes séparées :</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- Requête 1 : entités principales</span>
<span class="k">SELECT</span> <span class="k">c</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">CdeClients</span> <span class="k">c</span><span class="p">;</span>

<span class="c1">-- Requête 2 : première collection</span>
<span class="k">SELECT</span> <span class="n">l</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">LigCdeClients</span> <span class="n">l</span>
<span class="k">WHERE</span> <span class="n">l</span><span class="p">.</span><span class="n">CdeClientId</span> <span class="k">IN</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="p">...);</span>

<span class="c1">-- Requête 3 : deuxième collection</span>
<span class="k">SELECT</span> <span class="n">p</span><span class="p">.</span><span class="o">*</span>
<span class="k">FROM</span> <span class="n">Paiements</span> <span class="n">p</span>
<span class="k">WHERE</span> <span class="n">p</span><span class="p">.</span><span class="n">CdeClientId</span> <span class="k">IN</span> <span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="p">...);</span>
</code></pre></div></div>

<p>Chaque requête retourne exactement les lignes nécessaires, sans duplication.</p>

<hr />

<h1 id="3-assinglequery--revenir-au-comportement-par-défaut">3. <code class="language-plaintext highlighter-rouge">AsSingleQuery()</code> — revenir au comportement par défaut</h1>

<p>À l’inverse, <code class="language-plaintext highlighter-rouge">AsSingleQuery()</code> force EF Core à utiliser une seule requête, même si le comportement global a été changé (voir section 5).</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">CdeClients</span>
    <span class="p">.</span><span class="nf">Include</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">Lignes</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsSingleQuery</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<h1 id="4-quand-utiliser-assplitquery-">4. Quand utiliser <code class="language-plaintext highlighter-rouge">AsSplitQuery</code> ?</h1>

<table>
  <thead>
    <tr>
      <th>Situation</th>
      <th>Recommandation</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Inclusion de <strong>plusieurs collections</strong> volumineuses</td>
      <td><code class="language-plaintext highlighter-rouge">AsSplitQuery</code></td>
    </tr>
    <tr>
      <td>Inclusion d’<strong>une seule collection</strong> ou de peu de données</td>
      <td><code class="language-plaintext highlighter-rouge">AsSingleQuery</code> (défaut)</td>
    </tr>
    <tr>
      <td><strong>Cohérence transactionnelle</strong> requise entre les requêtes</td>
      <td><code class="language-plaintext highlighter-rouge">AsSingleQuery</code></td>
    </tr>
    <tr>
      <td>Requête avec <strong>filtres, tri ou pagination</strong> sur les collections incluses</td>
      <td>Évaluer les deux et mesurer</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Règle pratique</strong> : dès qu’une requête inclut au moins deux collections et que les performances sont un enjeu, envisager <code class="language-plaintext highlighter-rouge">AsSplitQuery</code>.</p>
</blockquote>

<hr />

<h1 id="5-configuration-globale">5. Configuration globale</h1>

<p>Il est possible de configurer <code class="language-plaintext highlighter-rouge">AsSplitQuery</code> comme comportement par défaut pour l’ensemble du <code class="language-plaintext highlighter-rouge">DbContext</code>, sans avoir à l’ajouter requête par requête.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">services</span><span class="p">.</span><span class="n">AddDbContext</span><span class="p">&lt;</span><span class="n">AppDbContext</span><span class="p">&gt;(</span><span class="n">options</span> <span class="p">=&gt;</span>
    <span class="n">options</span><span class="p">.</span><span class="nf">UseSqlServer</span><span class="p">(</span><span class="n">connectionString</span><span class="p">,</span> <span class="n">sqlOptions</span> <span class="p">=&gt;</span>
        <span class="n">sqlOptions</span><span class="p">.</span><span class="nf">UseQuerySplittingBehavior</span><span class="p">(</span><span class="n">QuerySplittingBehavior</span><span class="p">.</span><span class="n">SplitQuery</span><span class="p">)));</span>
</code></pre></div></div>

<p>Dans ce cas, toutes les requêtes utiliseront automatiquement les requêtes divisées, sauf celles explicitement marquées <code class="language-plaintext highlighter-rouge">.AsSingleQuery()</code>.</p>

<hr />

<h1 id="6-points-dattention-et-limitations">6. Points d’attention et limitations</h1>

<h2 id="61-cohérence-des-données">6.1. Cohérence des données</h2>

<p>Avec <code class="language-plaintext highlighter-rouge">AsSplitQuery</code>, les plusieurs requêtes SQL sont exécutées dans des <strong>appels distincts</strong> à la base de données. Si des données sont modifiées entre deux requêtes (par un autre processus), les résultats peuvent être <strong>incohérents</strong>.</p>

<p>Pour garantir la cohérence, il faut envelopper les requêtes dans une transaction explicite :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">var</span> <span class="n">transaction</span> <span class="p">=</span> <span class="k">await</span> <span class="n">db</span><span class="p">.</span><span class="n">Database</span><span class="p">.</span><span class="nf">BeginTransactionAsync</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="k">await</span> <span class="n">db</span><span class="p">.</span><span class="n">CdeClients</span>
    <span class="p">.</span><span class="nf">Include</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">Lignes</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Include</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">Paiements</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsSplitQuery</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">ToListAsync</span><span class="p">();</span>

<span class="k">await</span> <span class="n">transaction</span><span class="p">.</span><span class="nf">CommitAsync</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="62-plusieurs-allers-retours-réseau">6.2. Plusieurs allers-retours réseau</h2>

<p><code class="language-plaintext highlighter-rouge">AsSplitQuery</code> génère <strong>N + 1 requêtes</strong> (1 pour l’entité principale, 1 par collection incluse). Cela augmente le nombre d’allers-retours vers la base de données. Sur un réseau à forte latence, cela peut annuler le gain obtenu.</p>

<h2 id="63-incompatibilité-avec-certaines-opérations">6.3. Incompatibilité avec certaines opérations</h2>

<p><code class="language-plaintext highlighter-rouge">AsSplitQuery</code> n’est pas compatible avec toutes les situations. Par exemple, les requêtes utilisant des opérateurs tels que <code class="language-plaintext highlighter-rouge">Distinct</code>, <code class="language-plaintext highlighter-rouge">GroupBy</code>, <code class="language-plaintext highlighter-rouge">Skip</code>/<code class="language-plaintext highlighter-rouge">Take</code> sur les entités principales peuvent produire des comportements inattendus ou des erreurs.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Attention : combinaison potentiellement problématique</span>
<span class="kt">var</span> <span class="n">commandes</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">CdeClients</span>
    <span class="p">.</span><span class="nf">Include</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">Lignes</span><span class="p">)</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">Date</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Skip</span><span class="p">(</span><span class="m">10</span><span class="p">).</span><span class="nf">Take</span><span class="p">(</span><span class="m">5</span><span class="p">)</span>   <span class="c1">// pagination sur l'entité principale</span>
    <span class="p">.</span><span class="nf">AsSplitQuery</span><span class="p">()</span>     <span class="c1">// peut générer un avertissement ou une erreur</span>
    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<blockquote>
  <p>EF Core émet un avertissement dans ce cas. Il est conseillé de tester et de vérifier le SQL généré avec <code class="language-plaintext highlighter-rouge">.ToQueryString()</code>.</p>
</blockquote>

<h2 id="64-inspecter-le-sql-généré">6.4. Inspecter le SQL généré</h2>

<p>Pour vérifier les requêtes produites avant exécution :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">query</span> <span class="p">=</span> <span class="n">db</span><span class="p">.</span><span class="n">CdeClients</span>
    <span class="p">.</span><span class="nf">Include</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">Lignes</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">Include</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">Paiements</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsSplitQuery</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="p">.</span><span class="nf">ToQueryString</span><span class="p">());</span>
</code></pre></div></div>

<hr />

<h1 id="7-résumé">7. Résumé</h1>

<table>
  <thead>
    <tr>
      <th> </th>
      <th><code class="language-plaintext highlighter-rouge">AsSingleQuery</code></th>
      <th><code class="language-plaintext highlighter-rouge">AsSplitQuery</code></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Requêtes SQL générées</strong></td>
      <td>1</td>
      <td>N (1 + nb de collections)</td>
    </tr>
    <tr>
      <td><strong>Risque de produit cartésien</strong></td>
      <td>Oui</td>
      <td>Non</td>
    </tr>
    <tr>
      <td><strong>Cohérence transactionnelle</strong></td>
      <td>Garantie</td>
      <td>À gérer manuellement</td>
    </tr>
    <tr>
      <td><strong>Allers-retours réseau</strong></td>
      <td>1</td>
      <td>N</td>
    </tr>
    <tr>
      <td><strong>Recommandé si</strong></td>
      <td>Peu de collections, faible volume</td>
      <td>Plusieurs collections volumineuses</td>
    </tr>
  </tbody>
</table>

<p><code class="language-plaintext highlighter-rouge">AsSplitQuery</code> est un outil précieux pour éviter les dégradations de performance liées au produit cartésien. Il convient toutefois de peser les compromis, notamment en matière de cohérence et de latence réseau, avant de l’adopter globalement.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="efcore" /><category term="performance" /><category term="split-query" /><summary type="html"><![CDATA[Entity Framework Core charge par défaut toutes les relations avec une seule requête SQL. Cette approche peut provoquer une explosion cartésienne lorsque plusieurs collections sont incluses. AsSplitQuery résout ce problème en découpant la requête en plusieurs instructions SQL distinctes.]]></summary></entry><entry><title type="html">Mesurer et diagnostiquer les performances EF Core</title><link href="http://guym.fr/2026/03/21/EntityPart7.html" rel="alternate" type="text/html" title="Mesurer et diagnostiquer les performances EF Core" /><published>2026-03-21T00:00:00+01:00</published><updated>2026-03-21T00:00:00+01:00</updated><id>http://guym.fr/2026/03/21/EntityPart7</id><content type="html" xml:base="http://guym.fr/2026/03/21/EntityPart7.html"><![CDATA[<p>Entity Framework Core : instrumenter les requêtes, lire les métriques et corriger les vrais goulots d’étranglement</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Entity Framework</strong> : <strong> 7</strong> sur <strong>8</strong>.</p>
	<ul>
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2025/06/01/EntityPart1.html">Comprendre la "magie" derriere Entity Framework</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2025/06/14/EntityPart2.html">Le Database Context  (EFcore)</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2025/07/01/EntityPart3.html">Le Change Tracker (EFcore)</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2025/07/14/EntityPart4.html">Bonnes pratiques pour les requêtes (EFcore)</a>
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			<a href="/2025/08/09/EntityPart5.html">Manipuler le Change Tracker (EFcore)</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 6 - 
		
			<a href="/2026/03/15/EntityPart6.html">Optimiser les performances avec EF Core</a>
		
		</li>
	
	
	
		
		<li>Part 7 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 8 - 
		
			<a href="/2026/03/22/EntityPart6.html">AsSplitQuery — maîtriser le chargement de plusieurs relations (EFcore)</a>
		
		</li>
	
	
	</ul>
</div>

<h1 id="1-pourquoi-mesurer">1. Pourquoi mesurer</h1>

<p>Après les optimisations techniques (requêtes précompilées, cache, DbContext pooling), il reste une règle essentielle :</p>

<ul>
  <li>on ne corrige pas une impression,</li>
  <li>on corrige une mesure.</li>
</ul>

<p>Sans observabilité, on peut passer des heures à optimiser la mauvaise zone.</p>

<h1 id="2-activer-un-niveau-de-logs-utile">2. Activer un niveau de logs utile</h1>

<p>La première étape consiste à rendre les requêtes visibles.</p>

<h2 id="21-logging-ef-core">2.1. Logging EF Core</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="n">AddDbContextPool</span><span class="p">&lt;</span><span class="n">AppDbContext</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="nf">UseSqlServer</span><span class="p">(</span><span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">(</span><span class="s">"DefaultConnection"</span><span class="p">));</span>

    <span class="n">options</span><span class="p">.</span><span class="nf">LogTo</span><span class="p">(</span><span class="n">Console</span><span class="p">.</span><span class="n">WriteLine</span><span class="p">,</span> <span class="n">LogLevel</span><span class="p">.</span><span class="n">Information</span><span class="p">)</span>
           <span class="p">.</span><span class="nf">EnableDetailedErrors</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>

<p>En développement, ce niveau de log permet de voir :</p>

<ul>
  <li>la requête SQL envoyée,</li>
  <li>la durée d’exécution,</li>
  <li>les paramètres (si activés explicitement).</li>
</ul>

<h2 id="22-attention-aux-données-sensibles">2.2. Attention aux données sensibles</h2>

<p><code class="language-plaintext highlighter-rouge">EnableSensitiveDataLogging()</code> est pratique en debug mais à éviter en production.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="n">builder</span><span class="p">.</span><span class="n">Environment</span><span class="p">.</span><span class="nf">IsDevelopment</span><span class="p">())</span>
<span class="p">{</span>
    <span class="n">options</span><span class="p">.</span><span class="nf">EnableSensitiveDataLogging</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="3-taguer-les-requêtes-pour-les-retrouver">3. Taguer les requêtes pour les retrouver</h1>

<p>Quand plusieurs endpoints exécutent des requêtes proches, les tags facilitent l’analyse.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">products</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_db</span><span class="p">.</span><span class="n">Products</span>
    <span class="p">.</span><span class="nf">TagWith</span><span class="p">(</span><span class="s">"ProductsController.GetByCategory"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsNoTracking</span><span class="p">()</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">CategoryId</span> <span class="p">==</span> <span class="n">categoryId</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">OrderBy</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">Name</span><span class="p">)</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="nf">ProductDto</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">Id</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">Price</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>
</code></pre></div></div>

<p>Avec <code class="language-plaintext highlighter-rouge">TagWith</code>, on remonte plus vite la requête dans les logs, le profiler SQL ou l’APM.</p>

<h2 id="31-correlation-id-et-trace-distribuée">3.1. Correlation ID et trace distribuée</h2>

<p>Dans un système distribué, l’objectif n’est pas seulement d’identifier la requête SQL, mais de la rattacher à une trace de bout en bout.</p>

<p>Exemple : injecter un <code class="language-plaintext highlighter-rouge">correlation id</code> métier et le <code class="language-plaintext highlighter-rouge">trace id</code> OpenTelemetry dans le tag.</p>

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

<span class="kt">var</span> <span class="n">correlationId</span> <span class="p">=</span> <span class="n">httpContext</span><span class="p">.</span><span class="n">TraceIdentifier</span><span class="p">;</span>
<span class="kt">var</span> <span class="n">traceId</span> <span class="p">=</span> <span class="n">Activity</span><span class="p">.</span><span class="n">Current</span><span class="p">?.</span><span class="n">TraceId</span><span class="p">.</span><span class="nf">ToString</span><span class="p">()</span> <span class="p">??</span> <span class="s">"no-trace"</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">products</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_db</span><span class="p">.</span><span class="n">Products</span>
    <span class="p">.</span><span class="nf">TagWith</span><span class="p">(</span><span class="s">$"op=Products.GetByCategory;corr=</span><span class="p">{</span><span class="n">correlationId</span><span class="p">}</span><span class="s">;trace=</span><span class="p">{</span><span class="n">traceId</span><span class="p">}</span><span class="s">"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsNoTracking</span><span class="p">()</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">CategoryId</span> <span class="p">==</span> <span class="n">categoryId</span><span class="p">)</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="nf">ProductDto</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">Id</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">Price</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>
</code></pre></div></div>

<p>Résultat : dans les logs SQL, dans l’APM et dans vos traces, vous avez la même clé de corrélation.</p>

<h2 id="32-tagwithcallsite-pour-retrouver-lorigine-du-code">3.2. TagWithCallSite pour retrouver l’origine du code</h2>

<p>Quand plusieurs requêtes se ressemblent, <code class="language-plaintext highlighter-rouge">TagWithCallSite()</code> est pratique pour savoir rapidement d’où vient l’appel (fichier/méthode/ligne selon le contexte d’exécution).</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">products</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_db</span><span class="p">.</span><span class="n">Products</span>
    <span class="p">.</span><span class="nf">TagWithCallSite</span><span class="p">(</span><span class="s">"Products query"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AsNoTracking</span><span class="p">()</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">CategoryId</span> <span class="p">==</span> <span class="n">categoryId</span><span class="p">)</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="nf">ProductDto</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">Id</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">Price</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>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">TagWithCallSite()</code> est très utile en investigation, mais il ne remplace pas un tag métier stable ni un correlation id.</p>

<h2 id="33-auto-tagging-pour-éviter-le-manuel-partout">3.3. Auto-tagging pour éviter le manuel partout</h2>

<p>Faire <code class="language-plaintext highlighter-rouge">TagWith(...)</code> à la main dans chaque requête est vite pénible. Une approche robuste consiste à ajouter un <code class="language-plaintext highlighter-rouge">DbCommandInterceptor</code> qui préfixe automatiquement le SQL avec les identifiants de corrélation.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Data.Common</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Diagnostics</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.EntityFrameworkCore.Diagnostics</span><span class="p">;</span>

<span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">CorrelationCommandInterceptor</span> <span class="p">:</span> <span class="n">DbCommandInterceptor</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IHttpContextAccessor</span> <span class="n">_httpContextAccessor</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">CorrelationCommandInterceptor</span><span class="p">(</span><span class="n">IHttpContextAccessor</span> <span class="n">httpContextAccessor</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_httpContextAccessor</span> <span class="p">=</span> <span class="n">httpContextAccessor</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">void</span> <span class="nf">AddComment</span><span class="p">(</span><span class="n">DbCommand</span> <span class="n">command</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">corr</span> <span class="p">=</span> <span class="n">_httpContextAccessor</span><span class="p">.</span><span class="n">HttpContext</span><span class="p">?.</span><span class="n">TraceIdentifier</span> <span class="p">??</span> <span class="s">"no-corr"</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">trace</span> <span class="p">=</span> <span class="n">Activity</span><span class="p">.</span><span class="n">Current</span><span class="p">?.</span><span class="n">TraceId</span><span class="p">.</span><span class="nf">ToString</span><span class="p">()</span> <span class="p">??</span> <span class="s">"no-trace"</span><span class="p">;</span>

        <span class="n">command</span><span class="p">.</span><span class="n">CommandText</span> <span class="p">=</span> <span class="s">$"-- corr:</span><span class="p">{</span><span class="n">corr</span><span class="p">}</span><span class="s">; trace:</span><span class="p">{</span><span class="n">trace</span><span class="p">}</span><span class="s"> \n</span><span class="p">{</span><span class="n">command</span><span class="p">.</span><span class="n">CommandText</span><span class="p">}</span><span class="s">"</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="n">InterceptionResult</span><span class="p">&lt;</span><span class="n">DbDataReader</span><span class="p">&gt;</span> <span class="nf">ReaderExecuting</span><span class="p">(</span>
        <span class="n">DbCommand</span> <span class="n">command</span><span class="p">,</span>
        <span class="n">CommandEventData</span> <span class="n">eventData</span><span class="p">,</span>
        <span class="n">InterceptionResult</span><span class="p">&lt;</span><span class="n">DbDataReader</span><span class="p">&gt;</span> <span class="n">result</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">AddComment</span><span class="p">(</span><span class="n">command</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">base</span><span class="p">.</span><span class="nf">ReaderExecuting</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">eventData</span><span class="p">,</span> <span class="n">result</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="n">InterceptionResult</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;</span> <span class="nf">ScalarExecuting</span><span class="p">(</span>
        <span class="n">DbCommand</span> <span class="n">command</span><span class="p">,</span>
        <span class="n">CommandEventData</span> <span class="n">eventData</span><span class="p">,</span>
        <span class="n">InterceptionResult</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">&gt;</span> <span class="n">result</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">AddComment</span><span class="p">(</span><span class="n">command</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">base</span><span class="p">.</span><span class="nf">ScalarExecuting</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">eventData</span><span class="p">,</span> <span class="n">result</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="n">InterceptionResult</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">NonQueryExecuting</span><span class="p">(</span>
        <span class="n">DbCommand</span> <span class="n">command</span><span class="p">,</span>
        <span class="n">CommandEventData</span> <span class="n">eventData</span><span class="p">,</span>
        <span class="n">InterceptionResult</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="n">result</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="nf">AddComment</span><span class="p">(</span><span class="n">command</span><span class="p">);</span>
        <span class="k">return</span> <span class="k">base</span><span class="p">.</span><span class="nf">NonQueryExecuting</span><span class="p">(</span><span class="n">command</span><span class="p">,</span> <span class="n">eventData</span><span class="p">,</span> <span class="n">result</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Enregistrement :</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">AddHttpContextAccessor</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="n">AddScoped</span><span class="p">&lt;</span><span class="n">CorrelationCommandInterceptor</span><span class="p">&gt;();</span>

<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddDbContextPool</span><span class="p">&lt;</span><span class="n">AppDbContext</span><span class="p">&gt;((</span><span class="n">sp</span><span class="p">,</span> <span class="n">options</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">options</span><span class="p">.</span><span class="nf">UseSqlServer</span><span class="p">(</span><span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">(</span><span class="s">"DefaultConnection"</span><span class="p">));</span>
    <span class="n">options</span><span class="p">.</span><span class="nf">AddInterceptors</span><span class="p">(</span><span class="n">sp</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">CorrelationCommandInterceptor</span><span class="p">&gt;());</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Bonnes pratiques :</p>

<ul>
  <li>garder des tags courts et stables,</li>
  <li>ne jamais inclure de données sensibles,</li>
  <li>standardiser le format des clés (<code class="language-plaintext highlighter-rouge">op</code>, <code class="language-plaintext highlighter-rouge">corr</code>, <code class="language-plaintext highlighter-rouge">trace</code>) pour faciliter les recherches.</li>
</ul>

<h1 id="4-mesurer-avec-opentelemetry">4. Mesurer avec OpenTelemetry</h1>

<h2 id="41-instrumentation-minimale">4.1. Instrumentation minimale</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">AddOpenTelemetry</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithTracing</span><span class="p">(</span><span class="n">tracing</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">tracing</span>
            <span class="p">.</span><span class="nf">AddAspNetCoreInstrumentation</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">AddHttpClientInstrumentation</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">AddSqlClientInstrumentation</span><span class="p">();</span>
    <span class="p">});</span>
</code></pre></div></div>

<p>Cette configuration donne une vue bout en bout :</p>

<ul>
  <li>requête HTTP entrante,</li>
  <li>appels SQL réalisés,</li>
  <li>durée totale et durée par dépendance.</li>
</ul>

<h2 id="42-ce-quil-faut-regarder-en-priorité">4.2. Ce qu’il faut regarder en priorité</h2>

<ul>
  <li>p95 et p99 de latence (pas seulement la moyenne),</li>
  <li>nombre de requêtes SQL par endpoint,</li>
  <li>requêtes les plus lentes,</li>
  <li>fréquence des timeouts.</li>
</ul>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="efcore" /><category term="performance" /><category term="observability" /><category term="logging" /><category term="opentelemetry" /><summary type="html"><![CDATA[Entity Framework Core : instrumenter les requêtes, lire les métriques et corriger les vrais goulots d’étranglement]]></summary></entry><entry><title type="html">Optimiser les performances avec EF Core</title><link href="http://guym.fr/2026/03/15/EntityPart6.html" rel="alternate" type="text/html" title="Optimiser les performances avec EF Core" /><published>2026-03-15T00:00:00+01:00</published><updated>2026-03-15T00:00:00+01:00</updated><id>http://guym.fr/2026/03/15/EntityPart6</id><content type="html" xml:base="http://guym.fr/2026/03/15/EntityPart6.html"><![CDATA[<p>Entity Framework Core : accélérer les accès avec les requêtes précompilées, la mise en cache et le DbContext pooling</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Entity Framework</strong> : <strong> 6</strong> sur <strong>8</strong>.</p>
	<ul>
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2025/06/01/EntityPart1.html">Comprendre la "magie" derriere Entity Framework</a>
		
		</li>
	
	
	
		
		<li>Part 2 - 
		
			<a href="/2025/06/14/EntityPart2.html">Le Database Context  (EFcore)</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 3 - 
		
			<a href="/2025/07/01/EntityPart3.html">Le Change Tracker (EFcore)</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2025/07/14/EntityPart4.html">Bonnes pratiques pour les requêtes (EFcore)</a>
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			<a href="/2025/08/09/EntityPart5.html">Manipuler le Change Tracker (EFcore)</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 6 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 7 - 
		
			<a href="/2026/03/21/EntityPart7.html">Mesurer et diagnostiquer les performances EF Core</a>
		
		</li>
	
	
	
		
		<li>Part 8 - 
		
			<a href="/2026/03/22/EntityPart6.html">AsSplitQuery — maîtriser le chargement de plusieurs relations (EFcore)</a>
		
		</li>
	
	
	</ul>
</div>

<h1 id="1-requêtes-précompilées">1. Requêtes précompilées</h1>

<h2 id="11-principe">1.1. Principe</h2>

<p>Par défaut, EF Core analyse l’expression LINQ puis la traduit en SQL.
Quand une requête est exécutée très souvent, il est possible de compiler cette requête une fois, puis de réutiliser le délégué compilé.</p>

<h2 id="12-compiled-vs-precompiled-queries-et-selon-la-version-def">1.2. Compiled vs precompiled queries (et selon la version d’EF)</h2>

<p>Le vocabulaire peut porter à confusion, car on voit souvent les deux termes.</p>

<ul>
  <li>Compiled query : c’est le terme officiel des API EF Core avec <code class="language-plaintext highlighter-rouge">EF.CompileQuery</code> / <code class="language-plaintext highlighter-rouge">EF.CompileAsyncQuery</code>.</li>
  <li>Precompiled query : c’est souvent utilisé comme synonyme dans les articles, mais techniquement cela peut aussi désigner une génération anticipée au build (AOT) dans les versions récentes.</li>
</ul>

<p>En pratique, retenez ceci :</p>

<ul>
  <li>EF6 (Entity Framework “classique”) : on utilisait <code class="language-plaintext highlighter-rouge">CompiledQuery.Compile(...)</code> et le gain pouvait être significatif sur des requêtes très répétées.</li>
  <li>EF Core 1 à 7 : EF Core met déjà en cache une partie du pipeline de traduction. Les compiled queries explicites existent toujours et servent surtout sur les hot paths.</li>
  <li>EF Core 8+ : on garde <code class="language-plaintext highlighter-rouge">EF.CompileQuery</code> pour les scénarios classiques, et on peut aussi rencontrer la notion de precompiled queries dans le contexte Native AOT/source generation.</li>
</ul>

<p>Conclusion rapide : dans la plupart des projets, quand on dit “requête précompilée” en EF Core, on parle généralement des compiled queries via <code class="language-plaintext highlighter-rouge">EF.CompileQuery</code>.</p>

<h2 id="13-exemple">1.3. Exemple</h2>

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

<span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">ProductQueries</span>
<span class="p">{</span>
	<span class="c1">// Requête asynchrone compilée et réutilisable</span>
	<span class="k">public</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Func</span><span class="p">&lt;</span><span class="n">AppDbContext</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">ProductDto</span><span class="p">&gt;&gt;</span>
		<span class="n">GetActiveProductById</span> <span class="p">=</span> <span class="n">EF</span><span class="p">.</span><span class="nf">CompileAsyncQuery</span><span class="p">(</span>
			<span class="p">(</span><span class="n">AppDbContext</span> <span class="n">db</span><span class="p">,</span> <span class="kt">int</span> <span class="n">id</span><span class="p">)</span> <span class="p">=&gt;</span>
				<span class="n">db</span><span class="p">.</span><span class="n">Products</span>
					<span class="p">.</span><span class="nf">AsNoTracking</span><span class="p">()</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">Id</span> <span class="p">==</span> <span class="n">id</span> <span class="p">&amp;&amp;</span> <span class="n">p</span><span class="p">.</span><span class="n">IsActive</span><span class="p">)</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="nf">ProductDto</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">Id</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">Price</span><span class="p">))</span>
		<span class="p">);</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">sealed</span> <span class="n">record</span> <span class="nf">ProductDto</span><span class="p">(</span><span class="kt">int</span> <span class="n">Id</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Name</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">Price</span><span class="p">);</span>
</code></pre></div></div>

<p>Utilisation :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">dto</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ProductQueries</span>
	<span class="p">.</span><span class="nf">GetActiveProductById</span><span class="p">(</span><span class="n">context</span><span class="p">,</span> <span class="n">productId</span><span class="p">)</span>
	<span class="p">.</span><span class="nf">FirstOrDefaultAsync</span><span class="p">(</span><span class="n">cancellationToken</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="14-quand-lutiliser">1.4. Quand l’utiliser</h2>

<ul>
  <li>Requête très fréquente et stable (même forme, seuls les paramètres changent).</li>
  <li>Endpoints à fort trafic.</li>
  <li>Scénarios de lecture où la latence est critique.</li>
</ul>

<h2 id="15-limites">1.5. Limites</h2>

<ul>
  <li>Gain faible pour les requêtes occasionnelles.</li>
  <li>Complexité supplémentaire si la requête évolue souvent.</li>
  <li>La compilation aide surtout le pipeline EF, pas une requête SQL lente et mal indexée.</li>
</ul>

<h1 id="2-mise-en-cache-applicative">2. Mise en cache applicative</h1>

<h2 id="21-principe">2.1. Principe</h2>

<p>Si certaines données changent peu, inutile d’interroger la base à chaque appel.
On peut utiliser IMemoryCache pour les lectures rapides en local (ou un cache distribué pour plusieurs instances).</p>

<h2 id="22-exemple-avec-imemorycache">2.2. Exemple avec IMemoryCache</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Microsoft.Extensions.Caching.Memory</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductReadService</span>
<span class="p">{</span>
	<span class="k">private</span> <span class="k">readonly</span> <span class="n">AppDbContext</span> <span class="n">_db</span><span class="p">;</span>
	<span class="k">private</span> <span class="k">readonly</span> <span class="n">IMemoryCache</span> <span class="n">_cache</span><span class="p">;</span>

	<span class="k">public</span> <span class="nf">ProductReadService</span><span class="p">(</span><span class="n">AppDbContext</span> <span class="n">db</span><span class="p">,</span> <span class="n">IMemoryCache</span> <span class="n">cache</span><span class="p">)</span>
	<span class="p">{</span>
		<span class="n">_db</span> <span class="p">=</span> <span class="n">db</span><span class="p">;</span>
		<span class="n">_cache</span> <span class="p">=</span> <span class="n">cache</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">ProductDto</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="p">{</span>
		<span class="kt">var</span> <span class="n">cacheKey</span> <span class="p">=</span> <span class="s">$"product:</span><span class="p">{</span><span class="n">id</span><span class="p">}</span><span class="s">"</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">cacheKey</span><span class="p">,</span> <span class="k">out</span> <span class="n">ProductDto</span><span class="p">?</span> <span class="n">cached</span><span class="p">))</span>
		<span class="p">{</span>
			<span class="k">return</span> <span class="n">cached</span><span class="p">;</span>
		<span class="p">}</span>

		<span class="kt">var</span> <span class="n">dto</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_db</span><span class="p">.</span><span class="n">Products</span>
			<span class="p">.</span><span class="nf">AsNoTracking</span><span class="p">()</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">Id</span> <span class="p">==</span> <span class="n">id</span> <span class="p">&amp;&amp;</span> <span class="n">p</span><span class="p">.</span><span class="n">IsActive</span><span class="p">)</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="nf">ProductDto</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">Id</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">Price</span><span class="p">))</span>
			<span class="p">.</span><span class="nf">FirstOrDefaultAsync</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">dto</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span>
		<span class="p">{</span>
			<span class="k">return</span> <span class="k">null</span><span class="p">;</span>
		<span class="p">}</span>

		<span class="n">_cache</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="n">cacheKey</span><span class="p">,</span> <span class="n">dto</span><span class="p">,</span> <span class="k">new</span> <span class="n">MemoryCacheEntryOptions</span>
		<span class="p">{</span>
			<span class="n">SlidingExpiration</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">5</span><span class="p">),</span>
			<span class="n">AbsoluteExpirationRelativeToNow</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">30</span><span class="p">)</span>
		<span class="p">});</span>

		<span class="k">return</span> <span class="n">dto</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="23-bonnes-pratiques-cache">2.3. Bonnes pratiques cache</h2>

<ul>
  <li>Mettre en cache des DTO, pas des entités suivies par le Change Tracker.</li>
  <li>Définir une stratégie d’invalidation claire (TTL, événement, suppression explicite).</li>
  <li>Éviter un TTL trop long sur des données métier sensibles.</li>
  <li>Mesurer le ratio hit/miss pour valider le gain.</li>
</ul>

<h1 id="3-dbcontext-pooling">3. DbContext pooling</h1>

<h2 id="31-principe">3.1. Principe</h2>

<p>Créer un DbContext à chaque requête a un coût.
Le pooling permet de réutiliser des instances de contexte réinitialisées, ce qui réduit les allocations et la pression du GC.</p>

<h2 id="32-configuration">3.2. Configuration</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="n">AddDbContextPool</span><span class="p">&lt;</span><span class="n">AppDbContext</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="nf">UseSqlServer</span><span class="p">(</span><span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">(</span><span class="s">"DefaultConnection"</span><span class="p">));</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="33-points-dattention">3.3. Points d’attention</h2>

<ul>
  <li>Un DbContext reste non thread-safe : une instance, un flux d’exécution.</li>
  <li>Ne pas stocker d’état métier mutable dans le contexte.</li>
  <li>Préférer des traitements courts par unité de travail.</li>
  <li>Vérifier les comportements spécifiques si vous injectez des services dépendants du contexte.</li>
</ul>

<h1 id="4-combiner-les-3-leviers">4. Combiner les 3 leviers</h1>

<p>La combinaison la plus efficace en lecture fréquente :</p>

<ol>
  <li>cache en premier niveau,</li>
  <li>requête compilée en fallback,</li>
  <li>contexte poolé pour limiter le coût de création.</li>
</ol>

<p>Exemple de logique :</p>

<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">ProductDto</span><span class="p">?&gt;</span> <span class="nf">GetProductFastAsync</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="p">{</span>
	<span class="kt">var</span> <span class="n">key</span> <span class="p">=</span> <span class="s">$"product:</span><span class="p">{</span><span class="n">id</span><span class="p">}</span><span class="s">"</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">key</span><span class="p">,</span> <span class="k">out</span> <span class="n">ProductDto</span><span class="p">?</span> <span class="n">dto</span><span class="p">))</span>
	<span class="p">{</span>
		<span class="k">return</span> <span class="n">dto</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="n">dto</span> <span class="p">=</span> <span class="k">await</span> <span class="n">ProductQueries</span>
		<span class="p">.</span><span class="nf">GetActiveProductById</span><span class="p">(</span><span class="n">_db</span><span class="p">,</span> <span class="n">id</span><span class="p">)</span>
		<span class="p">.</span><span class="nf">FirstOrDefaultAsync</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">dto</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span><span class="p">)</span>
	<span class="p">{</span>
		<span class="n">_cache</span><span class="p">.</span><span class="nf">Set</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">dto</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">10</span><span class="p">));</span>
	<span class="p">}</span>

	<span class="k">return</span> <span class="n">dto</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="5-checklist-performance-ef-core">5. Checklist performance EF Core</h1>

<ul>
  <li>Requêtes de lecture en AsNoTracking().</li>
  <li>Projections DTO via <code class="language-plaintext highlighter-rouge">Select</code> pour limiter les colonnes.</li>
  <li>Requêtes précompilées seulement sur les hot paths.</li>
  <li>Cache avec invalidation explicite.</li>
  <li>AddDbContextPool pour les applications à charge soutenue.</li>
  <li>Index SQL alignés avec vos filtres et vos tris.</li>
  <li>Mesure réelle avec logs, traces et benchmarks avant/après.</li>
</ul>

<h1 id="conclusion">Conclusion</h1>

<p>Les performances EF Core ne reposent pas sur un seul bouton magique.
Le bon résultat vient de la combinaison :</p>

<ul>
  <li>requêtes efficaces,</li>
  <li>cache maîtrisé,</li>
  <li>cycle de vie du contexte optimisé.</li>
</ul>

<p>Commencez par mesurer vos endpoints les plus lents, appliquez ces trois leviers sur les chemins critiques, puis validez le gain en production avec de la télémétrie.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="efcore" /><category term="performance" /><category term="compiled-query" /><category term="cache" /><category term="dbcontext-pool" /><summary type="html"><![CDATA[Entity Framework Core : accélérer les accès avec les requêtes précompilées, la mise en cache et le DbContext pooling]]></summary></entry><entry><title type="html">Bonnes pratiques avec les fichiers en .NET</title><link href="http://guym.fr/2026/03/14/fichiers-bonnes-pratiques.html" rel="alternate" type="text/html" title="Bonnes pratiques avec les fichiers en .NET" /><published>2026-03-14T00:00:00+01:00</published><updated>2026-03-14T00:00:00+01:00</updated><id>http://guym.fr/2026/03/14/fichiers-bonnes-pratiques</id><content type="html" xml:base="http://guym.fr/2026/03/14/fichiers-bonnes-pratiques.html"><![CDATA[<p>La manipulation de fichiers est une opération courante qui cache de nombreux pièges : chemins non portables, ressources non libérées, fichiers temporaires oubliés, encodage incorrect… Cet article passe en revue les bonnes pratiques à adopter en .NET.</p>

<!--more-->

<h2 id="1-concaténation-de-chemins">1. Concaténation de chemins</h2>

<h3 id="ce-quil-ne-faut-jamais-faire">Ce qu’il ne faut jamais faire</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Fragile : séparateur en dur, non portable Linux/Windows</span>
<span class="kt">var</span> <span class="n">path</span> <span class="p">=</span> <span class="n">baseDir</span> <span class="p">+</span> <span class="s">"\\"</span> <span class="p">+</span> <span class="s">"subfolder"</span> <span class="p">+</span> <span class="s">"\\"</span> <span class="p">+</span> <span class="s">"file.txt"</span><span class="p">;</span>

<span class="c1">// Fragile : les slashes peuvent ne pas correspondre à l'OS</span>
<span class="kt">var</span> <span class="n">path</span> <span class="p">=</span> <span class="n">baseDir</span> <span class="p">+</span> <span class="s">"/subfolder/file.txt"</span><span class="p">;</span>
</code></pre></div></div>

<h3 id="pathcombine--la-référence"><code class="language-plaintext highlighter-rouge">Path.Combine</code> — la référence</h3>

<p><code class="language-plaintext highlighter-rouge">Path.Combine</code> gère automatiquement les séparateurs selon l’OS :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">path</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="s">"subfolder"</span><span class="p">,</span> <span class="s">"file.txt"</span><span class="p">);</span>
</code></pre></div></div>

<p>Attention : si un des segments est un chemin absolu, <code class="language-plaintext highlighter-rouge">Path.Combine</code> <strong>abandonne</strong> les segments précédents.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="s">"C:\\data"</span><span class="p">,</span> <span class="s">"C:\\other"</span><span class="p">,</span> <span class="s">"file.txt"</span><span class="p">);</span> <span class="c1">// → "C:\\other\\file.txt"</span>
</code></pre></div></div>

<h3 id="pathjoin-préféré-depuis-net-5"><code class="language-plaintext highlighter-rouge">Path.Join</code> (préféré depuis .NET 5)</h3>

<p><code class="language-plaintext highlighter-rouge">Path.Join</code> ne souffre pas de ce comportement : il concatène toujours tous les segments et ne tronque jamais :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">path</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="s">"subfolder"</span><span class="p">,</span> <span class="s">"file.txt"</span><span class="p">);</span>

<span class="c1">// Avec des Span&lt;char&gt; pour éviter les allocations</span>
<span class="kt">var</span> <span class="n">path</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="n">baseDir</span><span class="p">.</span><span class="nf">AsSpan</span><span class="p">(),</span> <span class="s">"subfolder"</span><span class="p">.</span><span class="nf">AsSpan</span><span class="p">(),</span> <span class="s">"file.txt"</span><span class="p">.</span><span class="nf">AsSpan</span><span class="p">());</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Règle</strong> : Utilisez <code class="language-plaintext highlighter-rouge">Path.Join</code> pour construire des chemins, <code class="language-plaintext highlighter-rouge">Path.Combine</code> uniquement si vous voulez explicitement qu’un segment absolu écrase les précédents.</p>
</blockquote>

<h3 id="normaliser-un-chemin">Normaliser un chemin</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Résout les ".." et les doubles séparateurs</span>
<span class="kt">var</span> <span class="n">normalized</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetFullPath</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>

<span class="c1">// Résoudre relativement à une base</span>
<span class="kt">var</span> <span class="n">full</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetFullPath</span><span class="p">(</span><span class="s">"../config.json"</span><span class="p">,</span> <span class="n">baseDir</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="2-lire-et-écrire-des-fichiers">2. Lire et écrire des fichiers</h2>

<h3 id="file--méthodes-statiques-pour-les-petits-fichiers"><code class="language-plaintext highlighter-rouge">File</code> — méthodes statiques pour les petits fichiers</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Lecture complète</span>
<span class="kt">string</span> <span class="n">content</span> <span class="p">=</span> <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">ReadAllTextAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>
<span class="kt">byte</span><span class="p">[]</span> <span class="n">bytes</span>    <span class="p">=</span> <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">ReadAllBytesAsync</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>
<span class="kt">string</span><span class="p">[]</span> <span class="n">lines</span>  <span class="p">=</span> <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">ReadAllLinesAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>

<span class="c1">// Écriture complète</span>
<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>
<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllBytesAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">bytes</span><span class="p">);</span>
<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllLinesAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">lines</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="streamreader--streamwriter-pour-les-gros-fichiers"><code class="language-plaintext highlighter-rouge">StreamReader</code> / <code class="language-plaintext highlighter-rouge">StreamWriter</code> pour les gros fichiers</h3>

<p>Pour les fichiers volumineux, lisez ligne par ligne pour éviter de tout charger en mémoire :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">reader</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StreamReader</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>
<span class="k">while</span> <span class="p">(!</span><span class="n">reader</span><span class="p">.</span><span class="n">EndOfStream</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">line</span> <span class="p">=</span> <span class="k">await</span> <span class="n">reader</span><span class="p">.</span><span class="nf">ReadLineAsync</span><span class="p">();</span>
    <span class="c1">// traiter line</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Écriture bufferisée :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">writer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StreamWriter</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">append</span><span class="p">:</span> <span class="k">false</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>
<span class="k">await</span> <span class="n">writer</span><span class="p">.</span><span class="nf">WriteLineAsync</span><span class="p">(</span><span class="s">"première ligne"</span><span class="p">);</span>
<span class="k">await</span> <span class="n">writer</span><span class="p">.</span><span class="nf">WriteLineAsync</span><span class="p">(</span><span class="s">"deuxième ligne"</span><span class="p">);</span>
<span class="c1">// flush automatique à la fin du using</span>
</code></pre></div></div>

<h3 id="filestream-pour-un-contrôle-fin"><code class="language-plaintext highlighter-rouge">FileStream</code> pour un contrôle fin</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">fs</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FileStream</span><span class="p">(</span>
    <span class="n">path</span><span class="p">,</span>
    <span class="n">FileMode</span><span class="p">.</span><span class="n">Create</span><span class="p">,</span>
    <span class="n">FileAccess</span><span class="p">.</span><span class="n">Write</span><span class="p">,</span>
    <span class="n">FileShare</span><span class="p">.</span><span class="n">None</span><span class="p">,</span>
    <span class="n">bufferSize</span><span class="p">:</span> <span class="m">4096</span><span class="p">,</span>
    <span class="n">useAsync</span><span class="p">:</span> <span class="k">true</span><span class="p">);</span>

<span class="k">await</span> <span class="n">fs</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="n">buffer</span><span class="p">.</span><span class="nf">AsMemory</span><span class="p">(</span><span class="m">0</span><span class="p">,</span> <span class="n">bytesRead</span><span class="p">));</span>
</code></pre></div></div>

<hr />

<h2 id="3-toujours-libérer-les-ressources">3. Toujours libérer les ressources</h2>

<p>Toute classe implémentant <code class="language-plaintext highlighter-rouge">IDisposable</code> ou <code class="language-plaintext highlighter-rouge">IAsyncDisposable</code> doit être utilisée dans un bloc <code class="language-plaintext highlighter-rouge">using</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Synchrone</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">stream</span> <span class="p">=</span> <span class="n">File</span><span class="p">.</span><span class="nf">OpenRead</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>

<span class="c1">// Asynchrone — préférez await using</span>
<span class="k">await</span> <span class="k">using</span> <span class="nn">var</span> <span class="n">stream</span> <span class="p">=</span> <span class="n">File</span><span class="p">.</span><span class="nf">OpenReadAsync</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>
</code></pre></div></div>

<p>Ne jamais faire :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">stream</span> <span class="p">=</span> <span class="n">File</span><span class="p">.</span><span class="nf">OpenRead</span><span class="p">(</span><span class="n">path</span><span class="p">);</span>
<span class="c1">// ... oubli de Dispose → le fichier reste verrouillé</span>
</code></pre></div></div>

<hr />

<h2 id="4-fichiers-temporaires">4. Fichiers temporaires</h2>

<h3 id="pathgettempfilename--le-piège-courant"><code class="language-plaintext highlighter-rouge">Path.GetTempFileName</code> — le piège courant</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Crée physiquement un fichier de 0 octet dans le dossier Temp de l'OS</span>
<span class="c1">// et retourne son chemin — risque de collision si mal géré</span>
<span class="kt">var</span> <span class="n">tempFile</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetTempFileName</span><span class="p">();</span>
</code></pre></div></div>

<p>Le problème : si votre programme plante, le fichier reste sur disque.</p>

<h3 id="pattern-recommandé">Pattern recommandé</h3>

<p>Créez vos propres fichiers temporaires avec un nom unique et nettoyez-les explicitement :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">tempPath</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">GetTempPath</span><span class="p">(),</span> <span class="s">$"</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="s">.tmp"</span><span class="p">);</span>
<span class="k">try</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">tempPath</span><span class="p">,</span> <span class="n">content</span><span class="p">);</span>
    <span class="c1">// ... traitements</span>
<span class="p">}</span>
<span class="k">finally</span>
<span class="p">{</span>
    <span class="n">File</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="n">tempPath</span><span class="p">);</span> <span class="c1">// garanti même en cas d'exception</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="encapsuler-dans-un-idisposable">Encapsuler dans un <code class="language-plaintext highlighter-rouge">IDisposable</code></h3>

<p>Pour une gestion propre et réutilisable :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">TempFile</span> <span class="p">:</span> <span class="n">IDisposable</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Path</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="n">System</span><span class="p">.</span><span class="n">IO</span><span class="p">.</span><span class="n">Path</span><span class="p">.</span><span class="nf">Combine</span><span class="p">(</span>
        <span class="n">System</span><span class="p">.</span><span class="n">IO</span><span class="p">.</span><span class="n">Path</span><span class="p">.</span><span class="nf">GetTempPath</span><span class="p">(),</span>
        <span class="s">$"</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="s">.tmp"</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Dispose</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">File</span><span class="p">.</span><span class="nf">Exists</span><span class="p">(</span><span class="n">Path</span><span class="p">))</span>
            <span class="n">File</span><span class="p">.</span><span class="nf">Delete</span><span class="p">(</span><span class="n">Path</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Utilisation</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">temp</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">TempFile</span><span class="p">();</span>
<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">temp</span><span class="p">.</span><span class="n">Path</span><span class="p">,</span> <span class="n">data</span><span class="p">);</span>
<span class="c1">// Suppression automatique à la sortie du using</span>
</code></pre></div></div>

<hr />

<h2 id="5-encodage">5. Encodage</h2>

<p>Spécifiez <strong>toujours</strong> l’encodage explicitement. Ne jamais se fier à l’encodage par défaut de la machine :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BOM inclus par défaut avec new UTF8Encoding(true)</span>
<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>

<span class="c1">// Sans BOM (courant pour les échanges inter-systèmes)</span>
<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="k">new</span> <span class="nf">UTF8Encoding</span><span class="p">(</span><span class="n">encoderShouldEmitUTF8Identifier</span><span class="p">:</span> <span class="k">false</span><span class="p">));</span>
</code></pre></div></div>

<p>Pour détecter l’encodage d’un fichier inconnu, utilisez un package comme <code class="language-plaintext highlighter-rouge">Ude.NetStandard</code> ou <code class="language-plaintext highlighter-rouge">UTF8Encoding.Default</code> en dernier recours.</p>

<hr />

<h2 id="6-vérifications-dexistence">6. Vérifications d’existence</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Fichier</span>
<span class="k">if</span> <span class="p">(</span><span class="n">File</span><span class="p">.</span><span class="nf">Exists</span><span class="p">(</span><span class="n">path</span><span class="p">))</span>
<span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>

<span class="c1">// Répertoire</span>
<span class="k">if</span> <span class="p">(</span><span class="n">Directory</span><span class="p">.</span><span class="nf">Exists</span><span class="p">(</span><span class="n">dirPath</span><span class="p">))</span>
<span class="p">{</span>
    <span class="c1">// ...</span>
<span class="p">}</span>

<span class="c1">// Créer le répertoire si absent (idempotent)</span>
<span class="n">Directory</span><span class="p">.</span><span class="nf">CreateDirectory</span><span class="p">(</span><span class="n">dirPath</span><span class="p">);</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">Directory.CreateDirectory</code> ne lève pas d’exception si le dossier existe déjà. Pas besoin de vérifier au préalable.</p>
</blockquote>

<hr />

<h2 id="7-créer-le-chemin-complet-dun-fichier-sil-nexiste-pas">7. Créer le chemin complet d’un fichier s’il n’existe pas</h2>

<p>Avant d’écrire dans un fichier, son <strong>répertoire parent</strong> doit exister. <code class="language-plaintext highlighter-rouge">File.WriteAllTextAsync</code> ne crée pas les dossiers intermédiaires et lèvera une <code class="language-plaintext highlighter-rouge">DirectoryNotFoundException</code> si l’un d’eux manque.</p>

<p>Le pattern à adopter :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">filePath</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="s">"reports"</span><span class="p">,</span> <span class="s">"2026"</span><span class="p">,</span> <span class="s">"rapport.pdf"</span><span class="p">);</span>

<span class="c1">// Crée tous les répertoires intermédiaires en une seule instruction (idempotent)</span>
<span class="n">Directory</span><span class="p">.</span><span class="nf">CreateDirectory</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">GetDirectoryName</span><span class="p">(</span><span class="n">filePath</span><span class="p">)!);</span>

<span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllBytesAsync</span><span class="p">(</span><span class="n">filePath</span><span class="p">,</span> <span class="n">pdfBytes</span><span class="p">);</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Path.GetDirectoryName</code> extrait la partie répertoire du chemin complet, et <code class="language-plaintext highlighter-rouge">Directory.CreateDirectory</code> crée récursivement tous les segments manquants.</p>

<p>Encapsuler cette logique dans une méthode utilitaire évite de l’oublier :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">WriteAllTextSafeAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">filePath</span><span class="p">,</span> <span class="kt">string</span> <span class="n">content</span><span class="p">,</span> <span class="n">Encoding</span><span class="p">?</span> <span class="n">encoding</span> <span class="p">=</span> <span class="k">null</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Directory</span><span class="p">.</span><span class="nf">CreateDirectory</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">GetDirectoryName</span><span class="p">(</span><span class="n">filePath</span><span class="p">)!);</span>
    <span class="k">await</span> <span class="n">File</span><span class="p">.</span><span class="nf">WriteAllTextAsync</span><span class="p">(</span><span class="n">filePath</span><span class="p">,</span> <span class="n">content</span><span class="p">,</span> <span class="n">encoding</span> <span class="p">??</span> <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p>Attention au <code class="language-plaintext highlighter-rouge">!</code> sur <code class="language-plaintext highlighter-rouge">Path.GetDirectoryName</code> : la méthode retourne <code class="language-plaintext highlighter-rouge">null</code> uniquement si le chemin est une racine (<code class="language-plaintext highlighter-rouge">"C:\\"</code> ou <code class="language-plaintext highlighter-rouge">"/"</code>). Dans tous les autres cas le résultat est non nul. Si votre chemin peut être une racine, ajoutez une vérification explicite.</p>
</blockquote>

<hr />

<h2 id="8-sécurité-des-chemins-path-traversal">8. Sécurité des chemins (Path Traversal)</h2>

<p>Si le chemin est fourni par un utilisateur, <strong>validez-le</strong> pour éviter les attaques de type path traversal :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="kt">string</span> <span class="nf">GetSecurePath</span><span class="p">(</span><span class="kt">string</span> <span class="n">baseDir</span><span class="p">,</span> <span class="kt">string</span> <span class="n">userInput</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Normaliser pour résoudre les ".."</span>
    <span class="kt">var</span> <span class="n">fullPath</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetFullPath</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="n">baseDir</span><span class="p">,</span> <span class="n">userInput</span><span class="p">));</span>

    <span class="c1">// Vérifier que le chemin résultant reste sous baseDir</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">fullPath</span><span class="p">.</span><span class="nf">StartsWith</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">GetFullPath</span><span class="p">(</span><span class="n">baseDir</span><span class="p">)</span> <span class="p">+</span> <span class="n">Path</span><span class="p">.</span><span class="n">DirectorySeparatorChar</span><span class="p">,</span>
                              <span class="n">StringComparison</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">UnauthorizedAccessException</span><span class="p">(</span><span class="s">"Accès refusé : chemin hors du répertoire autorisé."</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">return</span> <span class="n">fullPath</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="9-surveillance-de-fichiers-avec-filesystemwatcher">9. Surveillance de fichiers avec <code class="language-plaintext highlighter-rouge">FileSystemWatcher</code></h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">var</span> <span class="n">watcher</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FileSystemWatcher</span><span class="p">(</span><span class="n">dirPath</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">Filter</span> <span class="p">=</span> <span class="s">"*.json"</span><span class="p">,</span>
    <span class="n">NotifyFilter</span> <span class="p">=</span> <span class="n">NotifyFilters</span><span class="p">.</span><span class="n">LastWrite</span> <span class="p">|</span> <span class="n">NotifyFilters</span><span class="p">.</span><span class="n">FileName</span><span class="p">,</span>
    <span class="n">IncludeSubdirectories</span> <span class="p">=</span> <span class="k">false</span><span class="p">,</span>
    <span class="n">EnableRaisingEvents</span> <span class="p">=</span> <span class="k">true</span>
<span class="p">};</span>

<span class="n">watcher</span><span class="p">.</span><span class="n">Changed</span> <span class="p">+=</span> <span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Modifié : </span><span class="p">{</span><span class="n">e</span><span class="p">.</span><span class="n">FullPath</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="n">watcher</span><span class="p">.</span><span class="n">Created</span> <span class="p">+=</span> <span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Créé    : </span><span class="p">{</span><span class="n">e</span><span class="p">.</span><span class="n">FullPath</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="n">watcher</span><span class="p">.</span><span class="n">Deleted</span> <span class="p">+=</span> <span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">e</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Supprimé: </span><span class="p">{</span><span class="n">e</span><span class="p">.</span><span class="n">FullPath</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="c1">// Éviter les événements en double (le Changed se déclenche parfois deux fois)</span>
<span class="n">watcher</span><span class="p">.</span><span class="n">Changed</span> <span class="p">+=</span> <span class="n">OnChangedDebounced</span><span class="p">;</span>
</code></pre></div></div>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">FileSystemWatcher</code> peut déclencher plusieurs événements pour une seule modification (selon l’OS). Implémentez un <strong>debounce</strong> si nécessaire.</p>
</blockquote>

<hr />

<h2 id="10-opérations-sur-les-répertoires">10. Opérations sur les répertoires</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Lister les fichiers (non récursif)</span>
<span class="kt">var</span> <span class="n">files</span> <span class="p">=</span> <span class="n">Directory</span><span class="p">.</span><span class="nf">GetFiles</span><span class="p">(</span><span class="n">dirPath</span><span class="p">,</span> <span class="s">"*.txt"</span><span class="p">);</span>

<span class="c1">// Lister récursivement sans tout charger en mémoire</span>
<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">file</span> <span class="k">in</span> <span class="n">Directory</span><span class="p">.</span><span class="nf">EnumerateFiles</span><span class="p">(</span><span class="n">dirPath</span><span class="p">,</span> <span class="s">"*.txt"</span><span class="p">,</span> <span class="n">SearchOption</span><span class="p">.</span><span class="n">AllDirectories</span><span class="p">))</span>
<span class="p">{</span>
    <span class="c1">// traiter file</span>
<span class="p">}</span>

<span class="c1">// Copier un répertoire entier (non intégré nativement avant .NET 8)</span>
<span class="c1">// À partir de .NET 8 :</span>
<span class="c1">// Pas d'API built-in, mais on peut utiliser DirectoryInfo</span>
<span class="k">new</span> <span class="nf">DirectoryInfo</span><span class="p">(</span><span class="n">sourceDir</span><span class="p">).</span><span class="nf">CopyTo</span><span class="p">(</span><span class="n">destinationDir</span><span class="p">);</span> <span class="c1">// ⚠ n'existe pas</span>

<span class="c1">// Pattern manuel</span>
<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">file</span> <span class="k">in</span> <span class="n">Directory</span><span class="p">.</span><span class="nf">EnumerateFiles</span><span class="p">(</span><span class="n">sourceDir</span><span class="p">,</span> <span class="s">"*"</span><span class="p">,</span> <span class="n">SearchOption</span><span class="p">.</span><span class="n">AllDirectories</span><span class="p">))</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">relativePath</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">GetRelativePath</span><span class="p">(</span><span class="n">sourceDir</span><span class="p">,</span> <span class="n">file</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">destFile</span>     <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="n">destinationDir</span><span class="p">,</span> <span class="n">relativePath</span><span class="p">);</span>
    <span class="n">Directory</span><span class="p">.</span><span class="nf">CreateDirectory</span><span class="p">(</span><span class="n">Path</span><span class="p">.</span><span class="nf">GetDirectoryName</span><span class="p">(</span><span class="n">destFile</span><span class="p">)!);</span>
    <span class="n">File</span><span class="p">.</span><span class="nf">Copy</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="n">destFile</span><span class="p">,</span> <span class="n">overwrite</span><span class="p">:</span> <span class="k">true</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Préférez <code class="language-plaintext highlighter-rouge">Directory.EnumerateFiles</code> à <code class="language-plaintext highlighter-rouge">Directory.GetFiles</code> pour les grands répertoires : le premier est en <em>streaming</em> et n’alloue pas un tableau complet en mémoire.</p>

<hr />

<h2 id="11-portabilité-multi-os">11. Portabilité multi-OS</h2>

<table>
  <thead>
    <tr>
      <th>À éviter</th>
      <th>À préférer</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">"\\"</code> ou <code class="language-plaintext highlighter-rouge">"/"</code> en dur</td>
      <td><code class="language-plaintext highlighter-rouge">Path.DirectorySeparatorChar</code> ou <code class="language-plaintext highlighter-rouge">Path.Join</code></td>
    </tr>
    <tr>
      <td>Comparaison de chemins avec <code class="language-plaintext highlighter-rouge">==</code></td>
      <td><code class="language-plaintext highlighter-rouge">string.Equals(a, b, StringComparison.OrdinalIgnoreCase)</code> sur Windows, <code class="language-plaintext highlighter-rouge">Ordinal</code> sur Linux</td>
    </tr>
    <tr>
      <td>Chemins codés en dur (<code class="language-plaintext highlighter-rouge">C:\\data</code>)</td>
      <td><code class="language-plaintext highlighter-rouge">AppContext.BaseDirectory</code>, <code class="language-plaintext highlighter-rouge">Environment.GetFolderPath</code>, <code class="language-plaintext highlighter-rouge">IWebHostEnvironment.ContentRootPath</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Path.GetTempFileName</code> sans nettoyage</td>
      <td>Pattern <code class="language-plaintext highlighter-rouge">TempFile</code> avec <code class="language-plaintext highlighter-rouge">IDisposable</code></td>
    </tr>
  </tbody>
</table>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Emplacement portable pour les données d'application</span>
<span class="kt">var</span> <span class="n">appData</span> <span class="p">=</span> <span class="n">Environment</span><span class="p">.</span><span class="nf">GetFolderPath</span><span class="p">(</span><span class="n">Environment</span><span class="p">.</span><span class="n">SpecialFolder</span><span class="p">.</span><span class="n">ApplicationData</span><span class="p">);</span>
<span class="kt">var</span> <span class="n">configPath</span> <span class="p">=</span> <span class="n">Path</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="n">appData</span><span class="p">,</span> <span class="s">"MonApp"</span><span class="p">,</span> <span class="s">"config.json"</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="récapitulatif">Récapitulatif</h2>

<table>
  <thead>
    <tr>
      <th>Pratique</th>
      <th>API / Pattern</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Construire un chemin</td>
      <td><code class="language-plaintext highlighter-rouge">Path.Join</code></td>
    </tr>
    <tr>
      <td>Normaliser un chemin</td>
      <td><code class="language-plaintext highlighter-rouge">Path.GetFullPath</code></td>
    </tr>
    <tr>
      <td>Petits fichiers</td>
      <td><code class="language-plaintext highlighter-rouge">File.ReadAllTextAsync</code> / <code class="language-plaintext highlighter-rouge">WriteAllTextAsync</code></td>
    </tr>
    <tr>
      <td>Gros fichiers</td>
      <td><code class="language-plaintext highlighter-rouge">StreamReader</code> / <code class="language-plaintext highlighter-rouge">StreamWriter</code> en ligne par ligne</td>
    </tr>
    <tr>
      <td>Libérer les ressources</td>
      <td><code class="language-plaintext highlighter-rouge">await using</code> / <code class="language-plaintext highlighter-rouge">using</code></td>
    </tr>
    <tr>
      <td>Fichiers temporaires</td>
      <td><code class="language-plaintext highlighter-rouge">TempFile : IDisposable</code> + <code class="language-plaintext highlighter-rouge">finally</code></td>
    </tr>
    <tr>
      <td>Encodage</td>
      <td>Toujours explicite (<code class="language-plaintext highlighter-rouge">Encoding.UTF8</code>)</td>
    </tr>
    <tr>
      <td>Créer le chemin complet</td>
      <td><code class="language-plaintext highlighter-rouge">Directory.CreateDirectory(Path.GetDirectoryName(path)!)</code></td>
    </tr>
    <tr>
      <td>Sécurité chemin</td>
      <td><code class="language-plaintext highlighter-rouge">Path.GetFullPath</code> + vérification du préfixe</td>
    </tr>
    <tr>
      <td>Surveillance</td>
      <td><code class="language-plaintext highlighter-rouge">FileSystemWatcher</code> + debounce</td>
    </tr>
    <tr>
      <td>Lister un dossier</td>
      <td><code class="language-plaintext highlighter-rouge">Directory.EnumerateFiles</code> (streaming)</td>
    </tr>
  </tbody>
</table>

<p>La gestion des fichiers est un domaine qui semble simple mais concentre de nombreux bugs en production. Quelques habitudes bien ancrées — <code class="language-plaintext highlighter-rouge">Path.Join</code>, <code class="language-plaintext highlighter-rouge">await using</code>, nettoyage systématique des temporaires et validation des chemins utilisateurs — suffisent à éviter la grande majorité des problèmes.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="fichiers" /><category term="io" /><category term="bonnes-pratiques" /><summary type="html"><![CDATA[La manipulation de fichiers est une opération courante qui cache de nombreux pièges : chemins non portables, ressources non libérées, fichiers temporaires oubliés, encodage incorrect… Cet article passe en revue les bonnes pratiques à adopter en .NET.]]></summary></entry><entry><title type="html">Dapr : le Pub/Sub (Publish &amp;amp; Subscribe) en .NET</title><link href="http://guym.fr/2026/03/08/dapr-pub-sub.html" rel="alternate" type="text/html" title="Dapr : le Pub/Sub (Publish &amp;amp; Subscribe) en .NET" /><published>2026-03-08T00:00:00+01:00</published><updated>2026-03-08T00:00:00+01:00</updated><id>http://guym.fr/2026/03/08/dapr-pub-sub</id><content type="html" xml:base="http://guym.fr/2026/03/08/dapr-pub-sub.html"><![CDATA[<p>Le Pub/Sub (Publish &amp; Subscribe) est l’un des building blocks les plus utilisés de Dapr. Il permet à des services de communiquer de manière <strong>asynchrone</strong> via des événements, sans couplage direct entre l’émetteur et le(s) récepteur(s). En .NET, le SDK Dapr et ASP.NET Core rendent la publication et la souscription d’événements simples et déclaratives, quel que soit le broker sous-jacent (RabbitMQ, Kafka, Azure Service Bus, Redis Streams…).</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Dapr pour les développeurs .NET</strong> : <strong> 6</strong> sur <strong>6</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 - 
		
			<a href="/2026/02/22/dapr-service-invocation-2-grpc.html">Dapr : invocation de service gRPC en .NET</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2026/03/01/dapr-etats.html">Dapr : la gestion d'état (State Management) en .NET</a>
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			<a href="/2026/03/03/dapr-pub-sub-grpc.html">Dapr : Pub/Sub gRPC en .NET</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 6 - 
		
			Cet article
		
		</li>
	
	
	
	
	
	
	
	
	
	
	</ul>
</div>

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

<p>Dans une architecture microservices, les services ont souvent besoin de se notifier mutuellement sans s’appeler directement :</p>

<ul>
  <li><strong>Découplage</strong> : le service qui émet un événement ne devrait pas connaître (ni dépendre de) ceux qui le consomment.</li>
  <li><strong>Scalabilité</strong> : plusieurs consommateurs doivent pouvoir traiter le même événement indépendamment.</li>
  <li><strong>Fiabilité</strong> : les messages doivent être livrés au moins une fois (at-least-once), même si un consommateur est temporairement indisponible.</li>
  <li><strong>Portabilité</strong> : on veut pouvoir changer de broker (passer de RabbitMQ à Kafka, par exemple) sans modifier le code applicatif.</li>
  <li><strong>Format d’événements</strong> : chaque broker a ses propres SDK, formats de messages, modes de sérialisation.</li>
</ul>

<p>Sans Dapr, il faut intégrer le SDK du broker choisi, gérer la connexion, la sérialisation, les retries, les dead-letter queues, et coupler le code à un fournisseur spécifique. Dapr résout tout cela avec une API Pub/Sub unifiée.</p>

<h1 id="fonctionnement">Fonctionnement</h1>

<p>Le Pub/Sub de Dapr repose sur le modèle <strong>publish/subscribe</strong> classique, enrichi par le format <strong>CloudEvents</strong> :</p>

<ol>
  <li>Un service <strong>publie</strong> un événement sur un <strong>topic</strong> via son sidecar Dapr.</li>
  <li>Le sidecar transmet le message au <strong>broker</strong> configuré (RabbitMQ, Kafka, etc.).</li>
  <li>Les sidecars des services <strong>abonnés</strong> récupèrent le message et le transmettent à leur application via un appel HTTP ou gRPC sur <code class="language-plaintext highlighter-rouge">localhost</code>.</li>
</ol>

<pre><code class="language-mermaid">graph LR
    PubApp["Service A&lt;br/&gt;(Publisher)"]
    PubSidecar["Sidecar A"]
    Broker["Broker&lt;br/&gt;(RabbitMQ, Kafka,&lt;br/&gt;Azure Service Bus...)"]
    SubSidecar1["Sidecar B"]
    SubApp1["Service B&lt;br/&gt;(Subscriber)"]
    SubSidecar2["Sidecar C"]
    SubApp2["Service C&lt;br/&gt;(Subscriber)"]
    
    PubApp --&gt;|PublishEvent&lt;br/&gt;localhost| PubSidecar
    PubSidecar --&gt;|Publie| Broker
    Broker --&gt;|Distribue| SubSidecar1
    Broker --&gt;|Distribue| SubSidecar2
    SubSidecar1 --&gt;|POST /endpoint&lt;br/&gt;localhost| SubApp1
    SubSidecar2 --&gt;|POST /endpoint&lt;br/&gt;localhost| SubApp2
    
    style PubApp fill:#4A90E2
    style PubSidecar fill:#F5A623
    style Broker fill:#D0021B
    style SubSidecar1 fill:#F5A623
    style SubSidecar2 fill:#F5A623
    style SubApp1 fill:#7ED321
    style SubApp2 fill:#7ED321
</code></pre>

<p>Chaque message est automatiquement enveloppé au format <strong>CloudEvents</strong> (standard CNCF), ce qui garantit un format de métadonnées interopérable.</p>

<h1 id="configuration-du-composant">Configuration du composant</h1>

<p>Le composant Pub/Sub est défini dans un fichier YAML. Voici quelques exemples :</p>

<h2 id="redis-streams-développement-local">Redis Streams (développement local)</h2>

<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.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>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">redisPassword</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
</code></pre></div></div>

<h2 id="rabbitmq">RabbitMQ</h2>

<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>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">durable</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">true"</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">deletedWhenUnused</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">false"</span>
</code></pre></div></div>

<h2 id="apache-kafka">Apache Kafka</h2>

<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.kafka</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">brokers</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">localhost:9092"</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">consumerGroup</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">my-consumer-group"</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">authType</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">none"</span>
</code></pre></div></div>

<h2 id="azure-service-bus">Azure Service Bus</h2>

<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.azure.servicebus.topics</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">Endpoint=sb://my-namespace.servicebus.windows.net/;SharedAccessKeyName=...;SharedAccessKey=..."</span>
</code></pre></div></div>

<p>Le point essentiel : <strong>le code applicatif est identique</strong> quel que soit le broker. On passe de Redis à Kafka en modifiant uniquement le fichier YAML.</p>

<h1 id="pubsub-en-net">Pub/Sub en .NET</h1>

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

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

<h2 id="configuration-du-service">Configuration du 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="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 CloudEvents (désérialisation automatique)</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseCloudEvents</span><span class="p">();</span>

<span class="c1">// Enregistrer les souscriptions auprès du sidecar</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">Run</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">UseCloudEvents()</code> permet à ASP.NET Core de désérialiser automatiquement les enveloppes CloudEvents pour extraire le payload. <code class="language-plaintext highlighter-rouge">MapSubscribeHandler()</code> expose un endpoint <code class="language-plaintext highlighter-rouge">/dapr/subscribe</code> que le sidecar interroge au démarrage pour connaître les topics auxquels l’application est abonnée.</p>

<h1 id="publier-des-événements">Publier des événements</h1>

<h2 id="avec-daprclient">Avec <code class="language-plaintext highlighter-rouge">DaprClient</code></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">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">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">PubSubName</span> <span class="p">=</span> <span class="s">"pubsub"</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="k">public</span> <span class="k">async</span> <span class="n">Task</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">// Logique de création de commande...</span>
        <span class="n">order</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="c1">// Publier l'événement sur le topic "orders"</span>
        <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">PublishEventAsync</span><span class="p">(</span><span class="n">PubSubName</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="n">order</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
            <span class="n">CustomerId</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
            <span class="n">TotalAmount</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">TotalAmount</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>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="publication-depuis-un-endpoint-minimal-api">Publication depuis un endpoint Minimal API</h2>

<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="k">async</span> <span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">order</span><span class="p">.</span><span class="n">Id</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="nf">ToString</span><span class="p">();</span>
    <span class="n">order</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="c1">// Sauvegarder la commande (state store)</span>
    <span class="k">await</span> <span class="n">dapr</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-</span><span class="p">{</span><span class="n">order</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">order</span><span class="p">);</span>

    <span class="c1">// Publier l'événement</span>
    <span class="k">await</span> <span class="n">dapr</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="n">order</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
        <span class="n">CustomerId</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
        <span class="n">TotalAmount</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">TotalAmount</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>

    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Created</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">Id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="publication-avec-métadonnées">Publication avec métadonnées</h2>

<p>On peut ajouter des métadonnées au message (headers personnalisés, TTL, etc.) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">metadata</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="p">{</span> <span class="s">"ttlInSeconds"</span><span class="p">,</span> <span class="s">"600"</span> <span class="p">},</span>      <span class="c1">// Le message expire après 10 minutes</span>
    <span class="p">{</span> <span class="s">"rawPayload"</span><span class="p">,</span> <span class="s">"false"</span> <span class="p">}</span>       <span class="c1">// Utiliser le format CloudEvents (par défaut)</span>
<span class="p">};</span>

<span class="k">await</span> <span class="n">_daprClient</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="n">orderCreated</span><span class="p">,</span> <span class="n">metadata</span><span class="p">);</span>
</code></pre></div></div>

<h1 id="sabonner-aux-événements">S’abonner aux événements</h1>

<h2 id="souscription-déclarative-avec-lattribut-topic">Souscription déclarative avec l’attribut <code class="language-plaintext highlighter-rouge">[Topic]</code></h2>

<p>La manière la plus simple de s’abonner à un topic en ASP.NET Core :</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">"/order-created"</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">, montant : </span><span class="p">{</span><span class="n">evt</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">}</span><span class="s">€"</span><span class="p">);</span>
    <span class="c1">// Traitement de l'événement...</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>L’attribut <code class="language-plaintext highlighter-rouge">[Topic("pubsub", "orders")]</code> indique au sidecar Dapr que cet endpoint doit recevoir les messages publiés sur le topic <code class="language-plaintext highlighter-rouge">orders</code> du composant <code class="language-plaintext highlighter-rouge">pubsub</code>.</p>

<h2 id="souscription-avec-un-controller">Souscription avec un Controller</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">ApiController</span><span class="p">]</span>
<span class="p">[</span><span class="nf">Route</span><span class="p">(</span><span class="s">"[controller]"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">NotificationsController</span> <span class="p">:</span> <span class="n">ControllerBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">NotificationsController</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">NotificationsController</span><span class="p">(</span><span class="n">ILogger</span><span class="p">&lt;</span><span class="n">NotificationsController</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span> <span class="p">=</span> <span class="n">logger</span><span class="p">;</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="nf">HttpPost</span><span class="p">(</span><span class="s">"order-created"</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">IActionResult</span><span class="p">&gt;</span> <span class="nf">HandleOrderCreated</span><span class="p">(</span><span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Notification pour commande {OrderId}"</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="c1">// Envoyer un email, une notification push, etc.</span>
        <span class="k">await</span> <span class="nf">SendNotificationAsync</span><span class="p">(</span><span class="n">evt</span><span class="p">);</span>

        <span class="k">return</span> <span class="nf">Ok</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="n">Task</span> <span class="nf">SendNotificationAsync</span><span class="p">(</span><span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Logique de notification...</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="p">}</span>
</code></pre></div></div>

<h2 id="souscription-par-fichier-de-configuration-déclarative-externe">Souscription par fichier de configuration (déclarative externe)</h2>

<p>On peut aussi déclarer les souscriptions dans un fichier YAML, sans modifier le 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/v2alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Subscription</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">order-subscription</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">pubsubname</span><span class="pi">:</span> <span class="s">pubsub</span>
  <span class="na">topic</span><span class="pi">:</span> <span class="s">orders</span>
  <span class="na">routes</span><span class="pi">:</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">/order-created</span>
  <span class="na">scopes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">notification-service</span>
    <span class="pi">-</span> <span class="s">inventory-service</span>
</code></pre></div></div>

<p>Cette approche est utile quand on veut gérer les souscriptions de manière centralisée, ou quand le service cible ne peut pas être modifié.</p>

<h1 id="cloudevents">CloudEvents</h1>

<p>Dapr utilise le format <strong>CloudEvents</strong> (spécification CNCF) pour standardiser les messages. Chaque message publié est automatiquement encapsulé dans une enveloppe CloudEvents :</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">"specversion"</span><span class="p">:</span><span class="w"> </span><span class="s2">"1.0"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"com.dapr.event.sent"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"source"</span><span class="p">:</span><span class="w"> </span><span class="s2">"order-service"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="s2">"a1b2c3d4-e5f6-7890-abcd-ef1234567890"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"datacontenttype"</span><span class="p">:</span><span class="w"> </span><span class="s2">"application/json"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"data"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"orderId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"order-42"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"customerId"</span><span class="p">:</span><span class="w"> </span><span class="s2">"cust-123"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"totalAmount"</span><span class="p">:</span><span class="w"> </span><span class="mf">99.99</span><span class="p">,</span><span class="w">
    </span><span class="nl">"createdAt"</span><span class="p">:</span><span class="w"> </span><span class="s2">"2026-03-07T10:30:00Z"</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>

<table>
  <thead>
    <tr>
      <th>Champ</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">specversion</code></td>
      <td>Version de la spec CloudEvents</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">type</code></td>
      <td>Type de l’événement</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">source</code></td>
      <td>App-id du service émetteur</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">id</code></td>
      <td>Identifiant unique du message</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">datacontenttype</code></td>
      <td>Type MIME du payload</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">data</code></td>
      <td>Le contenu de l’événement (votre objet sérialisé)</td>
    </tr>
  </tbody>
</table>

<p>Grâce à <code class="language-plaintext highlighter-rouge">UseCloudEvents()</code>, le middleware ASP.NET Core extrait automatiquement le champ <code class="language-plaintext highlighter-rouge">data</code> et le désérialise dans le type attendu par l’endpoint.</p>

<h2 id="publication-en-raw-sans-cloudevents">Publication en raw (sans CloudEvents)</h2>

<p>Si un consommateur ne comprend pas le format CloudEvents, on peut publier en mode « raw » :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">metadata</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="p">{</span> <span class="s">"rawPayload"</span><span class="p">,</span> <span class="s">"true"</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">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="n">orderCreated</span><span class="p">,</span> <span class="n">metadata</span><span class="p">);</span>
</code></pre></div></div>

<p>Le message sera publié tel quel, sans enveloppe CloudEvents.</p>

<h1 id="sémantique-de-livraison">Sémantique de livraison</h1>

<h2 id="at-least-once-par-défaut">At-least-once (par défaut)</h2>

<p>Dapr garantit une livraison <strong>au moins une fois</strong> : si le subscriber ne retourne pas un succès (200 OK), le sidecar re-tente la livraison. Cela implique que le handler doit être <strong>idempotent</strong> (capable de traiter le même message plusieurs fois sans effet de bord).</p>

<h3 id="stratégies-didempotence">Stratégies d’idempotence</h3>

<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">"/order-created"</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="k">async</span> <span class="p">(</span>
    <span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="c1">// Vérifier si l'événement a déjà été traité (idempotence)</span>
    <span class="kt">var</span> <span class="n">alreadyProcessed</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="kt">bool</span><span class="p">&gt;(</span>
        <span class="s">"statestore"</span><span class="p">,</span> <span class="s">$"processed-</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">if</span> <span class="p">(</span><span class="n">alreadyProcessed</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Déjà traité, on retourne OK pour acquitter le message</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="c1">// Traiter l'événement...</span>
    <span class="k">await</span> <span class="nf">ProcessOrderAsync</span><span class="p">(</span><span class="n">evt</span><span class="p">);</span>

    <span class="c1">// Marquer comme traité</span>
    <span class="k">await</span> <span class="n">dapr</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">$"processed-</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">true</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>

<h2 id="contrôle-de-la-réponse-du-subscriber">Contrôle de la réponse du subscriber</h2>

<p>Le sidecar interprète la réponse HTTP du subscriber pour décider du sort du message :</p>

<table>
  <thead>
    <tr>
      <th>Code de réponse</th>
      <th>Action du sidecar</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">200</code> (OK)</td>
      <td>Message acquitté (ACK), supprimé de la queue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">404</code> (Not Found)</td>
      <td>Message abandonné (DROP), supprimé de la queue</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Autre</code> (5xx, timeout…)</td>
      <td>Message re-tenté (RETRY) selon la politique de résilience</td>
    </tr>
  </tbody>
</table>

<p>On peut aussi retourner un statut explicite via le body :</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">"/order-created"</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="k">try</span>
    <span class="p">{</span>
        <span class="nf">ProcessOrder</span><span class="p">(</span><span class="n">evt</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">status</span> <span class="p">=</span> <span class="s">"SUCCESS"</span> <span class="p">});</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">InvalidOperationException</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Message invalide, ne pas réessayer</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">status</span> <span class="p">=</span> <span class="s">"DROP"</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="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Erreur temporaire, réessayer</span>
        <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">StatusCode</span><span class="p">(</span><span class="m">500</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h1 id="dead-letter-topics">Dead Letter Topics</h1>

<p>Quand un message ne peut pas être traité après plusieurs tentatives, il est envoyé dans un <strong>dead letter topic</strong> pour analyse ultérieure. La configuration se fait dans le composant ou dans la souscription :</p>

<h2 id="configuration-dans-la-souscription">Configuration dans la souscription</h2>

<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/v2alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Subscription</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">order-subscription</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">pubsubname</span><span class="pi">:</span> <span class="s">pubsub</span>
  <span class="na">topic</span><span class="pi">:</span> <span class="s">orders</span>
  <span class="na">routes</span><span class="pi">:</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">/order-created</span>
  <span class="na">deadLetterTopic</span><span class="pi">:</span> <span class="s">orders-deadletter</span>
</code></pre></div></div>

<h2 id="consommer-les-dead-letters">Consommer les dead letters</h2>

<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">"/dead-letters"</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-deadletter"</span><span class="p">)]</span> <span class="p">(</span>
    <span class="n">CloudEvent</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;</span> <span class="n">deadLetter</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">$"Message en échec : </span><span class="p">{</span><span class="n">deadLetter</span><span class="p">.</span><span class="n">Data</span><span class="p">?.</span><span class="n">OrderId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="c1">// Logger, alerter, stocker pour investigation...</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>

<h1 id="routing-dévénements">Routing d’événements</h1>

<p>Dapr permet de router les messages vers différents endpoints selon leur contenu, grâce aux <strong>règles de routage</strong> :</p>

<h2 id="configuration-via-souscription-yaml">Configuration via souscription YAML</h2>

<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/v2alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Subscription</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">order-subscription</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">pubsubname</span><span class="pi">:</span> <span class="s">pubsub</span>
  <span class="na">topic</span><span class="pi">:</span> <span class="s">orders</span>
  <span class="na">routes</span><span class="pi">:</span>
    <span class="na">rules</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">match</span><span class="pi">:</span> <span class="s">event.data.status == "created"</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/orders/created</span>
      <span class="pi">-</span> <span class="na">match</span><span class="pi">:</span> <span class="s">event.data.status == "shipped"</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/orders/shipped</span>
      <span class="pi">-</span> <span class="na">match</span><span class="pi">:</span> <span class="s">event.data.status == "cancelled"</span>
        <span class="na">path</span><span class="pi">:</span> <span class="s">/orders/cancelled</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">/orders/unknown</span>
</code></pre></div></div>

<h2 id="configuration-dans-le-code-avec-lattribut-topicrule-aspnet-core">Configuration dans le code avec l’attribut <code class="language-plaintext highlighter-rouge">[TopicRule]</code> (ASP.NET Core)</h2>

<p>On peut également exprimer les règles de routage directement dans le code via l’attribut <code class="language-plaintext highlighter-rouge">[Topic]</code> enrichi :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Route par défaut</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/orders/default"</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="n">isDefault</span><span class="p">:</span> <span class="k">true</span><span class="p">)]</span>
    <span class="p">(</span><span class="n">OrderEvent</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">$"Événement ordre non classifié : </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="c1">// Route pour les créations</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/orders/created"</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="s">"event.data.status == \"created\""</span><span class="p">,</span> <span class="m">1</span><span class="p">)]</span>
    <span class="p">(</span><span class="n">OrderEvent</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 créée : </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="c1">// Route pour les expéditions</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/orders/shipped"</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="s">"event.data.status == \"shipped\""</span><span class="p">,</span> <span class="m">2</span><span class="p">)]</span>
    <span class="p">(</span><span class="n">OrderEvent</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 expédiée : </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>

<h1 id="scoping--restreindre-laccès-aux-topics">Scoping : restreindre l’accès aux topics</h1>

<p>Dapr permet de restreindre quels services peuvent publier ou s’abonner à quels topics :</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>
  <span class="na">scopes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">order-service</span>
    <span class="pi">-</span> <span class="s">notification-service</span>
    <span class="pi">-</span> <span class="s">inventory-service</span>
</code></pre></div></div>

<p>On peut affiner avec des règles de publication et de souscription :</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">Subscription</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">order-sub</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">pubsubname</span><span class="pi">:</span> <span class="s">pubsub</span>
  <span class="na">topic</span><span class="pi">:</span> <span class="s">orders</span>
  <span class="na">routes</span><span class="pi">:</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">/order-created</span>
  <span class="na">scopes</span><span class="pi">:</span>
    <span class="pi">-</span> <span class="s">notification-service</span>   <span class="c1"># Seul ce service peut s'abonner</span>
</code></pre></div></div>

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

<p>Comme pour les autres building blocks, Dapr permet de configurer des politiques de résilience sur le Pub/Sub :</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">pubsubRetry</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">5</span>
    <span class="na">circuitBreakers</span><span class="pi">:</span>
      <span class="na">pubsubBreaker</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">30s</span>
        <span class="na">timeout</span><span class="pi">:</span> <span class="s">60s</span>
        <span class="na">trip</span><span class="pi">:</span> <span class="s">consecutiveFailures &gt; </span><span class="m">3</span>
  <span class="na">targets</span><span class="pi">:</span>
    <span class="na">components</span><span class="pi">:</span>
      <span class="na">pubsub</span><span class="pi">:</span>
        <span class="na">outbound</span><span class="pi">:</span>
          <span class="na">retry</span><span class="pi">:</span> <span class="s">pubsubRetry</span>
          <span class="na">circuitBreaker</span><span class="pi">:</span> <span class="s">pubsubBreaker</span>
        <span class="na">inbound</span><span class="pi">:</span>
          <span class="na">retry</span><span class="pi">:</span> <span class="s">pubsubRetry</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Cible</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">outbound</code></td>
      <td>Politique appliquée lors de la <strong>publication</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">inbound</code></td>
      <td>Politique appliquée lors de la <strong>livraison</strong> au subscriber</td>
    </tr>
  </tbody>
</table>

<h1 id="bulk-publish-et-bulk-subscribe">Bulk Publish et Bulk Subscribe</h1>

<p>Pour les cas à haut débit, Dapr supporte la publication et la souscription en lots (bulk).</p>

<h2 id="bulk-publish">Bulk Publish</h2>

<p>Publier plusieurs événements en une seule requête, réduisant les allers-retours réseau :</p>

<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">PublishOrdersBatchAsync</span><span class="p">(</span><span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;</span> <span class="n">events</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">bulkEvents</span> <span class="p">=</span> <span class="n">events</span><span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">e</span> <span class="p">=&gt;</span>
        <span class="k">new</span> <span class="n">BulkPublishEntry</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;(</span>
            <span class="n">entryId</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="nf">ToString</span><span class="p">(),</span>
            <span class="n">@event</span><span class="p">:</span> <span class="n">e</span><span class="p">,</span>
            <span class="n">contentType</span><span class="p">:</span> <span class="s">"application/json"</span><span class="p">)).</span><span class="nf">ToList</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="nf">BulkPublishEventAsync</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="n">bulkEvents</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="n">FailedEntries</span><span class="p">.</span><span class="n">Count</span> <span class="p">&gt;</span> <span class="m">0</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">$"</span><span class="p">{</span><span class="n">response</span><span class="p">.</span><span class="n">FailedEntries</span><span class="p">.</span><span class="n">Count</span><span class="p">}</span><span class="s"> messages en échec."</span><span class="p">);</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">failed</span> <span class="k">in</span> <span class="n">response</span><span class="p">.</span><span class="n">FailedEntries</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">$"  - </span><span class="p">{</span><span class="n">failed</span><span class="p">.</span><span class="n">Entry</span><span class="p">.</span><span class="n">EntryId</span><span class="p">}</span><span class="s"> : </span><span class="p">{</span><span class="n">failed</span><span class="p">.</span><span class="n">ErrorMessage</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="bulk-subscribe">Bulk Subscribe</h2>

<p>Recevoir plusieurs messages en un seul appel HTTP pour améliorer le débit de consommation :</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/v2alpha1</span>
<span class="na">kind</span><span class="pi">:</span> <span class="s">Subscription</span>
<span class="na">metadata</span><span class="pi">:</span>
  <span class="na">name</span><span class="pi">:</span> <span class="s">bulk-order-subscription</span>
<span class="na">spec</span><span class="pi">:</span>
  <span class="na">pubsubname</span><span class="pi">:</span> <span class="s">pubsub</span>
  <span class="na">topic</span><span class="pi">:</span> <span class="s">orders</span>
  <span class="na">routes</span><span class="pi">:</span>
    <span class="na">default</span><span class="pi">:</span> <span class="s">/orders/bulk</span>
  <span class="na">bulkSubscribe</span><span class="pi">:</span>
    <span class="na">enabled</span><span class="pi">:</span> <span class="no">true</span>
    <span class="na">maxMessagesCount</span><span class="pi">:</span> <span class="m">100</span>
    <span class="na">maxAwaitDurationMs</span><span class="pi">:</span> <span class="m">1000</span>
</code></pre></div></div>

<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/bulk"</span><span class="p">,</span> <span class="p">(</span><span class="n">BulkSubscribeMessage</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;</span> <span class="n">bulkMessage</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">statuses</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">BulkSubscribeAppResponseEntry</span><span class="p">&gt;();</span>

    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">entry</span> <span class="k">in</span> <span class="n">bulkMessage</span><span class="p">.</span><span class="n">Entries</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">try</span>
        <span class="p">{</span>
            <span class="nf">ProcessOrder</span><span class="p">(</span><span class="n">entry</span><span class="p">.</span><span class="n">Event</span><span class="p">);</span>
            <span class="n">statuses</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">BulkSubscribeAppResponseEntry</span><span class="p">(</span>
                <span class="n">entry</span><span class="p">.</span><span class="n">EntryId</span><span class="p">,</span> <span class="n">BulkSubscribeAppResponseStatus</span><span class="p">.</span><span class="n">SUCCESS</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="p">)</span>
        <span class="p">{</span>
            <span class="n">statuses</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="nf">BulkSubscribeAppResponseEntry</span><span class="p">(</span>
                <span class="n">entry</span><span class="p">.</span><span class="n">EntryId</span><span class="p">,</span> <span class="n">BulkSubscribeAppResponseStatus</span><span class="p">.</span><span class="n">RETRY</span><span class="p">));</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">Ok</span><span class="p">(</span><span class="k">new</span> <span class="nf">BulkSubscribeAppResponse</span><span class="p">(</span><span class="n">statuses</span><span class="p">));</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Chaque message du lot peut être acquitté ou rejeté individuellement.</p>

<h1 id="api-http-sous-jacente">API HTTP sous-jacente</h1>

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

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST http://localhost:3500/v1.0/publish/pubsub/orders
Content-Type: application/json

{
  "orderId": "order-42",
  "customerId": "cust-123",
  "totalAmount": 99.99,
  "createdAt": "2026-03-07T10:30:00Z"
}
</span></code></pre></div></div>

<h2 id="lister-les-souscriptions">Lister les souscriptions</h2>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:3500/v1.0/metadata
</span></code></pre></div></div>

<h2 id="bulk-publish-1">Bulk Publish</h2>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST http://localhost:3500/v1.0-alpha1/publish/bulk/pubsub/orders
Content-Type: application/json

{
  "entries": [
    {
      "entryId": "1",
      "event": { "orderId": "order-1", "totalAmount": 50 },
      "contentType": "application/json"
    },
    {
      "entryId": "2",
      "event": { "orderId": "order-2", "totalAmount": 75 },
      "contentType": "application/json"
    }
  ]
}
</span></code></pre></div></div>

<h1 id="exemple-complet--architecture-événementielle">Exemple complet : architecture événementielle</h1>

<p>Voici un exemple concret avec trois services communiquant par Pub/Sub :</p>

<h2 id="le-publisher--orderservice">Le publisher : OrderService</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">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="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="k">async</span> <span class="p">(</span><span class="n">CreateOrderRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</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">Id</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="nf">ToString</span><span class="p">(),</span>
        <span class="n">CustomerId</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
        <span class="n">Items</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Items</span><span class="p">,</span>
        <span class="n">TotalAmount</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="nf">Sum</span><span class="p">(</span><span class="n">i</span> <span class="p">=&gt;</span> <span class="n">i</span><span class="p">.</span><span class="n">Price</span> <span class="p">*</span> <span class="n">i</span><span class="p">.</span><span class="n">Quantity</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="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>

    <span class="c1">// Sauvegarder la commande</span>
    <span class="k">await</span> <span class="n">dapr</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-</span><span class="p">{</span><span class="n">order</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">order</span><span class="p">);</span>

    <span class="c1">// Publier l'événement</span>
    <span class="k">await</span> <span class="n">dapr</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="n">order</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
        <span class="n">CustomerId</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
        <span class="n">Items</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">Items</span><span class="p">,</span>
        <span class="n">TotalAmount</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">,</span>
        <span class="n">CreatedAt</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CreatedAt</span>
    <span class="p">});</span>

    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Created</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">Id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">order</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="le-subscriber-1--inventoryservice">Le subscriber 1 : InventoryService</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">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="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">"/order-created"</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="k">async</span> <span class="p">(</span><span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">item</span> <span class="k">in</span> <span class="n">evt</span><span class="p">.</span><span class="n">Items</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Décrémenter le stock</span>
        <span class="kt">var</span> <span class="n">stock</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(</span><span class="s">"statestore"</span><span class="p">,</span> <span class="s">$"stock-</span><span class="p">{</span><span class="n">item</span><span class="p">.</span><span class="n">ProductId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
        <span class="n">stock</span> <span class="p">-=</span> <span class="n">item</span><span class="p">.</span><span class="n">Quantity</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">stock</span> <span class="p">&lt;</span> <span class="m">0</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="c1">// Publier un événement de rupture de stock</span>
            <span class="k">await</span> <span class="n">dapr</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">"stock-alerts"</span><span class="p">,</span> <span class="k">new</span> <span class="n">StockAlert</span>
            <span class="p">{</span>
                <span class="n">ProductId</span> <span class="p">=</span> <span class="n">item</span><span class="p">.</span><span class="n">ProductId</span><span class="p">,</span>
                <span class="n">CurrentStock</span> <span class="p">=</span> <span class="n">stock</span><span class="p">,</span>
                <span class="n">OrderId</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="p">}</span>

        <span class="k">await</span> <span class="n">dapr</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">$"stock-</span><span class="p">{</span><span class="n">item</span><span class="p">.</span><span class="n">ProductId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">stock</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">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="le-subscriber-2--notificationservice">Le subscriber 2 : NotificationService</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">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="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="c1">// Notification de commande</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/order-created"</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="k">async</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">$"📧 Email envoyé au client </span><span class="p">{</span><span class="n">evt</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">}</span><span class="s"> "</span> <span class="p">+</span>
                      <span class="s">$"pour la commande </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="n">evt</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">}</span><span class="s">€)"</span><span class="p">);</span>

    <span class="k">await</span> <span class="nf">SendEmailAsync</span><span class="p">(</span><span class="n">evt</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
        <span class="s">$"Votre commande </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"> a été confirmée !"</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="c1">// Alerte de stock</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/stock-alert"</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">"stock-alerts"</span><span class="p">)]</span> <span class="k">async</span> <span class="p">(</span><span class="n">StockAlert</span> <span class="n">alert</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">$"⚠️ Alerte stock : produit </span><span class="p">{</span><span class="n">alert</span><span class="p">.</span><span class="n">ProductId</span><span class="p">}</span><span class="s">, "</span> <span class="p">+</span>
                      <span class="s">$"stock actuel : </span><span class="p">{</span><span class="n">alert</span><span class="p">.</span><span class="n">CurrentStock</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

    <span class="k">await</span> <span class="nf">NotifyAdminAsync</span><span class="p">(</span><span class="n">alert</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>

<span class="k">async</span> <span class="n">Task</span> <span class="nf">SendEmailAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">customerId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">message</span><span class="p">)</span> <span class="p">=&gt;</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="m">100</span><span class="p">);</span> <span class="c1">// Simulation</span>

<span class="k">async</span> <span class="n">Task</span> <span class="nf">NotifyAdminAsync</span><span class="p">(</span><span class="n">StockAlert</span> <span class="n">alert</span><span class="p">)</span> <span class="p">=&gt;</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="m">100</span><span class="p">);</span> <span class="c1">// Simulation</span>
</code></pre></div></div>

<h2 id="les-modèles-partagés">Les modèles partagés</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">record</span> <span class="n">OrderCreated</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">OrderId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">CustomerId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">OrderItem</span><span class="p">&gt;</span> <span class="n">Items</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="p">[];</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">TotalAmount</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">DateTime</span> <span class="n">CreatedAt</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="n">OrderItem</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">ProductId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">Price</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Quantity</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="n">StockAlert</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">ProductId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">CurrentStock</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">OrderId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

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

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal 1 : OrderService (publisher)</span>
dapr run <span class="nt">--app-id</span> order-service <span class="nt">--app-port</span> 5000 <span class="se">\</span>
    <span class="nt">--resources-path</span> ./components <span class="nt">--</span> dotnet run <span class="nt">--project</span> OrderService

<span class="c"># Terminal 2 : InventoryService (subscriber)</span>
dapr run <span class="nt">--app-id</span> inventory-service <span class="nt">--app-port</span> 5001 <span class="se">\</span>
    <span class="nt">--resources-path</span> ./components <span class="nt">--</span> dotnet run <span class="nt">--project</span> InventoryService

<span class="c"># Terminal 3 : NotificationService (subscriber)</span>
dapr run <span class="nt">--app-id</span> notification-service <span class="nt">--app-port</span> 5002 <span class="se">\</span>
    <span class="nt">--resources-path</span> ./components <span class="nt">--</span> dotnet run <span class="nt">--project</span> NotificationService
</code></pre></div></div>

<h2 id="tester-la-publication">Tester la publication</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl <span class="nt">-X</span> POST http://localhost:5000/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">'{
    "customerId": "cust-123",
    "items": [
      { "productId": 1, "name": "Clavier", "price": 49.99, "quantity": 1 },
      { "productId": 2, "name": "Souris", "price": 29.99, "quantity": 2 }
    ]
  }'</span>
</code></pre></div></div>

<p>Les deux subscribers reçoivent automatiquement l’événement <code class="language-plaintext highlighter-rouge">OrderCreated</code> publié par <code class="language-plaintext highlighter-rouge">OrderService</code>.</p>

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

<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">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="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="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">pubSub</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="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">InventoryService</span><span class="p">&gt;(</span><span class="s">"inventory-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">pubSub</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="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">NotificationService</span><span class="p">&gt;(</span><span class="s">"notification-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">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>

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

<table>
  <thead>
    <tr>
      <th>Pratique</th>
      <th>Pourquoi</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Handlers idempotents</strong></td>
      <td>Le mode at-least-once peut livrer un message plusieurs fois</td>
    </tr>
    <tr>
      <td><strong>Événements fins et spécifiques</strong></td>
      <td>Préférer <code class="language-plaintext highlighter-rouge">OrderCreated</code>, <code class="language-plaintext highlighter-rouge">OrderShipped</code> à un générique <code class="language-plaintext highlighter-rouge">OrderUpdated</code></td>
    </tr>
    <tr>
      <td><strong>Schéma versionné</strong></td>
      <td>Ajouter un champ <code class="language-plaintext highlighter-rouge">version</code> ou <code class="language-plaintext highlighter-rouge">type</code> dans les événements pour gérer l’évolution</td>
    </tr>
    <tr>
      <td><strong>Dead letter topics</strong></td>
      <td>Toujours configurer un dead letter topic pour éviter de perdre des messages</td>
    </tr>
    <tr>
      <td><strong>Scoping</strong></td>
      <td>Restreindre l’accès aux topics aux seuls services concernés</td>
    </tr>
    <tr>
      <td><strong>TTL sur les messages</strong></td>
      <td>Éviter l’accumulation de messages obsolètes dans le broker</td>
    </tr>
    <tr>
      <td><strong>Monitoring</strong></td>
      <td>Surveiller les métriques Dapr (messages publiés, acquittés, rejetés) via les dashboards</td>
    </tr>
  </tbody>
</table>

<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 http://localhost:3500/v1.0/publish/{pubsub-name}/{topic}</code></td>
    </tr>
    <tr>
      <td><strong>Format</strong></td>
      <td>CloudEvents (standard CNCF) ou raw payload</td>
    </tr>
    <tr>
      <td><strong>Livraison</strong></td>
      <td>At-least-once par défaut</td>
    </tr>
    <tr>
      <td><strong>Souscription</strong></td>
      <td>Attribut <code class="language-plaintext highlighter-rouge">[Topic]</code> en code, ou fichier YAML externe</td>
    </tr>
    <tr>
      <td><strong>Routing</strong></td>
      <td>Règles de routage par contenu de l’événement</td>
    </tr>
    <tr>
      <td><strong>Dead letters</strong></td>
      <td>Topic de messages en échec, configurable</td>
    </tr>
    <tr>
      <td><strong>Bulk</strong></td>
      <td>Publication et souscription par lots pour le haut débit</td>
    </tr>
    <tr>
      <td><strong>Résilience</strong></td>
      <td>Retry, circuit breaker configurables en YAML</td>
    </tr>
    <tr>
      <td><strong>Scoping</strong></td>
      <td>Restriction d’accès par service</td>
    </tr>
    <tr>
      <td><strong>Brokers</strong></td>
      <td>Redis, RabbitMQ, Kafka, Azure Service Bus, Pulsar…</td>
    </tr>
    <tr>
      <td><strong>SDK .NET</strong></td>
      <td><code class="language-plaintext highlighter-rouge">DaprClient.PublishEventAsync</code>, attribut <code class="language-plaintext highlighter-rouge">[Topic]</code>, <code class="language-plaintext highlighter-rouge">UseCloudEvents()</code></td>
    </tr>
  </tbody>
</table>

<p>Le Pub/Sub Dapr offre une abstraction puissante et portable de la communication asynchrone entre services, avec des garanties de livraison, du routage conditionnel et une gestion des erreurs intégrée, tout en permettant de changer de broker sans modifier une seule ligne de code applicatif.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="dapr" /><category term="microservices" /><category term="pub-sub" /><category term="messaging" /><summary type="html"><![CDATA[Le Pub/Sub (Publish &amp; Subscribe) est l’un des building blocks les plus utilisés de Dapr. Il permet à des services de communiquer de manière asynchrone via des événements, sans couplage direct entre l’émetteur et le(s) récepteur(s). En .NET, le SDK Dapr et ASP.NET Core rendent la publication et la souscription d’événements simples et déclaratives, quel que soit le broker sous-jacent (RabbitMQ, Kafka, Azure Service Bus, Redis Streams…).]]></summary></entry><entry><title type="html">Bonnes pratiques pour développer une API .NET</title><link href="http://guym.fr/2026/03/07/api-bonnes-pratiques.html" rel="alternate" type="text/html" title="Bonnes pratiques pour développer une API .NET" /><published>2026-03-07T00:00:00+01:00</published><updated>2026-03-07T00:00:00+01:00</updated><id>http://guym.fr/2026/03/07/api-bonnes-pratiques</id><content type="html" xml:base="http://guym.fr/2026/03/07/api-bonnes-pratiques.html"><![CDATA[<p>Développer une API .NET robuste, maintenable et performante requiert bien plus qu’une simple mise en place d’un projet ASP.NET Core. Cet article rassemble les pratiques essentielles, de la conception à la mise en production.</p>

<!--more-->

<h2 id="1-concevoir-une-api-restful-cohérente">1. Concevoir une API RESTful cohérente</h2>

<h3 id="nommage-des-routes">Nommage des routes</h3>

<p>Les routes doivent décrire des <strong>ressources</strong> (noms, jamais des verbes), en minuscules et avec des tirets :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>GET    /api/orders           → liste des commandes
GET    /api/orders/{id}      → une commande
POST   /api/orders           → créer une commande
PUT    /api/orders/{id}      → remplacer une commande
PATCH  /api/orders/{id}      → modifier partiellement
DELETE /api/orders/{id}      → supprimer une commande
</code></pre></div></div>

<p>Évitez les routes du type <code class="language-plaintext highlighter-rouge">/api/getOrder</code> ou <code class="language-plaintext highlighter-rouge">/api/createOrder</code>.</p>

<h3 id="codes-http-corrects">Codes HTTP corrects</h3>

<table>
  <thead>
    <tr>
      <th>Situation</th>
      <th>Code</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Ressource retournée</td>
      <td><code class="language-plaintext highlighter-rouge">200 OK</code></td>
    </tr>
    <tr>
      <td>Ressource créée</td>
      <td><code class="language-plaintext highlighter-rouge">201 Created</code> + header <code class="language-plaintext highlighter-rouge">Location</code></td>
    </tr>
    <tr>
      <td>Traitement sans contenu</td>
      <td><code class="language-plaintext highlighter-rouge">204 No Content</code></td>
    </tr>
    <tr>
      <td>Données invalides</td>
      <td><code class="language-plaintext highlighter-rouge">400 Bad Request</code></td>
    </tr>
    <tr>
      <td>Non authentifié</td>
      <td><code class="language-plaintext highlighter-rouge">401 Unauthorized</code></td>
    </tr>
    <tr>
      <td>Accès interdit</td>
      <td><code class="language-plaintext highlighter-rouge">403 Forbidden</code></td>
    </tr>
    <tr>
      <td>Introuvable</td>
      <td><code class="language-plaintext highlighter-rouge">404 Not Found</code></td>
    </tr>
    <tr>
      <td>Erreur serveur</td>
      <td><code class="language-plaintext highlighter-rouge">500 Internal Server Error</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="2-versionner-son-api">2. Versionner son API</h2>

<p>Le versioning permet de faire évoluer son API sans casser les clients existants.</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">AddApiVersioning</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">DefaultApiVersion</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ApiVersion</span><span class="p">(</span><span class="m">1</span><span class="p">,</span> <span class="m">0</span><span class="p">);</span>
    <span class="n">options</span><span class="p">.</span><span class="n">AssumeDefaultVersionWhenUnspecified</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    <span class="n">options</span><span class="p">.</span><span class="n">ReportApiVersions</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>

<p>Plusieurs stratégies existent : par URL (<code class="language-plaintext highlighter-rouge">/api/v1/orders</code>), par header (<code class="language-plaintext highlighter-rouge">api-version: 1.0</code>) ou par query string (<code class="language-plaintext highlighter-rouge">?api-version=1.0</code>). La stratégie par URL est la plus lisible et la plus répandue.</p>

<hr />

<h2 id="3-séparer-les-modèles-internes-des-contrats-api">3. Séparer les modèles internes des contrats API</h2>

<p>Ne jamais exposer directement les entités de domaine ou de base de données. Utiliser des <strong>DTO (Data Transfer Objects)</strong> dédiés :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Entité interne — ne pas exposer</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">Order</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Guid</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">Guid</span> <span class="n">CustomerId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">OrderLine</span><span class="p">&gt;</span> <span class="n">Lines</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="p">[];</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">Total</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">OrderStatus</span> <span class="n">Status</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">DateTime</span> <span class="n">CreatedAt</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// DTO de réponse</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">OrderResponse</span><span class="p">(</span>
    <span class="n">Guid</span> <span class="n">Id</span><span class="p">,</span>
    <span class="kt">decimal</span> <span class="n">Total</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Status</span><span class="p">,</span>
    <span class="n">DateTime</span> <span class="n">CreatedAt</span>
<span class="p">);</span>

<span class="c1">// DTO de création</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">CreateOrderRequest</span><span class="p">(</span>
    <span class="n">Guid</span> <span class="n">CustomerId</span><span class="p">,</span>
    <span class="n">List</span><span class="p">&lt;</span><span class="n">OrderLineRequest</span><span class="p">&gt;</span> <span class="n">Lines</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Le mapping peut se faire manuellement ou via <strong>Mapperly</strong> (générateur de source, sans réflexion) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Mapper</span><span class="p">]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">OrderMapper</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">partial</span> <span class="n">OrderResponse</span> <span class="nf">ToResponse</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="4-valider-les-entrées">4. Valider les entrées</h2>

<h3 id="dataannotations-simple">DataAnnotations (simple)</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">record</span> <span class="nf">CreateOrderRequest</span><span class="p">(</span>
    <span class="p">[</span><span class="n">Required</span><span class="p">]</span> <span class="n">Guid</span> <span class="n">CustomerId</span><span class="p">,</span>
    <span class="p">[</span><span class="nf">MinLength</span><span class="p">(</span><span class="m">1</span><span class="p">)]</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">OrderLineRequest</span><span class="p">&gt;</span> <span class="n">Lines</span>
<span class="p">);</span>
</code></pre></div></div>

<p>Activer la validation automatique avec <code class="language-plaintext highlighter-rouge">[ApiController]</code> sur le contrôleur.</p>

<h3 id="fluentvalidation-recommandé-pour-les-cas-complexes">FluentValidation (recommandé pour les cas complexes)</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">CreateOrderRequestValidator</span> <span class="p">:</span> <span class="n">AbstractValidator</span><span class="p">&lt;</span><span class="n">CreateOrderRequest</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="nf">CreateOrderRequestValidator</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nf">RuleFor</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="n">CustomerId</span><span class="p">).</span><span class="nf">NotEmpty</span><span class="p">();</span>
        <span class="nf">RuleFor</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="n">Lines</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">NotEmpty</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">WithMessage</span><span class="p">(</span><span class="s">"Une commande doit contenir au moins une ligne."</span><span class="p">);</span>
        <span class="nf">RuleForEach</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="n">Lines</span><span class="p">).</span><span class="nf">SetValidator</span><span class="p">(</span><span class="k">new</span> <span class="nf">OrderLineRequestValidator</span><span class="p">());</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="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddValidatorsFromAssemblyContaining</span><span class="p">&lt;</span><span class="n">CreateOrderRequestValidator</span><span class="p">&gt;();</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddFluentValidationAutoValidation</span><span class="p">();</span>
</code></pre></div></div>

<hr />

<h2 id="5-gérer-les-erreurs-de-manière-uniforme">5. Gérer les erreurs de manière uniforme</h2>

<h3 id="problemdetails-rfc-9457">ProblemDetails (RFC 9457)</h3>

<p>ASP.NET Core expose nativement le format <code class="language-plaintext highlighter-rouge">ProblemDetails</code>, qui est la norme pour les réponses d’erreur :</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">"type"</span><span class="p">:</span><span class="w"> </span><span class="s2">"https://tools.ietf.org/html/rfc9110#section-15.5.5"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"title"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Not Found"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="mi">404</span><span class="p">,</span><span class="w">
  </span><span class="nl">"detail"</span><span class="p">:</span><span class="w"> </span><span class="s2">"La commande 'abc-123' est introuvable."</span><span class="p">,</span><span class="w">
  </span><span class="nl">"instance"</span><span class="p">:</span><span class="w"> </span><span class="s2">"/api/orders/abc-123"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>Activez le retour automatique de <code class="language-plaintext highlighter-rouge">ProblemDetails</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">AddProblemDetails</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="middleware-de-gestion-des-exceptions">Middleware de gestion des exceptions</h3>

<p>Centralisez la gestion des exceptions non attrapées pour éviter de laisser fuir des stacktraces en production :</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">UseExceptionHandler</span><span class="p">(</span><span class="n">exceptionHandlerApp</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">exceptionHandlerApp</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="k">async</span> <span class="n">context</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">exceptionHandlerFeature</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Features</span><span class="p">.</span><span class="n">Get</span><span class="p">&lt;</span><span class="n">IExceptionHandlerFeature</span><span class="p">&gt;();</span>
        <span class="kt">var</span> <span class="n">exception</span> <span class="p">=</span> <span class="n">exceptionHandlerFeature</span><span class="p">?.</span><span class="n">Error</span><span class="p">;</span>

        <span class="n">context</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="n">exception</span> <span class="k">switch</span>
        <span class="p">{</span>
            <span class="n">NotFoundException</span>  <span class="p">=&gt;</span> <span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status404NotFound</span><span class="p">,</span>
            <span class="n">ValidationException</span> <span class="p">=&gt;</span> <span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status400BadRequest</span><span class="p">,</span>
            <span class="n">UnauthorizedException</span> <span class="p">=&gt;</span> <span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status403Forbidden</span><span class="p">,</span>
            <span class="n">_</span> <span class="p">=&gt;</span> <span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status500InternalServerError</span>
        <span class="p">};</span>

        <span class="k">await</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Problem</span><span class="p">(</span>
            <span class="n">detail</span><span class="p">:</span> <span class="n">exception</span><span class="p">?.</span><span class="n">Message</span><span class="p">,</span>
            <span class="n">statusCode</span><span class="p">:</span> <span class="n">context</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="nf">ExecuteAsync</span><span class="p">(</span><span class="n">context</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">});</span>
</code></pre></div></div>

<hr />

<h2 id="6-authentification-et-autorisation">6. Authentification et autorisation</h2>

<h3 id="jwt">JWT</h3>

<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">AddAuthentication</span><span class="p">(</span><span class="n">JwtBearerDefaults</span><span class="p">.</span><span class="n">AuthenticationScheme</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddJwtBearer</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">TokenValidationParameters</span> <span class="p">=</span> <span class="k">new</span> <span class="n">TokenValidationParameters</span>
        <span class="p">{</span>
            <span class="n">ValidateIssuer</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">ValidateAudience</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">ValidateLifetime</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">ValidateIssuerSigningKey</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">ValidIssuer</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">[</span><span class="s">"Jwt:Issuer"</span><span class="p">],</span>
            <span class="n">ValidAudience</span> <span class="p">=</span> <span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">[</span><span class="s">"Jwt:Audience"</span><span class="p">],</span>
            <span class="n">IssuerSigningKey</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SymmetricSecurityKey</span><span class="p">(</span>
                <span class="n">Encoding</span><span class="p">.</span><span class="n">UTF8</span><span class="p">.</span><span class="nf">GetBytes</span><span class="p">(</span><span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">[</span><span class="s">"Jwt:Key"</span><span class="p">]!))</span>
        <span class="p">};</span>
    <span class="p">});</span>
</code></pre></div></div>

<h3 id="politiques-dautorisation">Politiques d’autorisation</h3>

<p>Préférez les <strong>politiques</strong> aux rôles bruts :</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">AddAuthorization</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="nf">AddPolicy</span><span class="p">(</span><span class="s">"AdminOnly"</span><span class="p">,</span> <span class="n">policy</span> <span class="p">=&gt;</span> <span class="n">policy</span><span class="p">.</span><span class="nf">RequireRole</span><span class="p">(</span><span class="s">"admin"</span><span class="p">));</span>
    <span class="n">options</span><span class="p">.</span><span class="nf">AddPolicy</span><span class="p">(</span><span class="s">"OwnerOrAdmin"</span><span class="p">,</span> <span class="n">policy</span> <span class="p">=&gt;</span>
        <span class="n">policy</span><span class="p">.</span><span class="nf">RequireAssertion</span><span class="p">(</span><span class="n">ctx</span> <span class="p">=&gt;</span>
            <span class="n">ctx</span><span class="p">.</span><span class="n">User</span><span class="p">.</span><span class="nf">IsInRole</span><span class="p">(</span><span class="s">"admin"</span><span class="p">)</span> <span class="p">||</span> <span class="n">ctx</span><span class="p">.</span><span class="n">User</span><span class="p">.</span><span class="nf">HasClaim</span><span class="p">(</span><span class="s">"owner"</span><span class="p">,</span> <span class="s">"true"</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="p">[</span><span class="nf">Authorize</span><span class="p">(</span><span class="n">Policy</span> <span class="p">=</span> <span class="s">"AdminOnly"</span><span class="p">)]</span>
<span class="p">[</span><span class="nf">HttpDelete</span><span class="p">(</span><span class="s">"{id}"</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">IActionResult</span><span class="p">&gt;</span> <span class="nf">Delete</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">)</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="7-limiter-le-débit-rate-limiting">7. Limiter le débit (Rate Limiting)</h2>

<p>Depuis .NET 7, le rate limiting est intégré :</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">AddRateLimiter</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="nf">AddFixedWindowLimiter</span><span class="p">(</span><span class="s">"fixed"</span><span class="p">,</span> <span class="n">limiterOptions</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">limiterOptions</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="n">limiterOptions</span><span class="p">.</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="n">limiterOptions</span><span class="p">.</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">limiterOptions</span><span class="p">.</span><span class="n">QueueLimit</span> <span class="p">=</span> <span class="m">10</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="n">options</span><span class="p">.</span><span class="n">RejectionStatusCode</span> <span class="p">=</span> <span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status429TooManyRequests</span><span class="p">;</span>
<span class="p">});</span>

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

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">EnableRateLimiting</span><span class="p">(</span><span class="s">"fixed"</span><span class="p">)]</span>
<span class="p">[</span><span class="n">HttpGet</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">IActionResult</span><span class="p">&gt;</span> <span class="nf">GetOrders</span><span class="p">()</span> <span class="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="8-logging-et-observabilité">8. Logging et observabilité</h2>

<h3 id="logging-structuré">Logging structuré</h3>

<p>Utilisez <code class="language-plaintext highlighter-rouge">ILogger&lt;T&gt;</code> avec des propriétés nommées plutôt que de l’interpolation de chaîne :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// À éviter</span>
<span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">$"Commande </span><span class="p">{</span><span class="n">orderId</span><span class="p">}</span><span class="s"> créée par </span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="c1">// Correct — permet l'indexation dans les outils de log (Seq, Loki, etc.)</span>
<span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Commande {OrderId} créée par {UserId}"</span><span class="p">,</span> <span class="n">orderId</span><span class="p">,</span> <span class="n">userId</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="opentelemetry">OpenTelemetry</h3>

<p>Intégrez OpenTelemetry pour les traces distribuées, métriques et logs :</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">AddOpenTelemetry</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithTracing</span><span class="p">(</span><span class="n">tracing</span> <span class="p">=&gt;</span> <span class="n">tracing</span>
        <span class="p">.</span><span class="nf">AddAspNetCoreInstrumentation</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">AddHttpClientInstrumentation</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">AddOtlpExporter</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="n">metrics</span>
        <span class="p">.</span><span class="nf">AddAspNetCoreInstrumentation</span><span class="p">()</span>
        <span class="p">.</span><span class="nf">AddOtlpExporter</span><span class="p">());</span>
</code></pre></div></div>

<hr />

<h2 id="9-asynchronisme-systématique">9. Asynchronisme systématique</h2>

<p>Chaque opération I/O (base de données, appels HTTP, fichiers) <strong>doit</strong> être asynchrone :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// À éviter</span>
<span class="k">public</span> <span class="n">IActionResult</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="n">_repository</span><span class="p">.</span><span class="nf">GetById</span><span class="p">(</span><span class="n">id</span><span class="p">);</span> <span class="c1">// bloque un thread</span>
    <span class="k">return</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Correct</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">GetOrder</span><span class="p">(</span><span class="n">Guid</span> <span class="n">id</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="p">=</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="k">return</span> <span class="n">order</span> <span class="k">is</span> <span class="k">null</span> <span class="p">?</span> <span class="nf">NotFound</span><span class="p">()</span> <span class="p">:</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Passez également les <code class="language-plaintext highlighter-rouge">CancellationToken</code> du contrôleur jusqu’aux appels les plus profonds :</p>

<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">IActionResult</span><span class="p">&gt;</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Guid</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="p">{</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="p">=</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="n">ct</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">order</span> <span class="k">is</span> <span class="k">null</span> <span class="p">?</span> <span class="nf">NotFound</span><span class="p">()</span> <span class="p">:</span> <span class="nf">Ok</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="10-caching">10. Caching</h2>

<h3 id="cache-en-mémoire">Cache en mémoire</h3>

<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">AddMemoryCache</span><span class="p">();</span>

<span class="c1">// Dans le service</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">OrderResponse</span><span class="p">?&gt;</span> <span class="nf">GetOrderAsync</span><span class="p">(</span><span class="n">Guid</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="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">_cache</span><span class="p">.</span><span class="nf">GetOrCreateAsync</span><span class="p">(</span><span class="s">$"order:</span><span class="p">{</span><span class="n">id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="k">async</span> <span class="n">entry</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">entry</span><span class="p">.</span><span class="n">AbsoluteExpirationRelativeToNow</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">5</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">order</span> <span class="p">=</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="n">ct</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">order</span> <span class="k">is</span> <span class="k">null</span> <span class="p">?</span> <span class="k">null</span> <span class="p">:</span> <span class="n">_mapper</span><span class="p">.</span><span class="nf">ToResponse</span><span class="p">(</span><span class="n">order</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="cache-de-réponse-http">Cache de réponse HTTP</h3>

<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">AddOutputCache</span><span class="p">();</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseOutputCache</span><span class="p">();</span>

<span class="p">[</span><span class="nf">OutputCache</span><span class="p">(</span><span class="n">Duration</span> <span class="p">=</span> <span class="m">60</span><span class="p">)]</span>
<span class="p">[</span><span class="nf">HttpGet</span><span class="p">(</span><span class="s">"{id}"</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">IActionResult</span><span class="p">&gt;</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Guid</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="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="11-documentation-avec-openapi">11. Documentation avec OpenAPI</h2>

<p>Depuis .NET 9, le support natif d’OpenAPI est amélioré :</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">AddOpenApi</span><span class="p">();</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapOpenApi</span><span class="p">();</span> <span class="c1">// expose /openapi/v1.json</span>
</code></pre></div></div>

<p>Enrichissez la documentation avec des attributs :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">/// &lt;summary&gt;Récupère une commande par son identifiant.&lt;/summary&gt;</span>
<span class="c1">/// &lt;param name="id"&gt;L'identifiant de la commande.&lt;/param&gt;</span>
<span class="c1">/// &lt;response code="200"&gt;La commande demandée.&lt;/response&gt;</span>
<span class="c1">/// &lt;response code="404"&gt;Commande introuvable.&lt;/response&gt;</span>
<span class="p">[</span><span class="nf">HttpGet</span><span class="p">(</span><span class="s">"{id}"</span><span class="p">)]</span>
<span class="p">[</span><span class="n">ProducesResponseType</span><span class="p">&lt;</span><span class="n">OrderResponse</span><span class="p">&gt;(</span><span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status200OK</span><span class="p">)]</span>
<span class="p">[</span><span class="n">ProducesResponseType</span><span class="p">&lt;</span><span class="n">ProblemDetails</span><span class="p">&gt;(</span><span class="n">StatusCodes</span><span class="p">.</span><span class="n">Status404NotFound</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">IActionResult</span><span class="p">&gt;</span> <span class="nf">GetOrder</span><span class="p">(</span><span class="n">Guid</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="p">{</span> <span class="p">...</span> <span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="12-tests">12. Tests</h2>

<h3 id="tests-unitaires">Tests unitaires</h3>

<p>Testez les services et la logique métier en isolation avec <strong>xUnit</strong> et <strong>NSubstitute</strong> :</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">OrderServiceTests</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IOrderRepository</span> <span class="n">_repository</span> <span class="p">=</span> <span class="n">Substitute</span><span class="p">.</span><span class="n">For</span><span class="p">&lt;</span><span class="n">IOrderRepository</span><span class="p">&gt;();</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">OrderService</span> <span class="n">_sut</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderServiceTests</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="n">_sut</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderService</span><span class="p">(</span><span class="n">_repository</span><span class="p">);</span>

    <span class="p">[</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">GetOrder_ReturnsNull_WhenNotFound</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">_repository</span><span class="p">.</span><span class="nf">GetByIdAsync</span><span class="p">(</span><span class="n">Arg</span><span class="p">.</span><span class="n">Any</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">&gt;(),</span> <span class="n">Arg</span><span class="p">.</span><span class="n">Any</span><span class="p">&lt;</span><span class="n">CancellationToken</span><span class="p">&gt;())</span>
                   <span class="p">.</span><span class="nf">Returns</span><span class="p">((</span><span class="n">Order</span><span class="p">?)</span><span class="k">null</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">_sut</span><span class="p">.</span><span class="nf">GetOrderAsync</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="k">default</span><span class="p">);</span>

        <span class="n">result</span><span class="p">.</span><span class="nf">Should</span><span class="p">().</span><span class="nf">BeNull</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="tests-dintégration">Tests d’intégration</h3>

<p>Utilisez <code class="language-plaintext highlighter-rouge">WebApplicationFactory&lt;TProgram&gt;</code> pour tester les endpoints de bout en bout :</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">OrdersControllerTests</span> <span class="p">:</span> <span class="n">IClassFixture</span><span class="p">&lt;</span><span class="n">WebApplicationFactory</span><span class="p">&lt;</span><span class="n">Program</span><span class="p">&gt;&gt;</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">HttpClient</span> <span class="n">_client</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrdersControllerTests</span><span class="p">(</span><span class="n">WebApplicationFactory</span><span class="p">&lt;</span><span class="n">Program</span><span class="p">&gt;</span> <span class="n">factory</span><span class="p">)</span>
        <span class="p">=&gt;</span> <span class="n">_client</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="p">[</span><span class="n">Fact</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">GetOrder_Returns404_WhenNotFound</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">_client</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">$"/api/orders/</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="s">"</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="nf">Should</span><span class="p">().</span><span class="nf">Be</span><span class="p">(</span><span class="n">HttpStatusCode</span><span class="p">.</span><span class="n">NotFound</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="récapitulatif">Récapitulatif</h2>

<table>
  <thead>
    <tr>
      <th>Pratique</th>
      <th>Outil / Approche</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Routes RESTful</td>
      <td>Conventions de nommage + verbes HTTP</td>
    </tr>
    <tr>
      <td>Versioning</td>
      <td><code class="language-plaintext highlighter-rouge">Asp.Versioning.Http</code></td>
    </tr>
    <tr>
      <td>Modèles d’échange</td>
      <td>DTO + Mapperly</td>
    </tr>
    <tr>
      <td>Validation</td>
      <td>FluentValidation</td>
    </tr>
    <tr>
      <td>Erreurs uniformes</td>
      <td><code class="language-plaintext highlighter-rouge">ProblemDetails</code> + middleware</td>
    </tr>
    <tr>
      <td>Authentification</td>
      <td>JWT Bearer</td>
    </tr>
    <tr>
      <td>Rate limiting</td>
      <td><code class="language-plaintext highlighter-rouge">RateLimiter</code> intégré (.NET 7+)</td>
    </tr>
    <tr>
      <td>Observabilité</td>
      <td>OpenTelemetry + logging structuré</td>
    </tr>
    <tr>
      <td>Performance</td>
      <td><code class="language-plaintext highlighter-rouge">async</code>/<code class="language-plaintext highlighter-rouge">await</code> + <code class="language-plaintext highlighter-rouge">CancellationToken</code></td>
    </tr>
    <tr>
      <td>Caching</td>
      <td><code class="language-plaintext highlighter-rouge">IMemoryCache</code> ou <code class="language-plaintext highlighter-rouge">OutputCache</code></td>
    </tr>
    <tr>
      <td>Documentation</td>
      <td>OpenAPI natif (.NET 9+)</td>
    </tr>
    <tr>
      <td>Tests</td>
      <td>xUnit + NSubstitute + <code class="language-plaintext highlighter-rouge">WebApplicationFactory</code></td>
    </tr>
  </tbody>
</table>

<p>Une API bien conçue ne s’arrête pas à faire fonctionner des endpoints : elle doit être <strong>prévisible</strong>, <strong>sécurisée</strong>, <strong>observable</strong> et <strong>testable</strong> dès le départ.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="api" /><category term="rest" /><category term="architecture" /><category term="bonnes-pratiques" /><summary type="html"><![CDATA[Développer une API .NET robuste, maintenable et performante requiert bien plus qu’une simple mise en place d’un projet ASP.NET Core. Cet article rassemble les pratiques essentielles, de la conception à la mise en production.]]></summary></entry><entry><title type="html">Dapr : Pub/Sub gRPC en .NET</title><link href="http://guym.fr/2026/03/03/dapr-pub-sub-grpc.html" rel="alternate" type="text/html" title="Dapr : Pub/Sub gRPC en .NET" /><published>2026-03-03T00:00:00+01:00</published><updated>2026-03-03T00:00:00+01:00</updated><id>http://guym.fr/2026/03/03/dapr-pub-sub-grpc</id><content type="html" xml:base="http://guym.fr/2026/03/03/dapr-pub-sub-grpc.html"><![CDATA[<p>Dans l’article précédent, nous avons vu le Pub/Sub Dapr avec des souscriptions HTTP (Minimal API, contrôleurs). Mais Dapr supporte aussi la communication <strong>gRPC</strong> entre le sidecar et l’application, y compris pour la livraison des messages Pub/Sub. gRPC apporte la sérialisation binaire (Protobuf), des contrats typés et de meilleures performances, ce qui est particulièrement intéressant pour les systèmes à haut débit. Dans cet article, on met en place un système Pub/Sub complet en gRPC.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Dapr pour les développeurs .NET</strong> : <strong> 5</strong> sur <strong>6</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 - 
		
			<a href="/2026/02/22/dapr-service-invocation-2-grpc.html">Dapr : invocation de service gRPC en .NET</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2026/03/01/dapr-etats.html">Dapr : la gestion d'état (State Management) en .NET</a>
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			Cet article
		
		</li>
	
	
	
	
	
		
		<li>Part 6 - 
		
			<a href="/2026/03/08/dapr-pub-sub.html">Dapr : le Pub/Sub (Publish &amp; Subscribe) en .NET</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	</ul>
</div>

<h1 id="rappel--http-vs-grpc-pour-le-pubsub">Rappel : HTTP vs gRPC pour le Pub/Sub</h1>

<p>Avec le Pub/Sub HTTP (article précédent), le sidecar Dapr appelle un endpoint HTTP de votre application pour lui livrer les messages :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sidecar → POST http://localhost:&lt;app-port&gt;/order-created → Application
</code></pre></div></div>

<p>Avec le <strong>Pub/Sub gRPC</strong>, le sidecar communique avec votre application via un <strong>service gRPC Dapr</strong> que votre application implémente. Le sidecar appelle la méthode <code class="language-plaintext highlighter-rouge">OnTopicEvent</code> de ce service pour livrer chaque message :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Sidecar → gRPC OnTopicEvent → Application
</code></pre></div></div>

<pre><code class="language-mermaid">graph LR
    Broker["Broker&lt;br/&gt;(RabbitMQ, Kafka...)"]
    Sidecar["Sidecar Dapr"]
    App["Application .NET&lt;br/&gt;(gRPC server)"]
    
    Broker --&gt;|Distribue| Sidecar
    Sidecar --&gt;|gRPC&lt;br/&gt;OnTopicEvent| App
    
    style Broker fill:#D0021B
    style Sidecar fill:#F5A623
    style App fill:#4A90E2
</code></pre>

<p>Les deux protocoles sont <strong>interchangeables</strong> : le publisher peut publier en HTTP et le subscriber recevoir en gRPC (ou inversement). Le broker et l’API de publication restent identiques. Seule la <strong>livraison au subscriber</strong> change.</p>

<h1 id="quand-utiliser-grpc-plutôt-que-http-">Quand utiliser gRPC plutôt que HTTP ?</h1>

<table>
  <thead>
    <tr>
      <th>Critère</th>
      <th>HTTP</th>
      <th>gRPC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Simplicité</strong></td>
      <td>Plus simple (Minimal API, <code class="language-plaintext highlighter-rouge">[Topic]</code>)</td>
      <td>Plus de code (Protobuf, service gRPC)</td>
    </tr>
    <tr>
      <td><strong>Performance</strong></td>
      <td>Bon (JSON, HTTP/1.1)</td>
      <td>Meilleur (binaire, HTTP/2)</td>
    </tr>
    <tr>
      <td><strong>Débit élevé</strong></td>
      <td>Suffisant pour la plupart des cas</td>
      <td>Préférable pour les volumes importants</td>
    </tr>
    <tr>
      <td><strong>Contrat</strong></td>
      <td>Implicite (sérialisation JSON)</td>
      <td>Explicite (fichier <code class="language-plaintext highlighter-rouge">.proto</code>)</td>
    </tr>
    <tr>
      <td><strong>Streaming</strong></td>
      <td>Non supporté</td>
      <td>Possible (server streaming)</td>
    </tr>
    <tr>
      <td><strong>Cas d’usage</strong></td>
      <td>La majorité des applications</td>
      <td>Systèmes à haut débit, événements volumineux</td>
    </tr>
  </tbody>
</table>

<p>En pratique, le mode HTTP convient à la majorité des scénarios. L’utilisation de gRPC se justifie principalement quand :</p>

<ul>
  <li>Vous traitez un <strong>volume élevé de messages</strong> et la performance de sérialisation/désérialisation est critique.</li>
  <li>Vous utilisez déjà des services <strong>gRPC</strong> et souhaitez unifier le protocole.</li>
  <li>Vous avez besoin de <strong>contrats Protobuf</strong> partagés pour vos événements.</li>
</ul>

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

<p>On va construire un système Pub/Sub gRPC complet :</p>

<ol>
  <li><strong>OrderService</strong> : publie des événements <code class="language-plaintext highlighter-rouge">OrderCreated</code> (en HTTP ou gRPC — la publication ne change pas).</li>
  <li><strong>PaymentService</strong> : subscriber gRPC qui reçoit les événements <code class="language-plaintext highlighter-rouge">OrderCreated</code> et simule un traitement de paiement.</li>
</ol>

<pre><code class="language-mermaid">graph LR
    OS["OrderService&lt;br/&gt;(Publisher)"]
    OSS["Sidecar Order"]
    Broker["Broker&lt;br/&gt;(Redis)"]
    PSS["Sidecar Payment"]
    PS["PaymentService&lt;br/&gt;(Subscriber gRPC)"]
    
    OS --&gt;|PublishEvent&lt;br/&gt;localhost| OSS
    OSS --&gt;|Publie| Broker
    Broker --&gt;|Distribue| PSS
    PSS --&gt;|gRPC&lt;br/&gt;OnTopicEvent| PS
    
    style OS fill:#4A90E2
    style OSS fill:#F5A623
    style Broker fill:#D0021B
    style PSS fill:#F5A623
    style PS fill:#7ED321
</code></pre>

<h1 id="étape-1--le-publisher-identique-à-http">Étape 1 : Le publisher (identique à HTTP)</h1>

<p>La publication d’événements ne change pas, qu’on utilise HTTP ou gRPC côté subscriber. Le publisher utilise toujours <code class="language-plaintext highlighter-rouge">DaprClient.PublishEventAsync</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">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="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="k">async</span> <span class="p">(</span><span class="n">CreateOrderRequest</span> <span class="n">request</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</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">Id</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="nf">ToString</span><span class="p">(),</span>
        <span class="n">CustomerId</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
        <span class="n">TotalAmount</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">TotalAmount</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="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>

    <span class="k">await</span> <span class="n">dapr</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="n">order</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
        <span class="n">CustomerId</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CustomerId</span><span class="p">,</span>
        <span class="n">TotalAmount</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">,</span>
        <span class="n">CreatedAt</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CreatedAt</span>
    <span class="p">});</span>

    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Created</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">Id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">order</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>

<h1 id="étape-2--le-subscriber-grpc">Étape 2 : Le subscriber gRPC</h1>

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

<pre><code class="language-dotnetcli">dotnet new grpc -n PaymentService
cd PaymentService
dotnet add package Dapr.AspNetCore
dotnet add package Dapr.Client
</code></pre>

<h2 id="comprendre-le-contrat-dapr-appcallback">Comprendre le contrat Dapr AppCallback</h2>

<p>Dapr fournit un fichier proto qui définit le service <code class="language-plaintext highlighter-rouge">AppCallback</code>. C’est ce service que votre application doit implémenter pour recevoir les événements via gRPC. Les méthodes clés sont :</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ListTopicSubscriptions</code> : appelée par le sidecar au démarrage pour connaître les topics auxquels l’application est abonnée.</li>
  <li><code class="language-plaintext highlighter-rouge">OnTopicEvent</code> : appelée par le sidecar pour chaque message reçu.</li>
</ul>

<p>Le fichier proto de Dapr (<code class="language-plaintext highlighter-rouge">appcallback.proto</code>) définit ces méthodes :</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="kn">package</span> <span class="nn">dapr</span><span class="o">.</span><span class="n">proto.runtime.v1</span><span class="p">;</span>

<span class="k">import</span> <span class="s">"google/protobuf/empty.proto"</span><span class="p">;</span>
<span class="k">import</span> <span class="s">"google/protobuf/any.proto"</span><span class="p">;</span>
<span class="k">import</span> <span class="s">"google/protobuf/struct.proto"</span><span class="p">;</span>

<span class="c1">// AppCallback est le service que l'application doit implémenter</span>
<span class="kd">service</span> <span class="n">AppCallback</span> <span class="p">{</span>
  <span class="c1">// Appelé par le sidecar pour connaître les souscriptions</span>
  <span class="k">rpc</span> <span class="n">ListTopicSubscriptions</span><span class="p">(</span><span class="n">google.protobuf.Empty</span><span class="p">)</span>
      <span class="k">returns</span> <span class="p">(</span><span class="n">ListTopicSubscriptionsResponse</span><span class="p">);</span>

  <span class="c1">// Appelé par le sidecar pour chaque événement reçu</span>
  <span class="k">rpc</span> <span class="n">OnTopicEvent</span><span class="p">(</span><span class="n">TopicEventRequest</span><span class="p">)</span> <span class="k">returns</span> <span class="p">(</span><span class="n">TopicEventResponse</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">TopicEventRequest</span> <span class="p">{</span>
  <span class="kt">string</span> <span class="na">id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>             <span class="c1">// ID unique de l'événement (CloudEvents)</span>
  <span class="kt">string</span> <span class="na">source</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>         <span class="c1">// Source de l'événement</span>
  <span class="kt">string</span> <span class="na">type</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>           <span class="c1">// Type de l'événement</span>
  <span class="kt">string</span> <span class="na">spec_version</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>   <span class="c1">// Version CloudEvents</span>
  <span class="kt">string</span> <span class="na">data_content_type</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span> <span class="c1">// Content-type du payload</span>
  <span class="kt">bytes</span> <span class="na">data</span> <span class="o">=</span> <span class="mi">6</span><span class="p">;</span>            <span class="c1">// Le payload sérialisé</span>
  <span class="kt">string</span> <span class="na">topic</span> <span class="o">=</span> <span class="mi">7</span><span class="p">;</span>          <span class="c1">// Le topic d'où vient le message</span>
  <span class="kt">string</span> <span class="na">pubsub_name</span> <span class="o">=</span> <span class="mi">8</span><span class="p">;</span>    <span class="c1">// Le nom du composant pub/sub</span>
  <span class="kt">string</span> <span class="na">path</span> <span class="o">=</span> <span class="mi">9</span><span class="p">;</span>           <span class="c1">// Le chemin de routage</span>
  <span class="n">map</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="err">&gt;</span> <span class="k">extensions</span> <span class="o">=</span> <span class="mi">10</span><span class="p">;</span> <span class="c1">// Extensions CloudEvents</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">TopicEventResponse</span> <span class="p">{</span>
  <span class="n">TopicEventResponseStatus</span> <span class="na">status</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">enum</span> <span class="n">TopicEventResponseStatus</span> <span class="p">{</span>
  <span class="na">SUCCESS</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>   <span class="c1">// Message traité avec succès</span>
  <span class="na">RETRY</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>     <span class="c1">// Demander au sidecar de re-livrer</span>
  <span class="na">DROP</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>      <span class="c1">// Abandonner le message</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">TopicSubscription</span> <span class="p">{</span>
  <span class="kt">string</span> <span class="na">pubsub_name</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">topic</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="n">map</span><span class="o">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="err">&gt;</span> <span class="na">metadata</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
  <span class="n">TopicRoutes</span> <span class="na">routes</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">dead_letter_topic</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
  <span class="n">BulkSubscribeConfig</span> <span class="na">bulk_subscribe</span> <span class="o">=</span> <span class="mi">6</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">TopicRoutes</span> <span class="p">{</span>
  <span class="k">repeated</span> <span class="n">TopicRule</span> <span class="na">rules</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">string</span> <span class="k">default</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">TopicRule</span> <span class="p">{</span>
  <span class="kt">string</span> <span class="na">match</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">path</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">BulkSubscribeConfig</span> <span class="p">{</span>
  <span class="kt">bool</span> <span class="na">enabled</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">int32</span> <span class="na">max_messages_count</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="kt">int32</span> <span class="na">max_await_duration_ms</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="p">}</span>

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

<blockquote>
  <p>Vous n’avez pas besoin de copier ce fichier manuellement : le package NuGet <code class="language-plaintext highlighter-rouge">Dapr.AspNetCore</code> inclut déjà les protos Dapr et le code généré.</p>
</blockquote>

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

<p>Le SDK Dapr pour .NET fournit la classe de base <code class="language-plaintext highlighter-rouge">AppCallback.AppCallbackBase</code> qu’on peut surcharger :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Text.Json</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Dapr.AppCallback.Autogen.Grpc.v1</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Dapr.Client.Autogen.Grpc.v1</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Google.Protobuf.WellKnownTypes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Grpc.Core</span><span class="p">;</span>

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

<span class="k">public</span> <span class="k">class</span> <span class="nc">DaprSubscriberService</span> <span class="p">:</span> <span class="n">AppCallback</span><span class="p">.</span><span class="n">AppCallbackBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">DaprSubscriberService</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">DaprSubscriberService</span><span class="p">(</span><span class="n">ILogger</span><span class="p">&lt;</span><span class="n">DaprSubscriberService</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span> <span class="p">=</span> <span class="n">logger</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="c1">/// &lt;summary&gt;</span>
    <span class="c1">/// Déclaration des souscriptions auprès du sidecar.</span>
    <span class="c1">/// Appelée automatiquement par le sidecar au démarrage.</span>
    <span class="c1">/// &lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">override</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ListTopicSubscriptionsResponse</span><span class="p">&gt;</span> <span class="nf">ListTopicSubscriptions</span><span class="p">(</span>
        <span class="n">Empty</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">response</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ListTopicSubscriptionsResponse</span><span class="p">();</span>

        <span class="c1">// S'abonner au topic "orders" du composant "pubsub"</span>
        <span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
        <span class="p">{</span>
            <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
            <span class="n">Topic</span> <span class="p">=</span> <span class="s">"orders"</span><span class="p">,</span>
            <span class="n">DeadLetterTopic</span> <span class="p">=</span> <span class="s">"orders-deadletter"</span>
        <span class="p">});</span>

        <span class="c1">// S'abonner au topic "payments" du composant "pubsub"</span>
        <span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
        <span class="p">{</span>
            <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
            <span class="n">Topic</span> <span class="p">=</span> <span class="s">"payments"</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">response</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="c1">/// &lt;summary&gt;</span>
    <span class="c1">/// Réception d'un événement. Appelée par le sidecar pour chaque message.</span>
    <span class="c1">/// &lt;/summary&gt;</span>
    <span class="k">public</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">TopicEventResponse</span><span class="p">&gt;</span> <span class="nf">OnTopicEvent</span><span class="p">(</span>
        <span class="n">TopicEventRequest</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="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
            <span class="s">"Événement reçu - Topic: {Topic}, Source: {Source}, Id: {Id}"</span><span class="p">,</span>
            <span class="n">request</span><span class="p">.</span><span class="n">Topic</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Source</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>

        <span class="k">try</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="n">request</span><span class="p">.</span><span class="n">Topic</span> <span class="k">switch</span>
            <span class="p">{</span>
                <span class="s">"orders"</span> <span class="p">=&gt;</span> <span class="k">await</span> <span class="nf">HandleOrderCreatedAsync</span><span class="p">(</span><span class="n">request</span><span class="p">),</span>
                <span class="s">"payments"</span> <span class="p">=&gt;</span> <span class="k">await</span> <span class="nf">HandlePaymentEventAsync</span><span class="p">(</span><span class="n">request</span><span class="p">),</span>
                <span class="n">_</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
                <span class="p">{</span>
                    <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Drop</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">_logger</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="n">ex</span><span class="p">,</span> <span class="s">"Erreur lors du traitement de l'événement {Id}"</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Id</span><span class="p">);</span>

            <span class="c1">// Demander au sidecar de re-livrer le message</span>
            <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
            <span class="p">{</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Retry</span>
            <span class="p">};</span>
        <span class="p">}</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">TopicEventResponse</span><span class="p">&gt;</span> <span class="nf">HandleOrderCreatedAsync</span><span class="p">(</span><span class="n">TopicEventRequest</span> <span class="n">request</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Désérialiser le payload depuis les bytes</span>
        <span class="kt">var</span> <span class="n">orderCreated</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;(</span>
            <span class="n">request</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="nf">ToStringUtf8</span><span class="p">(),</span>
            <span class="k">new</span> <span class="n">JsonSerializerOptions</span> <span class="p">{</span> <span class="n">PropertyNameCaseInsensitive</span> <span class="p">=</span> <span class="k">true</span> <span class="p">});</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">orderCreated</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
            <span class="p">{</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Drop</span>
            <span class="p">};</span>
        <span class="p">}</span>

        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
            <span class="s">"Traitement du paiement pour la commande {OrderId}, montant : {Amount}€"</span><span class="p">,</span>
            <span class="n">orderCreated</span><span class="p">.</span><span class="n">OrderId</span><span class="p">,</span> <span class="n">orderCreated</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">);</span>

        <span class="c1">// Simuler le traitement du paiement</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="m">200</span><span class="p">);</span>

        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Paiement validé pour la commande {OrderId}"</span><span class="p">,</span>
            <span class="n">orderCreated</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
        <span class="p">{</span>
            <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span>
        <span class="p">};</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">TopicEventResponse</span><span class="p">&gt;</span> <span class="nf">HandlePaymentEventAsync</span><span class="p">(</span><span class="n">TopicEventRequest</span> <span class="n">request</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Événement de paiement reçu : {Data}"</span><span class="p">,</span>
            <span class="n">request</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="nf">ToStringUtf8</span><span class="p">());</span>

        <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>

        <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
        <span class="p">{</span>
            <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</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="k">using</span> <span class="nn">PaymentService.Services</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">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="c1">// Enregistrer le service AppCallback pour le sidecar Dapr</span>
<span class="n">app</span><span class="p">.</span><span class="n">MapGrpcService</span><span class="p">&lt;</span><span class="n">DaprSubscriberService</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>

<h2 id="configuration-kestrel">Configuration Kestrel</h2>

<p>Le service doit é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:5010"</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>On utilise <code class="language-plaintext highlighter-rouge">http</code> (pas <code class="language-plaintext highlighter-rouge">https</code>) car le sidecar Dapr gère le mTLS entre les services. La communication entre l’application et son propre sidecar reste locale.</p>
</blockquote>

<h1 id="souscriptions-avancées-en-grpc">Souscriptions avancées en gRPC</h1>

<h2 id="souscription-avec-routage">Souscription avec routage</h2>

<p>On peut ajouter des règles de routage directement dans <code class="language-plaintext highlighter-rouge">ListTopicSubscriptions</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">override</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ListTopicSubscriptionsResponse</span><span class="p">&gt;</span> <span class="nf">ListTopicSubscriptions</span><span class="p">(</span>
    <span class="n">Empty</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">response</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ListTopicSubscriptionsResponse</span><span class="p">();</span>

    <span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
    <span class="p">{</span>
        <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
        <span class="n">Topic</span> <span class="p">=</span> <span class="s">"orders"</span><span class="p">,</span>
        <span class="n">Routes</span> <span class="p">=</span> <span class="k">new</span> <span class="n">TopicRoutes</span>
        <span class="p">{</span>
            <span class="n">Default</span> <span class="p">=</span> <span class="s">"default"</span><span class="p">,</span>
            <span class="n">Rules</span> <span class="p">=</span>
            <span class="p">{</span>
                <span class="k">new</span> <span class="n">TopicRule</span>
                <span class="p">{</span>
                    <span class="n">Match</span> <span class="p">=</span> <span class="s">"event.data.status == \"created\""</span><span class="p">,</span>
                    <span class="n">Path</span> <span class="p">=</span> <span class="s">"created"</span>
                <span class="p">},</span>
                <span class="k">new</span> <span class="n">TopicRule</span>
                <span class="p">{</span>
                    <span class="n">Match</span> <span class="p">=</span> <span class="s">"event.data.status == \"paid\""</span><span class="p">,</span>
                    <span class="n">Path</span> <span class="p">=</span> <span class="s">"paid"</span>
                <span class="p">}</span>
            <span class="p">}</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">response</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Le champ <code class="language-plaintext highlighter-rouge">Path</code> de la règle est ensuite accessible dans <code class="language-plaintext highlighter-rouge">TopicEventRequest.Path</code>, ce qui permet de dispatcher dans <code class="language-plaintext highlighter-rouge">OnTopicEvent</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">TopicEventResponse</span><span class="p">&gt;</span> <span class="nf">OnTopicEvent</span><span class="p">(</span>
    <span class="n">TopicEventRequest</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="k">return</span> <span class="n">request</span><span class="p">.</span><span class="n">Path</span> <span class="k">switch</span>
    <span class="p">{</span>
        <span class="s">"created"</span> <span class="p">=&gt;</span> <span class="k">await</span> <span class="nf">HandleOrderCreatedAsync</span><span class="p">(</span><span class="n">request</span><span class="p">),</span>
        <span class="s">"paid"</span> <span class="p">=&gt;</span> <span class="k">await</span> <span class="nf">HandleOrderPaidAsync</span><span class="p">(</span><span class="n">request</span><span class="p">),</span>
        <span class="s">"default"</span> <span class="p">=&gt;</span> <span class="k">await</span> <span class="nf">HandleDefaultAsync</span><span class="p">(</span><span class="n">request</span><span class="p">),</span>
        <span class="n">_</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
        <span class="p">{</span>
            <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Drop</span>
        <span class="p">}</span>
    <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="souscription-avec-métadonnées">Souscription avec métadonnées</h2>

<p>On peut ajouter des métadonnées à la souscription (par exemple, pour spécifier un consumer group Kafka) :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">subscription</span> <span class="p">=</span> <span class="k">new</span> <span class="n">TopicSubscription</span>
<span class="p">{</span>
    <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
    <span class="n">Topic</span> <span class="p">=</span> <span class="s">"orders"</span>
<span class="p">};</span>
<span class="n">subscription</span><span class="p">.</span><span class="n">Metadata</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"consumerGroup"</span><span class="p">,</span> <span class="s">"payment-processors"</span><span class="p">);</span>
<span class="n">subscription</span><span class="p">.</span><span class="n">Metadata</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="s">"maxConcurrency"</span><span class="p">,</span> <span class="s">"10"</span><span class="p">);</span>

<span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">subscription</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="souscription-avec-dead-letter-topic">Souscription avec dead letter topic</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
<span class="p">{</span>
    <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
    <span class="n">Topic</span> <span class="p">=</span> <span class="s">"orders"</span><span class="p">,</span>
    <span class="n">DeadLetterTopic</span> <span class="p">=</span> <span class="s">"orders-deadletter"</span>
<span class="p">});</span>

<span class="c1">// S'abonner aussi au dead letter topic pour monitoring</span>
<span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
<span class="p">{</span>
    <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
    <span class="n">Topic</span> <span class="p">=</span> <span class="s">"orders-deadletter"</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="souscription-en-bulk">Souscription en bulk</h2>

<p>Pour recevoir les messages par lots :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
<span class="p">{</span>
    <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
    <span class="n">Topic</span> <span class="p">=</span> <span class="s">"orders"</span><span class="p">,</span>
    <span class="n">BulkSubscribe</span> <span class="p">=</span> <span class="k">new</span> <span class="n">BulkSubscribeConfig</span>
    <span class="p">{</span>
        <span class="n">Enabled</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
        <span class="n">MaxMessagesCount</span> <span class="p">=</span> <span class="m">50</span><span class="p">,</span>
        <span class="n">MaxAwaitDurationMs</span> <span class="p">=</span> <span class="m">1000</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h1 id="pattern--séparation-par-handlers-typés">Pattern : séparation par handlers typés</h1>

<p>Pour un code plus maintenable, on peut introduire un pattern de dispatch typé :</p>

<h2 id="interface-de-handler">Interface de handler</h2>

<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">ITopicHandler</span><span class="p">&lt;</span><span class="n">TEvent</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">PubSubName</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
    <span class="kt">string</span> <span class="n">TopicName</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="p">}</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">&gt;</span> <span class="nf">HandleAsync</span><span class="p">(</span>
        <span class="n">TEvent</span> <span class="n">evt</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="implémentation-des-handlers">Implémentation des handlers</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">OrderCreatedHandler</span> <span class="p">:</span> <span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">OrderCreatedHandler</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderCreatedHandler</span><span class="p">(</span><span class="n">ILogger</span><span class="p">&lt;</span><span class="n">OrderCreatedHandler</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span> <span class="p">=</span> <span class="n">logger</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">PubSubName</span> <span class="p">=&gt;</span> <span class="s">"pubsub"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">TopicName</span> <span class="p">=&gt;</span> <span class="s">"orders"</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">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">&gt;</span> <span class="nf">HandleAsync</span><span class="p">(</span>
        <span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
            <span class="s">"Traitement du paiement pour {OrderId} ({Amount}€)"</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="n">evt</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">);</span>

        <span class="c1">// Logique de paiement...</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="m">100</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>

        <span class="k">return</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">PaymentConfirmedHandler</span> <span class="p">:</span> <span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">PaymentConfirmed</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">PaymentConfirmedHandler</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">PaymentConfirmedHandler</span><span class="p">(</span><span class="n">ILogger</span><span class="p">&lt;</span><span class="n">PaymentConfirmedHandler</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span> <span class="p">=</span> <span class="n">logger</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">PubSubName</span> <span class="p">=&gt;</span> <span class="s">"pubsub"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">TopicName</span> <span class="p">=&gt;</span> <span class="s">"payment-confirmed"</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">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">&gt;</span> <span class="nf">HandleAsync</span><span class="p">(</span>
        <span class="n">PaymentConfirmed</span> <span class="n">evt</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Paiement {PaymentId} confirmé"</span><span class="p">,</span> <span class="n">evt</span><span class="p">.</span><span class="n">PaymentId</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="k">return</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="service-appcallback-avec-dispatch">Service AppCallback avec dispatch</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">DaprSubscriberService</span> <span class="p">:</span> <span class="n">AppCallback</span><span class="p">.</span><span class="n">AppCallbackBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IServiceProvider</span> <span class="n">_serviceProvider</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">DaprSubscriberService</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="c1">// Mapping topic → (type événement, type handler)</span>
    <span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="p">(</span><span class="n">Type</span> <span class="n">EventType</span><span class="p">,</span> <span class="n">Type</span> <span class="n">HandlerType</span><span class="p">)&gt;</span> <span class="n">_topicMap</span> <span class="p">=</span> <span class="k">new</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="p">[</span><span class="s">"orders"</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">OrderCreated</span><span class="p">),</span> <span class="k">typeof</span><span class="p">(</span><span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;)),</span>
        <span class="p">[</span><span class="s">"payment-confirmed"</span><span class="p">]</span> <span class="p">=</span> <span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">PaymentConfirmed</span><span class="p">),</span> <span class="k">typeof</span><span class="p">(</span><span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">PaymentConfirmed</span><span class="p">&gt;))</span>
    <span class="p">};</span>

    <span class="k">public</span> <span class="nf">DaprSubscriberService</span><span class="p">(</span>
        <span class="n">IServiceProvider</span> <span class="n">serviceProvider</span><span class="p">,</span>
        <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">DaprSubscriberService</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_serviceProvider</span> <span class="p">=</span> <span class="n">serviceProvider</span><span class="p">;</span>
        <span class="n">_logger</span> <span class="p">=</span> <span class="n">logger</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">ListTopicSubscriptionsResponse</span><span class="p">&gt;</span> <span class="nf">ListTopicSubscriptions</span><span class="p">(</span>
        <span class="n">Empty</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">response</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ListTopicSubscriptionsResponse</span><span class="p">();</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="p">(</span><span class="n">topic</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="k">in</span> <span class="n">_topicMap</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">response</span><span class="p">.</span><span class="n">Subscriptions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">new</span> <span class="n">TopicSubscription</span>
            <span class="p">{</span>
                <span class="n">PubsubName</span> <span class="p">=</span> <span class="s">"pubsub"</span><span class="p">,</span>
                <span class="n">Topic</span> <span class="p">=</span> <span class="n">topic</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">response</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">TopicEventResponse</span><span class="p">&gt;</span> <span class="nf">OnTopicEvent</span><span class="p">(</span>
        <span class="n">TopicEventRequest</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="k">if</span> <span class="p">(!</span><span class="n">_topicMap</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">Topic</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">mapping</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="n">_logger</span><span class="p">.</span><span class="nf">LogWarning</span><span class="p">(</span><span class="s">"Topic inconnu : {Topic}"</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Topic</span><span class="p">);</span>
            <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
            <span class="p">{</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Drop</span>
            <span class="p">};</span>
        <span class="p">}</span>

        <span class="k">try</span>
        <span class="p">{</span>
            <span class="c1">// Désérialiser l'événement dans le type attendu</span>
            <span class="kt">var</span> <span class="n">evt</span> <span class="p">=</span> <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">Deserialize</span><span class="p">(</span>
                <span class="n">request</span><span class="p">.</span><span class="n">Data</span><span class="p">.</span><span class="nf">ToStringUtf8</span><span class="p">(),</span>
                <span class="n">mapping</span><span class="p">.</span><span class="n">EventType</span><span class="p">,</span>
                <span class="k">new</span> <span class="n">JsonSerializerOptions</span> <span class="p">{</span> <span class="n">PropertyNameCaseInsensitive</span> <span class="p">=</span> <span class="k">true</span> <span class="p">});</span>

            <span class="k">if</span> <span class="p">(</span><span class="n">evt</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span>
            <span class="p">{</span>
                <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
                <span class="p">{</span>
                    <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Drop</span>
                <span class="p">};</span>
            <span class="p">}</span>

            <span class="c1">// Résoudre le handler depuis le conteneur DI</span>
            <span class="kt">var</span> <span class="n">handler</span> <span class="p">=</span> <span class="n">_serviceProvider</span><span class="p">.</span><span class="nf">GetRequiredService</span><span class="p">(</span><span class="n">mapping</span><span class="p">.</span><span class="n">HandlerType</span><span class="p">);</span>

            <span class="c1">// Appeler HandleAsync via réflexion</span>
            <span class="kt">var</span> <span class="n">method</span> <span class="p">=</span> <span class="n">mapping</span><span class="p">.</span><span class="n">HandlerType</span><span class="p">.</span><span class="nf">GetMethod</span><span class="p">(</span><span class="s">"HandleAsync"</span><span class="p">)!;</span>
            <span class="kt">var</span> <span class="n">resultTask</span> <span class="p">=</span> <span class="p">(</span><span class="n">Task</span><span class="p">&lt;</span><span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">&gt;)</span>
                <span class="n">method</span><span class="p">.</span><span class="nf">Invoke</span><span class="p">(</span><span class="n">handler</span><span class="p">,</span> <span class="p">[</span><span class="n">evt</span><span class="p">,</span> <span class="n">context</span><span class="p">.</span><span class="n">CancellationToken</span><span class="p">])!;</span>

            <span class="kt">var</span> <span class="n">status</span> <span class="p">=</span> <span class="k">await</span> <span class="n">resultTask</span><span class="p">;</span>

            <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span> <span class="p">{</span> <span class="n">Status</span> <span class="p">=</span> <span class="n">status</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">_logger</span><span class="p">.</span><span class="nf">LogError</span><span class="p">(</span><span class="n">ex</span><span class="p">,</span> <span class="s">"Erreur sur le topic {Topic}"</span><span class="p">,</span> <span class="n">request</span><span class="p">.</span><span class="n">Topic</span><span class="p">);</span>
            <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
            <span class="p">{</span>
                <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Retry</span>
            <span class="p">};</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="enregistrement-di">Enregistrement DI</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="c1">// Enregistrer les handlers</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;,</span> <span class="n">OrderCreatedHandler</span><span class="p">&gt;();</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">PaymentConfirmed</span><span class="p">&gt;,</span> <span class="n">PaymentConfirmedHandler</span><span class="p">&gt;();</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">DaprSubscriberService</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>Ce pattern sépare proprement les responsabilités : le service <code class="language-plaintext highlighter-rouge">AppCallback</code> ne fait que du dispatch, et chaque handler contient la logique métier de son événement.</p>

<h1 id="événements-protobuf-au-lieu-de-json">Événements Protobuf (au lieu de JSON)</h1>

<p>Si vous voulez aller au bout de la logique gRPC, vous pouvez définir vos événements en Protobuf :</p>

<h2 id="définir-les-événements-dans-un-fichier-proto">Définir les événements dans un fichier proto</h2>

<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">"SharedContracts"</span><span class="p">;</span>

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

<span class="kd">message</span> <span class="nc">OrderCreatedEvent</span> <span class="p">{</span>
  <span class="kt">string</span> <span class="na">order_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">customer_id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="kt">double</span> <span class="na">total_amount</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
  <span class="kt">int64</span> <span class="na">created_at_unix</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>  <span class="c1">// Timestamp Unix en secondes</span>
  <span class="k">repeated</span> <span class="n">OrderItemEvent</span> <span class="na">items</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">OrderItemEvent</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">double</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
  <span class="kt">int32</span> <span class="na">quantity</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">PaymentConfirmedEvent</span> <span class="p">{</span>
  <span class="kt">string</span> <span class="na">payment_id</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">order_id</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
  <span class="kt">double</span> <span class="na">amount</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
  <span class="kt">string</span> <span class="na">payment_method</span> <span class="o">=</span> <span class="mi">4</span><span class="p">;</span>
  <span class="kt">int64</span> <span class="na">confirmed_at_unix</span> <span class="o">=</span> <span class="mi">5</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="publier-un-événement-protobuf">Publier un événement Protobuf</h2>

<p>Pour publier un message Protobuf via Dapr, on le sérialise en bytes et on utilise l’API HTTP du sidecar avec le content-type approprié :</p>

<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">PublishProtobufEventAsync</span><span class="p">(</span><span class="n">OrderCreatedEvent</span> <span class="n">evt</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Sérialiser en bytes Protobuf</span>
    <span class="kt">var</span> <span class="n">bytes</span> <span class="p">=</span> <span class="n">evt</span><span class="p">.</span><span class="nf">ToByteArray</span><span class="p">();</span>

    <span class="c1">// Publier via l'API HTTP du sidecar avec le content-type Protobuf</span>
    <span class="k">using</span> <span class="nn">var</span> <span class="n">httpClient</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">content</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ByteArrayContent</span><span class="p">(</span><span class="n">bytes</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/octet-stream"</span><span class="p">);</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="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">PostAsync</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/publish/pubsub/orders?metadata.rawPayload=true"</span><span class="p">,</span>
        <span class="n">content</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="p">}</span>
</code></pre></div></div>

<h2 id="recevoir-un-événement-protobuf">Recevoir un événement Protobuf</h2>

<p>Côté subscriber gRPC, on désérialise les bytes Protobuf depuis <code class="language-plaintext highlighter-rouge">TopicEventRequest.Data</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">TopicEventResponse</span><span class="p">&gt;</span> <span class="nf">HandleOrderCreatedProtobufAsync</span><span class="p">(</span>
    <span class="n">TopicEventRequest</span> <span class="n">request</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Désérialiser les bytes Protobuf</span>
    <span class="kt">var</span> <span class="n">orderCreated</span> <span class="p">=</span> <span class="n">OrderCreatedEvent</span><span class="p">.</span><span class="n">Parser</span><span class="p">.</span><span class="nf">ParseFrom</span><span class="p">(</span><span class="n">request</span><span class="p">.</span><span class="n">Data</span><span class="p">);</span>

    <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
        <span class="s">"Commande {OrderId} reçue (Protobuf), montant : {Amount}€, {ItemCount} articles"</span><span class="p">,</span>
        <span class="n">orderCreated</span><span class="p">.</span><span class="n">OrderId</span><span class="p">,</span>
        <span class="n">orderCreated</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">,</span>
        <span class="n">orderCreated</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>

    <span class="c1">// Traitement...</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="m">100</span><span class="p">);</span>

    <span class="k">return</span> <span class="k">new</span> <span class="n">TopicEventResponse</span>
    <span class="p">{</span>
        <span class="n">Status</span> <span class="p">=</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span>
    <span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>L’avantage de Protobuf : la sérialisation/désérialisation est <strong>beaucoup plus rapide</strong> et les messages sont <strong>plus compacts</strong> que le JSON, ce qui réduit la bande passante et la latence, surtout à haut débit.</p>

<h1 id="combiner-publication-grpc-et-souscription-grpc">Combiner publication gRPC et souscription gRPC</h1>

<p>On peut aussi publier des événements via l’API gRPC du sidecar au lieu de l’API HTTP :</p>

<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">public</span> <span class="k">class</span> <span class="nc">EventPublisher</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">EventPublisher</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="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">PublishOrderCreatedAsync</span><span class="p">(</span><span class="n">OrderCreated</span> <span class="n">order</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// DaprClient utilise gRPC par défaut pour communiquer avec le sidecar</span>
        <span class="k">await</span> <span class="n">_daprClient</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="n">order</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Pour forcer l’utilisation de gRPC côté <code class="language-plaintext highlighter-rouge">DaprClient</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">AddDaprClient</span><span class="p">(</span><span class="n">daprBuilder</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">grpcPort</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="n">daprBuilder</span><span class="p">.</span><span class="nf">UseGrpcEndpoint</span><span class="p">(</span><span class="s">$"http://localhost:</span><span class="p">{</span><span class="n">grpcPort</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<h1 id="exemple-complet--paymentservice-grpc">Exemple complet : PaymentService gRPC</h1>

<p>Voici le service complet avec handler, publication de réponse, et monitoring :</p>

<h2 id="programcs">Program.cs</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">PaymentService.Services</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">PaymentService.Handlers</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">AddGrpc</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="c1">// Enregistrer les handlers</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;,</span> <span class="n">OrderPaymentHandler</span><span class="p">&gt;();</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">DaprSubscriberService</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>

<h2 id="le-handler-métier">Le handler métier</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">namespace</span> <span class="nn">PaymentService.Handlers</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">OrderPaymentHandler</span> <span class="p">:</span> <span class="n">ITopicHandler</span><span class="p">&lt;</span><span class="n">OrderCreated</span><span class="p">&gt;</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">private</span> <span class="k">readonly</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">OrderPaymentHandler</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">OrderPaymentHandler</span><span class="p">(</span><span class="n">DaprClient</span> <span class="n">daprClient</span><span class="p">,</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">OrderPaymentHandler</span><span class="p">&gt;</span> <span class="n">logger</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="n">_logger</span> <span class="p">=</span> <span class="n">logger</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="kt">string</span> <span class="n">PubSubName</span> <span class="p">=&gt;</span> <span class="s">"pubsub"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">TopicName</span> <span class="p">=&gt;</span> <span class="s">"orders"</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">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">&gt;</span> <span class="nf">HandleAsync</span><span class="p">(</span>
        <span class="n">OrderCreated</span> <span class="n">evt</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
            <span class="s">"Début du traitement de paiement pour la commande {OrderId}"</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="c1">// Vérifier l'idempotence via le state store</span>
        <span class="kt">var</span> <span class="n">alreadyProcessed</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="kt">bool</span><span class="p">&gt;(</span>
            <span class="s">"statestore"</span><span class="p">,</span> <span class="s">$"payment-processed-</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="n">cancellationToken</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">alreadyProcessed</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
                <span class="s">"Commande {OrderId} déjà traitée, skip"</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="k">return</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="c1">// Simuler le traitement du paiement</span>
        <span class="kt">var</span> <span class="n">paymentResult</span> <span class="p">=</span> <span class="k">await</span> <span class="nf">ProcessPaymentAsync</span><span class="p">(</span><span class="n">evt</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">paymentResult</span><span class="p">.</span><span class="n">Success</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="c1">// Marquer comme traité</span>
            <span class="k">await</span> <span class="n">_daprClient</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">$"payment-processed-</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">true</span><span class="p">,</span>
                <span class="n">cancellationToken</span><span class="p">:</span> <span class="n">ct</span><span class="p">);</span>

            <span class="c1">// Publier un événement de confirmation</span>
            <span class="k">await</span> <span class="n">_daprClient</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">"payment-confirmed"</span><span class="p">,</span>
                <span class="k">new</span> <span class="n">PaymentConfirmed</span>
                <span class="p">{</span>
                    <span class="n">PaymentId</span> <span class="p">=</span> <span class="n">paymentResult</span><span class="p">.</span><span class="n">PaymentId</span><span class="p">,</span>
                    <span class="n">OrderId</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="n">Amount</span> <span class="p">=</span> <span class="n">evt</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">,</span>
                    <span class="n">ConfirmedAt</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span>
                <span class="p">},</span> <span class="n">ct</span><span class="p">);</span>

            <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span>
                <span class="s">"Paiement {PaymentId} confirmé pour la commande {OrderId}"</span><span class="p">,</span>
                <span class="n">paymentResult</span><span class="p">.</span><span class="n">PaymentId</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="k">return</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span><span class="p">;</span>
        <span class="p">}</span>

        <span class="n">_logger</span><span class="p">.</span><span class="nf">LogWarning</span><span class="p">(</span>
            <span class="s">"Échec du paiement pour la commande {OrderId} : {Reason}"</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="n">paymentResult</span><span class="p">.</span><span class="n">FailureReason</span><span class="p">);</span>

        <span class="c1">// Publier un événement d'échec</span>
        <span class="k">await</span> <span class="n">_daprClient</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">"payment-failed"</span><span class="p">,</span>
            <span class="k">new</span> <span class="n">PaymentFailed</span>
            <span class="p">{</span>
                <span class="n">OrderId</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="n">Reason</span> <span class="p">=</span> <span class="n">paymentResult</span><span class="p">.</span><span class="n">FailureReason</span> <span class="p">??</span> <span class="s">"Unknown"</span>
            <span class="p">},</span> <span class="n">ct</span><span class="p">);</span>

        <span class="c1">// Ne pas retrier : le paiement a échoué pour une raison métier</span>
        <span class="k">return</span> <span class="n">TopicEventResponse</span><span class="p">.</span><span class="n">Types</span><span class="p">.</span><span class="n">TopicEventResponseStatus</span><span class="p">.</span><span class="n">Success</span><span class="p">;</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">PaymentResult</span><span class="p">&gt;</span> <span class="nf">ProcessPaymentAsync</span><span class="p">(</span>
        <span class="n">OrderCreated</span> <span class="n">order</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Simulation de traitement</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="m">500</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>

        <span class="k">return</span> <span class="k">new</span> <span class="n">PaymentResult</span>
        <span class="p">{</span>
            <span class="n">Success</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">PaymentId</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="nf">ToString</span><span class="p">()</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="les-modèles">Les modèles</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">record</span> <span class="n">OrderCreated</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">OrderId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">CustomerId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">TotalAmount</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">DateTime</span> <span class="n">CreatedAt</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="n">PaymentConfirmed</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">PaymentId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">OrderId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">Amount</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="n">DateTime</span> <span class="n">ConfirmedAt</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="n">PaymentFailed</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">OrderId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Reason</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">public</span> <span class="n">record</span> <span class="n">PaymentResult</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">Success</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">PaymentId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span><span class="p">?</span> <span class="n">FailureReason</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="n">init</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

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

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Terminal 1 : OrderService (publisher HTTP)</span>
dapr run <span class="nt">--app-id</span> order-service <span class="nt">--app-port</span> 5000 <span class="se">\</span>
    <span class="nt">--resources-path</span> ./components <span class="nt">--</span> dotnet run <span class="nt">--project</span> OrderService

<span class="c"># Terminal 2 : PaymentService (subscriber gRPC)</span>
dapr run <span class="nt">--app-id</span> payment-service <span class="se">\</span>
         <span class="nt">--app-port</span> 5010 <span class="se">\</span>
         <span class="nt">--app-protocol</span> grpc <span class="se">\</span>
         <span class="nt">--resources-path</span> ./components <span class="se">\</span>
         <span class="nt">--</span> dotnet run <span class="nt">--project</span> PaymentService
</code></pre></div></div>

<p>Le flag <code class="language-plaintext highlighter-rouge">--app-protocol grpc</code> est essentiel : il indique au sidecar que l’application derrière lui communique en gRPC. Sans ce flag, le sidecar tentera de livrer les messages en HTTP, ce qui échouera.</p>

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

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Créer une commande</span>
curl <span class="nt">-X</span> POST http://localhost:5000/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">'{ "customerId": "cust-42", "totalAmount": 149.99 }'</span>
</code></pre></div></div>

<p>Dans les logs du PaymentService, vous devriez voir :</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>info: PaymentService.Handlers.OrderPaymentHandler
      Début du traitement de paiement pour la commande abc-123
info: PaymentService.Handlers.OrderPaymentHandler
      Paiement def-456 confirmé pour la commande abc-123
</code></pre></div></div>

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

<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">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="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="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">pubSub</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="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">PaymentService</span><span class="p">&gt;(</span><span class="s">"payment-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="p">.</span><span class="nf">WithReference</span><span class="p">(</span><span class="n">pubSub</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="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="http-vs-grpc--récapitulatif-pubsub">HTTP vs gRPC : récapitulatif Pub/Sub</h1>

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Pub/Sub HTTP</th>
      <th>Pub/Sub gRPC</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Souscription</strong></td>
      <td><code class="language-plaintext highlighter-rouge">[Topic]</code> attribut, Minimal API</td>
      <td><code class="language-plaintext highlighter-rouge">ListTopicSubscriptions</code> override</td>
    </tr>
    <tr>
      <td><strong>Réception</strong></td>
      <td>Endpoint HTTP POST</td>
      <td><code class="language-plaintext highlighter-rouge">OnTopicEvent</code> override</td>
    </tr>
    <tr>
      <td><strong>Sérialisation</strong></td>
      <td>JSON automatique</td>
      <td>JSON ou Protobuf (manuel)</td>
    </tr>
    <tr>
      <td><strong>Dispatch</strong></td>
      <td>Routage ASP.NET Core natif</td>
      <td>Switch/dispatch dans <code class="language-plaintext highlighter-rouge">OnTopicEvent</code></td>
    </tr>
    <tr>
      <td><strong>Performance</strong></td>
      <td>Bon</td>
      <td>Meilleur (HTTP/2 binaire)</td>
    </tr>
    <tr>
      <td><strong>Simplicité</strong></td>
      <td>Plus simple</td>
      <td>Plus de code</td>
    </tr>
    <tr>
      <td><strong>Flag sidecar</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--app-protocol http</code> (défaut)</td>
      <td><code class="language-plaintext highlighter-rouge">--app-protocol grpc</code></td>
    </tr>
    <tr>
      <td><strong>Cas d’usage</strong></td>
      <td>Majorité des applications</td>
      <td>Haut débit, écosystème gRPC existant</td>
    </tr>
  </tbody>
</table>

<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 entre le sidecar et l’application subscriber</td>
    </tr>
    <tr>
      <td><strong>Service</strong></td>
      <td>Implémenter <code class="language-plaintext highlighter-rouge">AppCallback.AppCallbackBase</code> (Dapr SDK)</td>
    </tr>
    <tr>
      <td><strong>Souscriptions</strong></td>
      <td><code class="language-plaintext highlighter-rouge">ListTopicSubscriptions</code> : déclare les topics, routes, dead letters</td>
    </tr>
    <tr>
      <td><strong>Réception</strong></td>
      <td><code class="language-plaintext highlighter-rouge">OnTopicEvent</code> : reçoit chaque message avec topic, payload, métadonnées</td>
    </tr>
    <tr>
      <td><strong>Réponse</strong></td>
      <td><code class="language-plaintext highlighter-rouge">Success</code> (ACK), <code class="language-plaintext highlighter-rouge">Retry</code> (re-livraison), <code class="language-plaintext highlighter-rouge">Drop</code> (abandon)</td>
    </tr>
    <tr>
      <td><strong>Sérialisation</strong></td>
      <td>JSON par défaut, Protobuf possible pour de meilleures performances</td>
    </tr>
    <tr>
      <td><strong>Flag</strong></td>
      <td><code class="language-plaintext highlighter-rouge">--app-protocol grpc</code> obligatoire au lancement du subscriber</td>
    </tr>
    <tr>
      <td><strong>Routage</strong></td>
      <td>Règles de routage via <code class="language-plaintext highlighter-rouge">TopicRoutes</code> dans la souscription</td>
    </tr>
    <tr>
      <td><strong>Publication</strong></td>
      <td>Identique (HTTP ou gRPC via <code class="language-plaintext highlighter-rouge">DaprClient.PublishEventAsync</code>)</td>
    </tr>
    <tr>
      <td><strong>Aspire</strong></td>
      <td><code class="language-plaintext highlighter-rouge">AppProtocol = "grpc"</code> dans <code class="language-plaintext highlighter-rouge">DaprSidecarOptions</code></td>
    </tr>
  </tbody>
</table>

<p>Le Pub/Sub gRPC de Dapr est le choix naturel pour les services déjà construits autour de gRPC ou pour les scénarios à haut débit où la performance de sérialisation et la compacité des messages font la différence, tout en conservant les mêmes garanties de livraison et la même portabilité entre brokers que le mode HTTP.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="dapr" /><category term="microservices" /><category term="pub-sub" /><category term="grpc" /><summary type="html"><![CDATA[Dans l’article précédent, nous avons vu le Pub/Sub Dapr avec des souscriptions HTTP (Minimal API, contrôleurs). Mais Dapr supporte aussi la communication gRPC entre le sidecar et l’application, y compris pour la livraison des messages Pub/Sub. gRPC apporte la sérialisation binaire (Protobuf), des contrats typés et de meilleures performances, ce qui est particulièrement intéressant pour les systèmes à haut débit. Dans cet article, on met en place un système Pub/Sub complet en gRPC.]]></summary></entry><entry><title type="html">Dapr : la gestion d’état (State Management) en .NET</title><link href="http://guym.fr/2026/03/01/dapr-etats.html" rel="alternate" type="text/html" title="Dapr : la gestion d’état (State Management) en .NET" /><published>2026-03-01T00:00:00+01:00</published><updated>2026-03-01T00:00:00+01:00</updated><id>http://guym.fr/2026/03/01/dapr-etats</id><content type="html" xml:base="http://guym.fr/2026/03/01/dapr-etats.html"><![CDATA[<p>La gestion d’état (State Management) est l’un des building blocks essentiels de Dapr. Il fournit une API clé/valeur unifiée pour stocker, lire, supprimer et transactionner de l’état, avec concurrence optimiste (ETags), sans coupler votre code à un store spécifique. En .NET, le SDK Dapr rend cette API simple et idiomatique.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>Dapr pour les développeurs .NET</strong> : <strong> 4</strong> sur <strong>6</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 - 
		
			<a href="/2026/02/22/dapr-service-invocation-2-grpc.html">Dapr : invocation de service gRPC en .NET</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 4 - 
		
			Cet article
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			<a href="/2026/03/03/dapr-pub-sub-grpc.html">Dapr : Pub/Sub gRPC en .NET</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 6 - 
		
			<a href="/2026/03/08/dapr-pub-sub.html">Dapr : le Pub/Sub (Publish &amp; Subscribe) en .NET</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	</ul>
</div>

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

<p>Dans une architecture microservices, chaque service gère idéalement son propre état. Mais stocker cet état soulève plusieurs questions :</p>

<ul>
  <li><strong>Quel store utiliser ?</strong> Redis, PostgreSQL, Cosmos DB, MongoDB… Chaque choix implique un SDK différent, une sérialisation différente, des patterns de connexion différents.</li>
  <li><strong>Concurrence</strong> : que se passe-t-il quand deux instances du même service tentent de modifier la même clé en même temps ?</li>
  <li><strong>Transactions</strong> : comment garantir l’atomicité de plusieurs écritures simultanées ?</li>
  <li><strong>Portabilité</strong> : comment changer de store (ex. : passer de Redis en développement à Cosmos DB en production) sans réécrire le code ?</li>
  <li><strong>Résolution de conflits</strong> : comment gérer les conflits de mise à jour (first-write-wins vs last-write-wins) ?</li>
</ul>

<p>Sans Dapr, il faut intégrer le SDK de chaque store, gérer les connexions, les retries, la sérialisation, et coupler fortement son code à un fournisseur particulier. Dapr résout tout cela avec une API unifiée exposée par le sidecar.</p>

<h1 id="fonctionnement">Fonctionnement</h1>

<p>Le state store de Dapr fonctionne sur le principe <strong>clé/valeur</strong> :</p>

<ol>
  <li>Votre application appelle le sidecar Dapr sur <code class="language-plaintext highlighter-rouge">localhost</code> (HTTP ou gRPC).</li>
  <li>Le sidecar transmet la requête au <strong>composant de state store</strong> configuré (Redis, PostgreSQL, Cosmos DB…).</li>
  <li>Le sidecar gère la sérialisation (JSON par défaut), les ETags pour la concurrence optimiste, et les retries.</li>
</ol>

<pre><code class="language-mermaid">graph LR
    App["Application .NET"]
    Sidecar["Sidecar Dapr"]
    Store["State Store&lt;br/&gt;(Redis, PostgreSQL,&lt;br/&gt;Cosmos DB...)"]
    
    App --&gt;|SaveState / GetState&lt;br/&gt;localhost| Sidecar
    Sidecar --&gt;|Lecture / Écriture| Store
    
    style App fill:#4A90E2
    style Sidecar fill:#F5A623
    style Store fill:#7ED321
</code></pre>

<p>Chaque état est stocké avec une <strong>clé</strong> composée automatiquement par Dapr : <code class="language-plaintext highlighter-rouge">&lt;app-id&gt;||&lt;key&gt;</code>. Cela évite les collisions entre services partageant le même store.</p>

<h1 id="configuration-du-composant">Configuration du composant</h1>

<p>Le composant de state store est défini dans un fichier YAML. Voici quelques exemples :</p>

<h2 id="redis-développement-local">Redis (développement local)</h2>

<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>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">redisPassword</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">"</span>
</code></pre></div></div>

<h2 id="postgresql">PostgreSQL</h2>

<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.postgresql</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">host=localhost</span><span class="nv"> </span><span class="s">user=postgres</span><span class="nv"> </span><span class="s">password=secret</span><span class="nv"> </span><span class="s">dbname=daprstate</span><span class="nv"> </span><span class="s">sslmode=disable"</span>
</code></pre></div></div>

<h2 id="azure-cosmos-db">Azure Cosmos DB</h2>

<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.azure.cosmosdb</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">url</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">https://myaccount.documents.azure.com:443/"</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">masterKey</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">&lt;cosmos-key&gt;"</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">database</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">daprstate"</span>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">collection</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">state"</span>
</code></pre></div></div>

<p>Le point clé : <strong>le code applicatif est identique</strong>, quel que soit le composant choisi. On change de store en modifiant uniquement le fichier YAML.</p>

<h1 id="gestion-détat-en-net">Gestion d’état en .NET</h1>

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

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

<h2 id="enregistrement-dans-le-conteneur-di">Enregistrement dans le conteneur DI</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">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="n">app</span><span class="p">.</span><span class="nf">Run</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="opérations-crud-de-base">Opérations CRUD de base</h2>

<h3 id="sauvegarder-un-état">Sauvegarder un état</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">CartService</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">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">StoreName</span> <span class="p">=</span> <span class="s">"statestore"</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">CartService</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="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">SaveCartAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">ShoppingCart</span> <span class="n">cart</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">SaveStateAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">cart</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>L’objet <code class="language-plaintext highlighter-rouge">ShoppingCart</code> est sérialisé automatiquement en JSON et stocké sous la clé <code class="language-plaintext highlighter-rouge">cart-{userId}</code>.</p>

<h3 id="lire-un-état">Lire un état</h3>

<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">ShoppingCart</span><span class="p">?&gt;</span> <span class="nf">GetCartAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</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">GetStateAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span><span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Si la clé n’existe pas, la méthode retourne <code class="language-plaintext highlighter-rouge">default(T)</code> (donc <code class="language-plaintext highlighter-rouge">null</code> pour un type référence).</p>

<h3 id="supprimer-un-état">Supprimer un état</h3>

<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">ClearCartAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</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">DeleteStateAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="exposition-via-des-endpoints">Exposition via des endpoints</h3>

<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">"/cart/{userId}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">ShoppingCart</span> <span class="n">cart</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">dapr</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">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">cart</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">MapGet</span><span class="p">(</span><span class="s">"/cart/{userId}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">cart</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span><span class="s">"statestore"</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">cart</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span> <span class="p">?</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="n">cart</span><span class="p">)</span> <span class="p">:</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</span><span class="p">();</span>
<span class="p">});</span>

<span class="n">app</span><span class="p">.</span><span class="nf">MapDelete</span><span class="p">(</span><span class="s">"/cart/{userId}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">dapr</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">$"cart-</span><span class="p">{</span><span class="n">userId</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">NoContent</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>

<h1 id="concurrence-optimiste-avec-les-etags">Concurrence optimiste avec les ETags</h1>

<p>La concurrence optimiste est gérée nativement par Dapr via les <strong>ETags</strong>. Un ETag est un identifiant de version associé à chaque valeur stockée. Lorsqu’on lit un état, on obtient l’ETag courant. Lors de l’écriture, on fournit cet ETag : si la valeur a été modifiée entre-temps par un autre processus, l’écriture échoue.</p>

<h2 id="lire-létat-avec-letag">Lire l’état avec l’ETag</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">UpdateCartSafelyAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">CartItem</span> <span class="n">newItem</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// GetStateAndETagAsync retourne la valeur ET l'ETag</span>
    <span class="kt">var</span> <span class="p">(</span><span class="n">cart</span><span class="p">,</span> <span class="n">etag</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="n">GetStateAndETagAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span>
        <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

    <span class="n">cart</span> <span class="p">??=</span> <span class="k">new</span> <span class="nf">ShoppingCart</span><span class="p">();</span>
    <span class="n">cart</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">newItem</span><span class="p">);</span>

    <span class="c1">// TrySaveStateAsync vérifie l'ETag avant d'écrire</span>
    <span class="kt">var</span> <span class="n">success</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">TrySaveStateAsync</span><span class="p">(</span>
        <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">cart</span><span class="p">,</span> <span class="n">etag</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(!</span><span class="n">success</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// L'état a été modifié par un autre processus entre la lecture et l'écriture.</span>
        <span class="c1">// On peut réessayer, fusionner ou signaler un conflit.</span>
        <span class="k">throw</span> <span class="k">new</span> <span class="nf">InvalidOperationException</span><span class="p">(</span>
            <span class="s">"Le panier a été modifié par un autre processus. Veuillez réessayer."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="supprimer-avec-etag">Supprimer avec ETag</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">DeleteCartSafelyAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="p">(</span><span class="n">_</span><span class="p">,</span> <span class="n">etag</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="n">GetStateAndETagAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span>
        <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

    <span class="kt">var</span> <span class="n">success</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">TryDeleteStateAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">etag</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(!</span><span class="n">success</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">"Le panier a été modifié entre-temps."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="pattern-retry-avec-concurrence-optimiste">Pattern retry avec concurrence optimiste</h2>

<p>Dans la pratique, on combine souvent la vérification d’ETag avec un pattern de retry :</p>

<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">AddItemToCartAsync</span><span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">CartItem</span> <span class="n">item</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">attempt</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">attempt</span> <span class="p">&lt;</span> <span class="n">maxRetries</span><span class="p">;</span> <span class="n">attempt</span><span class="p">++)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="p">(</span><span class="n">cart</span><span class="p">,</span> <span class="n">etag</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="n">GetStateAndETagAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span>
            <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

        <span class="n">cart</span> <span class="p">??=</span> <span class="k">new</span> <span class="nf">ShoppingCart</span><span class="p">();</span>
        <span class="n">cart</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">success</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">TrySaveStateAsync</span><span class="p">(</span>
            <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">cart</span><span class="p">,</span> <span class="n">etag</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">success</span><span class="p">)</span>
            <span class="k">return</span><span class="p">;</span>

        <span class="c1">// Attendre un court instant avant de réessayer</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">FromMilliseconds</span><span class="p">(</span><span class="m">50</span> <span class="p">*</span> <span class="p">(</span><span class="n">attempt</span> <span class="p">+</span> <span class="m">1</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">$"Impossible de mettre à jour le panier après </span><span class="p">{</span><span class="n">maxRetries</span><span class="p">}</span><span class="s"> tentatives."</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="stratégies-de-concurrence--first-write-wins-vs-last-write-wins">Stratégies de concurrence : first-write-wins vs last-write-wins</h1>

<p>Dapr supporte deux stratégies de concurrence, contrôlées par les <code class="language-plaintext highlighter-rouge">StateOptions</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// First-write-wins : l'écriture échoue si l'ETag ne correspond plus</span>
<span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">SaveStateAsync</span><span class="p">(</span>
    <span class="n">StoreName</span><span class="p">,</span> <span class="s">"my-key"</span><span class="p">,</span> <span class="k">value</span><span class="p">,</span>
    <span class="n">stateOptions</span><span class="p">:</span> <span class="k">new</span> <span class="n">StateOptions</span>
    <span class="p">{</span>
        <span class="n">Concurrency</span> <span class="p">=</span> <span class="n">ConcurrencyMode</span><span class="p">.</span><span class="n">FirstWrite</span>
    <span class="p">});</span>

<span class="c1">// Last-write-wins : l'écriture écrase toujours la valeur précédente</span>
<span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">SaveStateAsync</span><span class="p">(</span>
    <span class="n">StoreName</span><span class="p">,</span> <span class="s">"my-key"</span><span class="p">,</span> <span class="k">value</span><span class="p">,</span>
    <span class="n">stateOptions</span><span class="p">:</span> <span class="k">new</span> <span class="n">StateOptions</span>
    <span class="p">{</span>
        <span class="n">Concurrency</span> <span class="p">=</span> <span class="n">ConcurrencyMode</span><span class="p">.</span><span class="n">LastWrite</span>
    <span class="p">});</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Stratégie</th>
      <th>Comportement</th>
      <th>Cas d’usage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>FirstWrite</strong></td>
      <td>Échoue si l’ETag a changé</td>
      <td>Panier, stock, données critiques</td>
    </tr>
    <tr>
      <td><strong>LastWrite</strong></td>
      <td>Écrase sans vérification</td>
      <td>Cache, données temporaires, logs</td>
    </tr>
  </tbody>
</table>

<p>Par défaut, <code class="language-plaintext highlighter-rouge">SaveStateAsync</code> (sans ETag) utilise <strong>last-write-wins</strong>. Pour activer first-write-wins, utilisez <code class="language-plaintext highlighter-rouge">TrySaveStateAsync</code> avec l’ETag.</p>

<h1 id="cohérence--forte-vs-éventuelle">Cohérence : forte vs éventuelle</h1>

<p>Dapr permet de choisir le niveau de cohérence par opération :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Cohérence forte (lecture depuis le primaire)</span>
<span class="kt">var</span> <span class="n">cart</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span>
    <span class="n">StoreName</span><span class="p">,</span> <span class="s">"cart-42"</span><span class="p">,</span>
    <span class="n">consistencyMode</span><span class="p">:</span> <span class="n">ConsistencyMode</span><span class="p">.</span><span class="n">Strong</span><span class="p">);</span>

<span class="c1">// Cohérence éventuelle (lecture depuis une réplique, plus rapide)</span>
<span class="kt">var</span> <span class="n">cachedData</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">CachedData</span><span class="p">&gt;(</span>
    <span class="n">StoreName</span><span class="p">,</span> <span class="s">"cache-key"</span><span class="p">,</span>
    <span class="n">consistencyMode</span><span class="p">:</span> <span class="n">ConsistencyMode</span><span class="p">.</span><span class="n">Eventual</span><span class="p">);</span>
</code></pre></div></div>

<table>
  <thead>
    <tr>
      <th>Mode</th>
      <th>Garantie</th>
      <th>Performance</th>
      <th>Cas d’usage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Strong</strong></td>
      <td>Lecture la plus récente garantie</td>
      <td>Plus lent</td>
      <td>Solde de compte, stock</td>
    </tr>
    <tr>
      <td><strong>Eventual</strong></td>
      <td>La valeur peut être légèrement en retard</td>
      <td>Plus rapide</td>
      <td>Cache, préférences utilisateur</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>Le support de ces modes dépend du composant sous-jacent. Redis standalone ne supporte par exemple que la cohérence forte.</p>
</blockquote>

<h1 id="opérations-en-bulk">Opérations en bulk</h1>

<p>Pour des opérations sur plusieurs clés à la fois, Dapr offre des opérations bulk qui réduisent les allers-retours réseau :</p>

<h2 id="sauvegarder-plusieurs-états">Sauvegarder plusieurs états</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">SaveMultipleItemsAsync</span><span class="p">(</span><span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="n">Product</span><span class="p">&gt;</span> <span class="n">products</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">states</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="n">SaveStateItem</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;(</span><span class="n">StoreName</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">Key</span><span class="p">,</span> <span class="n">p</span><span class="p">.</span><span class="n">Value</span><span class="p">)).</span><span class="nf">ToList</span><span class="p">();</span>

    <span class="c1">// Utiliser l'API HTTP directement via le sidecar</span>
    <span class="c1">// ou construire les requêtes manuellement</span>

    <span class="c1">// Avec SaveStateAsync, on peut passer une liste</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">product</span><span class="p">)</span> <span class="k">in</span> <span class="n">products</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">SaveStateAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="n">key</span><span class="p">,</span> <span class="n">product</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="lire-plusieurs-états-en-une-seule-fois">Lire plusieurs états en une seule fois</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">IReadOnlyList</span><span class="p">&lt;</span><span class="n">BulkStateItem</span><span class="p">&gt;&gt;</span> <span class="nf">GetMultipleStatesAsync</span><span class="p">(</span>
    <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="n">keys</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">items</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">GetBulkStateAsync</span><span class="p">(</span>
        <span class="n">StoreName</span><span class="p">,</span> <span class="n">keys</span><span class="p">.</span><span class="nf">ToList</span><span class="p">(),</span> <span class="n">parallelism</span><span class="p">:</span> <span class="m">10</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">items</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>La méthode <code class="language-plaintext highlighter-rouge">GetBulkStateAsync</code> retourne une liste de <code class="language-plaintext highlighter-rouge">BulkStateItem</code> contenant la clé, la valeur (en <code class="language-plaintext highlighter-rouge">string</code> JSON) et l’ETag pour chaque entrée.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">keys</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;</span> <span class="p">{</span> <span class="s">"product-1"</span><span class="p">,</span> <span class="s">"product-2"</span><span class="p">,</span> <span class="s">"product-3"</span> <span class="p">};</span>
<span class="kt">var</span> <span class="n">results</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">GetBulkStateAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="n">keys</span><span class="p">,</span> <span class="n">parallelism</span><span class="p">:</span> <span class="m">10</span><span class="p">);</span>

<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">item</span> <span class="k">in</span> <span class="n">results</span><span class="p">)</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">item</span><span class="p">.</span><span class="n">Value</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">JsonSerializer</span><span class="p">.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;(</span><span class="n">item</span><span class="p">.</span><span class="n">Value</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">$"</span><span class="p">{</span><span class="n">item</span><span class="p">.</span><span class="n">Key</span><span class="p">}</span><span class="s"> → </span><span class="p">{</span><span class="n">product</span><span class="p">?.</span><span class="n">Name</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="transactions">Transactions</h1>

<p>Certains state stores supportent les <strong>transactions</strong> (Redis, PostgreSQL, Cosmos DB, MongoDB…). Dapr permet d’exécuter plusieurs opérations de manière atomique :</p>

<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">TransferStockAsync</span><span class="p">(</span>
    <span class="kt">string</span> <span class="n">sourceProductId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">targetProductId</span><span class="p">,</span> <span class="kt">int</span> <span class="n">quantity</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Lire les deux stocks</span>
    <span class="kt">var</span> <span class="n">sourceStock</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(</span>
        <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"stock-</span><span class="p">{</span><span class="n">sourceProductId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">targetStock</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(</span>
        <span class="n">StoreName</span><span class="p">,</span> <span class="s">$"stock-</span><span class="p">{</span><span class="n">targetProductId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">sourceStock</span> <span class="p">&lt;</span> <span class="n">quantity</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">"Stock insuffisant."</span><span class="p">);</span>

    <span class="c1">// Préparer les opérations transactionnelles</span>
    <span class="kt">var</span> <span class="n">operations</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">StateTransactionRequest</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="k">new</span><span class="p">(</span>
            <span class="s">$"stock-</span><span class="p">{</span><span class="n">sourceProductId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">SerializeToUtf8Bytes</span><span class="p">(</span><span class="n">sourceStock</span> <span class="p">-</span> <span class="n">quantity</span><span class="p">),</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Upsert</span><span class="p">),</span>
        <span class="k">new</span><span class="p">(</span>
            <span class="s">$"stock-</span><span class="p">{</span><span class="n">targetProductId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">SerializeToUtf8Bytes</span><span class="p">(</span><span class="n">targetStock</span> <span class="p">+</span> <span class="n">quantity</span><span class="p">),</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Upsert</span><span class="p">)</span>
    <span class="p">};</span>

    <span class="c1">// Exécuter la transaction : les deux écritures sont atomiques</span>
    <span class="k">await</span> <span class="n">_daprClient</span><span class="p">.</span><span class="nf">ExecuteStateTransactionAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="n">operations</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Les opérations disponibles dans une transaction sont :</p>

<table>
  <thead>
    <tr>
      <th>Opération</th>
      <th>Description</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Upsert</code></td>
      <td>Créer ou mettre à jour une clé</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Delete</code></td>
      <td>Supprimer une clé</td>
    </tr>
  </tbody>
</table>

<p>Si l’une des opérations échoue, <strong>aucune modification n’est appliquée</strong> (rollback automatique).</p>

<h2 id="exemple-concret--passer-une-commande">Exemple concret : passer une commande</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">PlaceOrderAsync</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">operations</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">StateTransactionRequest</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="c1">// Sauvegarder la commande</span>
        <span class="k">new</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="n">Id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">SerializeToUtf8Bytes</span><span class="p">(</span><span class="n">order</span><span class="p">),</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Upsert</span><span class="p">),</span>

        <span class="c1">// Mettre à jour le statut du panier</span>
        <span class="k">new</span><span class="p">(</span>
            <span class="s">$"cart-</span><span class="p">{</span><span class="n">order</span><span class="p">.</span><span class="n">UserId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">SerializeToUtf8Bytes</span><span class="p">(</span><span class="k">new</span> <span class="n">ShoppingCart</span> <span class="p">{</span> <span class="n">Status</span> <span class="p">=</span> <span class="s">"Ordered"</span> <span class="p">}),</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Upsert</span><span class="p">),</span>

        <span class="c1">// Supprimer le panier temporaire</span>
        <span class="k">new</span><span class="p">(</span>
            <span class="s">$"temp-cart-</span><span class="p">{</span><span class="n">order</span><span class="p">.</span><span class="n">UserId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="k">null</span><span class="p">,</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Delete</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">ExecuteStateTransactionAsync</span><span class="p">(</span><span class="n">StoreName</span><span class="p">,</span> <span class="n">operations</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h1 id="ttl-time-to-live">TTL (Time-To-Live)</h1>

<p>Dapr permet de définir un <strong>TTL</strong> (durée de vie) sur chaque état. L’état est automatiquement supprimé après l’expiration du TTL :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// L'état expirera automatiquement après 1 heure</span>
<span class="kt">var</span> <span class="n">metadata</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Dictionary</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">,</span> <span class="kt">string</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="p">{</span> <span class="s">"ttlInSeconds"</span><span class="p">,</span> <span class="s">"3600"</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">SaveStateAsync</span><span class="p">(</span>
    <span class="n">StoreName</span><span class="p">,</span> <span class="s">"session-abc123"</span><span class="p">,</span> <span class="n">sessionData</span><span class="p">,</span> <span class="n">metadata</span><span class="p">:</span> <span class="n">metadata</span><span class="p">);</span>
</code></pre></div></div>

<p>Cas d’usage typiques du TTL :</p>

<ul>
  <li><strong>Sessions utilisateur</strong> : expiration automatique après inactivité.</li>
  <li><strong>Cache temporaire</strong> : données mises en cache avec expiration.</li>
  <li><strong>Tokens temporaires</strong> : codes de vérification, OTP, etc.</li>
  <li><strong>Rate limiting</strong> : compteurs d’appels avec fenêtre glissante.</li>
</ul>

<blockquote>
  <p>Tous les state stores ne supportent pas le TTL. Redis, Cosmos DB et PostgreSQL le supportent nativement.</p>
</blockquote>

<h1 id="state-store-et-api-http">State Store et API HTTP</h1>

<p>Outre le SDK .NET, il est parfois utile de connaître l’API HTTP sous-jacente du sidecar :</p>

<h2 id="sauvegarder-un-état-1">Sauvegarder un état</h2>

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

[
  {
    "key": "cart-user42",
    "value": {
      "items": [
        { "productId": 1, "quantity": 2 }
      ]
    }
  }
]
</span></code></pre></div></div>

<h2 id="lire-un-état-1">Lire un état</h2>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">GET http://localhost:3500/v1.0/state/statestore/cart-user42
</span></code></pre></div></div>

<h2 id="supprimer-un-état-1">Supprimer un état</h2>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">DELETE http://localhost:3500/v1.0/state/statestore/cart-user42
</span></code></pre></div></div>

<h2 id="transaction">Transaction</h2>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST http://localhost:3500/v1.0/state/statestore/transaction
Content-Type: application/json

{
  "operations": [
    {
      "operation": "upsert",
      "request": {
        "key": "order-123",
        "value": { "status": "created" }
      }
    },
    {
      "operation": "delete",
      "request": {
        "key": "temp-cart-user42"
      }
    }
  ]
}
</span></code></pre></div></div>

<h1 id="requêtage-détat-state-query">Requêtage d’état (State Query)</h1>

<p>Certains composants de state store supportent le <strong>requêtage</strong> (query), qui permet de filtrer les états stockés sans connaître les clés à l’avance. Cette fonctionnalité est en alpha et disponible pour Cosmos DB, MongoDB et PostgreSQL.</p>

<div class="language-http highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="err">POST http://localhost:3500/v1.0-alpha1/state/statestore/query
Content-Type: application/json

{
  "filter": {
    "AND": [
      {
        "EQ": { "value.status": "active" }
      },
      {
        "GT": { "value.totalAmount": 100 }
      }
    ]
  },
  "sort": [
    { "key": "value.createdAt", "order": "DESC" }
  ],
  "page": {
    "limit": 10
  }
}
</span></code></pre></div></div>

<p>Cette API est utile pour des scénarios de type « recherche d’états par critères », mais pour des requêtes complexes, un modèle de lecture dédié (CQRS) reste plus approprié.</p>

<h1 id="chiffrement-détat">Chiffrement d’état</h1>

<p>Dapr supporte le <strong>chiffrement au repos</strong> (encryption at rest) pour les state stores. Cela est configuré au niveau du composant :</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>
    <span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">primaryEncryptionKey</span>
      <span class="na">value</span><span class="pi">:</span> <span class="s2">"</span><span class="s">&lt;clé</span><span class="nv"> </span><span class="s">AES</span><span class="nv"> </span><span class="s">256</span><span class="nv"> </span><span class="s">bits</span><span class="nv"> </span><span class="s">encodée</span><span class="nv"> </span><span class="s">en</span><span class="nv"> </span><span class="s">base64&gt;"</span>
</code></pre></div></div>

<p>Avec cette configuration, toutes les valeurs sont chiffrées avant d’être envoyées au store et déchiffrées à la lecture, de manière transparente pour l’application.</p>

<h1 id="exemple-complet--service-de-gestion-de-panier">Exemple complet : service de gestion de panier</h1>

<p>Voici un service complet combinant les différentes fonctionnalités :</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="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="k">const</span> <span class="kt">string</span> <span class="n">storeName</span> <span class="p">=</span> <span class="s">"statestore"</span><span class="p">;</span>

<span class="c1">// Récupérer le panier</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/cart/{userId}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">cart</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span><span class="n">storeName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">cart</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span> <span class="p">?</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="n">cart</span><span class="p">)</span> <span class="p">:</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="nf">ShoppingCart</span><span class="p">());</span>
<span class="p">});</span>

<span class="c1">// Ajouter un article au panier (avec concurrence optimiste)</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/cart/{userId}/items"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">CartItem</span> <span class="n">item</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">const</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="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="kt">var</span> <span class="p">(</span><span class="n">cart</span><span class="p">,</span> <span class="n">etag</span><span class="p">)</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="n">GetStateAndETagAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span>
            <span class="n">storeName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

        <span class="n">cart</span> <span class="p">??=</span> <span class="k">new</span> <span class="nf">ShoppingCart</span><span class="p">();</span>

        <span class="kt">var</span> <span class="n">existing</span> <span class="p">=</span> <span class="n">cart</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="nf">FirstOrDefault</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="n">ProductId</span> <span class="p">==</span> <span class="n">item</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">existing</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span><span class="p">)</span>
            <span class="n">existing</span><span class="p">.</span><span class="n">Quantity</span> <span class="p">+=</span> <span class="n">item</span><span class="p">.</span><span class="n">Quantity</span><span class="p">;</span>
        <span class="k">else</span>
            <span class="n">cart</span><span class="p">.</span><span class="n">Items</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>

        <span class="n">cart</span><span class="p">.</span><span class="n">UpdatedAt</span> <span class="p">=</span> <span class="n">DateTime</span><span class="p">.</span><span class="n">UtcNow</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="nf">TrySaveStateAsync</span><span class="p">(</span><span class="n">storeName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">cart</span><span class="p">,</span> <span class="n">etag</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="n">cart</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="m">50</span> <span class="p">*</span> <span class="p">(</span><span class="n">i</span> <span class="p">+</span> <span class="m">1</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">Conflict</span><span class="p">(</span><span class="s">"Le panier a été modifié par un autre processus."</span><span class="p">);</span>
<span class="p">});</span>

<span class="c1">// Passer la commande (transaction)</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapPost</span><span class="p">(</span><span class="s">"/cart/{userId}/checkout"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">userId</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">cart</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="n">GetStateAsync</span><span class="p">&lt;</span><span class="n">ShoppingCart</span><span class="p">&gt;(</span><span class="n">storeName</span><span class="p">,</span> <span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">cart</span> <span class="k">is</span> <span class="k">null</span> <span class="p">||</span> <span class="n">cart</span><span class="p">.</span><span class="n">Items</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">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">BadRequest</span><span class="p">(</span><span class="s">"Le panier est vide."</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">Id</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="nf">ToString</span><span class="p">(),</span>
        <span class="n">UserId</span> <span class="p">=</span> <span class="n">userId</span><span class="p">,</span>
        <span class="n">Items</span> <span class="p">=</span> <span class="n">cart</span><span class="p">.</span><span class="n">Items</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>
        <span class="n">Status</span> <span class="p">=</span> <span class="s">"Created"</span>
    <span class="p">};</span>

    <span class="kt">var</span> <span class="n">operations</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">StateTransactionRequest</span><span class="p">&gt;</span>
    <span class="p">{</span>
        <span class="k">new</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="n">Id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="n">JsonSerializer</span><span class="p">.</span><span class="nf">SerializeToUtf8Bytes</span><span class="p">(</span><span class="n">order</span><span class="p">),</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Upsert</span><span class="p">),</span>
        <span class="k">new</span><span class="p">(</span><span class="s">$"cart-</span><span class="p">{</span><span class="n">userId</span><span class="p">}</span><span class="s">"</span><span class="p">,</span>
            <span class="k">null</span><span class="p">,</span>
            <span class="n">StateOperationType</span><span class="p">.</span><span class="n">Delete</span><span class="p">)</span>
    <span class="p">};</span>

    <span class="k">await</span> <span class="n">dapr</span><span class="p">.</span><span class="nf">ExecuteStateTransactionAsync</span><span class="p">(</span><span class="n">storeName</span><span class="p">,</span> <span class="n">operations</span><span class="p">);</span>

    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Created</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">Id</span><span class="p">}</span><span class="s">"</span><span class="p">,</span> <span class="n">order</span><span class="p">);</span>
<span class="p">});</span>

<span class="c1">// Consulter une commande</span>
<span class="n">app</span><span class="p">.</span><span class="nf">MapGet</span><span class="p">(</span><span class="s">"/orders/{orderId}"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="kt">string</span> <span class="n">orderId</span><span class="p">,</span> <span class="n">DaprClient</span> <span class="n">dapr</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="k">await</span> <span class="n">dapr</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="n">storeName</span><span class="p">,</span> <span class="s">$"order-</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">order</span> <span class="k">is</span> <span class="n">not</span> <span class="k">null</span> <span class="p">?</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Ok</span><span class="p">(</span><span class="n">order</span><span class="p">)</span> <span class="p">:</span> <span class="n">Results</span><span class="p">.</span><span class="nf">NotFound</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>

<span class="c1">// --- Modèles ---</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">ShoppingCart</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">CartItem</span><span class="p">&gt;</span> <span class="n">Items</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="p">[];</span>
    <span class="k">public</span> <span class="n">DateTime</span> <span class="n">UpdatedAt</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">CartItem</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">ProductId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">Price</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Quantity</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">Order</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">UserId</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">CartItem</span><span class="p">&gt;</span> <span class="n">Items</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="p">[];</span>
    <span class="k">public</span> <span class="n">DateTime</span> <span class="n">CreatedAt</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Status</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

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

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Initialiser Dapr (si ce n'est pas déjà fait)</span>
dapr init

<span class="c"># Lancer l'application avec le sidecar Dapr</span>
dapr run <span class="nt">--app-id</span> cart-service <span class="nt">--app-port</span> 5000 <span class="nt">--resources-path</span> ./components <span class="nt">--</span> dotnet run
</code></pre></div></div>

<p>Le dossier <code class="language-plaintext highlighter-rouge">./components</code> contient le fichier YAML du state store (ex. : <code class="language-plaintext highlighter-rouge">statestore.yaml</code> avec Redis).</p>

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

<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="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">CartService</span><span class="p">&gt;(</span><span class="s">"cart-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="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="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">GET/POST/DELETE http://localhost:3500/v1.0/state/{store-name}/{key}</code></td>
    </tr>
    <tr>
      <td><strong>Concurrence</strong></td>
      <td>ETags, first-write-wins ou last-write-wins</td>
    </tr>
    <tr>
      <td><strong>Cohérence</strong></td>
      <td>Strong ou Eventual (selon le composant)</td>
    </tr>
    <tr>
      <td><strong>Transactions</strong></td>
      <td>Opérations atomiques multi-clés (upsert/delete)</td>
    </tr>
    <tr>
      <td><strong>TTL</strong></td>
      <td>Expiration automatique configurable par clé</td>
    </tr>
    <tr>
      <td><strong>Bulk</strong></td>
      <td>Lecture/écriture par lots pour réduire la latence</td>
    </tr>
    <tr>
      <td><strong>Chiffrement</strong></td>
      <td>Chiffrement au repos transparent</td>
    </tr>
    <tr>
      <td><strong>Stores supportés</strong></td>
      <td>Redis, PostgreSQL, Cosmos DB, MongoDB, MySQL, DynamoDB…</td>
    </tr>
    <tr>
      <td><strong>SDK .NET</strong></td>
      <td><code class="language-plaintext highlighter-rouge">DaprClient.SaveStateAsync</code>, <code class="language-plaintext highlighter-rouge">GetStateAsync</code>, <code class="language-plaintext highlighter-rouge">ExecuteStateTransactionAsync</code></td>
    </tr>
  </tbody>
</table>

<p>La gestion d’état Dapr offre une abstraction clé/valeur puissante et portable, avec des garanties de concurrence et de transaction intégrées, tout en découplant complètement le code applicatif du store sous-jacent.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="dapr" /><category term="microservices" /><category term="state-management" /><summary type="html"><![CDATA[La gestion d’état (State Management) est l’un des building blocks essentiels de Dapr. Il fournit une API clé/valeur unifiée pour stocker, lire, supprimer et transactionner de l’état, avec concurrence optimiste (ETags), sans coupler votre code à un store spécifique. En .NET, le SDK Dapr rend cette API simple et idiomatique.]]></summary></entry><entry><title type="html">gRPC Code-First en .NET : se passer des fichiers .proto</title><link href="http://guym.fr/2026/02/28/grpc-codefirst.html" rel="alternate" type="text/html" title="gRPC Code-First en .NET : se passer des fichiers .proto" /><published>2026-02-28T00:00:00+01:00</published><updated>2026-02-28T00:00:00+01:00</updated><id>http://guym.fr/2026/02/28/grpc-codefirst</id><content type="html" xml:base="http://guym.fr/2026/02/28/grpc-codefirst.html"><![CDATA[<p>L’approche classique de gRPC repose sur des fichiers <code class="language-plaintext highlighter-rouge">.proto</code> (Contract-First) : on définit le contrat Protobuf, puis on génère le code C# à partir de ce contrat. L’approche <strong>Code-First</strong> inverse le processus : on écrit des classes et interfaces C# classiques, et le contrat gRPC est généré automatiquement. Grâce à la bibliothèque <strong>protobuf-net.Grpc</strong>, on peut créer des services gRPC en .NET sans écrire une seule ligne de Protobuf, en utilisant des types C# natifs, des <code class="language-plaintext highlighter-rouge">DataContract</code>, ou de simples POCOs.</p>

<!--more-->

<div class="panel_seriesNote">
	<p>Cet article fait partie de la série <strong>gRPC</strong> : <strong> 2</strong> sur <strong>2</strong>.</p>
	<ul>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 1 - 
		
			<a href="/2026/01/24/Grpc.html">gRPC : comprendre le protocole et ses différences avec HTTP/REST</a>
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
		
		<li>Part 2 - 
		
			Cet article
		
		</li>
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	
	</ul>
</div>

<h1 id="contract-first-vs-code-first">Contract-First vs Code-First</h1>

<h2 id="lapproche-contract-first-classique">L’approche Contract-First (classique)</h2>

<p>C’est l’approche standard de gRPC :</p>

<ol>
  <li>On écrit un fichier <code class="language-plaintext highlighter-rouge">.proto</code> définissant les messages et services.</li>
  <li>Le compilateur <code class="language-plaintext highlighter-rouge">protoc</code> (ou les build tools .NET) génère du code C#.</li>
  <li>On implémente le service en héritant de la classe de base générée.</li>
</ol>

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

<span class="kd">service</span> <span class="n">ProductService</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">Product</span><span class="p">);</span>
<span class="p">}</span>

<span class="kd">message</span> <span class="nc">GetProductRequest</span> <span class="p">{</span>
  <span class="kt">int32</span> <span class="na">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">Product</span> <span class="p">{</span>
  <span class="kt">int32</span> <span class="na">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">double</span> <span class="na">price</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Avantages</strong> : contrat multi-langage, standard Protobuf, interop avec Go, Java, Python, etc.</p>

<p><strong>Inconvénients</strong> : outillage <code class="language-plaintext highlighter-rouge">.proto</code>, génération de code, types générés parfois verbeux, pas de réutilisation directe des types du domaine.</p>

<h2 id="lapproche-code-first">L’approche Code-First</h2>

<p>On écrit directement en C# :</p>

<ol>
  <li>On définit des <strong>interfaces</strong> pour les services et des <strong>classes</strong> pour les messages.</li>
  <li>On implémente le service normalement.</li>
  <li>La bibliothèque <code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code> se charge de la sérialisation et du mapping gRPC.</li>
</ol>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Contrat partagé (C# pur)</span>
<span class="p">[</span><span class="n">ServiceContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IProductService</span>
<span class="p">{</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;</span> <span class="nf">GetProductAsync</span><span class="p">(</span><span class="n">GetProductRequest</span> <span class="n">request</span><span class="p">);</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">GetProductRequest</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">1</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">Product</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">1</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">2</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="s">""</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">3</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">double</span> <span class="n">Price</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><strong>Avantages</strong> : pas de <code class="language-plaintext highlighter-rouge">.proto</code>, pas de génération de code, types C# natifs, intégration naturelle avec le domaine.</p>

<p><strong>Inconvénients</strong> : limité à l’écosystème .NET (pas d’interop directe avec d’autres langages), dépendance à <code class="language-plaintext highlighter-rouge">protobuf-net</code>.</p>

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

<h2 id="structure-de-la-solution">Structure de la solution</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Solution/
├── Shared/                    ← Contrats partagés (interfaces + DTOs)
│   └── Shared.csproj
├── ProductService/            ← Serveur gRPC
│   └── ProductService.csproj
└── ProductClient/             ← Client gRPC
    └── ProductClient.csproj
</code></pre></div></div>

<p>Le projet <strong>Shared</strong> contient les interfaces de service et les classes de messages. Il est référencé par le serveur et le client.</p>

<h2 id="étape-1--le-projet-partagé-shared">Étape 1 : Le projet partagé (Shared)</h2>

<pre><code class="language-dotnetcli">dotnet new classlib -n Shared
cd Shared
dotnet add package protobuf-net.Grpc
</code></pre>

<h3 id="définir-les-messages">Définir les messages</h3>

<p>Les messages sont de simples classes C# décorées avec <code class="language-plaintext highlighter-rouge">[DataContract]</code> et <code class="language-plaintext highlighter-rouge">[DataMember]</code> :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.Runtime.Serialization</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">Shared</span><span class="p">;</span>

<span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">GetProductRequest</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">1</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductResponse</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">1</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Id</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">2</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="s">""</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">3</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Description</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="s">""</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">4</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">Price</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">5</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">InStock</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductListResponse</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">1</span><span class="p">)]</span>
    <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="n">Products</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="p">[];</span>
<span class="p">}</span>

<span class="p">[</span><span class="n">DataContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">CreateProductRequest</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">1</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="s">""</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">2</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Description</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="s">""</span><span class="p">;</span>

    <span class="p">[</span><span class="nf">DataMember</span><span class="p">(</span><span class="n">Order</span> <span class="p">=</span> <span class="m">3</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">decimal</span> <span class="n">Price</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<blockquote>
  <p><strong>Note</strong> : <code class="language-plaintext highlighter-rouge">[DataMember(Order = N)]</code> correspond aux identifiants de champ Protobuf. L’ordre est crucial pour la compatibilité binaire — ne jamais le changer une fois en production.</p>
</blockquote>

<h3 id="définir-le-contrat-de-service">Définir le contrat de service</h3>

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

<span class="k">namespace</span> <span class="nn">Shared</span><span class="p">;</span>

<span class="p">[</span><span class="n">ServiceContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IProductService</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">GetProductAsync</span><span class="p">(</span><span class="n">GetProductRequest</span> <span class="n">request</span><span class="p">);</span>

    <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductListResponse</span><span class="p">&gt;</span> <span class="nf">GetAllProductsAsync</span><span class="p">();</span>

    <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">CreateProductAsync</span><span class="p">(</span><span class="n">CreateProductRequest</span> <span class="n">request</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Les attributs <code class="language-plaintext highlighter-rouge">[ServiceContract]</code> et <code class="language-plaintext highlighter-rouge">[OperationContract]</code> proviennent de <code class="language-plaintext highlighter-rouge">System.ServiceModel</code> (inclus via <code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code>). Ils ressemblent à WCF, mais ici ils servent uniquement de marqueurs pour <code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code>.</p>

<blockquote>
  <p><strong>Remarque</strong> : les méthodes sans paramètre (comme <code class="language-plaintext highlighter-rouge">GetAllProductsAsync()</code>) sont supportées. <code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code> gère automatiquement les appels sans message d’entrée.</p>
</blockquote>

<h2 id="étape-2--le-serveur-productservice">Étape 2 : Le serveur (ProductService)</h2>

<pre><code class="language-dotnetcli">dotnet new web -n ProductService
cd ProductService
dotnet add package protobuf-net.Grpc.AspNetCore
dotnet add reference ../Shared/Shared.csproj
</code></pre>

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

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">Shared</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">ProductServiceImpl</span> <span class="p">:</span> <span class="n">IProductService</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">ProductResponse</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">Id</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">"Switches Cherry MX Red"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">89.99</span><span class="n">m</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">Id</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">"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="n">m</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">Id</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\" 4K"</span><span class="p">,</span> <span class="n">Description</span> <span class="p">=</span> <span class="s">"IPS, 144Hz, HDR600"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">449.99</span><span class="n">m</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">private</span> <span class="k">static</span> <span class="kt">int</span> <span class="n">_nextId</span> <span class="p">=</span> <span class="m">4</span><span class="p">;</span>

    <span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">GetProductAsync</span><span class="p">(</span><span class="n">GetProductRequest</span> <span class="n">request</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">Id</span> <span class="p">==</span> <span class="n">request</span><span class="p">.</span><span class="n">Id</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="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">Id</span><span class="p">}</span><span class="s"> introuvable"</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="n">Task</span><span class="p">&lt;</span><span class="n">ProductListResponse</span><span class="p">&gt;</span> <span class="nf">GetAllProductsAsync</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="k">new</span> <span class="n">ProductListResponse</span>
        <span class="p">{</span>
            <span class="n">Products</span> <span class="p">=</span> <span class="n">Products</span><span class="p">.</span><span class="nf">ToList</span><span class="p">()</span>
        <span class="p">});</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">CreateProductAsync</span><span class="p">(</span><span class="n">CreateProductRequest</span> <span class="n">request</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="k">new</span> <span class="n">ProductResponse</span>
        <span class="p">{</span>
            <span class="n">Id</span> <span class="p">=</span> <span class="n">_nextId</span><span class="p">++,</span>
            <span class="n">Name</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="n">Description</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Description</span><span class="p">,</span>
            <span class="n">Price</span> <span class="p">=</span> <span class="n">request</span><span class="p">.</span><span class="n">Price</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="n">Products</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">product</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="p">}</span>
</code></pre></div></div>

<p>C’est une implémentation C# classique d’une interface. Pas de classe de base gRPC générée, pas de <code class="language-plaintext highlighter-rouge">ServerCallContext</code> obligatoire.</p>

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

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">ProductService.Services</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">ProtoBuf.Grpc.Server</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">// Ajouter les services gRPC code-first</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">AddCodeFirstGrpc</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">// Mapper le service</span>
<span class="n">app</span><span class="p">.</span><span class="n">MapGrpcService</span><span class="p">&lt;</span><span class="n">ProductServiceImpl</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>On utilise <code class="language-plaintext highlighter-rouge">AddCodeFirstGrpc()</code> au lieu de <code class="language-plaintext highlighter-rouge">AddGrpc()</code>, et <code class="language-plaintext highlighter-rouge">MapGrpcService&lt;T&gt;()</code> fonctionne exactement de la même manière.</p>

<h3 id="configuration-kestrel">Configuration Kestrel</h3>

<p>Dans <code class="language-plaintext highlighter-rouge">appsettings.json</code>, on configure le port 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>

<h2 id="étape-3--le-client-productclient">Étape 3 : Le client (ProductClient)</h2>

<pre><code class="language-dotnetcli">dotnet new console -n ProductClient
cd ProductClient
dotnet add package Grpc.Net.Client
dotnet add package protobuf-net.Grpc
dotnet add reference ../Shared/Shared.csproj
</code></pre>

<h3 id="appeler-le-service">Appeler le service</h3>

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

<span class="c1">// Créer le channel gRPC</span>
<span class="k">using</span> <span class="nn">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:5001"</span><span class="p">);</span>

<span class="c1">// Créer le client code-first à partir de l'interface</span>
<span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="n">channel</span><span class="p">.</span><span class="n">CreateGrpcService</span><span class="p">&lt;</span><span class="n">IProductService</span><span class="p">&gt;();</span>

<span class="c1">// Récupérer tous les produits</span>
<span class="kt">var</span> <span class="n">allProducts</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">GetAllProductsAsync</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">"=== Tous les produits ==="</span><span class="p">);</span>
<span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">p</span> <span class="k">in</span> <span class="n">allProducts</span><span class="p">.</span><span class="n">Products</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">$"  [</span><span class="p">{</span><span class="n">p</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">p</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s"> - </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">C</span><span class="p">}</span><span class="s"> </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="s">"✓"</span> <span class="p">:</span> <span class="s">"✗"</span><span class="p">)}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// Récupérer un produit par ID</span>
<span class="kt">var</span> <span class="n">product</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">Id</span> <span class="p">=</span> <span class="m">1</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">$"\nProduit #1 : </span><span class="p">{</span><span class="n">product</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s"> - </span><span class="p">{</span><span class="n">product</span><span class="p">.</span><span class="n">Description</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>

<span class="c1">// Créer un nouveau produit</span>
<span class="kt">var</span> <span class="n">created</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">CreateProductAsync</span><span class="p">(</span><span class="k">new</span> <span class="n">CreateProductRequest</span>
<span class="p">{</span>
    <span class="n">Name</span> <span class="p">=</span> <span class="s">"Webcam 4K"</span><span class="p">,</span>
    <span class="n">Description</span> <span class="p">=</span> <span class="s">"Autofocus, HDR, microphone intégré"</span><span class="p">,</span>
    <span class="n">Price</span> <span class="p">=</span> <span class="m">129.99</span><span class="n">m</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">$"\nCréé : [</span><span class="p">{</span><span class="n">created</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">created</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s"> - </span><span class="p">{</span><span class="n">created</span><span class="p">.</span><span class="n">Price</span><span class="p">:</span><span class="n">C</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>La méthode clé est <code class="language-plaintext highlighter-rouge">channel.CreateGrpcService&lt;IProductService&gt;()</code> (méthode d’extension de <code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code>). Elle crée un proxy client à partir de l’interface partagée, sans code généré.</p>

<h1 id="utiliser-avec-linjection-de-dépendances">Utiliser avec l’injection de dépendances</h1>

<h2 id="enregistrer-le-client-dans-le-conteneur-di">Enregistrer le client dans le conteneur DI</h2>

<p>Pour une application ASP.NET Core qui consomme un service gRPC code-first :</p>

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

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">ProtoBuf.Grpc.ClientFactory</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Shared</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">// Enregistrer le client gRPC code-first 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">AddCodeFirstGrpcClient</span><span class="p">&lt;</span><span class="n">IProductService</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:5001"</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="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">IProductService</span> <span class="n">productService</span><span class="p">)</span> <span class="p">=&gt;</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">productService</span><span class="p">.</span><span class="nf">GetAllProductsAsync</span><span class="p">();</span>
    <span class="k">return</span> <span class="n">result</span><span class="p">.</span><span class="n">Products</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/{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">IProductService</span> <span class="n">productService</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">product</span> <span class="p">=</span> <span class="k">await</span> <span class="n">productService</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">Id</span> <span class="p">=</span> <span class="n">id</span> <span class="p">});</span>
    <span class="k">return</span> <span class="n">product</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><code class="language-plaintext highlighter-rouge">AddCodeFirstGrpcClient&lt;IProductService&gt;</code> enregistre l’interface directement dans le conteneur DI. On peut ensuite l’injecter dans n’importe quel service ou endpoint, comme n’importe quelle dépendance.</p>

<h1 id="types-supportés">Types supportés</h1>

<p><code class="language-plaintext highlighter-rouge">protobuf-net</code> supporte un large éventail de types C# natifs :</p>

<table>
  <thead>
    <tr>
      <th>Type C#</th>
      <th>Protobuf équivalent</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">long</code>, <code class="language-plaintext highlighter-rouge">uint</code>, <code class="language-plaintext highlighter-rouge">ulong</code></td>
      <td><code class="language-plaintext highlighter-rouge">int32</code>, <code class="language-plaintext highlighter-rouge">int64</code>, <code class="language-plaintext highlighter-rouge">uint32</code>, <code class="language-plaintext highlighter-rouge">uint64</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">float</code>, <code class="language-plaintext highlighter-rouge">double</code></td>
      <td><code class="language-plaintext highlighter-rouge">float</code>, <code class="language-plaintext highlighter-rouge">double</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">bool</code></td>
      <td><code class="language-plaintext highlighter-rouge">bool</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">string</code></td>
      <td><code class="language-plaintext highlighter-rouge">string</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">byte[]</code></td>
      <td><code class="language-plaintext highlighter-rouge">bytes</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">decimal</code></td>
      <td>Extension protobuf-net (mappé en <code class="language-plaintext highlighter-rouge">bcl.Decimal</code>)</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">DateTime</code>, <code class="language-plaintext highlighter-rouge">DateTimeOffset</code></td>
      <td>Extension protobuf-net</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">TimeSpan</code></td>
      <td>Extension protobuf-net</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Guid</code></td>
      <td>Extension protobuf-net</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">List&lt;T&gt;</code>, <code class="language-plaintext highlighter-rouge">T[]</code></td>
      <td><code class="language-plaintext highlighter-rouge">repeated</code></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">Dictionary&lt;TKey, TValue&gt;</code></td>
      <td><code class="language-plaintext highlighter-rouge">map</code></td>
    </tr>
    <tr>
      <td>types <code class="language-plaintext highlighter-rouge">nullable</code> (<code class="language-plaintext highlighter-rouge">int?</code>, etc.)</td>
      <td>Champs optionnels</td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">enum</code></td>
      <td><code class="language-plaintext highlighter-rouge">enum</code> Protobuf</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>Attention</strong> : les types spécifiques .NET (<code class="language-plaintext highlighter-rouge">decimal</code>, <code class="language-plaintext highlighter-rouge">DateTime</code>, <code class="language-plaintext highlighter-rouge">Guid</code>, etc.) utilisent des extensions <code class="language-plaintext highlighter-rouge">protobuf-net</code> qui ne sont <strong>pas interopérables</strong> avec des clients dans d’autres langages. Si vous avez besoin d’interop multi-langage, restez sur les types de base.</p>
</blockquote>

<h1 id="streaming">Streaming</h1>

<p><code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code> supporte les 4 modes de streaming gRPC via <code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code> :</p>

<h2 id="server-streaming">Server Streaming</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Contrat</span>
<span class="p">[</span><span class="n">ServiceContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IProductService</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span>
    <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">StreamAllProductsAsync</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>
</code></pre></div></div>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Implémentation serveur</span>
<span class="k">public</span> <span class="k">async</span> <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">StreamAllProductsAsync</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">cancellationToken</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">product</span> <span class="k">in</span> <span class="n">Products</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">cancellationToken</span><span class="p">.</span><span class="nf">ThrowIfCancellationRequested</span><span class="p">();</span>
        <span class="k">yield</span> <span class="k">return</span> <span class="n">product</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="m">500</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span> <span class="c1">// Simule un traitement</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="c1">// Client</span>
<span class="k">await</span> <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">product</span> <span class="k">in</span> <span class="n">client</span><span class="p">.</span><span class="nf">StreamAllProductsAsync</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">$"Reçu : </span><span class="p">{</span><span class="n">product</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="client-streaming">Client Streaming</h2>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Contrat</span>
<span class="p">[</span><span class="n">ServiceContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IProductService</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">ImportSummary</span><span class="p">&gt;</span> <span class="nf">ImportProductsAsync</span><span class="p">(</span>
        <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">CreateProductRequest</span><span class="p">&gt;</span> <span class="n">products</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>
</code></pre></div></div>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Client</span>
<span class="k">async</span> <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">CreateProductRequest</span><span class="p">&gt;</span> <span class="nf">GenerateProducts</span><span class="p">()</span>
<span class="p">{</span>
    <span class="k">yield</span> <span class="k">return</span> <span class="k">new</span> <span class="n">CreateProductRequest</span> <span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Produit A"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">10.0</span><span class="n">m</span> <span class="p">};</span>
    <span class="k">yield</span> <span class="k">return</span> <span class="k">new</span> <span class="n">CreateProductRequest</span> <span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Produit B"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">20.0</span><span class="n">m</span> <span class="p">};</span>
    <span class="k">yield</span> <span class="k">return</span> <span class="k">new</span> <span class="n">CreateProductRequest</span> <span class="p">{</span> <span class="n">Name</span> <span class="p">=</span> <span class="s">"Produit C"</span><span class="p">,</span> <span class="n">Price</span> <span class="p">=</span> <span class="m">30.0</span><span class="n">m</span> <span class="p">};</span>
<span class="p">}</span>

<span class="kt">var</span> <span class="n">summary</span> <span class="p">=</span> <span class="k">await</span> <span class="n">client</span><span class="p">.</span><span class="nf">ImportProductsAsync</span><span class="p">(</span><span class="nf">GenerateProducts</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">$"Importés : </span><span class="p">{</span><span class="n">summary</span><span class="p">.</span><span class="n">Count</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
</code></pre></div></div>

<p>L’utilisation de <code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code> est idiomatique en C# et bien plus naturelle que les <code class="language-plaintext highlighter-rouge">IServerStreamWriter&lt;T&gt;</code> / <code class="language-plaintext highlighter-rouge">IAsyncStreamReader&lt;T&gt;</code> de l’approche contract-first.</p>

<h1 id="accéder-au-callcontext">Accéder au <code class="language-plaintext highlighter-rouge">CallContext</code></h1>

<p>Si vous avez besoin d’accéder aux metadata gRPC, aux headers ou au <code class="language-plaintext highlighter-rouge">CancellationToken</code> côté serveur, vous pouvez ajouter un paramètre <code class="language-plaintext highlighter-rouge">CallContext</code> :</p>

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

<span class="p">[</span><span class="n">ServiceContract</span><span class="p">]</span>
<span class="k">public</span> <span class="k">interface</span> <span class="nc">IProductService</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">OperationContract</span><span class="p">]</span>
    <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">GetProductAsync</span><span class="p">(</span>
        <span class="n">GetProductRequest</span> <span class="n">request</span><span class="p">,</span>
        <span class="n">CallContext</span> <span class="n">context</span> <span class="p">=</span> <span class="k">default</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="c1">// Implémentation serveur</span>
<span class="k">public</span> <span class="n">Task</span><span class="p">&lt;</span><span class="n">ProductResponse</span><span class="p">&gt;</span> <span class="nf">GetProductAsync</span><span class="p">(</span>
    <span class="n">GetProductRequest</span> <span class="n">request</span><span class="p">,</span>
    <span class="n">CallContext</span> <span class="n">context</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Accéder aux headers de la requête</span>
    <span class="kt">var</span> <span class="n">authHeader</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">RequestHeaders</span><span class="p">?.</span><span class="nf">GetValue</span><span class="p">(</span><span class="s">"authorization"</span><span class="p">);</span>

    <span class="c1">// Accéder au ServerCallContext natif gRPC</span>
    <span class="kt">var</span> <span class="n">serverContext</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">ServerCallContext</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">peer</span> <span class="p">=</span> <span class="n">serverContext</span><span class="p">?.</span><span class="n">Peer</span><span class="p">;</span>

    <span class="c1">// ...</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>
</code></pre></div></div>

<p>Le <code class="language-plaintext highlighter-rouge">CallContext</code> est optionnel (<code class="language-plaintext highlighter-rouge">= default</code>), donc les clients qui ne le fournissent pas fonctionnent normalement.</p>

<h1 id="combiner-avec-dapr">Combiner avec Dapr</h1>

<p>L’approche code-first fonctionne aussi avec le proxy gRPC de Dapr. Le client pointe vers le sidecar au lieu du service directement :</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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="n">builder</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">AddCodeFirstGrpcClient</span><span class="p">&lt;</span><span class="n">IProductService</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="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>
</code></pre></div></div>

<p>Le sidecar Dapr route l’appel vers le bon service, avec découverte automatique, mTLS et résilience intégrés.</p>

<h1 id="générer-le-proto-à-partir-du-code">Générer le <code class="language-plaintext highlighter-rouge">.proto</code> à partir du code</h1>

<p>Si vous avez besoin du fichier <code class="language-plaintext highlighter-rouge">.proto</code> (pour de la documentation ou de l’interop avec d’autres langages), <code class="language-plaintext highlighter-rouge">protobuf-net</code> peut le générer à partir de vos types C# :</p>

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

<span class="kt">var</span> <span class="n">generator</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SchemaGenerator</span><span class="p">();</span>
<span class="kt">var</span> <span class="n">schema</span> <span class="p">=</span> <span class="n">generator</span><span class="p">.</span><span class="n">GetSchema</span><span class="p">&lt;</span><span class="n">IProductService</span><span class="p">&gt;();</span>

<span class="n">File</span><span class="p">.</span><span class="nf">WriteAllText</span><span class="p">(</span><span class="s">"product.proto"</span><span class="p">,</span> <span class="n">schema</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">schema</span><span class="p">);</span>
</code></pre></div></div>

<p>Cela produit un fichier <code class="language-plaintext highlighter-rouge">.proto</code> standard qui peut être utilisé par des clients Go, Java, Python, etc.</p>

<h1 id="contract-first-vs-code-first--quand-choisir-quoi-">Contract-First vs Code-First : quand choisir quoi ?</h1>

<table>
  <thead>
    <tr>
      <th>Critère</th>
      <th>Contract-First (<code class="language-plaintext highlighter-rouge">.proto</code>)</th>
      <th>Code-First (<code class="language-plaintext highlighter-rouge">protobuf-net</code>)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Interop multi-langage</strong></td>
      <td>Natif</td>
      <td>Limité (.NET uniquement, sauf export <code class="language-plaintext highlighter-rouge">.proto</code>)</td>
    </tr>
    <tr>
      <td><strong>Génération de code</strong></td>
      <td>Obligatoire (protoc/build)</td>
      <td>Aucune</td>
    </tr>
    <tr>
      <td><strong>Types supportés</strong></td>
      <td>Types Protobuf de base</td>
      <td>Types C# natifs (<code class="language-plaintext highlighter-rouge">decimal</code>, <code class="language-plaintext highlighter-rouge">DateTime</code>, <code class="language-plaintext highlighter-rouge">Guid</code>…)</td>
    </tr>
    <tr>
      <td><strong>Streaming</strong></td>
      <td><code class="language-plaintext highlighter-rouge">IServerStreamWriter</code>, <code class="language-plaintext highlighter-rouge">IAsyncStreamReader</code></td>
      <td><code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code> (idiomatique C#)</td>
    </tr>
    <tr>
      <td><strong>Courbe d’apprentissage</strong></td>
      <td>Syntaxe <code class="language-plaintext highlighter-rouge">.proto</code> + outillage</td>
      <td>C# pur, attributs familiers</td>
    </tr>
    <tr>
      <td><strong>WCF migration</strong></td>
      <td>Réécriture</td>
      <td>Transition naturelle (<code class="language-plaintext highlighter-rouge">[ServiceContract]</code>, <code class="language-plaintext highlighter-rouge">[DataContract]</code>)</td>
    </tr>
    <tr>
      <td><strong>Écosystème</strong></td>
      <td>Standard gRPC</td>
      <td>Communauté <code class="language-plaintext highlighter-rouge">protobuf-net</code></td>
    </tr>
    <tr>
      <td><strong>Performance</strong></td>
      <td>Optimale</td>
      <td>Comparable (même transport gRPC/HTTP/2)</td>
    </tr>
  </tbody>
</table>

<h3 id="choisir-contract-first-quand-">Choisir Contract-First quand :</h3>

<ul>
  <li>Les clients sont dans <strong>plusieurs langages</strong> (Go, Java, Python…).</li>
  <li>Vous voulez le <strong>standard Protobuf</strong> pur.</li>
  <li>Vous travaillez dans un écosystème gRPC existant.</li>
</ul>

<h3 id="choisir-code-first-quand-">Choisir Code-First quand :</h3>

<ul>
  <li>Tous les clients et serveurs sont en <strong>.NET</strong>.</li>
  <li>Vous voulez partager les <strong>types du domaine</strong> directement.</li>
  <li>Vous migrez depuis <strong>WCF</strong>.</li>
  <li>Vous préférez éviter la complexité des fichiers <code class="language-plaintext highlighter-rouge">.proto</code> et de la génération de code.</li>
  <li>Vous voulez utiliser des types C# riches (<code class="language-plaintext highlighter-rouge">decimal</code>, <code class="language-plaintext highlighter-rouge">DateTime</code>, <code class="language-plaintext highlighter-rouge">IAsyncEnumerable</code>).</li>
</ul>

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

<table>
  <thead>
    <tr>
      <th>Aspect</th>
      <th>Détail</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Bibliothèque</strong></td>
      <td><code class="language-plaintext highlighter-rouge">protobuf-net.Grpc</code> (serveur : <code class="language-plaintext highlighter-rouge">.AspNetCore</code>, client : <code class="language-plaintext highlighter-rouge">.ClientFactory</code>)</td>
    </tr>
    <tr>
      <td><strong>Contrat</strong></td>
      <td>Interface C# avec <code class="language-plaintext highlighter-rouge">[ServiceContract]</code> + classes <code class="language-plaintext highlighter-rouge">[DataContract]</code></td>
    </tr>
    <tr>
      <td><strong>Pas de <code class="language-plaintext highlighter-rouge">.proto</code></strong></td>
      <td>Le mapping Protobuf est déduit des attributs C#</td>
    </tr>
    <tr>
      <td><strong>Streaming</strong></td>
      <td>Via <code class="language-plaintext highlighter-rouge">IAsyncEnumerable&lt;T&gt;</code>, idiomatique en C#</td>
    </tr>
    <tr>
      <td><strong>DI</strong></td>
      <td><code class="language-plaintext highlighter-rouge">AddCodeFirstGrpc()</code> côté serveur, <code class="language-plaintext highlighter-rouge">AddCodeFirstGrpcClient&lt;T&gt;()</code> côté client</td>
    </tr>
    <tr>
      <td><strong>Interop</strong></td>
      <td>Export <code class="language-plaintext highlighter-rouge">.proto</code> possible via <code class="language-plaintext highlighter-rouge">SchemaGenerator</code></td>
    </tr>
    <tr>
      <td><strong>Compatible Dapr</strong></td>
      <td>Oui, via le proxy gRPC du sidecar</td>
    </tr>
  </tbody>
</table>

<p>L’approche Code-First de gRPC offre une expérience 100% C#, sans fichier <code class="language-plaintext highlighter-rouge">.proto</code> ni génération de code, tout en conservant les performances de gRPC/HTTP/2. C’est un excellent choix pour les projets .NET-only qui veulent la performance de gRPC avec la simplicité du développement C# classique.</p>]]></content><author><name>Guym</name></author><category term="dotnet" /><category term="grpc" /><category term="code-first" /><category term="protobuf-net" /><category term="api" /><summary type="html"><![CDATA[L’approche classique de gRPC repose sur des fichiers .proto (Contract-First) : on définit le contrat Protobuf, puis on génère le code C# à partir de ce contrat. L’approche Code-First inverse le processus : on écrit des classes et interfaces C# classiques, et le contrat gRPC est généré automatiquement. Grâce à la bibliothèque protobuf-net.Grpc, on peut créer des services gRPC en .NET sans écrire une seule ligne de Protobuf, en utilisant des types C# natifs, des DataContract, ou de simples POCOs.]]></summary></entry><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>6</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>
	
	
	
	
	
		
		<li>Part 4 - 
		
			<a href="/2026/03/01/dapr-etats.html">Dapr : la gestion d'état (State Management) en .NET</a>
		
		</li>
	
	
	
		
		<li>Part 5 - 
		
			<a href="/2026/03/03/dapr-pub-sub-grpc.html">Dapr : Pub/Sub gRPC en .NET</a>
		
		</li>
	
	
	
	
	
		
		<li>Part 6 - 
		
			<a href="/2026/03/08/dapr-pub-sub.html">Dapr : le Pub/Sub (Publish &amp; Subscribe) 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></feed>