<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://animatlabs.com/feed.xml" rel="self" type="application/atom+xml" /><link href="https://animatlabs.com/" rel="alternate" type="text/html" /><updated>2026-06-20T18:17:47+05:30</updated><id>https://animatlabs.com/feed.xml</id><title type="html">AnimatLabs</title><subtitle>This initiative is an effort in the direction of sharing my active and ever-evolving playground and learnings with the larger community. I believe we learn better when we share, therefore, looking forward to explore, learn and collaborate to develop better solutions. </subtitle><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><entry><title type="html">CDC with Debezium and Kafka: PostgreSQL Changes to Typed .NET Events</title><link href="https://animatlabs.com/technical/.net/data%20engineering/cdc-debezium-kafka/" rel="alternate" type="text/html" title="CDC with Debezium and Kafka: PostgreSQL Changes to Typed .NET Events" /><published>2026-04-27T00:00:00+05:30</published><updated>2026-04-27T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/data%20engineering/cdc-debezium-kafka</id><content type="html" xml:base="https://animatlabs.com/technical/.net/data%20engineering/cdc-debezium-kafka/"><![CDATA[<p>I had a PostgreSQL app where the write model was already fine. Orders were going into tables, the API was boring in the best possible way, and nobody wanted a rewrite.</p>

<p>The missing piece was downstream reactions:</p>

<ul>
  <li>a read model that stays current when orders change</li>
  <li>search index updates without polling</li>
  <li>cross-service notifications that don’t add writes inside the request</li>
</ul>

<p>Marten and EventStoreDB are proper event stores. I like both in the right system.</p>

<p>But this app didn’t need a new persistence model. I wanted a smaller move: keep PostgreSQL as the source of truth and listen to its write-ahead log.</p>

<p>That is what this sample does. Debezium reads the WAL, Kafka carries the change events, and a .NET consumer maps the raw envelope into <code class="language-plaintext highlighter-rouge">OrderCreated</code>, <code class="language-plaintext highlighter-rouge">OrderUpdated</code>, <code class="language-plaintext highlighter-rouge">OrderDeleted</code> output. Practical CDC for an existing relational app, not a replacement for event sourcing.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/CdcEventSourcing" class="btn btn--primary">GitHub Repo</a></p>

<h2 id="how-it-fits-together">How It Fits Together</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>PostgreSQL WAL -&gt; Debezium Connect -&gt; Apache Kafka -&gt; .NET consumer
</code></pre></div></div>

<p>PostgreSQL already writes every change to its WAL for crash recovery. Setting <code class="language-plaintext highlighter-rouge">wal_level=logical</code> tells Postgres to include enough detail for logical replication. Debezium connects as a replication client, reads those changes, wraps them in a before/after envelope, and publishes to a Kafka topic named after the table.</p>

<p>The app that writes orders does not publish anything. It only writes to PostgreSQL. CDC sits beside it.</p>

<h2 id="the-docker-setup">The Docker Setup</h2>

<p>Four containers: PostgreSQL with logical replication, Kafka in KRaft mode, Debezium Connect, and Kafka UI for debugging.</p>

<p>Every image is free and open-source. Kafka uses the official Apache image (Apache 2.0), not the Confluent distribution. Works identically with Podman or Rancher Desktop.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">postgres</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">postgres:16-alpine</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">cdc-postgres</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">5432:5432"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">POSTGRES_DB</span><span class="pi">:</span> <span class="s">orders</span>
      <span class="na">POSTGRES_USER</span><span class="pi">:</span> <span class="s">postgres</span>
      <span class="na">POSTGRES_PASSWORD</span><span class="pi">:</span> <span class="s">postgres</span>
    <span class="na">command</span><span class="pi">:</span> <span class="pi">&gt;</span>
      <span class="s">postgres</span>
      <span class="s">-c wal_level=logical</span>
      <span class="s">-c max_replication_slots=4</span>
      <span class="s">-c max_wal_senders=4</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">./setup.sql:/docker-entrypoint-initdb.d/setup.sql</span>

  <span class="na">kafka</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">apache/kafka:3.7.0</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">cdc-kafka</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">9092:9092"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">KAFKA_NODE_ID</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">KAFKA_PROCESS_ROLES</span><span class="pi">:</span> <span class="s">broker,controller</span>
      <span class="na">KAFKA_LISTENERS</span><span class="pi">:</span> <span class="s">PLAINTEXT://0.0.0.0:29092,CONTROLLER://0.0.0.0:9093,PLAINTEXT_HOST://0.0.0.0:9092</span>
      <span class="na">KAFKA_ADVERTISED_LISTENERS</span><span class="pi">:</span> <span class="s">PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092</span>
      <span class="na">KAFKA_LISTENER_SECURITY_PROTOCOL_MAP</span><span class="pi">:</span> <span class="s">PLAINTEXT:PLAINTEXT,CONTROLLER:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT</span>
      <span class="na">KAFKA_CONTROLLER_LISTENER_NAMES</span><span class="pi">:</span> <span class="s">CONTROLLER</span>
      <span class="na">KAFKA_CONTROLLER_QUORUM_VOTERS</span><span class="pi">:</span> <span class="s">1@kafka:9093</span>
      <span class="na">KAFKA_INTER_BROKER_LISTENER_NAME</span><span class="pi">:</span> <span class="s">PLAINTEXT</span>
      <span class="na">KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR</span><span class="pi">:</span> <span class="m">1</span>

  <span class="na">debezium</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">debezium/connect:2.5</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">cdc-debezium</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="na">kafka</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
      <span class="na">postgres</span><span class="pi">:</span>
        <span class="na">condition</span><span class="pi">:</span> <span class="s">service_started</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8083:8083"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">BOOTSTRAP_SERVERS</span><span class="pi">:</span> <span class="s">kafka:29092</span>
      <span class="na">GROUP_ID</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">CONFIG_STORAGE_TOPIC</span><span class="pi">:</span> <span class="s">debezium_configs</span>
      <span class="na">OFFSET_STORAGE_TOPIC</span><span class="pi">:</span> <span class="s">debezium_offsets</span>
      <span class="na">STATUS_STORAGE_TOPIC</span><span class="pi">:</span> <span class="s">debezium_statuses</span>
      <span class="na">CONFIG_STORAGE_REPLICATION_FACTOR</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">OFFSET_STORAGE_REPLICATION_FACTOR</span><span class="pi">:</span> <span class="m">1</span>
      <span class="na">STATUS_STORAGE_REPLICATION_FACTOR</span><span class="pi">:</span> <span class="m">1</span>

  <span class="na">kafka-ui</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">provectuslabs/kafka-ui:latest</span>
    <span class="na">container_name</span><span class="pi">:</span> <span class="s">cdc-kafka-ui</span>
    <span class="na">depends_on</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">kafka</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8080:8080"</span>
    <span class="na">environment</span><span class="pi">:</span>
      <span class="na">KAFKA_CLUSTERS_0_NAME</span><span class="pi">:</span> <span class="s">local</span>
      <span class="na">KAFKA_CLUSTERS_0_BOOTSTRAPSERVERS</span><span class="pi">:</span> <span class="s">kafka:29092</span>
      <span class="na">KAFKA_CLUSTERS_0_KAFKACONNECT_0_NAME</span><span class="pi">:</span> <span class="s">debezium</span>
      <span class="na">KAFKA_CLUSTERS_0_KAFKACONNECT_0_ADDRESS</span><span class="pi">:</span> <span class="s">http://debezium:8083</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">command</code> override on PostgreSQL is the key part. Without <code class="language-plaintext highlighter-rouge">wal_level=logical</code>, Debezium cannot connect. <code class="language-plaintext highlighter-rouge">max_replication_slots=4</code> and <code class="language-plaintext highlighter-rouge">max_wal_senders=4</code> give enough room for the connector plus any other replication you might add later.</p>

<p>The <code class="language-plaintext highlighter-rouge">setup.sql</code> file (mounted into the init directory) creates the orders table and seeds three rows:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">IF</span> <span class="k">NOT</span> <span class="k">EXISTS</span> <span class="n">orders</span> <span class="p">(</span>
    <span class="n">id</span> <span class="nb">SERIAL</span> <span class="k">PRIMARY</span> <span class="k">KEY</span><span class="p">,</span>
    <span class="n">customer</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="n">product</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="n">quantity</span> <span class="nb">INT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="mi">1</span><span class="p">,</span>
    <span class="n">total_amount</span> <span class="nb">DECIMAL</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span><span class="mi">2</span><span class="p">)</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
    <span class="n">status</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="s1">'pending'</span><span class="p">,</span>
    <span class="n">created_at</span> <span class="n">TIMESTAMPTZ</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="n">now</span><span class="p">(),</span>
    <span class="n">updated_at</span> <span class="n">TIMESTAMPTZ</span> <span class="k">NOT</span> <span class="k">NULL</span> <span class="k">DEFAULT</span> <span class="n">now</span><span class="p">()</span>
<span class="p">);</span>

<span class="k">INSERT</span> <span class="k">INTO</span> <span class="n">orders</span> <span class="p">(</span><span class="n">customer</span><span class="p">,</span> <span class="n">product</span><span class="p">,</span> <span class="n">quantity</span><span class="p">,</span> <span class="n">total_amount</span><span class="p">,</span> <span class="n">status</span><span class="p">)</span> <span class="k">VALUES</span>
    <span class="p">(</span><span class="s1">'alice'</span><span class="p">,</span> <span class="s1">'Widget A'</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">49</span><span class="p">.</span><span class="mi">98</span><span class="p">,</span> <span class="s1">'pending'</span><span class="p">),</span>
    <span class="p">(</span><span class="s1">'bob'</span><span class="p">,</span> <span class="s1">'Widget B'</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">24</span><span class="p">.</span><span class="mi">99</span><span class="p">,</span> <span class="s1">'confirmed'</span><span class="p">),</span>
    <span class="p">(</span><span class="s1">'carol'</span><span class="p">,</span> <span class="s1">'Widget C'</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">124</span><span class="p">.</span><span class="mi">95</span><span class="p">,</span> <span class="s1">'shipped'</span><span class="p">);</span>

<span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="n">REPLICA</span> <span class="k">IDENTITY</span> <span class="k">FULL</span><span class="p">;</span>
</code></pre></div></div>

<p>One detail matters for updates and deletes:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">ALTER</span> <span class="k">TABLE</span> <span class="n">orders</span> <span class="n">REPLICA</span> <span class="k">IDENTITY</span> <span class="k">FULL</span><span class="p">;</span>
</code></pre></div></div>

<p>PostgreSQL’s default replica identity only sends the primary key for update/delete before-images. Debezium can’t show previous <code class="language-plaintext highlighter-rouge">status</code>, <code class="language-plaintext highlighter-rouge">customer</code>, or <code class="language-plaintext highlighter-rouge">total_amount</code> without it.</p>

<p><code class="language-plaintext highlighter-rouge">FULL</code> includes the entire previous row. Small line, big difference: <code class="language-plaintext highlighter-rouge">-&gt; shipped</code> vs <code class="language-plaintext highlighter-rouge">pending -&gt; shipped</code>.</p>

<p>Start everything:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker-compose up <span class="nt">-d</span>
</code></pre></div></div>

<p>Wait for Kafka and Debezium to stabilize, then register the connector.</p>

<h2 id="registering-the-debezium-connector">Registering the Debezium Connector</h2>

<p>Debezium Connect exposes a REST API. You POST a connector config:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>curl.exe <span class="nt">-X</span> POST http://localhost:8083/connectors <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> @register-connector.json
</code></pre></div></div>

<p>On macOS/Linux, <code class="language-plaintext highlighter-rouge">curl</code> is fine. On Windows PowerShell, use <code class="language-plaintext highlighter-rouge">curl.exe</code> so PowerShell does not route the command through its <code class="language-plaintext highlighter-rouge">Invoke-WebRequest</code> alias.</p>

<p>The connector config tells Debezium which database to watch and which tables to capture:</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">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders-connector"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"config"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"connector.class"</span><span class="p">:</span><span class="w"> </span><span class="s2">"io.debezium.connector.postgresql.PostgresConnector"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.hostname"</span><span class="p">:</span><span class="w"> </span><span class="s2">"postgres"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.port"</span><span class="p">:</span><span class="w"> </span><span class="s2">"5432"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.user"</span><span class="p">:</span><span class="w"> </span><span class="s2">"postgres"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.password"</span><span class="p">:</span><span class="w"> </span><span class="s2">"postgres"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"database.dbname"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"topic.prefix"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"table.include.list"</span><span class="p">:</span><span class="w"> </span><span class="s2">"public.orders"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"plugin.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pgoutput"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"slot.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders_slot"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"publication.name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"orders_pub"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"decimal.handling.mode"</span><span class="p">:</span><span class="w"> </span><span class="s2">"string"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"schema.history.internal.kafka.bootstrap.servers"</span><span class="p">:</span><span class="w"> </span><span class="s2">"kafka:29092"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"schema.history.internal.kafka.topic"</span><span class="p">:</span><span class="w"> </span><span class="s2">"schema-changes"</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>

<p><code class="language-plaintext highlighter-rouge">decimal.handling.mode: string</code> tells Debezium to serialize NUMERIC/DECIMAL columns as plain strings instead of base64-encoded bytes. Without this, a <code class="language-plaintext highlighter-rouge">DECIMAL(10,2)</code> value of <code class="language-plaintext highlighter-rouge">49.98</code> arrives as <code class="language-plaintext highlighter-rouge">"E4Y="</code> and your consumer has to decode it manually. With <code class="language-plaintext highlighter-rouge">string</code>, it arrives as <code class="language-plaintext highlighter-rouge">"49.98"</code> and <code class="language-plaintext highlighter-rouge">System.Text.Json</code> handles the rest.</p>

<p><code class="language-plaintext highlighter-rouge">topic.prefix</code> combined with the schema and table name gives you the Kafka topic: <code class="language-plaintext highlighter-rouge">orders.public.orders</code>. Once registered, Debezium starts streaming. You can verify in Kafka UI at http://localhost:8080.</p>

<h2 id="what-debezium-events-look-like">What Debezium Events Look Like</h2>

<p>Every CDC event has a <code class="language-plaintext highlighter-rouge">payload</code> with <code class="language-plaintext highlighter-rouge">before</code>, <code class="language-plaintext highlighter-rouge">after</code>, and <code class="language-plaintext highlighter-rouge">op</code> fields. For an INSERT:</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">"payload"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"before"</span><span class="p">:</span><span class="w"> </span><span class="kc">null</span><span class="p">,</span><span class="w">
    </span><span class="nl">"after"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
      </span><span class="nl">"id"</span><span class="p">:</span><span class="w"> </span><span class="mi">4</span><span class="p">,</span><span class="w">
      </span><span class="nl">"customer"</span><span class="p">:</span><span class="w"> </span><span class="s2">"dave"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"product"</span><span class="p">:</span><span class="w"> </span><span class="s2">"Widget D"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"quantity"</span><span class="p">:</span><span class="w"> </span><span class="mi">3</span><span class="p">,</span><span class="w">
      </span><span class="nl">"total_amount"</span><span class="p">:</span><span class="w"> </span><span class="s2">"74.97"</span><span class="p">,</span><span class="w">
      </span><span class="nl">"status"</span><span class="p">:</span><span class="w"> </span><span class="s2">"pending"</span><span class="w">
    </span><span class="p">},</span><span class="w">
    </span><span class="nl">"op"</span><span class="p">:</span><span class="w"> </span><span class="s2">"c"</span><span class="p">,</span><span class="w">
    </span><span class="nl">"ts_ms"</span><span class="p">:</span><span class="w"> </span><span class="mi">1713600000000</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>

<p><code class="language-plaintext highlighter-rouge">op: "c"</code> means create, <code class="language-plaintext highlighter-rouge">"u"</code> means update, <code class="language-plaintext highlighter-rouge">"d"</code> means delete, and <code class="language-plaintext highlighter-rouge">"r"</code> means read (the initial snapshot). For updates, both <code class="language-plaintext highlighter-rouge">before</code> and <code class="language-plaintext highlighter-rouge">after</code> are populated so you can see exactly what changed.</p>

<h2 id="the-net-consumer">The .NET Consumer</h2>

<p>A console app using <code class="language-plaintext highlighter-rouge">Confluent.Kafka</code>. The deserializer maps the Debezium envelope into domain event records.</p>

<p>The envelope type:</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">System.Text.Json.Serialization</span><span class="p">;</span>

<span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">DebeziumEnvelope</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"before"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="n">JsonElement</span><span class="p">?</span> <span class="n">Before</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">JsonPropertyName</span><span class="p">(</span><span class="s">"after"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="n">JsonElement</span><span class="p">?</span> <span class="n">After</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">JsonPropertyName</span><span class="p">(</span><span class="s">"op"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Operation</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><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"ts_ms"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">long</span> <span class="n">TimestampMs</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">bool</span> <span class="n">IsCreate</span> <span class="p">=&gt;</span> <span class="n">Operation</span> <span class="p">==</span> <span class="s">"c"</span> <span class="p">||</span> <span class="n">Operation</span> <span class="p">==</span> <span class="s">"r"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsUpdate</span> <span class="p">=&gt;</span> <span class="n">Operation</span> <span class="p">==</span> <span class="s">"u"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">IsDelete</span> <span class="p">=&gt;</span> <span class="n">Operation</span> <span class="p">==</span> <span class="s">"d"</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">DebeziumMessage</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"payload"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="n">DebeziumEnvelope</span><span class="p">?</span> <span class="n">Payload</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>The domain events:</p>

<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">OrderCreated</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">Customer</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Product</span><span class="p">,</span>
    <span class="kt">int</span> <span class="n">Quantity</span><span class="p">,</span>
    <span class="kt">decimal</span> <span class="n">TotalAmount</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Status</span><span class="p">,</span>
    <span class="n">DateTimeOffset</span> <span class="n">CreatedAt</span><span class="p">);</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">OrderUpdated</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="p">?</span> <span class="n">PreviousStatus</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">CurrentStatus</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Customer</span><span class="p">,</span>
    <span class="kt">string</span> <span class="n">Product</span><span class="p">,</span>
    <span class="kt">decimal</span> <span class="n">TotalAmount</span><span class="p">,</span>
    <span class="n">DateTimeOffset</span> <span class="n">UpdatedAt</span><span class="p">);</span>
</code></pre></div></div>

<p>And the consumer loop in <code class="language-plaintext highlighter-rouge">Program.cs</code>:</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">System.Text.Json.Serialization</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.CdcEventSourcing</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.CdcEventSourcing.DomainEvents</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Confluent.Kafka</span><span class="p">;</span>

<span class="kt">var</span> <span class="n">config</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ConsumerConfig</span>
<span class="p">{</span>
    <span class="n">BootstrapServers</span> <span class="p">=</span> <span class="s">"localhost:9092"</span><span class="p">,</span>
    <span class="n">GroupId</span> <span class="p">=</span> <span class="s">"cdc-consumer"</span><span class="p">,</span>
    <span class="n">AutoOffsetReset</span> <span class="p">=</span> <span class="n">AutoOffsetReset</span><span class="p">.</span><span class="n">Earliest</span><span class="p">,</span>
    <span class="n">EnableAutoCommit</span> <span class="p">=</span> <span class="k">true</span>
<span class="p">};</span>

<span class="kt">var</span> <span class="n">topic</span> <span class="p">=</span> <span class="s">"orders.public.orders"</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">consumer</span> <span class="p">=</span> <span class="k">new</span> <span class="n">ConsumerBuilder</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="n">config</span><span class="p">).</span><span class="nf">Build</span><span class="p">();</span>
<span class="n">consumer</span><span class="p">.</span><span class="nf">Subscribe</span><span class="p">(</span><span class="n">topic</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">$"Listening on </span><span class="p">{</span><span class="n">topic</span><span class="p">}</span><span class="s">. Insert or update rows in the orders table to see events."</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">"Press Ctrl+C to stop."</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="kt">var</span> <span class="n">cts</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">CancellationTokenSource</span><span class="p">();</span>
<span class="n">Console</span><span class="p">.</span><span class="n">CancelKeyPress</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="p">{</span> <span class="n">e</span><span class="p">.</span><span class="n">Cancel</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span> <span class="n">cts</span><span class="p">.</span><span class="nf">Cancel</span><span class="p">();</span> <span class="p">};</span>

<span class="k">try</span>
<span class="p">{</span>
    <span class="k">while</span> <span class="p">(!</span><span class="n">cts</span><span class="p">.</span><span class="n">Token</span><span class="p">.</span><span class="n">IsCancellationRequested</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">Consume</span><span class="p">(</span><span class="n">cts</span><span class="p">.</span><span class="n">Token</span><span class="p">);</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">result</span><span class="p">?.</span><span class="n">Message</span><span class="p">?.</span><span class="n">Value</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>

        <span class="kt">var</span> <span class="n">message</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">DebeziumMessage</span><span class="p">&gt;(</span><span class="n">result</span><span class="p">.</span><span class="n">Message</span><span class="p">.</span><span class="n">Value</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">envelope</span> <span class="p">=</span> <span class="n">message</span><span class="p">?.</span><span class="n">Payload</span><span class="p">;</span>
        <span class="k">if</span> <span class="p">(</span><span class="n">envelope</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">IsCreate</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">row</span> <span class="p">=</span> <span class="n">envelope</span><span class="p">.</span><span class="n">After</span><span class="p">?.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">OrderRow</span><span class="p">&gt;();</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">row</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>

            <span class="kt">var</span> <span class="n">created</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderCreated</span><span class="p">(</span>
                <span class="n">row</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span> <span class="n">row</span><span class="p">.</span><span class="n">Customer</span><span class="p">,</span> <span class="n">row</span><span class="p">.</span><span class="n">Product</span><span class="p">,</span>
                <span class="n">row</span><span class="p">.</span><span class="n">Quantity</span><span class="p">,</span> <span class="n">row</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">,</span> <span class="n">row</span><span class="p">.</span><span class="n">Status</span><span class="p">,</span>
                <span class="n">DateTimeOffset</span><span class="p">.</span><span class="nf">FromUnixTimeMilliseconds</span><span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">TimestampMs</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">$"[OrderCreated] #</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">Customer</span><span class="p">}</span><span class="s"> "</span> <span class="p">+</span>
                <span class="s">$"bought </span><span class="p">{</span><span class="n">created</span><span class="p">.</span><span class="n">Quantity</span><span class="p">}</span><span class="s">x </span><span class="p">{</span><span class="n">created</span><span class="p">.</span><span class="n">Product</span><span class="p">}</span><span class="s"> for </span><span class="p">{</span><span class="n">created</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">:</span><span class="n">C</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">IsUpdate</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">before</span> <span class="p">=</span> <span class="n">envelope</span><span class="p">.</span><span class="n">Before</span><span class="p">?.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">OrderRow</span><span class="p">&gt;();</span>
            <span class="kt">var</span> <span class="n">after</span> <span class="p">=</span> <span class="n">envelope</span><span class="p">.</span><span class="n">After</span><span class="p">?.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">OrderRow</span><span class="p">&gt;();</span>
            <span class="k">if</span> <span class="p">(</span><span class="n">after</span> <span class="k">is</span> <span class="k">null</span><span class="p">)</span> <span class="k">continue</span><span class="p">;</span>

            <span class="kt">var</span> <span class="n">updated</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderUpdated</span><span class="p">(</span>
                <span class="n">after</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span> <span class="n">before</span><span class="p">?.</span><span class="n">Status</span><span class="p">,</span> <span class="n">after</span><span class="p">.</span><span class="n">Status</span><span class="p">,</span>
                <span class="n">after</span><span class="p">.</span><span class="n">Customer</span><span class="p">,</span> <span class="n">after</span><span class="p">.</span><span class="n">Product</span><span class="p">,</span> <span class="n">after</span><span class="p">.</span><span class="n">TotalAmount</span><span class="p">,</span>
                <span class="n">DateTimeOffset</span><span class="p">.</span><span class="nf">FromUnixTimeMilliseconds</span><span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">TimestampMs</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">$"[OrderUpdated] #</span><span class="p">{</span><span class="n">updated</span><span class="p">.</span><span class="n">Id</span><span class="p">}</span><span class="s"> "</span> <span class="p">+</span>
                <span class="s">$"</span><span class="p">{</span><span class="n">updated</span><span class="p">.</span><span class="n">PreviousStatus</span><span class="p">}</span><span class="s"> -&gt; </span><span class="p">{</span><span class="n">updated</span><span class="p">.</span><span class="n">CurrentStatus</span><span class="p">}</span><span class="s"> (</span><span class="p">{</span><span class="n">updated</span><span class="p">.</span><span class="n">Customer</span><span class="p">}</span><span class="s">)"</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">envelope</span><span class="p">.</span><span class="n">IsDelete</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">row</span> <span class="p">=</span> <span class="n">envelope</span><span class="p">.</span><span class="n">Before</span><span class="p">?.</span><span class="n">Deserialize</span><span class="p">&lt;</span><span class="n">OrderRow</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">$"[OrderDeleted] #</span><span class="p">{</span><span class="n">row</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">row</span><span class="p">?.</span><span class="n">Customer</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>
<span class="k">catch</span> <span class="p">(</span><span class="n">OperationCanceledException</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="k">finally</span> <span class="p">{</span> <span class="n">consumer</span><span class="p">.</span><span class="nf">Close</span><span class="p">();</span> <span class="p">}</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">OrderRow</code> maps the JSON payload to a C# object. Debezium sends numeric columns as strings (because of <code class="language-plaintext highlighter-rouge">decimal.handling.mode: string</code>), so <code class="language-plaintext highlighter-rouge">JsonNumberHandling.AllowReadingFromString</code> handles the conversion:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nf">JsonNumberHandling</span><span class="p">(</span><span class="n">JsonNumberHandling</span><span class="p">.</span><span class="n">AllowReadingFromString</span><span class="p">)]</span>
<span class="k">internal</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">OrderRow</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"id"</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">JsonPropertyName</span><span class="p">(</span><span class="s">"customer"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Customer</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">JsonPropertyName</span><span class="p">(</span><span class="s">"product"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Product</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">JsonPropertyName</span><span class="p">(</span><span class="s">"quantity"</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="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"total_amount"</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="k">set</span><span class="p">;</span> <span class="p">}</span>

    <span class="p">[</span><span class="nf">JsonPropertyName</span><span class="p">(</span><span class="s">"status"</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="s">""</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Run the consumer:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>AnimatLabs.CdcEventSourcing
dotnet run
</code></pre></div></div>

<p>It immediately reads the initial snapshot:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Listening on orders.public.orders. Insert or update rows in the orders table to see events.
Press Ctrl+C to stop.

[OrderCreated] #1 alice bought 2x Widget A for $49.98
[OrderCreated] #2 bob bought 1x Widget B for $24.99
[OrderCreated] #3 carol bought 5x Widget C for $124.95
</code></pre></div></div>

<p>Open another terminal and insert a row:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec </span>cdc-postgres psql <span class="nt">-U</span> postgres <span class="nt">-d</span> orders <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"INSERT INTO orders (customer, product, quantity, total_amount) VALUES ('dave', 'Widget D', 3, 74.97);"</span>
</code></pre></div></div>

<p>Update it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec </span>cdc-postgres psql <span class="nt">-U</span> postgres <span class="nt">-d</span> orders <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"UPDATE orders SET status = 'shipped' WHERE customer = 'dave';"</span>
</code></pre></div></div>

<p>Delete it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>docker <span class="nb">exec </span>cdc-postgres psql <span class="nt">-U</span> postgres <span class="nt">-d</span> orders <span class="nt">-c</span> <span class="se">\</span>
  <span class="s2">"DELETE FROM orders WHERE customer = 'dave';"</span>
</code></pre></div></div>

<p>This is the verified output from my local run:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[OrderCreated] #4 dave bought 3x Widget D for $74.97
[OrderUpdated] #4 pending -&gt; shipped (dave)
[OrderDeleted] #4 dave
</code></pre></div></div>

<h2 id="what-this-gives-you">What This Gives You</h2>

<p>The same database write can now feed a few separate jobs:</p>

<ul>
  <li>Keep raw events in Kafka for audit and replay</li>
  <li>Evict Redis entries when the source row changes instead of guessing TTLs</li>
  <li>Feed a search index or read model from the same topic</li>
</ul>

<p>These are all independent consumers on the same Kafka topic. Add them as you need them. The database does not care.</p>

<h2 id="what-i-would-not-use-this-for">What I Would Not Use This For</h2>

<p>I would not use CDC as a shortcut around domain modeling. If your system needs event-sourced aggregates, explicit commands, versioned domain events, and business-time replay, start with a real event store or a framework designed for that model.</p>

<p>CDC is strongest when:</p>

<ul>
  <li>the relational schema already exists</li>
  <li>other services need to react to committed changes</li>
  <li>you want the application write path to stay simple</li>
  <li>eventual consistency is acceptable</li>
</ul>

<p>It is weaker when consumers need perfect domain intent. A row update can tell you that <code class="language-plaintext highlighter-rouge">status</code> changed from <code class="language-plaintext highlighter-rouge">pending</code> to <code class="language-plaintext highlighter-rouge">shipped</code>; it cannot tell you whether that happened because a warehouse scan completed, a support agent overrode the order, or a migration script fixed old data. If that distinction matters, publish an explicit domain event.</p>

<h2 id="things-to-watch-for">Things to Watch For</h2>

<p><strong>Connector lag.</strong> If Debezium falls behind, changes pile up in the WAL. Watch replication slot lag in PostgreSQL and the Debezium metrics endpoint.</p>

<p><strong>Replica identity.</strong> If you need previous row values for updates/deletes, set <code class="language-plaintext highlighter-rouge">REPLICA IDENTITY FULL</code> on the captured table. Otherwise delete events may only include the primary key.</p>

<p><strong>Schema changes.</strong> Adding a column is usually fine. Renaming or removing columns can break consumers. Version the events you expose from your consumer if other teams depend on them.</p>

<p><strong>At-least-once delivery.</strong> Kafka consumers can see duplicates. Make handlers idempotent with the row primary key, Debezium metadata, or your own processed-event table.</p>

<p><strong>Startup order.</strong> Kafka and Debezium take a few seconds to settle. The README keeps the commands separate on purpose so you can see each moving part.</p>

<p>A follow-up post covers piping these same CDC events to the browser over SignalR and SSE. Same Kafka topic, different consumer.</p>

<hr />]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Data Engineering" /><category term=".NET" /><category term="CDC" /><category term="Debezium" /><category term="Kafka" /><category term="PostgreSQL" /><category term="Event Sourcing" /><category term="Event Streaming" /><summary type="html"><![CDATA[Capture PostgreSQL row changes with Debezium, stream them through Apache Kafka, and turn them into typed .NET events without changing the application write path.]]></summary></entry><entry><title type="html">Build a Real-Time HTMX Dashboard in .NET Without JavaScript</title><link href="https://animatlabs.com/technical/.net/workflow/htmx-dotnet/" rel="alternate" type="text/html" title="Build a Real-Time HTMX Dashboard in .NET Without JavaScript" /><published>2026-03-24T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/workflow/htmx-dotnet</id><content type="html" xml:base="https://animatlabs.com/technical/.net/workflow/htmx-dotnet/"><![CDATA[<p>I needed a workflow status page. Click a button, watch steps execute, show success or rollback. React plus WebSockets was my first instinct. Then I looked at the actual requirements: one-way data, server renders the UI, no client state. Overkill.</p>

<p>HTMX has an SSE extension that opens an <code class="language-plaintext highlighter-rouge">EventSource</code> from attributes. The server sends HTML fragments, HTMX swaps them into the page. I paired it with WorkflowForge 2.1.1 for a five-step order workflow with automatic compensation. <strong>Zero</strong> custom JavaScript.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/WorkflowForge/AnimatLabs.WorkflowForge.HtmxDashboard" class="btn btn--primary">GitHub Repo</a></p>

<blockquote>
  <p>If you’re new to SSE in .NET, I wrote about the <a href="https://animatlabs.com/technical/.net/server-sent-events-dotnet/">three SSE patterns from scratch</a> in the previous post.</p>
</blockquote>

<h2 id="the-flow">The Flow</h2>

<p>Two endpoints, one flow:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Browser                                 Server
  │                                        │
  ├─ hx-get="/workflow/reset" ────────────&gt;│ Returns HTML fragment
  │                                        │   with sse-connect attribute
  │&lt;────── &lt;div sse-connect="/stream"&gt; ────┤
  │                                        │
  ├─ HTMX opens EventSource ─────────────&gt;│ WorkflowForge runs 5 ops
  │                                        │   Channel pipes events
  │&lt;──────── event: step ──────────────────┤
  │&lt;──────── event: step ──────────────────┤
  │&lt;──────── event: done ──────────────────┤
</code></pre></div></div>

<p>Button click fetches an HTML fragment with SSE attributes. HTMX opens the stream and the server pushes step updates as HTML. Click “Run with Failure” and ChargePayment throws. WorkflowForge then runs compensations in reverse order for every step that had completed up to the failure—including <code class="language-plaintext highlighter-rouge">ChargePayment</code>’s own <code class="language-plaintext highlighter-rouge">RestoreAsync</code> (which logs that there is no charge to reverse when the gateway never completed), then <code class="language-plaintext highlighter-rouge">ReserveStock</code>, then <code class="language-plaintext highlighter-rouge">ValidateOrder</code> - all streamed live.</p>

<h2 id="the-html">The HTML</h2>

<p>The entire interactive UI:</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;div&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">hx-get=</span><span class="s">"/workflow/reset"</span>
            <span class="na">hx-target=</span><span class="s">"#output"</span>
            <span class="na">hx-swap=</span><span class="s">"innerHTML"</span><span class="nt">&gt;</span>Run Order Workflow<span class="nt">&lt;/button&gt;</span>
    <span class="nt">&lt;button</span> <span class="na">hx-get=</span><span class="s">"/workflow/reset?fail=true"</span>
            <span class="na">hx-target=</span><span class="s">"#output"</span>
            <span class="na">hx-swap=</span><span class="s">"innerHTML"</span><span class="nt">&gt;</span>Run with Failure<span class="nt">&lt;/button&gt;</span>
<span class="nt">&lt;/div&gt;</span>

<span class="nt">&lt;div</span> <span class="na">id=</span><span class="s">"output"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;p&gt;</span>Click a button to start a workflow.<span class="nt">&lt;/p&gt;</span>
<span class="nt">&lt;/div&gt;</span>
</code></pre></div></div>

<p>No <code class="language-plaintext highlighter-rouge">onclick</code>. No <code class="language-plaintext highlighter-rouge">EventSource</code> in JavaScript. The <code class="language-plaintext highlighter-rouge">&lt;head&gt;</code> loads HTMX and the SSE extension. <code class="language-plaintext highlighter-rouge">hx-ext="sse"</code> goes on the <code class="language-plaintext highlighter-rouge">&lt;body&gt;</code> tag. Everything else is server-driven.</p>

<h2 id="the-server-side">The Server Side</h2>

<p>The reset endpoint returns an HTML fragment pre-wired for SSE. When HTMX inserts this into the page, it sees the <code class="language-plaintext highlighter-rouge">sse-connect</code> attribute and opens an EventSource to the stream URL:</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">MapGet</span><span class="p">(</span><span class="s">"/workflow/reset"</span><span class="p">,</span> <span class="p">(</span><span class="kt">bool</span> <span class="n">fail</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">html</span> <span class="p">=</span> <span class="s">$"""
</span>        <span class="p">&lt;</span><span class="n">div</span> <span class="n">sse</span><span class="p">-</span><span class="n">connect</span><span class="p">=</span><span class="s">"/workflow/stream?fail={fail.ToString().ToLowerInvariant()}"</span> <span class="n">sse</span><span class="p">-</span><span class="n">close</span><span class="p">=</span><span class="s">"close"</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="n">div</span> <span class="n">id</span><span class="p">=</span><span class="s">"steps"</span> <span class="n">sse</span><span class="p">-</span><span class="n">swap</span><span class="p">=</span><span class="s">"step"</span> <span class="n">hx</span><span class="p">-</span><span class="n">swap</span><span class="p">=</span><span class="s">"beforeend"</span><span class="p">&gt;&lt;/</span><span class="n">div</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="n">div</span> <span class="n">id</span><span class="p">=</span><span class="s">"final-status"</span> <span class="n">sse</span><span class="p">-</span><span class="n">swap</span><span class="p">=</span><span class="s">"done"</span> <span class="n">hx</span><span class="p">-</span><span class="n">swap</span><span class="p">=</span><span class="s">"innerHTML"</span><span class="p">&gt;&lt;/</span><span class="n">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="n">div</span><span class="p">&gt;</span>
        <span class="s">""";
</span>    <span class="k">return</span> <span class="n">Results</span><span class="p">.</span><span class="nf">Content</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s">"text/html"</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sse-swap="step"</code> means “when an SSE event named <code class="language-plaintext highlighter-rouge">step</code> arrives, swap this div.” <code class="language-plaintext highlighter-rouge">hx-swap="beforeend"</code> appends each step instead of replacing. <code class="language-plaintext highlighter-rouge">sse-close="close"</code> shuts down the connection when the workflow finishes.</p>

<p>The stream endpoint runs the workflow on a background task and pushes events through a Channel:</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">MapGet</span><span class="p">(</span><span class="s">"/workflow/stream"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">HttpContext</span> <span class="n">ctx</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">fail</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">bufferingFeature</span> <span class="p">=</span> <span class="n">ctx</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">IHttpResponseBodyFeature</span><span class="p">&gt;();</span>
    <span class="n">bufferingFeature</span><span class="p">?.</span><span class="nf">DisableBuffering</span><span class="p">();</span>

    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">ContentType</span> <span class="p">=</span> <span class="s">"text/event-stream"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">CacheControl</span> <span class="p">=</span> <span class="s">"no-cache"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">Connection</span> <span class="p">=</span> <span class="s">"keep-alive"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">[</span><span class="s">"X-Accel-Buffering"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"no"</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">sink</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ChannelEventSink</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">ct</span> <span class="p">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">RequestAborted</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">OrderProcessingWorkflow</span><span class="p">.</span><span class="nf">Build</span><span class="p">(</span><span class="n">sink</span><span class="p">,</span> <span class="n">fail</span><span class="p">);</span>

    <span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WF</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span>
        <span class="n">workflowName</span><span class="p">:</span> <span class="n">workflow</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
        <span class="n">initialProperties</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">object</span><span class="p">?&gt;</span>
        <span class="p">{</span>
            <span class="p">[</span><span class="n">OrderKeys</span><span class="p">.</span><span class="n">ShouldFail</span><span class="p">]</span> <span class="p">=</span> <span class="n">fail</span>
        <span class="p">});</span>

    <span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WF</span><span class="p">.</span><span class="nf">CreateSmith</span><span class="p">(</span><span class="k">new</span> <span class="nf">ConsoleLogger</span><span class="p">(</span><span class="s">"WF"</span><span class="p">));</span>
    <span class="kt">string</span><span class="p">?</span> <span class="n">finalHtml</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>

    <span class="k">try</span>
    <span class="p">{</span>
        <span class="n">_</span> <span class="p">=</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="k">try</span>
            <span class="p">{</span>
                <span class="k">await</span> <span class="n">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
                <span class="n">finalHtml</span> <span class="p">=</span> <span class="s">"&lt;p&gt;&lt;strong&gt;All steps completed successfully.&lt;/strong&gt;&lt;/p&gt;"</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">catch</span> <span class="p">(</span><span class="n">OperationCanceledException</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
            <span class="k">catch</span>
            <span class="p">{</span>
                <span class="n">finalHtml</span> <span class="p">=</span> <span class="s">"&lt;p&gt;&lt;strong&gt;Workflow failed -- compensation complete.&lt;/strong&gt;&lt;/p&gt;"</span><span class="p">;</span>
            <span class="p">}</span>
            <span class="k">finally</span>
            <span class="p">{</span>
                <span class="n">sink</span><span class="p">.</span><span class="nf">Complete</span><span class="p">();</span>
            <span class="p">}</span>
        <span class="p">},</span> <span class="n">ct</span><span class="p">);</span>

        <span class="k">await</span> <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">evt</span> <span class="k">in</span> <span class="n">sink</span><span class="p">.</span><span class="n">Reader</span><span class="p">.</span><span class="nf">ReadAllAsync</span><span class="p">(</span><span class="n">ct</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="kt">var</span> <span class="n">html</span> <span class="p">=</span> <span class="nf">BuildStepHtml</span><span class="p">(</span><span class="n">evt</span><span class="p">);</span>
            <span class="k">await</span> <span class="nf">SendSseAsync</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="s">"step"</span><span class="p">,</span> <span class="n">html</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">finalHtml</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="k">await</span> <span class="nf">SendSseAsync</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="s">"done"</span><span class="p">,</span> <span class="n">finalHtml</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
            <span class="k">await</span> <span class="nf">SendSseAsync</span><span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="s">"close"</span><span class="p">,</span> <span class="s">""</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="k">catch</span> <span class="p">(</span><span class="n">OperationCanceledException</span><span class="p">)</span> <span class="p">{</span> <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">close</code> event triggers <code class="language-plaintext highlighter-rouge">sse-close</code> on the client, shutting down the EventSource so it doesn’t auto-reconnect.</p>

<h2 id="bridging-workflowforge-to-sse">Bridging WorkflowForge to SSE</h2>

<p><code class="language-plaintext highlighter-rouge">System.Threading.Channels</code> connects the two. Workflow operations write events, the SSE loop reads them:</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">ChannelEventSink</span> <span class="p">:</span> <span class="n">IWorkflowEventSink</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">Channel</span><span class="p">&lt;</span><span class="n">WorkflowEvent</span><span class="p">&gt;</span> <span class="n">_channel</span> <span class="p">=</span>
        <span class="n">Channel</span><span class="p">.</span><span class="n">CreateUnbounded</span><span class="p">&lt;</span><span class="n">WorkflowEvent</span><span class="p">&gt;(</span><span class="k">new</span> <span class="n">UnboundedChannelOptions</span>
        <span class="p">{</span>
            <span class="n">SingleReader</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
            <span class="n">SingleWriter</span> <span class="p">=</span> <span class="k">false</span>
        <span class="p">});</span>

    <span class="k">public</span> <span class="n">ChannelReader</span><span class="p">&lt;</span><span class="n">WorkflowEvent</span><span class="p">&gt;</span> <span class="n">Reader</span> <span class="p">=&gt;</span> <span class="n">_channel</span><span class="p">.</span><span class="n">Reader</span><span class="p">;</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Report</span><span class="p">(</span><span class="kt">string</span> <span class="n">operationName</span><span class="p">,</span> <span class="kt">string</span> <span class="n">status</span><span class="p">,</span> <span class="kt">string</span> <span class="n">detail</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_channel</span><span class="p">.</span><span class="n">Writer</span><span class="p">.</span><span class="nf">TryWrite</span><span class="p">(</span><span class="k">new</span> <span class="n">WorkflowEvent</span>
        <span class="p">{</span>
            <span class="n">OperationName</span> <span class="p">=</span> <span class="n">operationName</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="n">Detail</span> <span class="p">=</span> <span class="n">detail</span>
        <span class="p">});</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">void</span> <span class="nf">Complete</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="n">_channel</span><span class="p">.</span><span class="n">Writer</span><span class="p">.</span><span class="nf">TryComplete</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">SingleWriter = false</code> because multiple operations can report concurrently. The reader is the SSE loop: one consumer, serialized writes to the response stream.</p>

<h2 id="the-workflow-and-compensation">The Workflow and Compensation</h2>

<p>Five operations with WorkflowForge 2.1.1:</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="n">IWorkflow</span> <span class="nf">Build</span><span class="p">(</span><span class="n">IWorkflowEventSink</span> <span class="n">sink</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">shouldFail</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">WF</span>
        <span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"OrderProcessing"</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">ValidateOrderOperation</span><span class="p">(</span><span class="n">sink</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">ReserveStockOperation</span><span class="p">(</span><span class="n">sink</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">ChargePaymentOperation</span><span class="p">(</span><span class="n">sink</span><span class="p">,</span> <span class="n">shouldFail</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">CreateShipmentOperation</span><span class="p">(</span><span class="n">sink</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">SendNotificationOperation</span><span class="p">(</span><span class="n">sink</span><span class="p">))</span>
        <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Each operation implements <code class="language-plaintext highlighter-rouge">ForgeAsyncCore</code> (do the work) and <code class="language-plaintext highlighter-rouge">RestoreAsync</code> (undo it). When any step throws, WorkflowForge walks backward and invokes <code class="language-plaintext highlighter-rouge">RestoreAsync</code> for each completed step in reverse order (including the step that failed, when it has cleanup or a no-op path). The <code class="language-plaintext highlighter-rouge">ChargePaymentOperation</code> has a <code class="language-plaintext highlighter-rouge">shouldFail</code> flag that simulates a payment gateway timeout:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
    <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">sink</span><span class="p">.</span><span class="nf">Report</span><span class="p">(</span><span class="n">Name</span><span class="p">,</span> <span class="s">"running"</span><span class="p">,</span> <span class="s">"Charging payment method..."</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">1200</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">shouldFail</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">sink</span><span class="p">.</span><span class="nf">Report</span><span class="p">(</span><span class="n">Name</span><span class="p">,</span> <span class="s">"failed"</span><span class="p">,</span> <span class="s">"Payment gateway timeout -- triggering compensation"</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">"Payment gateway timeout"</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="n">foundry</span><span class="p">.</span><span class="nf">SetProperty</span><span class="p">(</span><span class="n">OrderKeys</span><span class="p">.</span><span class="n">PaymentCharged</span><span class="p">,</span> <span class="k">true</span><span class="p">);</span>
    <span class="n">sink</span><span class="p">.</span><span class="nf">Report</span><span class="p">(</span><span class="n">Name</span><span class="p">,</span> <span class="s">"completed"</span><span class="p">,</span> <span class="s">"$149.99 charged to card ending 4242"</span><span class="p">);</span>
    <span class="k">return</span> <span class="n">inputData</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>In <code class="language-plaintext highlighter-rouge">RestoreAsync</code>, it checks if a charge actually happened via <code class="language-plaintext highlighter-rouge">GetPropertyOrDefault</code>. No charge means no refund (the kind of detail that breaks things in production if you skip it).</p>

<p>Each step event becomes plain HTML:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">static</span> <span class="kt">string</span> <span class="nf">BuildStepHtml</span><span class="p">(</span><span class="n">WorkflowEvent</span> <span class="n">evt</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">label</span> <span class="p">=</span> <span class="n">evt</span><span class="p">.</span><span class="n">Status</span><span class="p">.</span><span class="nf">ToUpperInvariant</span><span class="p">();</span>
    <span class="k">return</span> <span class="s">$"""&lt;div class="</span><span class="n">step</span><span class="s">"&gt;[{label}] &lt;strong&gt;{evt.OperationName}&lt;/strong&gt; {WebUtility.HtmlEncode(evt.Detail)}&lt;/div&gt;"""</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>No client-side rendering. The browser inserts HTML fragments directly.</p>

<h2 id="where-this-fits">Where This Fits</h2>

<p>HTMX + SSE works well for admin dashboards, status monitors, internal tools: anything where the server owns the state and the client displays it. The browser handles reconnection.</p>

<p>Chat or collaborative editing? Wrong tool. WebSockets or SignalR when you need traffic both ways, which is a longer story than this write-up has room for but you already know when you need it.</p>

<p>To try it:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>playground/WorkflowForge/AnimatLabs.WorkflowForge.HtmxDashboard
dotnet run
</code></pre></div></div>

<p>Open <code class="language-plaintext highlighter-rouge">http://localhost:5075</code>. Two buttons: one happy path, one failure. On failure, compensations run newest-first across all completed steps (including <code class="language-plaintext highlighter-rouge">ChargePayment</code>’s rollback hook), and you can see each step stream in.</p>

<table>
  <thead>
    <tr>
      <th>What</th>
      <th>Where</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>WorkflowForge</td>
      <td><a href="https://github.com/animatlabs/workflow-forge">GitHub</a> | <a href="https://www.nuget.org/packages/WorkflowForge">NuGet</a></td>
    </tr>
    <tr>
      <td>HTMX</td>
      <td><a href="https://htmx.org">htmx.org</a></td>
    </tr>
    <tr>
      <td>Playground</td>
      <td><a href="https://github.com/animat089/playground/tree/main/WorkflowForge/AnimatLabs.WorkflowForge.HtmxDashboard">WorkflowForge/HtmxDashboard</a></td>
    </tr>
  </tbody>
</table>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="more-on-this-topic">More on This Topic</h2>

<ul>
  <li><a href="/technical/.net/server-sent-events-dotnet/">Server-Sent Events in ASP.NET Core</a></li>
  <li><a href="/technical/.net/workflow/workflow-forge-introduction/">WorkflowForge introduction</a></li>
  <li><a href="/technical/.net/workflow/masstransit-workflowforge-saga/">MassTransit saga with WorkflowForge</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Workflow" /><category term=".NET" /><category term="HTMX" /><category term="WorkflowForge" /><category term="Server-Sent Events" /><category term="Real-Time" /><category term="ASP.NET Core" /><category term="Compensation" /><summary type="html"><![CDATA[Build a live workflow dashboard with HTMX SSE extension and WorkflowForge. Steps stream in, failures trigger compensation, all server-rendered with zero JavaScript.]]></summary></entry><entry><title type="html">Server-Sent Events in ASP.NET Core: Real-Time Streaming Without SignalR</title><link href="https://animatlabs.com/technical/.net/server-sent-events-dotnet/" rel="alternate" type="text/html" title="Server-Sent Events in ASP.NET Core: Real-Time Streaming Without SignalR" /><published>2026-03-16T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/server-sent-events-dotnet</id><content type="html" xml:base="https://animatlabs.com/technical/.net/server-sent-events-dotnet/"><![CDATA[<p>I added SignalR to a project that only needed server-to-client updates. 200 lines of hub code, connection management, and a JavaScript dependency. All of that for a dashboard that never sends data back. SSE does it in 15 lines. I still ask “does the client ever push?” before I touch SignalR.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/ServerSentEvents" class="btn btn--primary">GitHub Repo</a></p>

<h2 id="what-sse-actually-is">What SSE Actually Is</h2>

<p>HTTP. Set <code class="language-plaintext highlighter-rouge">Content-Type: text/event-stream</code>, keep the connection open, write <code class="language-plaintext highlighter-rouge">data: something\n\n</code>. Each double-newline ends an event.</p>

<p>The browser’s <code class="language-plaintext highlighter-rouge">EventSource</code> API parses it. That’s the whole spec.</p>

<p>No WebSocket upgrade. Proxies treat it like a long-lived HTTP response; most already allow it. <code class="language-plaintext highlighter-rouge">EventSource</code> reconnects automatically and sends <code class="language-plaintext highlighter-rouge">Last-Event-ID</code> so the server knows where to resume.</p>

<p>Three optional fields: <code class="language-plaintext highlighter-rouge">data</code> (payload), <code class="language-plaintext highlighter-rouge">event</code> (name for multiple types on one stream), <code class="language-plaintext highlighter-rouge">id</code> (for reconnection).</p>

<p>If your UI only needs the server to talk and the browser to listen, you skip the WebSocket handshake, you skip hub abstractions, and you stay on plain HTTP semantics that ops already know how to load-balance and cache-policy, which is why I reach for SSE first on internal dashboards.</p>

<h2 id="the-simplest-stream-a-clock">The Simplest Stream: A Clock</h2>

<p>Start with the bare minimum: one event type, one value, fire every second:</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">MapGet</span><span class="p">(</span><span class="s">"/events/clock"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">HttpContext</span> <span class="n">ctx</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">ContentType</span> <span class="p">=</span> <span class="s">"text/event-stream"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">CacheControl</span> <span class="p">=</span> <span class="s">"no-cache"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">Connection</span> <span class="p">=</span> <span class="s">"keep-alive"</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">ct</span> <span class="p">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">RequestAborted</span><span class="p">;</span>

    <span class="k">await</span> <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">tick</span> <span class="k">in</span> <span class="nf">StreamClock</span><span class="p">(</span><span class="n">ct</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="s">$"data: </span><span class="p">{</span><span class="n">tick</span><span class="p">:</span><span class="n">HH</span><span class="p">:</span><span class="n">mm</span><span class="p">:</span><span class="n">ss</span><span class="p">}</span><span class="s">\n\n"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Body</span><span class="p">.</span><span class="nf">FlushAsync</span><span class="p">(</span><span class="n">ct</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>

<span class="k">static</span> <span class="k">async</span> <span class="n">IAsyncEnumerable</span><span class="p">&lt;</span><span class="n">DateTime</span><span class="p">&gt;</span> <span class="nf">StreamClock</span><span class="p">(</span>
    <span class="p">[</span><span class="n">EnumeratorCancellation</span><span class="p">]</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">while</span> <span class="p">(!</span><span class="n">ct</span><span class="p">.</span><span class="n">IsCancellationRequested</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">yield</span> <span class="k">return</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">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="m">1000</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IAsyncEnumerable</code> keeps the stream lazy. Each tick becomes a <code class="language-plaintext highlighter-rouge">data:</code> line, <code class="language-plaintext highlighter-rouge">\n\n</code> ends the event. Flush after each write or the client sees nothing until the buffer fills (I forgot this the first time and stared at a blank page for five minutes).</p>

<p>On the browser side, <code class="language-plaintext highlighter-rouge">EventSource</code> is native. No library:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">clock</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="dl">'</span><span class="s1">/events/clock</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">clock</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="nx">e</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">clock</span><span class="dl">'</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">;</span>
<span class="p">};</span>
<span class="nx">clock</span><span class="p">.</span><span class="nx">onopen</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">clock-status</span><span class="dl">'</span><span class="p">).</span><span class="nx">classList</span><span class="p">.</span><span class="nx">remove</span><span class="p">(</span><span class="dl">'</span><span class="s1">off</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">clock</span><span class="p">.</span><span class="nx">onerror</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">clock-status</span><span class="dl">'</span><span class="p">).</span><span class="nx">classList</span><span class="p">.</span><span class="nx">add</span><span class="p">(</span><span class="dl">'</span><span class="s1">off</span><span class="dl">'</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="named-events">Named Events</h2>

<p>What if you need multiple event types on one connection? The <code class="language-plaintext highlighter-rouge">event:</code> field handles that. Order stream example: it pushes both <code class="language-plaintext highlighter-rouge">placed</code> and <code class="language-plaintext highlighter-rouge">cancelled</code> events.</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">MapGet</span><span class="p">(</span><span class="s">"/events/orders"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">HttpContext</span> <span class="n">ctx</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">ContentType</span> <span class="p">=</span> <span class="s">"text/event-stream"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">CacheControl</span> <span class="p">=</span> <span class="s">"no-cache"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">Connection</span> <span class="p">=</span> <span class="s">"keep-alive"</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">ct</span> <span class="p">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">RequestAborted</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">orderNum</span> <span class="p">=</span> <span class="m">1000</span><span class="p">;</span>

    <span class="k">while</span> <span class="p">(!</span><span class="n">ct</span><span class="p">.</span><span class="n">IsCancellationRequested</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="n">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">1500</span><span class="p">,</span> <span class="m">4000</span><span class="p">),</span> <span class="n">ct</span><span class="p">);</span>

        <span class="n">orderNum</span><span class="p">++;</span>
        <span class="kt">var</span> <span class="n">amount</span> <span class="p">=</span> <span class="n">Math</span><span class="p">.</span><span class="nf">Round</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">NextDouble</span><span class="p">()</span> <span class="p">*</span> <span class="m">500</span> <span class="p">+</span> <span class="m">10</span><span class="p">,</span> <span class="m">2</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">status</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">10</span><span class="p">)</span> <span class="p">&lt;</span> <span class="m">8</span> <span class="p">?</span> <span class="s">"placed"</span> <span class="p">:</span> <span class="s">"cancelled"</span><span class="p">;</span>

        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="s">$"event: </span><span class="p">{</span><span class="n">status</span><span class="p">}</span><span class="s">\n"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="s">$"data: Order #</span><span class="p">{</span><span class="n">orderNum</span><span class="p">}</span><span class="s"> — $</span><span class="p">{</span><span class="n">amount</span><span class="p">}</span><span class="s">\n\n"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Body</span><span class="p">.</span><span class="nf">FlushAsync</span><span class="p">(</span><span class="n">ct</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">event:</code> line goes before <code class="language-plaintext highlighter-rouge">data:</code>. On the client, you use <code class="language-plaintext highlighter-rouge">addEventListener</code> instead of <code class="language-plaintext highlighter-rouge">onmessage</code>. That’s the key difference. <code class="language-plaintext highlighter-rouge">onmessage</code> only fires for unnamed events:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">orders</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="dl">'</span><span class="s1">/events/orders</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">orders</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">placed</span><span class="dl">'</span><span class="p">,</span> <span class="nx">e</span> <span class="o">=&gt;</span> <span class="nx">addOrderLine</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="dl">'</span><span class="s1">placed</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">orders</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">cancelled</span><span class="dl">'</span><span class="p">,</span> <span class="nx">e</span> <span class="o">=&gt;</span> <span class="nx">addOrderLine</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">,</span> <span class="dl">'</span><span class="s1">cancelled</span><span class="dl">'</span><span class="p">));</span>
</code></pre></div></div>

<p>One connection, multiple handlers.</p>

<h2 id="reconnection-with-event-ids">Reconnection with Event IDs</h2>

<p>The server sends <code class="language-plaintext highlighter-rouge">id:</code> with each event. Connection drops, the browser reconnects and sends <code class="language-plaintext highlighter-rouge">Last-Event-ID</code> in the header. Server picks up where it left off. No custom retry logic.</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">MapGet</span><span class="p">(</span><span class="s">"/events/metrics"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">HttpContext</span> <span class="n">ctx</span><span class="p">)</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">ContentType</span> <span class="p">=</span> <span class="s">"text/event-stream"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">CacheControl</span> <span class="p">=</span> <span class="s">"no-cache"</span><span class="p">;</span>
    <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="n">Connection</span> <span class="p">=</span> <span class="s">"keep-alive"</span><span class="p">;</span>

    <span class="kt">var</span> <span class="n">ct</span> <span class="p">=</span> <span class="n">ctx</span><span class="p">.</span><span class="n">RequestAborted</span><span class="p">;</span>
    <span class="kt">var</span> <span class="n">lastId</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">ctx</span><span class="p">.</span><span class="n">Request</span><span class="p">.</span><span class="n">Headers</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span><span class="s">"Last-Event-ID"</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">lastEventId</span><span class="p">)</span>
        <span class="p">&amp;&amp;</span> <span class="kt">int</span><span class="p">.</span><span class="nf">TryParse</span><span class="p">(</span><span class="n">lastEventId</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">parsed</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="n">lastId</span> <span class="p">=</span> <span class="n">parsed</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="kt">var</span> <span class="n">seq</span> <span class="p">=</span> <span class="n">lastId</span><span class="p">;</span>
    <span class="k">while</span> <span class="p">(!</span><span class="n">ct</span><span class="p">.</span><span class="n">IsCancellationRequested</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="m">2000</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="n">seq</span><span class="p">++;</span>

        <span class="kt">var</span> <span class="n">cpu</span> <span class="p">=</span> <span class="n">Math</span><span class="p">.</span><span class="nf">Round</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">NextDouble</span><span class="p">()</span> <span class="p">*</span> <span class="m">60</span> <span class="p">+</span> <span class="m">10</span><span class="p">,</span> <span class="m">1</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">mem</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">40</span><span class="p">,</span> <span class="m">85</span><span class="p">);</span>

        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="s">$"id: </span><span class="p">{</span><span class="n">seq</span><span class="p">}</span><span class="s">\n"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="nf">WriteAsync</span><span class="p">(</span><span class="s">$"data: </span><span class="p">{{</span><span class="err">\</span><span class="s">"cpu\":{cpu},\"mem\":{mem},\"seq\":{seq}}}\n\n"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">ctx</span><span class="p">.</span><span class="n">Response</span><span class="p">.</span><span class="n">Body</span><span class="p">.</span><span class="nf">FlushAsync</span><span class="p">(</span><span class="n">ct</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span><span class="s">);
</span></code></pre></div></div>

<p>Client side is just JSON parsing:</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">metrics</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">EventSource</span><span class="p">(</span><span class="dl">'</span><span class="s1">/events/metrics</span><span class="dl">'</span><span class="p">);</span>
<span class="nx">metrics</span><span class="p">.</span><span class="nx">onmessage</span> <span class="o">=</span> <span class="nx">e</span> <span class="o">=&gt;</span> <span class="p">{</span>
    <span class="kd">const</span> <span class="nx">d</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">e</span><span class="p">.</span><span class="nx">data</span><span class="p">);</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">cpu</span><span class="dl">'</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">d</span><span class="p">.</span><span class="nx">cpu</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">%</span><span class="dl">'</span><span class="p">;</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">mem</span><span class="dl">'</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">d</span><span class="p">.</span><span class="nx">mem</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">%</span><span class="dl">'</span><span class="p">;</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="dl">'</span><span class="s1">seq</span><span class="dl">'</span><span class="p">).</span><span class="nx">textContent</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">#</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">d</span><span class="p">.</span><span class="nx">seq</span><span class="p">;</span>
<span class="p">};</span>
</code></pre></div></div>

<p>The browser handles the reconnection loop and sends <code class="language-plaintext highlighter-rouge">Last-Event-ID</code> automatically. You don’t write that code.</p>

<h2 id="when-to-use-what">When to Use What</h2>

<table>
  <thead>
    <tr>
      <th>Use SSE when</th>
      <th>Use SignalR when</th>
      <th>Use WebSockets when</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Server pushes, client never sends</td>
      <td>Bidirectional, groups, auth</td>
      <td>Raw bidirectional, custom protocol</td>
    </tr>
    <tr>
      <td>Proxies must work (corporate, CDN)</td>
      <td>You need hub abstractions</td>
      <td>You control both ends</td>
    </tr>
    <tr>
      <td>You want built-in reconnect</td>
      <td>You need rooms, presence</td>
      <td>You need binary frames</td>
    </tr>
    <tr>
      <td>HTTP/2 multiplexing is fine</td>
      <td>You’re already in the ecosystem</td>
      <td>You’re building a game or trading feed</td>
    </tr>
  </tbody>
</table>

<p>Dashboards, notifications, live logs, progress bars? Server-Sent Events. Chat, collaborative editing, anything where clients push back heavily? SignalR. If you need binary frames or a custom wire protocol, drop to WebSockets.</p>

<p>Really comes down to data direction and what your infra team will allow through the proxy.</p>

<h2 id="gotchas">Gotchas</h2>

<p><strong>HTTP/1.1 connection limits.</strong> Browsers cap connections per domain at ~6. Open 6 SSE streams and your next fetch queues.</p>

<p>HTTP/2 multiplexes over one connection, so the problem disappears. If you’re on HTTP/1.1, keep streams under the limit.</p>

<p><strong>Text only.</strong> No binary. Base64 if you must, but at that point you probably want WebSockets.</p>

<p>The playground has all three patterns running on <code class="language-plaintext highlighter-rouge">http://localhost:5074</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>playground/ServerSentEvents/AnimatLabs.ServerSentEvents
dotnet run
</code></pre></div></div>

<p>No NuGet packages, no hub classes: just a GET endpoint and a loop. Full code and setup in the <a href="https://github.com/animat089/playground/tree/main/ServerSentEvents">playground README</a>.</p>

<p>Follow-up piece: SSE plus HTMX for a workflow dashboard where the server pushes HTML fragments. No custom JavaScript. I wanted that article to exist mostly so I’d stop re-explaining EventSource to myself every six months.</p>

<hr />

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/technical/.net/workflow/htmx-dotnet/">HTMX dashboard in .NET</a></li>
  <li><a href="/technical/.net/reactive-programming/">Reactive programming with System.Reactive</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="C#" /><category term=".NET" /><category term="Server-Sent Events" /><category term="SSE" /><category term="Real-Time" /><category term="ASP.NET Core" /><summary type="html"><![CDATA[Server-Sent Events gives you real-time server-to-client streaming in 15 lines of C#. No SignalR hub, no JavaScript library, no WebSocket handshake.]]></summary></entry><entry><title type="html">MassTransit Saga Pattern in .NET: Implement Compensation and Rollback with WorkflowForge</title><link href="https://animatlabs.com/technical/.net/workflow/masstransit-workflowforge-saga/" rel="alternate" type="text/html" title="MassTransit Saga Pattern in .NET: Implement Compensation and Rollback with WorkflowForge" /><published>2026-03-15T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/workflow/masstransit-workflowforge-saga</id><content type="html" xml:base="https://animatlabs.com/technical/.net/workflow/masstransit-workflowforge-saga/"><![CDATA[<p>I spent a while looking for a .NET saga example that actually showed the compensation code. Not a diagram, not a blog post that stops at “and then you’d roll back the previous steps.” Actual running code where a payment fails and stock gets released.</p>

<p>Couldn’t find one I liked. Built this instead. MassTransit handles message routing, WorkflowForge handles the rollback logic. (I’ve sat through enough saga talks where the speaker waved at a box labeled “compensate” and moved on. This is the opposite of that.)</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/WorkflowForge/AnimatLabs.WorkflowForge.MassTransitSaga.OrderService" class="btn btn--primary">GitHub Repo</a></p>

<h2 id="prerequisites">Prerequisites</h2>

<p>You need .NET 8 and the WorkflowForge solution. The demo runs <strong>InMemory transport</strong> by default; no RabbitMQ required.</p>

<p>Want RabbitMQ instead? The project has a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code>:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>playground/WorkflowForge/AnimatLabs.WorkflowForge.MassTransitSaga.OrderService
docker-compose up <span class="nt">-d</span>
</code></pre></div></div>

<p>That starts RabbitMQ on port <strong>5672</strong> with the management UI on <strong>15672</strong> (guest/guest). Swap <code class="language-plaintext highlighter-rouge">UsingInMemory</code> for <code class="language-plaintext highlighter-rouge">UsingRabbitMq</code> in Program.cs and you’re set.</p>

<h2 id="the-problem">The Problem</h2>

<p>You’ve got an order flow: reserve stock, charge payment, create shipment. If any step fails, you undo the previous ones. Saga pattern.</p>

<p>In real life those steps spread across buses, databases, and vendors that all fail in creatively annoying ways at 2 a.m., but this demo stays in one process so you can watch compensation without blaming the network.</p>

<p>The hard part (the part most tutorials skip) is the compensation logic. When a payment gateway times out, who releases the stock? Who refunds the charge? How do you keep that logic in one place instead of scattered across consumers?</p>

<h2 id="the-split-masstransit--workflowforge">The Split: MassTransit + WorkflowForge</h2>

<p>MassTransit distributes the messages. WorkflowForge 2.1.1 compensates the failures. One library does pub/sub. The other does the orchestration and rollback.</p>

<p>The flow:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>SubmitOrder → ReserveStock → ChargePayment → CreateShipment → OrderAccepted
                                  ↓ (failure)
                            ChargePayment.Compensate → ReserveStock.Compensate → OrderFailed
</code></pre></div></div>

<p>Orders over $500 simulate a payment gateway timeout. When that happens, WorkflowForge runs the compensation in reverse order: refund payment, then release stock.</p>

<h2 id="this-is-a-single-service-demo">This Is a Single-Service Demo</h2>

<p>This demo runs in <strong>one process</strong> (OrderService). No separate Inventory, Payments, or Shipping services.</p>

<p>The step-level events (ReserveStock, ChargePayment, CreateShipment) are published to the bus but <strong>no consumers</strong> handle them. They’re fire-and-forget; the workflow steps do the work directly and publish for visibility or future use.</p>

<p>In a real system you’d add consumers in separate services. For this demo, the goal is to show the compensation pattern without the extra moving parts.</p>

<h2 id="messages">Messages</h2>

<p>Contracts are plain records. No shared state, only events.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">namespace</span> <span class="nn">AnimatLabs.WorkflowForge.Workflows.Sample.OrderSaga.Contracts</span><span class="p">;</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">SubmitOrder</span><span class="p">(</span><span class="n">Guid</span> <span class="n">OrderId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">CustomerEmail</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">Amount</span><span class="p">);</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">OrderAccepted</span><span class="p">(</span><span class="n">Guid</span> <span class="n">OrderId</span><span class="p">);</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">OrderFailed</span><span class="p">(</span><span class="n">Guid</span> <span class="n">OrderId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Reason</span><span class="p">);</span>

<span class="k">public</span> <span class="n">record</span> <span class="nf">ReserveStock</span><span class="p">(</span><span class="n">Guid</span> <span class="n">OrderId</span><span class="p">,</span> <span class="kt">int</span> <span class="n">Quantity</span><span class="p">);</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">ChargePayment</span><span class="p">(</span><span class="n">Guid</span> <span class="n">OrderId</span><span class="p">,</span> <span class="kt">decimal</span> <span class="n">Amount</span><span class="p">);</span>
<span class="k">public</span> <span class="n">record</span> <span class="nf">CreateShipment</span><span class="p">(</span><span class="n">Guid</span> <span class="n">OrderId</span><span class="p">,</span> <span class="kt">string</span> <span class="n">CustomerEmail</span><span class="p">);</span>
</code></pre></div></div>

<p>These live in the shared <code class="language-plaintext highlighter-rouge">Workflows.Sample</code> library so any execution project can reference them. The file also defines <code class="language-plaintext highlighter-rouge">StockReserved</code>, <code class="language-plaintext highlighter-rouge">PaymentFailed</code>, and similar; those are there for a future multi-service setup. In this demo the workflow steps publish <code class="language-plaintext highlighter-rouge">ReserveStock</code>, <code class="language-plaintext highlighter-rouge">ChargePayment</code>, <code class="language-plaintext highlighter-rouge">CreateShipment</code> to the bus, but nothing consumes them yet.</p>

<h2 id="the-consumer">The Consumer</h2>

<p><code class="language-plaintext highlighter-rouge">OrderSubmittedConsumer</code> receives the message, builds a workflow, and runs it. The key: <code class="language-plaintext highlighter-rouge">WF.CreateFoundry</code> holds the saga state (OrderId, Amount, CustomerEmail). <code class="language-plaintext highlighter-rouge">WF.CreateSmith</code> runs the workflow.</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">OrderSubmittedConsumer</span><span class="p">(</span><span class="n">IBus</span> <span class="n">bus</span><span class="p">,</span> <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">OrderSubmittedConsumer</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span> <span class="p">:</span> <span class="n">IConsumer</span><span class="p">&lt;</span><span class="n">SubmitOrder</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="nf">Consume</span><span class="p">(</span><span class="n">ConsumeContext</span><span class="p">&lt;</span><span class="n">SubmitOrder</span><span class="p">&gt;</span> <span class="n">context</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">context</span><span class="p">.</span><span class="n">Message</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">"Received order {OrderId} for ${Amount}"</span><span class="p">,</span> <span class="n">order</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">Amount</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">shouldFail</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">Amount</span> <span class="p">&gt;</span> <span class="m">500</span><span class="p">;</span>
        <span class="kt">var</span> <span class="n">massTransitBus</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MassTransitBus</span><span class="p">(</span><span class="n">bus</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">OrderSagaWorkflow</span><span class="p">.</span><span class="nf">Build</span><span class="p">(</span><span class="n">massTransitBus</span><span class="p">,</span> <span class="n">shouldFail</span><span class="p">);</span>

        <span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WF</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span>
            <span class="n">workflowName</span><span class="p">:</span> <span class="n">workflow</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="n">initialProperties</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">object</span><span class="p">?&gt;</span>
            <span class="p">{</span>
                <span class="p">[</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">OrderId</span><span class="p">]</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">OrderId</span><span class="p">,</span>
                <span class="p">[</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">Amount</span><span class="p">]</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">Amount</span><span class="p">,</span>
                <span class="p">[</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">CustomerEmail</span><span class="p">]</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">CustomerEmail</span>
            <span class="p">});</span>

        <span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WF</span><span class="p">.</span><span class="nf">CreateSmith</span><span class="p">(</span><span class="k">new</span> <span class="nf">ConsoleLogger</span><span class="p">(</span><span class="s">"WF-Saga"</span><span class="p">));</span>

        <span class="k">try</span>
        <span class="p">{</span>
            <span class="k">await</span> <span class="n">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="n">foundry</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="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
            <span class="k">await</span> <span class="n">context</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="k">new</span> <span class="nf">OrderAccepted</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="n">OrderId</span><span class="p">)).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
            <span class="n">logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Order {OrderId} completed successfully"</span><span class="p">,</span> <span class="n">order</span><span class="p">.</span><span class="n">OrderId</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="k">await</span> <span class="n">context</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="k">new</span> <span class="nf">OrderFailed</span><span class="p">(</span><span class="n">order</span><span class="p">.</span><span class="n">OrderId</span><span class="p">,</span> <span class="n">ex</span><span class="p">.</span><span class="n">Message</span><span class="p">)).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</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">"Order {OrderId} failed -- compensation executed"</span><span class="p">,</span> <span class="n">order</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Workflow steps depend on <code class="language-plaintext highlighter-rouge">IMessageBus</code> (async publish). The OrderService project adapts MassTransit’s <code class="language-plaintext highlighter-rouge">IBus</code> with a thin wrapper:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">interface</span> <span class="nc">IMessageBus</span>
<span class="p">{</span>
    <span class="n">Task</span> <span class="n">PublishAsync</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">T</span> <span class="n">message</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="k">class</span><span class="err">;</span>
<span class="err">}</span>

<span class="nc">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">MassTransitBus</span><span class="p">(</span><span class="n">IBus</span> <span class="n">bus</span><span class="p">)</span> <span class="p">:</span> <span class="n">IMessageBus</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Task</span> <span class="n">PublishAsync</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="n">T</span> <span class="n">message</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">ct</span><span class="p">)</span> <span class="k">where</span> <span class="n">T</span> <span class="p">:</span> <span class="k">class</span>
        <span class="err">=&gt;</span> <span class="nc">bus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="n">message</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IMessageBus</code> lives in the shared <code class="language-plaintext highlighter-rouge">Workflows.Sample</code> library; <code class="language-plaintext highlighter-rouge">MassTransitBus</code> lives next to the consumer in <code class="language-plaintext highlighter-rouge">AnimatLabs.WorkflowForge.MassTransitSaga.OrderService</code>.</p>

<h2 id="the-workflow">The Workflow</h2>

<p>Each step extends <code class="language-plaintext highlighter-rouge">WorkflowOperationBase</code>. Forge does the work. Restore does the rollback.</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">class</span> <span class="nc">OrderSagaWorkflow</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">IWorkflow</span> <span class="nf">Build</span><span class="p">(</span><span class="n">IMessageBus</span> <span class="n">bus</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">simulatePaymentFailure</span> <span class="p">=</span> <span class="k">false</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="n">WF</span>
            <span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"OrderSaga"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">ReserveStockStep</span><span class="p">(</span><span class="n">bus</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">ChargePaymentStep</span><span class="p">(</span><span class="n">bus</span><span class="p">,</span> <span class="n">simulatePaymentFailure</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">CreateShipmentStep</span><span class="p">(</span><span class="n">bus</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="the-steps">The Steps</h2>

<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">ReserveStockStep</span><span class="p">(</span><span class="n">IMessageBus</span> <span class="n">bus</span><span class="p">)</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"ReserveStock"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">orderId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">&gt;(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>
        <span class="n">foundry</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">"[ReserveStock] Reserving stock for order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</span><span class="p">);</span>

        <span class="k">await</span> <span class="n">bus</span><span class="p">.</span><span class="nf">PublishAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">ReserveStock</span><span class="p">(</span><span class="n">orderId</span><span class="p">,</span> <span class="m">1</span><span class="p">),</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="k">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="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>

        <span class="n">foundry</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">"[ReserveStock] Stock reserved for order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">orderId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">&gt;(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>
        <span class="n">foundry</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">"[ReserveStock] COMPENSATING: Releasing stock for order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</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">300</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="n">foundry</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">"[ReserveStock] Stock released for order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</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="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">ChargePaymentStep</span><span class="p">(</span><span class="n">IMessageBus</span> <span class="n">bus</span><span class="p">,</span> <span class="kt">bool</span> <span class="n">simulateFailure</span><span class="p">)</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"ChargePayment"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">orderId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">&gt;(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">amount</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="kt">decimal</span><span class="p">&gt;(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">Amount</span><span class="p">);</span>
        <span class="n">foundry</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">"[ChargePayment] Charging ${Amount} for order {OrderId}"</span><span class="p">,</span> <span class="n">amount</span><span class="p">,</span> <span class="n">orderId</span><span class="p">);</span>

        <span class="k">await</span> <span class="n">bus</span><span class="p">.</span><span class="nf">PublishAsync</span><span class="p">(</span><span class="k">new</span> <span class="nf">ChargePayment</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">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="m">800</span><span class="p">,</span> <span class="n">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">simulateFailure</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">foundry</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="s">"[ChargePayment] Payment gateway timeout for order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</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">$"Payment gateway timeout for 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="p">}</span>

        <span class="kt">var</span> <span class="n">txId</span> <span class="p">=</span> <span class="s">$"TXN-</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">10000</span><span class="p">,</span> <span class="m">99999</span><span class="p">)}</span><span class="s">"</span><span class="p">;</span>
        <span class="n">foundry</span><span class="p">.</span><span class="nf">SetProperty</span><span class="p">(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">TransactionId</span><span class="p">,</span> <span class="n">txId</span><span class="p">);</span>
        <span class="n">foundry</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">"[ChargePayment] Payment {TransactionId} charged for order {OrderId}"</span><span class="p">,</span> <span class="n">txId</span><span class="p">,</span> <span class="n">orderId</span><span class="p">);</span>

        <span class="k">return</span> <span class="n">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">orderId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">Guid</span><span class="p">&gt;(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">OrderId</span><span class="p">);</span>
        <span class="kt">var</span> <span class="n">txId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;(</span><span class="n">SagaKeys</span><span class="p">.</span><span class="n">TransactionId</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">txId</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="n">foundry</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">"[ChargePayment] COMPENSATING: Refunding {TransactionId} for order {OrderId}"</span><span class="p">,</span> <span class="n">txId</span><span class="p">,</span> <span class="n">orderId</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">ct</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
            <span class="n">foundry</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">"[ChargePayment] Refund issued for {TransactionId}"</span><span class="p">,</span> <span class="n">txId</span><span class="p">);</span>
        <span class="p">}</span>
        <span class="k">else</span>
        <span class="p">{</span>
            <span class="n">foundry</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">"[ChargePayment] No charge to reverse for order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ChargePaymentStep.RestoreAsync</code> checks <code class="language-plaintext highlighter-rouge">TransactionId</code> before refunding. If payment never succeeded, there’s nothing to reverse. That’s the kind of detail that breaks production sagas.</p>

<h2 id="wiring-it-up">Wiring It Up</h2>

<p>InMemory transport means no RabbitMQ needed. Run it, watch two orders: $99 succeeds, $999 fails and triggers compensation.</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">AddMassTransit</span><span class="p">(</span><span class="n">cfg</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="n">cfg</span><span class="p">.</span><span class="n">AddConsumer</span><span class="p">&lt;</span><span class="n">OrderSubmittedConsumer</span><span class="p">&gt;();</span>

    <span class="n">cfg</span><span class="p">.</span><span class="nf">UsingInMemory</span><span class="p">((</span><span class="n">context</span><span class="p">,</span> <span class="n">inmem</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">inmem</span><span class="p">.</span><span class="nf">ConfigureEndpoints</span><span class="p">(</span><span class="n">context</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">});</span>

<span class="c1">// Fire a test order after the bus starts</span>
<span class="n">_</span> <span class="p">=</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Run</span><span class="p">(</span><span class="k">async</span> <span class="p">()</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">await</span> <span class="n">Task</span><span class="p">.</span><span class="nf">Delay</span><span class="p">(</span><span class="m">2000</span><span class="p">);</span>
    <span class="kt">var</span> <span class="n">bus</span> <span class="p">=</span> <span class="n">host</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IBus</span><span class="p">&gt;();</span>

    <span class="k">await</span> <span class="n">bus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="k">new</span> <span class="nf">SubmitOrder</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">"happy@example.com"</span><span class="p">,</span> <span class="m">99.99</span><span class="n">m</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">5000</span><span class="p">);</span>

    <span class="k">await</span> <span class="n">bus</span><span class="p">.</span><span class="nf">Publish</span><span class="p">(</span><span class="k">new</span> <span class="nf">SubmitOrder</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">"sad@example.com"</span><span class="p">,</span> <span class="m">999.99</span><span class="n">m</span><span class="p">));</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="what-i-learned-building-this">What I Learned Building This</h2>

<p>The part I like most about this approach: compensation logic lives in one place: the <code class="language-plaintext highlighter-rouge">RestoreAsync</code> methods. No scattered event handlers, no “if payment failed then fire ReleaseStock” across multiple consumers. WorkflowForge runs the compensation cascade automatically when any step throws.</p>

<p>For RabbitMQ, swap <code class="language-plaintext highlighter-rouge">UsingInMemory</code> for <code class="language-plaintext highlighter-rouge">UsingRabbitMq</code> in <code class="language-plaintext highlighter-rouge">Program.cs</code>. Same code, same workflow, different transport. The project has a <code class="language-plaintext highlighter-rouge">docker-compose.yml</code> with RabbitMQ ready to go if you want to test it.</p>

<p>To try it out:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>playground/WorkflowForge
dotnet run <span class="nt">--project</span> AnimatLabs.WorkflowForge.MassTransitSaga.OrderService
</code></pre></div></div>

<p>The app auto-submits two orders. Watch the logs for the $999 failure and the compensation cascade running in reverse.</p>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="see-also">See Also</h2>

<ul>
  <li><a href="/technical/.net/workflow/workflow-forge-introduction/">WorkflowForge introduction</a></li>
  <li><a href="/technical/.net/workflow/workflowforge-coravel-scheduled-workflows/">WorkflowForge with Coravel</a></li>
  <li><a href="/technical/.net/workflow/htmx-dotnet/">HTMX dashboard in .NET</a></li>
  <li><a href="/technical/.net/.net-core/polly-v8-resilience-patterns/">Polly v8 resilience patterns</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Workflow" /><category term=".NET" /><category term="MassTransit" /><category term="WorkflowForge" /><category term="Saga Pattern" /><category term="Compensation" /><category term="Messaging" /><category term="RabbitMQ" /><summary type="html"><![CDATA[Implement the saga pattern in .NET with MassTransit for messaging and WorkflowForge for automatic compensation. Includes working rollback code, not just diagrams.]]></summary></entry><entry><title type="html">Traefik Reverse Proxy for .NET Docker Services: Eliminate Port Conflicts</title><link href="https://animatlabs.com/technical/.net/infra/dotnet-docker-traefik/" rel="alternate" type="text/html" title="Traefik Reverse Proxy for .NET Docker Services: Eliminate Port Conflicts" /><published>2026-03-14T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/infra/dotnet-docker-traefik</id><content type="html" xml:base="https://animatlabs.com/technical/.net/infra/dotnet-docker-traefik/"><![CDATA[<h2 id="i-kept-a-port-lookup-reference">I Kept a Port Lookup Reference</h2>

<p>Five .NET services, a React frontend, Redis, and PostgreSQL. I had a text file. Service A on 5000. Service B on 5001. Frontend on 3000.</p>

<p>Someone would clone the repo, run <code class="language-plaintext highlighter-rouge">dotnet run</code>, and Service A would crash because their machine already had something on 5000. Every. Single. Time.</p>

<p>I added instructions to the README: “Change the port in launchSettings.json.” Nobody read them. I changed the ports. Someone else changed them back. I gave up.</p>

<p>Traefik fixed all of this. Every service runs on the same internal port, and routing happens by hostname instead of port number.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/TraefikDotNet" class="btn btn--primary">GitHub Repo</a></p>

<h2 id="the-docker-compose">The docker-compose</h2>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik:v3.6</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--api.insecure=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.exposedbydefault=false"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entryPoints.web.address=:80"</span>
    <span class="na">ports</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">80:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">8080:8080"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik-net</span>

  <span class="na">api-service</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">./ApiService</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host(`api.localhost`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.entrypoints=web"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.api.loadbalancer.server.port=8080"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik-net</span>

  <span class="na">web-service</span><span class="pi">:</span>
    <span class="na">build</span><span class="pi">:</span> <span class="s">./WebService</span>
    <span class="na">labels</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.enable=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.web.rule=Host(`web.localhost`)"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.web.entrypoints=web"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.services.web.loadbalancer.server.port=8080"</span>
    <span class="na">networks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">traefik-net</span>

<span class="na">networks</span><span class="pi">:</span>
  <span class="na">traefik-net</span><span class="pi">:</span>
    <span class="na">driver</span><span class="pi">:</span> <span class="s">bridge</span>
</code></pre></div></div>

<p>That’s the full thing. Traefik plus two .NET services. <code class="language-plaintext highlighter-rouge">docker-compose up --build</code> and you get:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">http://api.localhost</code> (API)</li>
  <li><code class="language-plaintext highlighter-rouge">http://web.localhost</code> (Web frontend)</li>
  <li><code class="language-plaintext highlighter-rouge">http://localhost:8080</code> (Traefik dashboard)</li>
</ul>

<p>Neither service publishes a port to the host. Third service? Add another block with a different hostname. Traefik picks it up. You don’t touch any ports.</p>

<p>All images are free and open-source (Traefik is MIT). The compose commands work with Docker, Podman, or Rancher Desktop.</p>

<h2 id="the-dockerfile">The Dockerfile</h2>

<p>Both services share the same Dockerfile pattern. <code class="language-plaintext highlighter-rouge">ApiService/Dockerfile</code>:</p>

<div class="language-dockerfile highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">FROM</span><span class="w"> </span><span class="s">mcr.microsoft.com/dotnet/aspnet:9.0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">base</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">EXPOSE</span><span class="s"> 8080</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">mcr.microsoft.com/dotnet/sdk:9.0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span>
<span class="k">WORKDIR</span><span class="s"> /src</span>
<span class="k">COPY</span><span class="s"> ["ApiService.csproj", "."]</span>
<span class="k">RUN </span>dotnet restore
<span class="k">COPY</span><span class="s"> . .</span>
<span class="k">RUN </span>dotnet build <span class="nt">-c</span> Release <span class="nt">-o</span> /app/build

<span class="k">FROM</span><span class="w"> </span><span class="s">build</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">publish</span>
<span class="k">RUN </span>dotnet publish <span class="nt">-c</span> Release <span class="nt">-o</span> /app/publish /p:UseAppHost<span class="o">=</span><span class="nb">false</span>

<span class="k">FROM</span><span class="w"> </span><span class="s">base</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">final</span>
<span class="k">WORKDIR</span><span class="s"> /app</span>
<span class="k">COPY</span><span class="s"> --from=publish /app/publish .</span>
<span class="k">ENTRYPOINT</span><span class="s"> ["dotnet", "ApiService.dll"]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">WebService/Dockerfile</code> is identical (swap <code class="language-plaintext highlighter-rouge">ApiService</code> for <code class="language-plaintext highlighter-rouge">WebService</code>). Both expose 8080.</p>

<h2 id="how-traefik-routes">How Traefik Routes</h2>

<p>Three concepts: entrypoints (ports Traefik listens on), routers (rules that match requests), and services (the backends that handle them).</p>

<p>In our compose, <code class="language-plaintext highlighter-rouge">web</code> is an entrypoint on port 80. The router rule <code class="language-plaintext highlighter-rouge">Host(</code>api.localhost<code class="language-plaintext highlighter-rouge">)</code> matches the HTTP Host header. And <code class="language-plaintext highlighter-rouge">loadbalancer.server.port=8080</code> tells Traefik which port <em>inside the container</em> to hit.</p>

<p>So <code class="language-plaintext highlighter-rouge">http://api.localhost/orders</code> goes to Traefik on port 80, Host header matches, Traefik forwards to <code class="language-plaintext highlighter-rouge">api-service:8080</code>. No port published on the host. No coordination lookup.</p>

<p>You can combine matchers too: <code class="language-plaintext highlighter-rouge">Host(</code>api.localhost<code class="language-plaintext highlighter-rouge">) &amp;&amp; PathPrefix(</code>/v2<code class="language-plaintext highlighter-rouge">)</code> for path-based routing on top of hostnames.</p>

<h2 id="https-with-lets-encrypt">HTTPS With Let’s Encrypt</h2>

<p>For staging or production, Traefik handles certs. You don’t install certbot, you don’t set up cron jobs, you don’t think about renewal.</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">services</span><span class="pi">:</span>
  <span class="na">traefik</span><span class="pi">:</span>
    <span class="na">image</span><span class="pi">:</span> <span class="s">traefik:v3.6</span>
    <span class="na">command</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--providers.docker.exposedbydefault=false"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entryPoints.web.address=:80"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entryPoints.websecure.address=:443"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.le.acme.httpchallenge=true"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.le.acme.httpchallenge.entrypoint=web"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.le.acme.email=you@example.com"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--certificatesresolvers.le.acme.storage=/letsencrypt/acme.json"</span>
      <span class="pi">-</span> <span class="s2">"</span><span class="s">--entryPoints.web.http.redirections.entrypoint.to=websecure"</span>
    <span class="na">volumes</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="s">/var/run/docker.sock:/var/run/docker.sock</span>
      <span class="pi">-</span> <span class="s">./letsencrypt:/letsencrypt</span>
</code></pre></div></div>

<p>And the service labels:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">labels</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host(`api.yourdomain.com`)"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.entrypoints=websecure"</span>
  <span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.tls.certresolver=le"</span>
</code></pre></div></div>

<p>Issues the cert. Renews before expiry. Redirects HTTP to HTTPS. I set this up once six months ago and haven’t touched it since.</p>

<h2 id="debugging">Debugging</h2>

<p>I hit all of these at least once.</p>

<p>The 502 I burned the most time on was a port mismatch. My <code class="language-plaintext highlighter-rouge">loadbalancer.server.port</code> label said 5000 but the container was listening on 8080. Dashboard at <code class="language-plaintext highlighter-rouge">localhost:8080</code> showed the router in red. Obvious in hindsight. Check <code class="language-plaintext highlighter-rouge">docker ps</code>, verify the container is alive, then look at the dashboard.</p>

<p>The 404-on-everything problem? Missing <code class="language-plaintext highlighter-rouge">traefik.enable=true</code>. We set <code class="language-plaintext highlighter-rouge">exposedbydefault=false</code> in the compose (you should), which means every service needs that label. I missed it on a third service and spent twenty minutes reading Traefik docs before I noticed.</p>

<p>Then there’s the backtick thing:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Broken</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host('api.localhost')"</span>

<span class="c1"># Works</span>
<span class="pi">-</span> <span class="s2">"</span><span class="s">traefik.http.routers.api.rule=Host(`api.localhost`)"</span>
</code></pre></div></div>

<p>Single quotes vs backticks. The error message doesn’t help. You just have to know.</p>

<p>One more: if service A calls service B, skip Traefik entirely. Use the compose service name and internal port:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">client</span> <span class="p">=</span> <span class="k">new</span> <span class="n">HttpClient</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://api-service:8080"</span><span class="p">)</span> <span class="p">};</span>
</code></pre></div></div>

<p>That stays inside the Docker network.</p>

<h2 id="how-i-actually-use-it">How I Actually Use It</h2>

<p>I don’t run everything in Docker during development. Too slow for hot reload. What I do: start Traefik once with <code class="language-plaintext highlighter-rouge">docker-compose up -d traefik</code>, then run individual services with <code class="language-plaintext highlighter-rouge">dotnet watch</code>. When I need the full containerized setup (integration testing, demo), <code class="language-plaintext highlighter-rouge">docker-compose up --build</code> brings everything up.</p>

<p>Staging gets TLS and health checks. Production adds rate limiting and access logs on top. Same compose file, different labels. Haven’t changed the core config in months.</p>

<p>The playground has two .NET services behind Traefik:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">cd </span>playground/TraefikDotNet
docker-compose up <span class="nt">--build</span>
</code></pre></div></div>

<p>Open <code class="language-plaintext highlighter-rouge">http://api.localhost</code>, <code class="language-plaintext highlighter-rouge">http://web.localhost</code>, and <code class="language-plaintext highlighter-rouge">http://localhost:8080</code> (dashboard). Both services run on 8080 internally. No port conflicts.</p>

<p><strong>Playground:</strong> <a href="https://github.com/animat089/playground/tree/main/TraefikDotNet">TraefikDotNet</a></p>

<hr />

<h2 id="related">Related</h2>

<ul>
  <li><a href="/technical/infra/wsl2/wsl2-installation-windows/">WSL2 on Windows</a></li>
  <li><a href="/technical/infra/wsl2/wsl2-visual-studio-docker-without-docker-desktop/">Docker on WSL2 with Visual Studio</a></li>
  <li><a href="/technical/.net/open-source/shipping-quality-dotnet-oss-release/">Shipping a quality .NET OSS release</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Infra" /><category term=".NET" /><category term="Docker" /><category term="Traefik" /><category term="Containers" /><category term="DevOps" /><category term="Reverse Proxy" /><summary type="html"><![CDATA[Configure Traefik as a reverse proxy for multiple .NET Docker services. Eliminate port conflicts, add automatic HTTPS, and simplify local development.]]></summary></entry><entry><title type="html">Polly v8 in .NET: Retry, Circuit Breaker, and Timeout Resilience Patterns</title><link href="https://animatlabs.com/technical/.net/.net-core/polly-v8-resilience-patterns/" rel="alternate" type="text/html" title="Polly v8 in .NET: Retry, Circuit Breaker, and Timeout Resilience Patterns" /><published>2026-03-10T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/polly-v8-resilience-patterns</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/polly-v8-resilience-patterns/"><![CDATA[<p>Most .NET apps I’ve worked on had zero resilience code until something actually broke. A third-party API times out, the connection pool fills up, and suddenly completely unrelated endpoints start failing. The usual pattern: someone adds a try-catch, wraps it in a retry loop with <code class="language-plaintext highlighter-rouge">Thread.Sleep</code>, and calls it a day. Works until it doesn’t.</p>

<p>Polly v8 does this properly. I want to walk through how I set it up, what the defaults give you, and where I’ve had to go beyond them. Retries without jitter are basically a distributed denial of service you aimed at yourself, which is a mouthful, but it is also the fastest way to turn a brownout into an outage when every pod wakes up and slams the same downstream at the same millisecond.</p>

<h2 id="start-here">Start Here</h2>

<p><code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code> gives you a pre-configured pipeline with one line:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Microsoft.Extensions.Http.Resilience
</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="nf">AddHttpClient</span><span class="p">(</span><span class="s">"MyApi"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span>
</code></pre></div></div>

<p>That gets you exponential backoff with jitter, a circuit breaker, and a timeout. Sensible defaults. For most HTTP clients in a typical web app, this is enough. Honestly, I wish more teams would start here instead of building custom retry logic from scratch.</p>

<p>If this covers your case, stop reading. What follows is for the other 20%.</p>

<h2 id="going-beyond-the-defaults">Going Beyond the Defaults</h2>

<p>I reach for custom Polly pipelines when the one-liner doesn’t fit. In my experience that’s usually one of these situations:</p>

<ul>
  <li>A payment gateway needs 2 retries with a 5-second timeout while an analytics endpoint is fine with 5 retries and 30 seconds</li>
  <li>Database calls, message queues, file I/O. Anything that isn’t <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code></li>
  <li>The SLA is tight enough (say 500ms) that the default circuit breaker settings are too generous</li>
  <li>I need <code class="language-plaintext highlighter-rouge">OnRetry</code> or <code class="language-plaintext highlighter-rouge">OnOpened</code> callbacks wired into our structured logging</li>
</ul>

<h2 id="retries">Retries</h2>

<p>If something fails, try again. Simple idea, easy to get wrong. Naive retries with fixed delays can turn a struggling service into a dead one because every instance retries at the same interval and you get a thundering herd.</p>

<p>The fix is jitter. <code class="language-plaintext highlighter-rouge">UseJitter = true</code> adds randomness to the backoff so retries spread out instead of hitting the downstream service in waves. I’ve seen this make the difference between a service recovering on its own and a full cascading failure.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">retryPipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
        <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">),</span>
        <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span><span class="p">,</span>
        <span class="n">UseJitter</span> <span class="p">=</span> <span class="k">true</span><span class="p">,</span>
        <span class="n">OnRetry</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">$"Retry </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">AttemptNumber</span><span class="p">}</span><span class="s"> after </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">RetryDelay</span><span class="p">}</span><span class="s">"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="kt">var</span> <span class="n">result</span> <span class="p">=</span> <span class="k">await</span> <span class="n">retryPipeline</span><span class="p">.</span><span class="nf">ExecuteAsync</span><span class="p">(</span><span class="k">async</span> <span class="n">ct</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="k">await</span> <span class="n">_httpClient</span><span class="p">.</span><span class="nf">GetAsync</span><span class="p">(</span><span class="s">"/api/data"</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div></div>

<p>One thing to watch: don’t retry non-idempotent operations without safeguards. Retrying a payment charge can double-bill someone. Retrying a 400 is pointless because it’ll fail the same way every time.</p>

<h2 id="circuit-breaker">Circuit Breaker</h2>

<p>When a downstream service is unhealthy, retrying just delays the inevitable. Circuit breakers flip the approach: once failures cross a threshold, stop trying altogether and fail fast.</p>

<p>The circuit has three states. Closed (normal flow), Open (requests fail immediately), and Half-Open (a few test requests probe whether the service recovered). Pretty standard pattern, but there’s a setting most people miss:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">circuitBreakerPipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddCircuitBreaker</span><span class="p">(</span><span class="k">new</span> <span class="n">CircuitBreakerStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">FailureRatio</span> <span class="p">=</span> <span class="m">0.5</span><span class="p">,</span>
        <span class="n">SamplingDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">10</span><span class="p">),</span>
        <span class="n">MinimumThroughput</span> <span class="p">=</span> <span class="m">8</span><span class="p">,</span>
        <span class="n">BreakDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">),</span>
        <span class="n">OnOpened</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Circuit opened - failing fast"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">},</span>
        <span class="n">OnClosed</span> <span class="p">=</span> <span class="n">args</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Circuit closed - back to normal"</span><span class="p">);</span>
            <span class="k">return</span> <span class="n">ValueTask</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">MinimumThroughput</code> is the one. Without it, a single failure during low-traffic hours (say, 1 request in 10 seconds) would open the circuit because 1/1 = 100% failure. I set it to 8 so the circuit only evaluates after enough requests to be meaningful.</p>

<h2 id="timeouts">Timeouts</h2>

<p>This is the one that bit us the hardest. Without timeouts, a slow downstream service silently consumes your connection pool and thread pool until the whole app stalls. No errors, no exceptions. Just increasing latency and then everything stops.</p>

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

<p>I use two levels: an overall timeout for the entire operation (including retries) and a per-attempt timeout for individual calls. A common mistake is setting the timeout to 30 seconds when the SLA requires a 2-second response. By the time it fires, you’ve already violated the SLA.</p>

<h2 id="putting-it-together">Putting It Together</h2>

<p>Order matters when you compose strategies. They apply outer to inner:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">resiliencePipeline</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ResiliencePipelineBuilder</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">RetryStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
        <span class="n">Delay</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">),</span>
        <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddCircuitBreaker</span><span class="p">(</span><span class="k">new</span> <span class="n">CircuitBreakerStrategyOptions</span>
    <span class="p">{</span>
        <span class="n">FailureRatio</span> <span class="p">=</span> <span class="m">0.5</span><span class="p">,</span>
        <span class="n">BreakDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">)</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">))</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>So each HTTP call gets 5 seconds max. Failures retry up to 3 times with exponential backoff. If 50% of calls fail within the sampling window, the circuit opens for 30 seconds. And the whole thing wraps in a 30-second overall timeout.</p>

<h2 id="per-client-configuration">Per-Client Configuration</h2>

<p>When <code class="language-plaintext highlighter-rouge">AddStandardResilienceHandler()</code> isn’t enough, <code class="language-plaintext highlighter-rouge">AddResilienceHandler</code> gives you full control per 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">"PaymentGateway"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddResilienceHandler</span><span class="p">(</span><span class="s">"strict"</span><span class="p">,</span> <span class="n">builder</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">builder</span>
            <span class="p">.</span><span class="nf">AddTimeout</span><span class="p">(</span><span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">5</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">AddRetry</span><span class="p">(</span><span class="k">new</span> <span class="n">HttpRetryStrategyOptions</span>
            <span class="p">{</span>
                <span class="n">MaxRetryAttempts</span> <span class="p">=</span> <span class="m">2</span><span class="p">,</span>
                <span class="n">BackoffType</span> <span class="p">=</span> <span class="n">DelayBackoffType</span><span class="p">.</span><span class="n">Exponential</span>
            <span class="p">})</span>
            <span class="p">.</span><span class="nf">AddCircuitBreaker</span><span class="p">(</span><span class="k">new</span> <span class="n">HttpCircuitBreakerStrategyOptions</span>
            <span class="p">{</span>
                <span class="n">FailureRatio</span> <span class="p">=</span> <span class="m">0.3</span><span class="p">,</span>
                <span class="n">SamplingDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">10</span><span class="p">),</span>
                <span class="n">BreakDuration</span> <span class="p">=</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">)</span>
            <span class="p">});</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">AddHttpClient</span><span class="p">(</span><span class="s">"AnalyticsApi"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddStandardResilienceHandler</span><span class="p">();</span>
</code></pre></div></div>

<p>Payment gateway gets tight timeouts, fewer retries, and an aggressive circuit breaker. Analytics API is fine with defaults. <code class="language-plaintext highlighter-rouge">HttpRetryStrategyOptions</code> and <code class="language-plaintext highlighter-rouge">HttpCircuitBreakerStrategyOptions</code> handle transient HTTP errors (5xx, network failures) so you don’t have to specify which exceptions to catch.</p>

<h2 id="workflowforge-integration">WorkflowForge Integration</h2>

<p>If you’re using <a href="https://github.com/animatlabs/workflow-forge">WorkflowForge</a> for workflow orchestration, the Polly extension adds resilience as middleware on the foundry:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// dotnet add package WorkflowForge.Extensions.Resilience.Polly</span>
<span class="k">using</span> <span class="nn">WorkflowForge.Extensions.Resilience.Polly</span><span class="p">;</span>

<span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span><span class="s">"OrderProcessing"</span><span class="p">);</span>
<span class="n">foundry</span><span class="p">.</span><span class="nf">UsePollyRetry</span><span class="p">(</span><span class="n">maxRetryAttempts</span><span class="p">:</span> <span class="m">3</span><span class="p">,</span> <span class="n">baseDelay</span><span class="p">:</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">));</span>
</code></pre></div></div>

<p>Or go all-in with retry, circuit breaker, and timeout together:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">foundry</span><span class="p">.</span><span class="nf">UsePollyComprehensive</span><span class="p">(</span>
    <span class="n">maxRetryAttempts</span><span class="p">:</span> <span class="m">3</span><span class="p">,</span>
    <span class="n">circuitBreakerThreshold</span><span class="p">:</span> <span class="m">5</span><span class="p">,</span>
    <span class="n">timeoutDuration</span><span class="p">:</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">30</span><span class="p">));</span>
</code></pre></div></div>

<p>You can also wrap individual operations instead of the entire foundry:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">resilientOp</span> <span class="p">=</span> <span class="n">PollyRetryOperation</span><span class="p">.</span><span class="nf">WithRetryPolicy</span><span class="p">(</span>
    <span class="k">new</span> <span class="nf">ActionWorkflowOperation</span><span class="p">(</span><span class="s">"CallApi"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="c1">// call external API</span>
    <span class="p">}),</span>
    <span class="n">maxRetryAttempts</span><span class="p">:</span> <span class="m">3</span><span class="p">,</span>
    <span class="n">baseDelay</span><span class="p">:</span> <span class="n">TimeSpan</span><span class="p">.</span><span class="nf">FromSeconds</span><span class="p">(</span><span class="m">1</span><span class="p">));</span>
</code></pre></div></div>

<h2 id="what-i-actually-run-in-production">What I Actually Run in Production</h2>

<table>
  <thead>
    <tr>
      <th>Pattern</th>
      <th>Value</th>
      <th>Why</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Retry attempts</td>
      <td>3</td>
      <td>Enough for transient blips, not so many it delays failure detection</td>
    </tr>
    <tr>
      <td>Initial delay</td>
      <td>1 second</td>
      <td>Gives transient issues time to clear</td>
    </tr>
    <tr>
      <td>Backoff</td>
      <td>Exponential + jitter</td>
      <td>Prevents thundering herd</td>
    </tr>
    <tr>
      <td>Circuit breaker ratio</td>
      <td>50%</td>
      <td>Catches real problems without tripping on normal variance</td>
    </tr>
    <tr>
      <td>Break duration</td>
      <td>30 seconds</td>
      <td>Long enough for recovery, short enough to detect when services come back</td>
    </tr>
    <tr>
      <td>Timeout</td>
      <td>10s per-call, 30s overall</td>
      <td>Adjust to your SLA</td>
    </tr>
  </tbody>
</table>

<p>The patterns above are what I’ve settled on after running these in production for a while. Circuit breakers on every external HTTP client, structured logging in <code class="language-plaintext highlighter-rouge">OnRetry</code> and <code class="language-plaintext highlighter-rouge">OnOpened</code> so we actually know when things degrade, and separate configs for critical vs best-effort services.</p>

<p>The snippets above are standalone. Copy them into any .NET 8+ project with <code class="language-plaintext highlighter-rouge">dotnet add package Polly</code> (or <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Http.Resilience</code> for the <code class="language-plaintext highlighter-rouge">IHttpClientFactory</code> integration). For WorkflowForge, add <code class="language-plaintext highlighter-rouge">WorkflowForge.Extensions.Resilience.Polly</code>.</p>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="more-on-this-topic">More on This Topic</h2>

<ul>
  <li><a href="/technical/.net/.net-core/redis-distributed-locking/">Redis distributed locking in .NET</a></li>
  <li><a href="/technical/.net/.net-core/refit-api-sdk/">Refit API clients</a></li>
  <li><a href="/technical/.net/workflow/masstransit-workflowforge-saga/">MassTransit saga with WorkflowForge</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term="Polly" /><category term="Resilience" /><category term="Circuit Breaker" /><category term="Retry" /><category term="Microservices" /><summary type="html"><![CDATA[Practical Polly v8 resilience patterns for .NET including retry, circuit breaker, timeout, and rate limiting. From the one-liner that covers 80% of cases to custom pipelines.]]></summary></entry><entry><title type="html">Ship a Quality .NET Open Source Release: CI/CD, SBOM, and NuGet Publishing Pipeline</title><link href="https://animatlabs.com/technical/.net/open%20source/shipping-quality-dotnet-oss-release/" rel="alternate" type="text/html" title="Ship a Quality .NET Open Source Release: CI/CD, SBOM, and NuGet Publishing Pipeline" /><published>2026-03-08T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/open%20source/shipping-quality-dotnet-oss-release</id><content type="html" xml:base="https://animatlabs.com/technical/.net/open%20source/shipping-quality-dotnet-oss-release/"><![CDATA[<h2 id="from-laptop-to-pipeline">From Laptop to Pipeline</h2>

<p>WorkflowForge 2.0 shipped from my laptop. <code class="language-plaintext highlighter-rouge">dotnet pack</code>, <code class="language-plaintext highlighter-rouge">dotnet nuget push</code>, done. Thirteen packages on NuGet.org, no CI to speak of.</p>

<p>For the 2.1 release (sixty issues on a feature branch, thirteen packages targeting .NET Framework 4.8, .NET 8.0, and .NET 10.0), I wanted to do it properly. Static analysis. Automated testing across all three frameworks. Supply chain attestation. A software bill of materials. Gated publish with human approval.</p>

<p>What follows is every piece of infrastructure that went into that transition. Not a changelog, not a feature announcement. Just the tooling, the configuration, and the places where things broke in ways I did not expect. Some of those breaks were embarrassing.</p>

<p>If you maintain a .NET open-source project and ship to NuGet, this is the checklist I wish I had before I started.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animatlabs/workflow-forge" class="btn btn--primary">GitHub Repo</a></p>

<hr />

<h2 id="static-analysis-free-for-public-repos">Static Analysis: Free for Public Repos</h2>

<p>SonarCloud is free for any public GitHub repository. There is no reason not to use it.</p>

<p>Setting it up requires a Java runtime for the scanner, a project token stored as a GitHub Actions secret, and three commands wrapping the build:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Install SonarScanner for .NET</span>
  <span class="na">run</span><span class="pi">:</span> <span class="s">dotnet tool install --global dotnet-sonarscanner</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Build and Test with SonarCloud</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">dotnet sonarscanner begin \</span>
      <span class="s">/k:"animatlabs_workflow-forge" \</span>
      <span class="s">/o:"animatlabs" \</span>
      <span class="s">/d:sonar.token="$SONAR_TOKEN" \</span>
      <span class="s">/d:sonar.host.url="https://sonarcloud.io" \</span>
      <span class="s">/d:sonar.cs.opencover.reportsPaths="**/TestResults/**/coverage.opencover.xml"</span>
    <span class="s">dotnet build WorkflowForge.sln --configuration Release</span>
    <span class="s">dotnet test WorkflowForge.sln --configuration Release \</span>
      <span class="s">--collect:"XPlat Code Coverage" \</span>
      <span class="s">-- DataCollectionRunSettings.DataCollectors.DataCollector.Configuration.Format=opencover</span>
    <span class="s">dotnet sonarscanner end /d:sonar.token="$SONAR_TOKEN"</span>
</code></pre></div></div>

<p>The scanner wraps the build, collects coverage data, and uploads results. First run on the WorkflowForge codebase: quality gate passed, 88.9% coverage on new code, 2.2% duplication. But it also flagged dozens of issues I had not noticed.</p>

<h3 id="what-it-found">What It Found</h3>

<p>The most common finding was structured logging violations:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Looks innocent. Breaks structured logging.</span>
<span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">$"Processing 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="c1">// Template + args: OrderId is a real field in Seq, App Insights, etc.</span>
<span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Processing order {OrderId}"</span><span class="p">,</span> <span class="n">orderId</span><span class="p">);</span>
</code></pre></div></div>

<p>The interpolated version compiles and runs. The log output looks identical. But <code class="language-plaintext highlighter-rouge">orderId</code> gets baked into the message string instead of being a separate structured field. In production, you cannot filter or aggregate by order ID. SonarCloud flagged every instance across the codebase.</p>

<p>It also caught missing <code class="language-plaintext highlighter-rouge">sealed</code> modifiers on classes that were not designed for inheritance, inconsistent visibility on internal constants, and unused event declarations in test doubles. Legitimate code smells that manual review had missed.</p>

<h3 id="what-it-did-not-find">What It Did Not Find</h3>

<p>The hardest bugs in this release required reasoning about concurrency, shared state, and disposal patterns. No static analyzer handles those well today.</p>

<p><strong>Thread safety in conditional operations:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Shared bool without a memory barrier</span>
<span class="k">private</span> <span class="kt">bool</span> <span class="n">_lastConditionResult</span><span class="p">;</span>

<span class="c1">// volatile so compensation does not read another run's stale flag</span>
<span class="k">private</span> <span class="k">volatile</span> <span class="kt">bool</span> <span class="n">_lastConditionResult</span><span class="p">;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">ConditionalWorkflowOperation</code> evaluates a condition during execution and reads the result during compensation. Without <code class="language-plaintext highlighter-rouge">volatile</code>, one workflow’s compensation could read another workflow’s stale condition result. The fix is one keyword, but finding the bug requires understanding the concurrent execution model.</p>

<p><strong>O(n^2) indexing in persistence middleware:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// O(n) scan on every middleware hop</span>
<span class="kt">var</span> <span class="n">index</span> <span class="p">=</span> <span class="n">workflow</span><span class="p">.</span><span class="n">Operations</span><span class="p">.</span><span class="nf">ToList</span><span class="p">().</span><span class="nf">IndexOf</span><span class="p">(</span><span class="n">currentOperation</span><span class="p">);</span>

<span class="c1">// Read the index the foundry already tracks</span>
<span class="k">private</span> <span class="k">static</span> <span class="kt">int</span> <span class="nf">GetCurrentOperationIndex</span><span class="p">(</span><span class="n">IWorkflowFoundry</span> <span class="n">foundry</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">foundry</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="nf">TryGetValue</span><span class="p">(</span>
        <span class="n">FoundryPropertyKeys</span><span class="p">.</span><span class="n">CurrentOperationIndex</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">obj</span><span class="p">)</span> <span class="p">&amp;&amp;</span> <span class="n">obj</span> <span class="k">is</span> <span class="kt">int</span> <span class="n">idx</span><span class="p">)</span>
        <span class="k">return</span> <span class="n">idx</span><span class="p">;</span>
    <span class="c1">// ...fallback counter logic</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The foundry now sets <code class="language-plaintext highlighter-rouge">CurrentOperationIndex</code> before each operation executes. The middleware reads it directly instead of scanning.</p>

<p><strong>Allocation on every property access:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// New ReadOnlyCollection every time someone touches Operations</span>
<span class="k">internal</span> <span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">IWorkflowOperation</span><span class="p">&gt;</span> <span class="n">Operations</span> <span class="p">=&gt;</span>
    <span class="k">new</span> <span class="n">ReadOnlyCollection</span><span class="p">&lt;</span><span class="n">IWorkflowOperation</span><span class="p">&gt;(</span><span class="n">_operations</span><span class="p">);</span>

<span class="c1">// Cache until the underlying list changes</span>
<span class="k">private</span> <span class="n">ReadOnlyCollection</span><span class="p">&lt;</span><span class="n">IWorkflowOperation</span><span class="p">&gt;?</span> <span class="n">_cachedOperations</span><span class="p">;</span>

<span class="k">internal</span> <span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">IWorkflowOperation</span><span class="p">&gt;</span> <span class="n">Operations</span> <span class="p">=&gt;</span>
    <span class="n">_cachedOperations</span> <span class="p">??=</span> <span class="k">new</span> <span class="n">ReadOnlyCollection</span><span class="p">&lt;</span><span class="n">IWorkflowOperation</span><span class="p">&gt;(</span><span class="n">_operations</span><span class="p">);</span>
</code></pre></div></div>

<p>In hot paths that inspect the operation list, the old version created a new <code class="language-plaintext highlighter-rouge">ReadOnlyCollection</code> wrapper on every access. The fix caches it and clears the cache when operations are added.</p>

<p><strong>Event handler memory leak:</strong></p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Dispose() never dropped event subscriptions</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">_disposed</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="n">_disposed</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>
    <span class="n">_concurrencyLimiter</span><span class="p">?.</span><span class="nf">Dispose</span><span class="p">();</span>
<span class="p">}</span>

<span class="c1">// ...same, but clear delegates so GC can collect peers</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">_disposed</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
    <span class="n">_disposed</span> <span class="p">=</span> <span class="k">true</span><span class="p">;</span>

    <span class="n">_concurrencyLimiter</span><span class="p">?.</span><span class="nf">Dispose</span><span class="p">();</span>

    <span class="n">WorkflowStarted</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="n">WorkflowCompleted</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="n">WorkflowFailed</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="n">CompensationTriggered</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="n">CompensationCompleted</span> <span class="p">=</span> <span class="k">null</span><span class="p">;</span>
    <span class="c1">// ...all event fields set to null</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">WorkflowSmith</code> and <code class="language-plaintext highlighter-rouge">WorkflowFoundry</code> subscribed to each other’s events but never unsubscribed on dispose. In long-running applications, disposed objects stayed alive through event handler references.</p>

<p><strong>Silent recovery failures:</strong></p>

<p>The recovery extension caught exceptions during resume and returned successfully, silently swallowing errors. The persistence middleware overwrote restored operation outputs with input data during the resume path, undoing the point of checkpointing. Both required reading the code paths carefully and writing tests for the failure scenarios.</p>

<p>SonarCloud earned its keep. It is also not enough. The badge is not a substitute for reading your own code.</p>

<hr />

<h2 id="multi-target-testing">Multi-Target Testing</h2>

<p>WorkflowForge targets .NET Framework 4.8, .NET 8.0, and .NET 10.0. Each framework has different runtime behavior, and the differences can be subtle.</p>

<p>The CI pipeline runs the full test suite against all three:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">dotnet test WorkflowForge.sln --framework net8.0 \</span>
  <span class="s">--collect:"XPlat Code Coverage"</span>
<span class="s">dotnet test WorkflowForge.sln --framework net10.0 \</span>
  <span class="s">--collect:"XPlat Code Coverage"</span>
<span class="s">dotnet test WorkflowForge.sln --framework net48</span>
</code></pre></div></div>

<p>Coverage is collected on .NET 8.0 and 10.0 (OpenCover format for SonarCloud ingestion). .NET Framework 4.8 runs the tests but without coverage instrumentation, since the XPlat collector does not support it.</p>

<p>This caught real issues. .NET Framework 4.8 enforces strong-name validation strictly. If assembly A is signed and references unsigned assembly B, the runtime throws a <code class="language-plaintext highlighter-rouge">FileLoadException</code>. .NET Core and later ignore strong names entirely. Without multi-TFM testing, the benchmark suite worked on .NET 8.0 and 10.0 but failed on 4.8 due to a <code class="language-plaintext highlighter-rouge">SignAssembly</code> inheritance issue in <code class="language-plaintext highlighter-rouge">Directory.Build.props</code>.</p>

<p>Coverage reports are uploaded as separate artifacts for independent auditing, alongside the <code class="language-plaintext highlighter-rouge">.trx</code> test result files.</p>

<hr />

<h2 id="the-pipeline">The Pipeline</h2>

<p>This is the full CI/CD workflow that runs on every push, every PR, and on-demand for publish:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌──────────────────────────────────────────────────────────────────┐
│  Build Job                                                       │
│                                                                  │
│  Checkout ─► Setup SDKs ─► Restore ─► SonarScanner Begin        │
│  ─► Build ─► Test (net8.0) ─► Test (net10.0) ─► Test (net48)   │
│  ─► SonarScanner End ─► Upload Results                          │
│  ─► Pack ─► Generate SBOM ─► Upload Packages                   │
└──────────────────────┬───────────────────────────────────────────┘
                       │ (requires manual approval)
┌──────────────────────▼───────────────────────────────────────────┐
│  Publish Job  (nuget-publish environment)                        │
│                                                                  │
│  Download Packages ─► Sign (optional) ─► Attest Provenance      │
│  ─► Attest SBOM ─► Push to NuGet.org                            │
└──────────────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p>A few design decisions worth explaining.</p>

<p><strong>Environment gate.</strong> The publish job runs in a <code class="language-plaintext highlighter-rouge">nuget-publish</code> GitHub Environment that requires manual approval. No accidental publishes. The concurrency group prevents parallel publish attempts to the same branch:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">publish</span><span class="pi">:</span>
  <span class="na">needs</span><span class="pi">:</span> <span class="s">build</span>
  <span class="na">environment</span><span class="pi">:</span> <span class="s">nuget-publish</span>
  <span class="na">concurrency</span><span class="pi">:</span>
    <span class="na">group</span><span class="pi">:</span> <span class="s">publish-${{ github.ref }}</span>
    <span class="na">cancel-in-progress</span><span class="pi">:</span> <span class="no">false</span>
</code></pre></div></div>

<p><strong>Minimal permissions.</strong> The top-level <code class="language-plaintext highlighter-rouge">permissions: {}</code> drops all default GitHub token permissions. Each job requests only what it needs: <code class="language-plaintext highlighter-rouge">contents: read</code> for the build, <code class="language-plaintext highlighter-rouge">id-token: write</code> and <code class="language-plaintext highlighter-rouge">attestations: write</code> for the publish.</p>

<p>The full workflow is <a href="https://github.com/animatlabs/workflow-forge/blob/main/.github/workflows/build-test.yml">on GitHub</a>.</p>

<hr />

<h2 id="supply-chain-hardening">Supply Chain Hardening</h2>

<p>Four things happen before any package reaches NuGet.org.</p>

<h3 id="sha-pinned-actions">SHA-Pinned Actions</h3>

<p>Every GitHub Action in the workflow is pinned to a full commit SHA instead of a mutable version tag:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd</span>  <span class="c1"># v6</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/setup-dotnet@c2fa09f4bde5ebb9d1777cf28262a3eb3db3ced7</span>  <span class="c1"># v5</span>
<span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32</span>  <span class="c1"># v4.1.0</span>
</code></pre></div></div>

<p>Tags like <code class="language-plaintext highlighter-rouge">v6</code> can be moved to point to a different commit at any time. A compromised action author could push malicious code under the same tag. SHAs are immutable. If the hash does not match, the step fails.</p>

<p>The tradeoff is maintenance: when actions release new versions, you need to update the hash. Dependabot handles this automatically with weekly update PRs for both GitHub Actions and NuGet dependencies.</p>

<p>One gotcha: precision matters. During the 2.1 publish, the <code class="language-plaintext highlighter-rouge">attest-build-provenance</code> step failed because the SHA had a single-character typo (<code class="language-plaintext highlighter-rouge">ecc</code> instead of <code class="language-plaintext highlighter-rouge">ecd</code>). The entire pipeline stopped. There is no “close enough” with SHA-pinning.</p>

<h3 id="sigstore-build-attestation">Sigstore Build Attestation</h3>

<p>Every <code class="language-plaintext highlighter-rouge">.nupkg</code>, <code class="language-plaintext highlighter-rouge">.snupkg</code>, and SBOM file gets a cryptographic attestation before reaching NuGet.org:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Attest NuGet Package Provenance</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32</span>  <span class="c1"># v4.1.0</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">subject-path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./packages/*.nupkg'</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Attest Symbol Package Provenance</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32</span>  <span class="c1"># v4.1.0</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">subject-path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./packages/*.snupkg'</span>

<span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Attest SBOM</span>
  <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32</span>  <span class="c1"># v4.1.0</span>
  <span class="na">with</span><span class="pi">:</span>
    <span class="na">subject-path</span><span class="pi">:</span> <span class="s1">'</span><span class="s">./packages/bom.json'</span>
</code></pre></div></div>

<p>This proves, cryptographically, that each artifact was built by a specific GitHub Actions workflow, from a specific commit, in a specific repository. Consumers can verify with <code class="language-plaintext highlighter-rouge">gh attestation verify</code>. No long-lived signing keys to manage. GitHub’s Sigstore integration uses short-lived OIDC tokens.</p>

<h3 id="cyclonedx-sbom">CycloneDX SBOM</h3>

<p>One command generates a complete dependency manifest in CycloneDX JSON format:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="pi">-</span> <span class="na">name</span><span class="pi">:</span> <span class="s">Generate CycloneDX SBOM</span>
  <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
    <span class="s">dotnet tool install --global CycloneDX</span>
    <span class="s">dotnet cyclonedx WorkflowForge.sln -o ./packages --json</span>
</code></pre></div></div>

<p>The resulting <code class="language-plaintext highlighter-rouge">bom.json</code> lists every dependency, its version, and its license. Increasingly required by enterprise consumers doing compliance reviews. The SBOM is uploaded alongside the packages and gets its own build attestation.</p>

<h3 id="nuget-vulnerability-auditing">NuGet Vulnerability Auditing</h3>

<p>In <code class="language-plaintext highlighter-rouge">Directory.Build.props</code>, three properties make every <code class="language-plaintext highlighter-rouge">dotnet restore</code> an audit:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;NuGetAudit&gt;</span>true<span class="nt">&lt;/NuGetAudit&gt;</span>
<span class="nt">&lt;NuGetAuditMode&gt;</span>all<span class="nt">&lt;/NuGetAuditMode&gt;</span>
<span class="nt">&lt;NuGetAuditLevel&gt;</span>low<span class="nt">&lt;/NuGetAuditLevel&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">all</code> checks both direct and transitive dependencies. <code class="language-plaintext highlighter-rouge">low</code> means any known vulnerability at any severity level fails the build. No silent CVEs in the dependency tree.</p>

<hr />

<h2 id="nuget-packaging-the-debugtype-trap">NuGet Packaging: The DebugType Trap</h2>

<p>This is the part that broke.</p>

<p><code class="language-plaintext highlighter-rouge">Directory.Build.props</code> centralizes build settings for the entire solution. Two of those settings handle debug symbols and NuGet symbol packages:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;DebugType&gt;</span>portable<span class="nt">&lt;/DebugType&gt;</span>
<span class="nt">&lt;IncludeSymbols&gt;</span>true<span class="nt">&lt;/IncludeSymbols&gt;</span>
<span class="nt">&lt;SymbolPackageFormat&gt;</span>snupkg<span class="nt">&lt;/SymbolPackageFormat&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">portable</code> produces a separate <code class="language-plaintext highlighter-rouge">.pdb</code> file alongside the DLL. <code class="language-plaintext highlighter-rouge">snupkg</code> packages that <code class="language-plaintext highlighter-rouge">.pdb</code> for NuGet.org’s symbol server, enabling step-through debugging for consumers.</p>

<p>The problem: nine of thirteen project <code class="language-plaintext highlighter-rouge">.csproj</code> files also had this line:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;DebugType&gt;</span>embedded<span class="nt">&lt;/DebugType&gt;</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">embedded</code> bakes the PDB directly into the DLL. No separate <code class="language-plaintext highlighter-rouge">.pdb</code> file. The <code class="language-plaintext highlighter-rouge">.csproj</code> setting overrides <code class="language-plaintext highlighter-rouge">Directory.Build.props</code>, so <code class="language-plaintext highlighter-rouge">dotnet pack</code> generated <code class="language-plaintext highlighter-rouge">.snupkg</code> files containing nothing. NuGet.org rejected them:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>BadRequest https://www.nuget.org/api/v2/symbolpackage/ 77ms
error: Response status code does not indicate success:
  400 (The package does not contain any symbol (.pdb) files.)
</code></pre></div></div>

<p>Nine of thirteen packages failed. The four that succeeded happened not to have the <code class="language-plaintext highlighter-rouge">DebugType=embedded</code> override.</p>

<p>Fix: remove <code class="language-plaintext highlighter-rouge">DebugType=embedded</code> from every individual <code class="language-plaintext highlighter-rouge">.csproj</code> and let all projects inherit <code class="language-plaintext highlighter-rouge">portable</code> from <code class="language-plaintext highlighter-rouge">Directory.Build.props</code>. One property, nine projects, fourteen files.</p>

<p>The consequence was not. NuGet.org does not allow re-pushing the same package version. The thirteen <code class="language-plaintext highlighter-rouge">.nupkg</code> files had already been accepted. So we had to bump every project to 2.1.1, update version references across documentation and CI, add a CHANGELOG entry, and deprecate the thirteen partially-published 2.1.0 packages individually.</p>

<p>If you are setting up NuGet packaging for a multi-project solution: <strong>check your DebugType before your first publish.</strong></p>

<hr />

<h2 id="sourcelink-and-symbol-packages">SourceLink and Symbol Packages</h2>

<p>Getting debug symbols right required centralizing several properties in <code class="language-plaintext highlighter-rouge">Directory.Build.props</code>:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;DebugType&gt;</span>portable<span class="nt">&lt;/DebugType&gt;</span>
<span class="nt">&lt;EmbedUntrackedSources&gt;</span>true<span class="nt">&lt;/EmbedUntrackedSources&gt;</span>
<span class="nt">&lt;PublishRepositoryUrl&gt;</span>true<span class="nt">&lt;/PublishRepositoryUrl&gt;</span>
<span class="nt">&lt;IncludeSymbols&gt;</span>true<span class="nt">&lt;/IncludeSymbols&gt;</span>
<span class="nt">&lt;SymbolPackageFormat&gt;</span>snupkg<span class="nt">&lt;/SymbolPackageFormat&gt;</span>
</code></pre></div></div>

<p>Plus a SourceLink package reference:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;PackageReference</span> <span class="na">Include=</span><span class="s">"Microsoft.SourceLink.GitHub"</span> <span class="na">Version=</span><span class="s">"8.0.0"</span> <span class="na">PrivateAssets=</span><span class="s">"All"</span><span class="nt">/&gt;</span>
</code></pre></div></div>

<p>Together, these give consumers of the NuGet package the ability to step into the source code directly from their debugger. <code class="language-plaintext highlighter-rouge">EmbedUntrackedSources</code> ensures generated files are included. <code class="language-plaintext highlighter-rouge">PublishRepositoryUrl</code> embeds the repository URL in the package metadata.</p>

<p>The key lesson: these properties must live in one place. The moment individual <code class="language-plaintext highlighter-rouge">.csproj</code> files start overriding <code class="language-plaintext highlighter-rouge">DebugType</code>, the symbol package pipeline breaks silently (<code class="language-plaintext highlighter-rouge">dotnet pack</code> does not warn you that the <code class="language-plaintext highlighter-rouge">.snupkg</code> is empty).</p>

<hr />

<h2 id="what-actually-shipped">What Actually Shipped</h2>

<p>Beyond the infrastructure, three user-facing changes made it into 2.1.</p>

<p><strong>Inline compensation:</strong> attach a restore delegate directly to <code class="language-plaintext highlighter-rouge">AddOperation</code> instead of writing a separate class:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"OrderProcessing"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="s">"ProcessPayment"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">foundry</span><span class="p">.</span><span class="n">Properties</span><span class="p">[</span><span class="s">"payment_id"</span><span class="p">]</span> <span class="p">=</span> <span class="s">"PAY-123"</span><span class="p">;</span>
    <span class="p">},</span>
    <span class="n">restoreAction</span><span class="p">:</span> <span class="k">async</span> <span class="p">(</span><span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">paymentId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">Properties</span><span class="p">[</span><span class="s">"payment_id"</span><span class="p">];</span>
        <span class="c1">// Issue refund...</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<p>For workflows where the compensation logic is simple, this eliminates an entire class per operation.</p>

<p><strong><code class="language-plaintext highlighter-rouge">GetOperationOutput</code>:</strong> inspect what any completed operation returned, by name or index, from the orchestrator level.</p>

<p><strong>Multi-target test validation:</strong> all tests run across .NET Framework 4.8, .NET 8.0, and .NET 10.0 on every CI build. API compatibility issues get caught before they ship.</p>

<hr />

<h2 id="the-checklist">The Checklist</h2>

<p>If I were setting up a new .NET OSS project today, this is what I would add before the first NuGet publish:</p>

<ul class="task-list">
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Static analysis:</strong> SonarCloud or equivalent, free for public repos</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Code coverage</strong> with quality gate (coverage on new code, not just overall)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Multi-target testing</strong> across every framework you ship</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>CI/CD pipeline</strong> with environment-gated publish and manual approval</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>SHA-pinned GitHub Actions</strong> with Dependabot for automated updates</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Build provenance attestation</strong> via Sigstore</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>SBOM generation</strong> (CycloneDX or SPDX)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>NuGet vulnerability auditing</strong> (<code class="language-plaintext highlighter-rouge">NuGetAudit=true</code> with <code class="language-plaintext highlighter-rouge">all</code> mode)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>SourceLink + symbol packages</strong> (check DebugType before first publish)</li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>SDK version pinning</strong> via <code class="language-plaintext highlighter-rouge">global.json</code></li>
  <li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" /><strong>Strong-name signing</strong> with key in <code class="language-plaintext highlighter-rouge">Directory.Build.props</code></li>
</ul>

<p>None of this is individually complex. The complexity is in getting all of it working together without one setting silently breaking another. That is what took sixty issues to sort out.</p>

<hr />

<h2 id="resources">Resources</h2>

<table>
  <thead>
    <tr>
      <th>What</th>
      <th>Where</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GitHub</td>
      <td><a href="https://github.com/animatlabs/workflow-forge">github.com/animatlabs/workflow-forge</a></td>
    </tr>
    <tr>
      <td>NuGet</td>
      <td><a href="https://www.nuget.org/packages/WorkflowForge">nuget.org/packages/WorkflowForge</a></td>
    </tr>
    <tr>
      <td>Documentation</td>
      <td><a href="https://animatlabs.com/workflow-forge">animatlabs.com/workflow-forge</a></td>
    </tr>
    <tr>
      <td>CI/CD Workflow</td>
      <td><a href="https://github.com/animatlabs/workflow-forge/blob/main/.github/workflows/build-test.yml">build-test.yml</a></td>
    </tr>
    <tr>
      <td>CHANGELOG</td>
      <td><a href="https://github.com/animatlabs/workflow-forge/blob/main/CHANGELOG.md">CHANGELOG.md</a></td>
    </tr>
  </tbody>
</table>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/technical/.net/workflow/workflow-forge-2-performance-unleashed/">WorkflowForge 2.0 benchmarks</a></li>
  <li><a href="/technical/.net/infra/dotnet-docker-traefik/">Traefik with .NET Docker services</a></li>
  <li><a href="/technical/.net/.net-core/polly-v8-resilience-patterns/">Polly v8 resilience patterns</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Open Source" /><category term=".NET" /><category term="Open Source" /><category term="CI/CD" /><category term="NuGet" /><category term="WorkflowForge" /><summary type="html"><![CDATA[SonarCloud, Sigstore attestation, CycloneDX SBOM, multi-TFM testing, and environment-gated NuGet publishing. Every piece of infrastructure for a production-grade .NET OSS release pipeline.]]></summary></entry><entry><title type="html">Scheduled Workflows in .NET: WorkflowForge with Coravel Job Scheduling</title><link href="https://animatlabs.com/technical/.net/workflow/workflowforge-coravel-scheduled-workflows/" rel="alternate" type="text/html" title="Scheduled Workflows in .NET: WorkflowForge with Coravel Job Scheduling" /><published>2026-02-09T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/workflow/workflowforge-coravel-scheduled-workflows</id><content type="html" xml:base="https://animatlabs.com/technical/.net/workflow/workflowforge-coravel-scheduled-workflows/"><![CDATA[<h2 id="the-problem-scheduled-workflows-are-overcomplicated">The Problem: Scheduled Workflows Are Overcomplicated</h2>

<p>You need to run a multi-step business process on a schedule: nightly order reconciliation, hourly data syncs, daily report generation. The typical approach? Hangfire for scheduling, custom code for the workflow logic, and manual error handling for each step. That’s three concerns tangled together, plus a Redis or SQL dependency just for scheduling.</p>

<p>What if you could have:</p>

<ul>
  <li><strong>Zero external dependencies</strong> for scheduling (no Redis, no SQL)</li>
  <li>Automatic compensation if any step fails (saga pattern, built-in)</li>
  <li><strong>Microsecond execution</strong> instead of milliseconds</li>
  <li>Clean separation between “when to run” and “what to run”</li>
</ul>

<p>That’s exactly what happens when you pair <strong>Coravel</strong> (scheduling) with <strong>WorkflowForge</strong> (workflow orchestration). I like that split. Took me a while to stop stuffing everything into one scheduler-shaped lump.</p>

<p>I’ve built a complete runnable sample that demonstrates this pattern end-to-end:</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/WorkflowForge" class="btn btn--primary">GitHub Repo</a></p>

<hr />

<h2 id="the-architecture">The Architecture</h2>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>┌─────────────────────────────────────────────────────────┐
│                  .NET Console Host                      │
├─────────────────────────────────────────────────────────┤
│  Coravel Scheduler                                      │
│  └─ "Run ReconciliationJob every N seconds"             │
│       │                                                 │
│       ▼                                                 │
│  WorkflowForge Workflow                                 │
│  └─ Step 1: Fetch unprocessed orders                    │
│  └─ Step 2: Process payments    ← (refund on failure)   │
│  └─ Step 3: Reserve inventory   ← (release on failure)  │
│  └─ Step 4: [MaybeFail]         ← toggle to demo saga   │
│  └─ Step 5: Send confirmations                          │
└─────────────────────────────────────────────────────────┘
</code></pre></div></div>

<p>Coravel decides <strong>when</strong> to run. WorkflowForge handles <strong>what</strong> to run, including automatic rollback of completed steps when something fails downstream.</p>

<hr />

<h2 id="solution-layout">Solution Layout</h2>

<p>The sample splits concerns cleanly across two projects:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>WorkflowForge/
├── AnimatLabs.WorkflowForge.Workflows.Sample/        # Business logic
│   └── NightlyReconciliation/
│       ├── Workflow.cs                                # Workflow builder
│       ├── ReconciliationKeys.cs                      # Shared state keys
│       ├── Models/
│       │   ├── Order.cs
│       │   └── PaymentTransaction.cs
│       ├── Operations/
│       │   ├── FetchUnprocessedOrdersOperation.cs
│       │   ├── ProcessPaymentsOperation.cs
│       │   ├── UpdateInventoryOperation.cs
│       │   ├── MaybeFailOperation.cs
│       │   └── SendConfirmationEmailsOperation.cs
│       └── Services/                                  # Interfaces only
│           ├── IOrderRepository.cs
│           ├── IPaymentService.cs
│           ├── IInventoryService.cs
│           └── IEmailSender.cs
├── AnimatLabs.WorkflowForge.CoravelScheduledWorkflows/  # Host + fakes
│   ├── Program.cs
│   ├── appsettings.json
│   ├── Jobs/ReconciliationJob.cs
│   ├── Options/ReconciliationJobOptions.cs
│   └── Services/
│       ├── InMemoryOrderRepository.cs
│       ├── FakePaymentService.cs
│       ├── FakeInventoryService.cs
│       └── FakeEmailSender.cs
└── AnimatLabs.WorkflowForge.sln
</code></pre></div></div>

<p>The <strong>workflows project</strong> contains only business logic: operations, models, and service interfaces. It has a single dependency: <code class="language-plaintext highlighter-rouge">WorkflowForge</code>.</p>

<p>The <strong>host project</strong> wires Coravel scheduling, provides fake service implementations for demo purposes, and depends on <code class="language-plaintext highlighter-rouge">Coravel</code>, <code class="language-plaintext highlighter-rouge">WorkflowForge</code>, and <code class="language-plaintext highlighter-rouge">Microsoft.Extensions.Hosting</code>.</p>

<hr />

<h2 id="setup-two-nuget-packages">Setup: Two NuGet Packages</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package Coravel
dotnet add package WorkflowForge
</code></pre></div></div>

<p>No database migrations. No connection strings for the scheduler.</p>

<hr />

<h2 id="the-workflow-definition">The Workflow Definition</h2>

<p>The entire workflow is defined in one method. Chain the operations and build:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">AnimatLabs.WorkflowForge.Workflows.Sample.NightlyReconciliation.Operations</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.WorkflowForge.Workflows.Sample.NightlyReconciliation.Services</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">WorkflowForge.Abstractions</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">WF</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="n">WorkflowForge</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">AnimatLabs.WorkflowForge.Workflows.Sample.NightlyReconciliation</span><span class="p">;</span>

<span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">NightlyReconciliationWorkflow</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">IWorkflow</span> <span class="nf">Build</span><span class="p">(</span>
        <span class="n">IOrderRepository</span> <span class="n">orderRepository</span><span class="p">,</span>
        <span class="n">IPaymentService</span> <span class="n">paymentService</span><span class="p">,</span>
        <span class="n">IInventoryService</span> <span class="n">inventoryService</span><span class="p">,</span>
        <span class="n">IEmailSender</span> <span class="n">emailSender</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="n">WF</span>
            <span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"NightlyReconciliation"</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">FetchUnprocessedOrdersOperation</span><span class="p">(</span><span class="n">orderRepository</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">ProcessPaymentsOperation</span><span class="p">(</span><span class="n">paymentService</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">UpdateInventoryOperation</span><span class="p">(</span><span class="n">inventoryService</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">MaybeFailOperation</span><span class="p">())</span>
            <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">SendConfirmationEmailsOperation</span><span class="p">(</span><span class="n">emailSender</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Dependencies are injected into operations via constructors (clean, testable, no service locator).</p>

<hr />

<h2 id="the-models">The Models</h2>

<p>Simple domain objects that flow through the workflow:</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">Order</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">required</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="n">init</span><span class="p">;</span> <span class="p">}</span> <span class="p">=</span> <span class="k">null</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">required</span> <span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="kt">string</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="k">null</span><span class="p">!;</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">sealed</span> <span class="k">class</span> <span class="nc">PaymentTransaction</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">required</span> <span class="kt">string</span> <span class="n">TransactionId</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">null</span><span class="p">!;</span>
    <span class="k">public</span> <span class="n">required</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="k">null</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="p">}</span>
</code></pre></div></div>

<h2 id="shared-state-keys">Shared State Keys</h2>

<p>Operations communicate through the foundry’s property bag. The keys are simple constants, no magic strings scattered across the codebase:</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">class</span> <span class="nc">ReconciliationKeys</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">BatchSize</span> <span class="p">=</span> <span class="s">"recon.batch_size"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">DemoFailure</span> <span class="p">=</span> <span class="s">"recon.demo_failure"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">Orders</span> <span class="p">=</span> <span class="s">"recon.orders"</span><span class="p">;</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">PaymentTransactions</span> <span class="p">=</span> <span class="s">"recon.payment_transactions"</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<hr />

<h2 id="operations-with-automatic-compensation">Operations with Automatic Compensation</h2>

<p>This is where WorkflowForge shines. Each operation extends <code class="language-plaintext highlighter-rouge">WorkflowOperationBase</code> and can implement both <strong>execution</strong> (<code class="language-plaintext highlighter-rouge">ForgeAsyncCore</code>) and <strong>compensation</strong> (<code class="language-plaintext highlighter-rouge">RestoreAsync</code>). If a downstream step fails, completed operations are automatically rolled back in reverse order.</p>

<h3 id="fetching-orders">Fetching Orders</h3>

<p>The first step loads unprocessed orders and stores them in the foundry for downstream operations:</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">FetchUnprocessedOrdersOperation</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IOrderRepository</span> <span class="n">_orderRepository</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">FetchUnprocessedOrdersOperation</span><span class="p">(</span><span class="n">IOrderRepository</span> <span class="n">orderRepository</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_orderRepository</span> <span class="p">=</span> <span class="n">orderRepository</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"FetchUnprocessedOrders"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">batchSize</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">BatchSize</span><span class="p">,</span> <span class="m">3</span><span class="p">);</span>
        <span class="n">foundry</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">"Fetching up to {BatchSize} order(s)"</span><span class="p">,</span> <span class="n">batchSize</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_orderRepository</span>
            <span class="p">.</span><span class="nf">GetUnprocessedOrdersAsync</span><span class="p">(</span><span class="n">batchSize</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>

        <span class="n">foundry</span><span class="p">.</span><span class="nf">SetProperty</span><span class="p">(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">Orders</span><span class="p">,</span> <span class="n">orders</span><span class="p">);</span>
        <span class="n">foundry</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">"Fetched {Count} order(s)"</span><span class="p">,</span> <span class="n">orders</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="n">foundry</span><span class="p">.</span><span class="n">Properties</span><span class="p">.</span><span class="nf">TryRemove</span><span class="p">(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">Orders</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">Task</span><span class="p">.</span><span class="n">CompletedTask</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="processing-payments-with-refund-compensation">Processing Payments (with Refund Compensation)</h3>

<p>This is the core compensation pattern. <code class="language-plaintext highlighter-rouge">ForgeAsyncCore</code> charges each order and stores the transactions. <code class="language-plaintext highlighter-rouge">RestoreAsync</code> refunds them if anything fails downstream:</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">ProcessPaymentsOperation</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IPaymentService</span> <span class="n">_paymentService</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">ProcessPaymentsOperation</span><span class="p">(</span><span class="n">IPaymentService</span> <span class="n">paymentService</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_paymentService</span> <span class="p">=</span> <span class="n">paymentService</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"ProcessPayments"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;&gt;(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">Orders</span><span class="p">)</span>
            <span class="p">??</span> <span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;();</span>

        <span class="n">foundry</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">"Processing payments for {Count} order(s)"</span><span class="p">,</span> <span class="n">orders</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">transactions</span> <span class="p">=</span> <span class="k">new</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">PaymentTransaction</span><span class="p">&gt;(</span><span class="n">orders</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">order</span> <span class="k">in</span> <span class="n">orders</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="kt">var</span> <span class="n">tx</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_paymentService</span><span class="p">.</span><span class="nf">ChargeAsync</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
            <span class="n">transactions</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">tx</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="n">foundry</span><span class="p">.</span><span class="nf">SetProperty</span><span class="p">(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">PaymentTransactions</span><span class="p">,</span> <span class="n">transactions</span><span class="p">);</span>
        <span class="n">foundry</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">"Processed {Count} payment(s)"</span><span class="p">,</span> <span class="n">transactions</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">transactions</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">PaymentTransaction</span><span class="p">&gt;&gt;(</span>
            <span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">PaymentTransactions</span><span class="p">)</span> <span class="p">??</span> <span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="n">PaymentTransaction</span><span class="p">&gt;();</span>

        <span class="n">foundry</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">"Compensating payments: refunding {Count} transaction(s)"</span><span class="p">,</span>
            <span class="n">transactions</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">tx</span> <span class="k">in</span> <span class="n">transactions</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">await</span> <span class="n">_paymentService</span><span class="p">.</span><span class="nf">RefundAsync</span><span class="p">(</span><span class="n">tx</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="updating-inventory-with-release-compensation">Updating Inventory (with Release Compensation)</h3>

<p>Same pattern: reserve on execute, release on compensate:</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">UpdateInventoryOperation</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IInventoryService</span> <span class="n">_inventoryService</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">UpdateInventoryOperation</span><span class="p">(</span><span class="n">IInventoryService</span> <span class="n">inventoryService</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_inventoryService</span> <span class="p">=</span> <span class="n">inventoryService</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"UpdateInventory"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;&gt;(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">Orders</span><span class="p">)</span>
            <span class="p">??</span> <span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;();</span>

        <span class="n">foundry</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">"Updating inventory for {Count} order(s)"</span><span class="p">,</span> <span class="n">orders</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">order</span> <span class="k">in</span> <span class="n">orders</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">await</span> <span class="n">_inventoryService</span><span class="p">.</span><span class="nf">ReserveAsync</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">order</span><span class="p">.</span><span class="n">Items</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;&gt;(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">Orders</span><span class="p">)</span>
            <span class="p">??</span> <span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;();</span>

        <span class="n">foundry</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">"Compensating inventory: releasing {Count} reservation(s)"</span><span class="p">,</span>
            <span class="n">orders</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">order</span> <span class="k">in</span> <span class="n">orders</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">await</span> <span class="n">_inventoryService</span><span class="p">.</span><span class="nf">ReleaseAsync</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">order</span><span class="p">.</span><span class="n">Items</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="the-simulated-failure-toggle">The Simulated Failure Toggle</h3>

<p>This operation exists purely to demonstrate compensation. When <code class="language-plaintext highlighter-rouge">DemoFailure</code> is set to <code class="language-plaintext highlighter-rouge">true</code>, it throws, which makes WorkflowForge roll back payments and inventory automatically:</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">MaybeFailOperation</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"MaybeFail"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">demoFailure</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="kt">bool</span><span class="p">&gt;(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">DemoFailure</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">demoFailure</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">foundry</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="s">"Simulated failure triggered to demonstrate compensation"</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">"Simulated failure (DemoFailure=true)"</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">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
        <span class="p">=&gt;</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>

<h3 id="sending-confirmations">Sending Confirmations</h3>

<p>The final step. No meaningful compensation needed (you can’t unsend an email):</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">SendConfirmationEmailsOperation</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IEmailSender</span> <span class="n">_emailSender</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">SendConfirmationEmailsOperation</span><span class="p">(</span><span class="n">IEmailSender</span> <span class="n">emailSender</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_emailSender</span> <span class="p">=</span> <span class="n">emailSender</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">override</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"SendConfirmationEmails"</span><span class="p">;</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetPropertyOrDefault</span><span class="p">&lt;</span><span class="n">IReadOnlyList</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;&gt;(</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">Orders</span><span class="p">)</span>
            <span class="p">??</span> <span class="n">Array</span><span class="p">.</span><span class="n">Empty</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;();</span>

        <span class="n">foundry</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">"Sending confirmations for {Count} order(s)"</span><span class="p">,</span> <span class="n">orders</span><span class="p">.</span><span class="n">Count</span><span class="p">);</span>

        <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">order</span> <span class="k">in</span> <span class="n">orders</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">await</span> <span class="n">_emailSender</span><span class="p">.</span><span class="nf">SendConfirmationAsync</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">cancellationToken</span><span class="p">)</span>
                <span class="p">.</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
        <span class="p">}</span>

        <span class="k">return</span> <span class="n">inputData</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="nf">RestoreAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">CancellationToken</span> <span class="n">cancellationToken</span><span class="p">)</span>
        <span class="p">=&gt;</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>

<blockquote>
  <p><strong>Why do non-mutating operations override <code class="language-plaintext highlighter-rouge">RestoreAsync</code>?</strong> WorkflowForge triggers compensation when any operation that overrides <code class="language-plaintext highlighter-rouge">RestoreAsync</code> has completed before a failure. Even no-op restore implementations (like <code class="language-plaintext highlighter-rouge">MaybeFail</code> and <code class="language-plaintext highlighter-rouge">SendConfirmationEmails</code>) should override it so the engine knows they participated in the workflow’s compensation chain.</p>
</blockquote>

<hr />

<h2 id="the-coravel-scheduling-layer">The Coravel Scheduling Layer</h2>

<h3 id="the-job">The Job</h3>

<p>The <code class="language-plaintext highlighter-rouge">ReconciliationJob</code> implements Coravel’s <code class="language-plaintext highlighter-rouge">IInvocable</code>. It builds the workflow, creates a foundry with initial properties, and runs it:</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">ReconciliationJob</span> <span class="p">:</span> <span class="n">IInvocable</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IOrderRepository</span> <span class="n">_orderRepository</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IPaymentService</span> <span class="n">_paymentService</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IInventoryService</span> <span class="n">_inventoryService</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IEmailSender</span> <span class="n">_emailSender</span><span class="p">;</span>
    <span class="k">private</span> <span class="k">readonly</span> <span class="n">IOptions</span><span class="p">&lt;</span><span class="n">ReconciliationJobOptions</span><span class="p">&gt;</span> <span class="n">_options</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">ReconciliationJob</span><span class="p">&gt;</span> <span class="n">_logger</span><span class="p">;</span>

    <span class="k">public</span> <span class="nf">ReconciliationJob</span><span class="p">(</span>
        <span class="n">IOrderRepository</span> <span class="n">orderRepository</span><span class="p">,</span>
        <span class="n">IPaymentService</span> <span class="n">paymentService</span><span class="p">,</span>
        <span class="n">IInventoryService</span> <span class="n">inventoryService</span><span class="p">,</span>
        <span class="n">IEmailSender</span> <span class="n">emailSender</span><span class="p">,</span>
        <span class="n">IOptions</span><span class="p">&lt;</span><span class="n">ReconciliationJobOptions</span><span class="p">&gt;</span> <span class="n">options</span><span class="p">,</span>
        <span class="n">ILogger</span><span class="p">&lt;</span><span class="n">ReconciliationJob</span><span class="p">&gt;</span> <span class="n">logger</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">_orderRepository</span> <span class="p">=</span> <span class="n">orderRepository</span><span class="p">;</span>
        <span class="n">_paymentService</span> <span class="p">=</span> <span class="n">paymentService</span><span class="p">;</span>
        <span class="n">_inventoryService</span> <span class="p">=</span> <span class="n">inventoryService</span><span class="p">;</span>
        <span class="n">_emailSender</span> <span class="p">=</span> <span class="n">emailSender</span><span class="p">;</span>
        <span class="n">_options</span> <span class="p">=</span> <span class="n">options</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">async</span> <span class="n">Task</span> <span class="nf">Invoke</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">options</span> <span class="p">=</span> <span class="n">_options</span><span class="p">.</span><span class="n">Value</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">"Starting scheduled reconciliation workflow (DemoFailure={DemoFailure})"</span><span class="p">,</span>
            <span class="n">options</span><span class="p">.</span><span class="n">DemoFailure</span><span class="p">);</span>

        <span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">NightlyReconciliationWorkflow</span><span class="p">.</span><span class="nf">Build</span><span class="p">(</span>
            <span class="n">_orderRepository</span><span class="p">,</span> <span class="n">_paymentService</span><span class="p">,</span> <span class="n">_inventoryService</span><span class="p">,</span> <span class="n">_emailSender</span><span class="p">);</span>

        <span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WF</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span>
            <span class="n">workflowName</span><span class="p">:</span> <span class="n">workflow</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="n">initialProperties</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">object</span><span class="p">?&gt;</span>
            <span class="p">{</span>
                <span class="p">[</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">BatchSize</span><span class="p">]</span> <span class="p">=</span> <span class="m">3</span><span class="p">,</span>
                <span class="p">[</span><span class="n">ReconciliationKeys</span><span class="p">.</span><span class="n">DemoFailure</span><span class="p">]</span> <span class="p">=</span> <span class="n">options</span><span class="p">.</span><span class="n">DemoFailure</span>
            <span class="p">});</span>

        <span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WF</span><span class="p">.</span><span class="nf">CreateSmith</span><span class="p">(</span><span class="k">new</span> <span class="nf">ConsoleLogger</span><span class="p">(</span><span class="s">"WF"</span><span class="p">));</span>

        <span class="k">try</span>
        <span class="p">{</span>
            <span class="k">await</span> <span class="n">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="n">foundry</span><span class="p">).</span><span class="nf">ConfigureAwait</span><span class="p">(</span><span class="k">false</span><span class="p">);</span>
            <span class="n">_logger</span><span class="p">.</span><span class="nf">LogInformation</span><span class="p">(</span><span class="s">"Reconciliation workflow finished successfully"</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">"Reconciliation workflow failed (compensation should have run for completed steps)"</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="configuration">Configuration</h3>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"ReconciliationJob"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w">
    </span><span class="nl">"ScheduleSeconds"</span><span class="p">:</span><span class="w"> </span><span class="mi">10</span><span class="p">,</span><span class="w">
    </span><span class="nl">"DemoFailure"</span><span class="p">:</span><span class="w"> </span><span class="kc">false</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>

<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">ReconciliationJobOptions</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">SectionName</span> <span class="p">=</span> <span class="s">"ReconciliationJob"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">ScheduleSeconds</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="m">10</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">DemoFailure</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">false</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="host-setup-programcs">Host Setup (Program.cs)</h3>

<p>Standard .NET hosting with Coravel scheduler. The entire host in one file:</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">Host</span><span class="p">.</span><span class="nf">CreateApplicationBuilder</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">Logging</span><span class="p">.</span><span class="nf">ClearProviders</span><span class="p">();</span>
<span class="n">builder</span><span class="p">.</span><span class="n">Logging</span><span class="p">.</span><span class="nf">AddSimpleConsole</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">SingleLine</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">TimestampFormat</span> <span class="p">=</span> <span class="s">"HH:mm:ss "</span><span class="p">;</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">Configure</span><span class="p">&lt;</span><span class="n">ReconciliationJobOptions</span><span class="p">&gt;(</span>
    <span class="n">builder</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="nf">GetSection</span><span class="p">(</span><span class="n">ReconciliationJobOptions</span><span class="p">.</span><span class="n">SectionName</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">AddScheduler</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">AddTransient</span><span class="p">&lt;</span><span class="n">ReconciliationJob</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">AddSingleton</span><span class="p">&lt;</span><span class="n">IOrderRepository</span><span class="p">,</span> <span class="n">InMemoryOrderRepository</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">AddSingleton</span><span class="p">&lt;</span><span class="n">IPaymentService</span><span class="p">,</span> <span class="n">FakePaymentService</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">AddSingleton</span><span class="p">&lt;</span><span class="n">IInventoryService</span><span class="p">,</span> <span class="n">FakeInventoryService</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">AddSingleton</span><span class="p">&lt;</span><span class="n">IEmailSender</span><span class="p">,</span> <span class="n">FakeEmailSender</span><span class="p">&gt;();</span>

<span class="kt">var</span> <span class="n">host</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">host</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="nf">UseScheduler</span><span class="p">(</span><span class="n">scheduler</span> <span class="p">=&gt;</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">options</span> <span class="p">=</span> <span class="n">host</span><span class="p">.</span><span class="n">Services</span><span class="p">.</span><span class="n">GetRequiredService</span><span class="p">&lt;</span><span class="n">IOptions</span><span class="p">&lt;</span><span class="n">ReconciliationJobOptions</span><span class="p">&gt;&gt;().</span><span class="n">Value</span><span class="p">;</span>

    <span class="n">scheduler</span>
        <span class="p">.</span><span class="n">Schedule</span><span class="p">&lt;</span><span class="n">ReconciliationJob</span><span class="p">&gt;()</span>
        <span class="p">.</span><span class="nf">EverySeconds</span><span class="p">(</span><span class="n">options</span><span class="p">.</span><span class="n">ScheduleSeconds</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">PreventOverlapping</span><span class="p">(</span><span class="k">nameof</span><span class="p">(</span><span class="n">ReconciliationJob</span><span class="p">));</span>
<span class="p">});</span>

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

<p><code class="language-plaintext highlighter-rouge">PreventOverlapping</code> ensures a new run doesn’t start if the previous one is still going. Important for workflows that touch external systems.</p>

<hr />

<h2 id="try-it-yourself">Try It Yourself</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Happy path -- all steps succeed</span>
dotnet run <span class="nt">--project</span> AnimatLabs.WorkflowForge.CoravelScheduledWorkflows

<span class="c"># Toggle simulated failure to see compensation in action</span>
dotnet run <span class="nt">--project</span> AnimatLabs.WorkflowForge.CoravelScheduledWorkflows <span class="nt">--</span> <span class="se">\</span>
    ReconciliationJob:DemoFailure<span class="o">=</span><span class="nb">true</span>
</code></pre></div></div>

<p>With <code class="language-plaintext highlighter-rouge">DemoFailure=true</code>, the <code class="language-plaintext highlighter-rouge">MaybeFailOperation</code> throws after payments and inventory are processed. Watch the logs. You’ll see WorkflowForge automatically refund payments and release inventory reservations. The saga pattern, without writing a single line of orchestration code.</p>

<hr />

<h2 id="why-this-combination-works">Why This Combination Works</h2>

<table>
  <thead>
    <tr>
      <th>Concern</th>
      <th>Who Handles It</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>When</strong> to run</td>
      <td>Coravel (cron-like expressions, overlap prevention)</td>
    </tr>
    <tr>
      <td><strong>What</strong> to run</td>
      <td>WorkflowForge (operation sequence, data flow)</td>
    </tr>
    <tr>
      <td><strong>What if it fails</strong></td>
      <td>WorkflowForge (automatic compensation in reverse)</td>
    </tr>
    <tr>
      <td><strong>Configuration</strong></td>
      <td>Standard .NET <code class="language-plaintext highlighter-rouge">IOptions&lt;T&gt;</code></td>
    </tr>
    <tr>
      <td><strong>DI</strong></td>
      <td>Standard <code class="language-plaintext highlighter-rouge">IServiceCollection</code></td>
    </tr>
  </tbody>
</table>

<p>Each tool does one thing well. No overlap, no conflict.</p>

<hr />

<h2 id="when-to-use-this-pattern">When to Use This Pattern</h2>

<p><strong>Good fit:</strong></p>
<ul>
  <li>Multi-step business processes on a schedule</li>
  <li>Operations that need rollback if later steps fail (payments, inventory, external APIs)</li>
  <li>Teams that want to avoid Hangfire/Quartz complexity</li>
  <li>Applications where in-memory scheduling is sufficient</li>
</ul>

<p><strong>Not ideal:</strong></p>
<ul>
  <li>Jobs that must survive application restarts (use Hangfire with persistence)</li>
  <li>Distributed job coordination across instances (use Hangfire or Quartz)</li>
  <li>Visual workflow designers for business users (use Elsa)</li>
</ul>

<hr />

<h2 id="resources">Resources</h2>

<table>
  <thead>
    <tr>
      <th>What</th>
      <th>Where</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>WorkflowForge</td>
      <td><a href="https://github.com/animatlabs/workflow-forge">GitHub</a> | <a href="https://www.nuget.org/packages/WorkflowForge">NuGet</a> | <a href="https://animatlabs.com/workflow-forge">Docs</a></td>
    </tr>
    <tr>
      <td>Coravel</td>
      <td><a href="https://github.com/jamesmh/coravel">GitHub</a> | <a href="https://docs.coravel.net">Docs</a></td>
    </tr>
    <tr>
      <td>This Sample</td>
      <td><a href="https://github.com/animat089/playground/tree/main/WorkflowForge">Playground Repo</a></td>
    </tr>
    <tr>
      <td>Benchmarks</td>
      <td><a href="https://animatlabs.com/workflow-forge/performance/competitive-analysis/">511x faster than alternatives</a></td>
    </tr>
  </tbody>
</table>

<hr />

<p><em>If you’ve tried a different scheduling approach with WorkflowForge, I’d be curious how it compared.</em></p>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="see-also">See Also</h2>

<ul>
  <li><a href="/technical/.net/workflow/workflow-forge-introduction/">WorkflowForge introduction</a></li>
  <li><a href="/technical/.net/workflow/workflow-forge-2-performance-unleashed/">WorkflowForge 2.0 benchmarks</a></li>
  <li><a href="/technical/.net/workflow/masstransit-workflowforge-saga/">MassTransit saga with WorkflowForge</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Workflow" /><category term=".NET" /><category term="Workflow" /><category term="WorkflowForge" /><category term="Coravel" /><category term="Job Scheduling" /><category term="Background Jobs" /><category term="Compensation" /><category term="Saga Pattern" /><summary type="html"><![CDATA[Coravel handles when to run, WorkflowForge handles what to run. Together they create lightweight scheduled workflows with automatic compensation, no Hangfire required.]]></summary></entry><entry><title type="html">C# Source Generators: Compile-Time Code Generation with Roslyn in .NET</title><link href="https://animatlabs.com/technical/.net/.net-core/source-generators-csharp/" rel="alternate" type="text/html" title="C# Source Generators: Compile-Time Code Generation with Roslyn in .NET" /><published>2026-02-03T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/source-generators-csharp</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/source-generators-csharp/"><![CDATA[<h2 id="the-problem-death-by-ceremony">The Problem: Death by Ceremony</h2>

<p>Every .NET developer has been here. You’re building a system with 50 DTOs. Same five chores, every class. For each one you need:</p>

<ul>
  <li>A mapper to convert between entities and DTOs</li>
  <li>Validation logic</li>
  <li>Equality comparisons</li>
  <li><code class="language-plaintext highlighter-rouge">ToString()</code> overrides for debugging</li>
  <li>JSON serialization hints</li>
</ul>

<p>That’s 5 pieces of boilerplate per class. 250 methods that are 90% identical, differing only in property names. You write them by hand, copy-paste errors creep in, and when a property changes, you forget to update the mapper.</p>

<p><strong>The traditional solutions all have problems:</strong></p>

<table>
  <thead>
    <tr>
      <th>Approach</th>
      <th>Problem</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Reflection (AutoMapper)</td>
      <td>Runtime overhead, no compile-time safety</td>
    </tr>
    <tr>
      <td>T4 Templates</td>
      <td>Clunky tooling, poor IDE support, runs pre-build</td>
    </tr>
    <tr>
      <td>Code snippets</td>
      <td>Still manual, still error-prone</td>
    </tr>
    <tr>
      <td>IL Weaving (Fody)</td>
      <td>Post-compilation magic, hard to debug</td>
    </tr>
  </tbody>
</table>

<p>Source generators offer a different path: <strong>generate the code at compile time, as if you wrote it yourself</strong>.</p>

<h2 id="what-source-generators-actually-do">What Source Generators Actually Do</h2>

<p>Source generators hook into the Roslyn compiler. They:</p>

<ol>
  <li><strong>Inspect</strong> your code via syntax trees and semantic models</li>
  <li><strong>Generate</strong> new C# source files</li>
  <li><strong>Add them</strong> to your compilation</li>
</ol>

<p>The generated code is real C# - visible in your IDE, navigable with “Go to Definition”, fully debuggable. There’s zero runtime overhead for the generation itself. Took me a while to trust that, but the debugger does not lie.</p>

<p>They sit in the build pipeline like this:</p>

<pre><code class="language-mermaid">flowchart TD
    A[Your Code] --&gt; B[Roslyn Parser]
    B --&gt; C[Syntax Trees]
    C --&gt; D[Source Generators]
    D --&gt; E[Additional Code]
    E --&gt; F[Compilation]
    F --&gt; G[IL]
</code></pre>

<p>The key difference from reflection: by the time your app runs, the generated code is already compiled. No <code class="language-plaintext highlighter-rouge">Type.GetProperties()</code>, no <code class="language-plaintext highlighter-rouge">Activator.CreateInstance()</code>, no JIT compilation of dynamic methods.</p>

<h2 id="the-code-a-complete-playground-repo">The Code: A Complete Playground Repo</h2>

<p>I’ve put all the samples into a runnable playground solution:</p>

<p><strong>You can access the full sample code here:</strong> <a href="https://github.com/animat089/playground/tree/main/SourceGenerators" class="btn btn--primary">GitHub Repo</a></p>

<p>It includes both Roslyn <strong>source generators</strong> and the equivalent <strong>T4-generated output</strong> (checked in) so you can compare the two approaches.</p>

<h2 id="what-gets-generated-in-this-repo">What Gets Generated (In This Repo)</h2>

<p>The playground focuses on four patterns that show up constantly in real .NET codebases.</p>

<h3 id="1-strongly-typed-configuration-binding">1) Strongly-Typed Configuration Binding</h3>

<p>Define a POCO and mark it:</p>

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

<span class="k">namespace</span> <span class="nn">AnimatLabs.SourceGenerators.Demo.Models</span><span class="p">;</span>

<span class="p">[</span><span class="nf">GenerateConfiguration</span><span class="p">(</span><span class="n">SectionName</span> <span class="p">=</span> <span class="s">"Database"</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">DatabaseSettings</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">ConnectionString</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">int</span> <span class="n">Timeout</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="m">30</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">RetrySettings</span> <span class="n">Retry</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">new</span><span class="p">();</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">RetrySettings</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">MaxAttempts</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="m">3</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">DelayMs</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="m">1000</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The generator emits <code class="language-plaintext highlighter-rouge">Bind(IConfiguration)</code> plus nested binders for complex properties.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Generated (shape): DatabaseSettings.Configuration.g.cs</span>
<span class="k">public</span> <span class="k">static</span> <span class="k">global</span><span class="p">::</span><span class="n">AnimatLabs</span><span class="p">.</span><span class="n">SourceGenerators</span><span class="p">.</span><span class="n">Demo</span><span class="p">.</span><span class="n">Models</span><span class="p">.</span><span class="n">DatabaseSettings</span> <span class="nf">Bind</span><span class="p">(</span>
    <span class="k">global</span><span class="p">::</span><span class="n">Microsoft</span><span class="p">.</span><span class="n">Extensions</span><span class="p">.</span><span class="n">Configuration</span><span class="p">.</span><span class="n">IConfiguration</span> <span class="n">configuration</span><span class="p">)</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">section</span> <span class="p">=</span> <span class="n">configuration</span><span class="p">.</span><span class="nf">GetSection</span><span class="p">(</span><span class="s">"Database"</span><span class="p">);</span>
    <span class="k">return</span> <span class="nf">BindSection_AnimatLabs_SourceGenerators_Demo_Models_DatabaseSettings</span><span class="p">(</span><span class="n">section</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Supported property types: <code class="language-plaintext highlighter-rouge">string</code>, <code class="language-plaintext highlighter-rouge">int</code>, <code class="language-plaintext highlighter-rouge">bool</code>, <code class="language-plaintext highlighter-rouge">double</code>, <code class="language-plaintext highlighter-rouge">decimal</code>, enums, and nested classes. Unsupported types are skipped.</p>

<h3 id="2-enum-extensions-display-names--parsing">2) Enum Extensions (Display Names + Parsing)</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System.ComponentModel.DataAnnotations</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.SourceGenerators.Attributes</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">AnimatLabs.SourceGenerators.Demo.Models</span><span class="p">;</span>

<span class="p">[</span><span class="n">GenerateEnumExtensions</span><span class="p">]</span>
<span class="k">public</span> <span class="k">enum</span> <span class="n">OrderStatus</span>
<span class="p">{</span>
    <span class="p">[</span><span class="nf">Display</span><span class="p">(</span><span class="n">Name</span> <span class="p">=</span> <span class="s">"Pending Approval"</span><span class="p">)]</span> <span class="n">Pending</span><span class="p">,</span>
    <span class="p">[</span><span class="nf">Display</span><span class="p">(</span><span class="n">Name</span> <span class="p">=</span> <span class="s">"In Progress"</span><span class="p">)]</span> <span class="n">Processing</span><span class="p">,</span>
    <span class="n">Shipped</span><span class="p">,</span>
    <span class="n">Delivered</span><span class="p">,</span>
    <span class="n">Cancelled</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The generator emits:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">ToDisplayName()</code> (uses <code class="language-plaintext highlighter-rouge">DisplayAttribute.Name</code> when present)</li>
  <li><code class="language-plaintext highlighter-rouge">TryParse(string, out OrderStatus)</code> (accepts both member name and display name)</li>
  <li><code class="language-plaintext highlighter-rouge">GetAll()</code></li>
</ul>

<h3 id="3-dto-mapping-without-reflection">3) DTO Mapping Without Reflection</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">AnimatLabs.SourceGenerators.Attributes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.SourceGenerators.Demo.Models</span><span class="p">;</span>

<span class="k">namespace</span> <span class="nn">AnimatLabs.SourceGenerators.Demo.Mappers</span><span class="p">;</span>

<span class="p">[</span><span class="n">GenerateMapper</span><span class="p">]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">UserMapper</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">partial</span> <span class="n">UserDto</span> <span class="nf">ToDto</span><span class="p">(</span><span class="n">User</span> <span class="n">entity</span><span class="p">);</span>
    <span class="k">public</span> <span class="k">partial</span> <span class="n">User</span> <span class="nf">ToEntity</span><span class="p">(</span><span class="n">UserDto</span> <span class="n">dto</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The generator implements each method by assigning properties where <strong>name and type match</strong>.</p>

<h3 id="4-auto-generated-tostring">4) Auto-Generated <code class="language-plaintext highlighter-rouge">ToString()</code></h3>

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

<span class="k">namespace</span> <span class="nn">AnimatLabs.SourceGenerators.Demo.Models</span><span class="p">;</span>

<span class="p">[</span><span class="n">AutoToString</span><span class="p">]</span>
<span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">Person</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">FirstName</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">LastName</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">int</span> <span class="n">Age</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>The generator emits an override using <code class="language-plaintext highlighter-rouge">StringBuilder</code> (and supports <code class="language-plaintext highlighter-rouge">Exclude</code> + <code class="language-plaintext highlighter-rouge">IncludePrivate</code> options).</p>

<h2 id="how-the-playground-generator-is-structured">How The Playground Generator Is Structured</h2>

<p>The repo uses a single incremental generator (<code class="language-plaintext highlighter-rouge">AnimatLabsSourceGenerators</code>) that registers four independent pipelines using <code class="language-plaintext highlighter-rouge">ForAttributeWithMetadataName(...)</code>.</p>

<h3 id="how-its-wired-into-a-consumer-project">How It’s Wired Into A Consumer Project</h3>

<p>The demo project shows the standard setup:</p>

<ul>
  <li>reference the attributes project normally</li>
  <li>reference the generator project as an analyzer (so it runs at compile time)</li>
</ul>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;ItemGroup&gt;</span>
    <span class="nt">&lt;ProjectReference</span> <span class="na">Include=</span><span class="s">"..\AnimatLabs.SourceGenerators.Attributes\AnimatLabs.SourceGenerators.Attributes.csproj"</span> <span class="nt">/&gt;</span>
    <span class="nt">&lt;ProjectReference</span> <span class="na">Include=</span><span class="s">"..\AnimatLabs.SourceGenerators\AnimatLabs.SourceGenerators.csproj"</span>
                                        <span class="na">OutputItemType=</span><span class="s">"Analyzer"</span>
                                        <span class="na">ReferenceOutputAssembly=</span><span class="s">"false"</span> <span class="nt">/&gt;</span>
<span class="nt">&lt;/ItemGroup&gt;</span>

<span class="nt">&lt;PropertyGroup&gt;</span>
    <span class="nt">&lt;EmitCompilerGeneratedFiles&gt;</span>true<span class="nt">&lt;/EmitCompilerGeneratedFiles&gt;</span>
<span class="nt">&lt;/PropertyGroup&gt;</span>
</code></pre></div></div>

<h2 id="debugging-and-testing-generators">Debugging and Testing Generators</h2>

<h3 id="view-generated-files">View Generated Files</h3>

<p>Enable compiler-generated files in the consuming project (the playground demo already does this):</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;PropertyGroup&gt;</span>
    <span class="nt">&lt;EmitCompilerGeneratedFiles&gt;</span>true<span class="nt">&lt;/EmitCompilerGeneratedFiles&gt;</span>
<span class="nt">&lt;/PropertyGroup&gt;</span>
</code></pre></div></div>

<p>Then inspect <code class="language-plaintext highlighter-rouge">obj/Debug/net8.0/</code> for the emitted <code class="language-plaintext highlighter-rouge">.g.cs</code> files.</p>

<h3 id="attach-debugger-during-build">Attach Debugger During Build</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">void</span> <span class="nf">Initialize</span><span class="p">(</span><span class="n">IncrementalGeneratorInitializationContext</span> <span class="n">context</span><span class="p">)</span>
<span class="p">{</span>
    <span class="err">#</span><span class="k">if</span> <span class="n">DEBUG</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">Debugger</span><span class="p">.</span><span class="n">IsAttached</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">Debugger</span><span class="p">.</span><span class="nf">Launch</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="err">#</span><span class="n">endif</span>
    
    <span class="c1">// ... rest of initialization</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="unit-test-generators-properly">Unit Test Generators Properly</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">using</span> <span class="nn">System</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">System.Linq</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.SourceGenerators</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">AnimatLabs.SourceGenerators.Attributes</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.CodeAnalysis</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Microsoft.CodeAnalysis.CSharp</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">Xunit</span><span class="p">;</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">GeneratorTests</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">void</span> <span class="nf">AutoToString_GeneratesOverride</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">source</span> <span class="p">=</span> <span class="s">"""
</span>            <span class="k">using</span> <span class="nn">AnimatLabs.SourceGenerators.Attributes</span><span class="p">;</span>
            <span class="k">namespace</span> <span class="nn">Demo</span><span class="p">;</span>

            <span class="p">[</span><span class="n">AutoToString</span><span class="p">]</span>
            <span class="k">public</span> <span class="k">partial</span> <span class="k">class</span> <span class="nc">Person</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="p">}</span>
            <span class="s">""";
</span>
        <span class="kt">var</span> <span class="n">output</span> <span class="p">=</span> <span class="nf">RunGenerator</span><span class="p">(</span><span class="n">source</span><span class="p">);</span>

        <span class="n">Assert</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"public override string ToString()"</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">Ordinal</span><span class="p">);</span>
        <span class="n">Assert</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"Name ="</span><span class="p">,</span> <span class="n">output</span><span class="p">,</span> <span class="n">StringComparison</span><span class="p">.</span><span class="n">Ordinal</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">private</span> <span class="k">static</span> <span class="kt">string</span> <span class="nf">RunGenerator</span><span class="p">(</span><span class="kt">string</span> <span class="n">source</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">syntaxTree</span> <span class="p">=</span> <span class="n">CSharpSyntaxTree</span><span class="p">.</span><span class="nf">ParseText</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="k">new</span> <span class="nf">CSharpParseOptions</span><span class="p">(</span><span class="n">LanguageVersion</span><span class="p">.</span><span class="n">Latest</span><span class="p">));</span>

        <span class="kt">var</span> <span class="n">references</span> <span class="p">=</span> <span class="n">AppDomain</span><span class="p">.</span><span class="n">CurrentDomain</span><span class="p">.</span><span class="nf">GetAssemblies</span><span class="p">()</span>
            <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">assembly</span> <span class="p">=&gt;</span> <span class="p">!</span><span class="n">assembly</span><span class="p">.</span><span class="n">IsDynamic</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">assembly</span> <span class="p">=&gt;</span> <span class="n">assembly</span><span class="p">.</span><span class="n">Location</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">location</span> <span class="p">=&gt;</span> <span class="p">!</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">location</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">Distinct</span><span class="p">(</span><span class="n">StringComparer</span><span class="p">.</span><span class="n">OrdinalIgnoreCase</span><span class="p">)</span>
            <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">location</span> <span class="p">=&gt;</span> <span class="n">MetadataReference</span><span class="p">.</span><span class="nf">CreateFromFile</span><span class="p">(</span><span class="n">location</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>

        <span class="n">references</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="n">MetadataReference</span><span class="p">.</span><span class="nf">CreateFromFile</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">AutoToStringAttribute</span><span class="p">).</span><span class="n">Assembly</span><span class="p">.</span><span class="n">Location</span><span class="p">));</span>

        <span class="kt">var</span> <span class="n">compilation</span> <span class="p">=</span> <span class="n">CSharpCompilation</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span>
            <span class="n">assemblyName</span><span class="p">:</span> <span class="s">"Tests"</span><span class="p">,</span>
            <span class="n">syntaxTrees</span><span class="p">:</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">syntaxTree</span> <span class="p">},</span>
            <span class="n">references</span><span class="p">:</span> <span class="n">references</span><span class="p">,</span>
            <span class="n">options</span><span class="p">:</span> <span class="k">new</span> <span class="nf">CSharpCompilationOptions</span><span class="p">(</span><span class="n">OutputKind</span><span class="p">.</span><span class="n">DynamicallyLinkedLibrary</span><span class="p">));</span>

        <span class="kt">var</span> <span class="n">driver</span> <span class="p">=</span> <span class="n">CSharpGeneratorDriver</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="k">new</span> <span class="nf">AnimatLabsSourceGenerators</span><span class="p">());</span>
        <span class="n">driver</span><span class="p">.</span><span class="nf">RunGeneratorsAndUpdateCompilation</span><span class="p">(</span><span class="n">compilation</span><span class="p">,</span> <span class="k">out</span> <span class="kt">var</span> <span class="n">outputCompilation</span><span class="p">,</span> <span class="k">out</span> <span class="n">_</span><span class="p">);</span>

        <span class="k">return</span> <span class="kt">string</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="s">"\n"</span><span class="p">,</span> <span class="n">outputCompilation</span><span class="p">.</span><span class="n">SyntaxTrees</span>
            <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">tree</span> <span class="p">=&gt;</span> <span class="n">tree</span><span class="p">.</span><span class="n">FilePath</span><span class="p">.</span><span class="nf">EndsWith</span><span class="p">(</span><span class="s">".g.cs"</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="nf">Select</span><span class="p">(</span><span class="n">tree</span> <span class="p">=&gt;</span> <span class="n">tree</span><span class="p">.</span><span class="nf">ToString</span><span class="p">()));</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="performance-making-generators-fast">Performance: Making Generators Fast</h2>

<p>Generators run on every keystroke. A slow generator destroys the IDE experience.</p>

<h3 id="do-use-incremental-generators">Do: Use Incremental Generators</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GOOD - Only regenerates when inputs change</span>
<span class="kt">var</span> <span class="n">provider</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">SyntaxProvider</span><span class="p">.</span><span class="nf">ForAttributeWithMetadataName</span><span class="p">(</span>
    <span class="s">"MyAttribute"</span><span class="p">,</span>
    <span class="n">predicate</span><span class="p">:</span> <span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="k">true</span><span class="p">,</span>
    <span class="n">transform</span><span class="p">:</span> <span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="nf">ProcessNode</span><span class="p">(</span><span class="n">ctx</span><span class="p">));</span>
</code></pre></div></div>

<h3 id="do-filter-early">Do: Filter Early</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// GOOD - Cheap syntax check before expensive semantic analysis</span>
<span class="n">predicate</span><span class="p">:</span> <span class="k">static</span> <span class="p">(</span><span class="n">node</span><span class="p">,</span> <span class="n">_</span><span class="p">)</span> <span class="p">=&gt;</span> 
    <span class="n">node</span> <span class="k">is</span> <span class="n">ClassDeclarationSyntax</span> <span class="p">{</span> <span class="n">AttributeLists</span><span class="p">.</span><span class="n">Count</span><span class="p">:</span> <span class="p">&gt;</span> <span class="m">0</span> <span class="p">}</span>
</code></pre></div></div>

<h3 id="dont-process-everything">Don’t: Process Everything</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BAD - Analyzes entire compilation on every keystroke</span>
<span class="n">context</span><span class="p">.</span><span class="nf">RegisterSourceOutput</span><span class="p">(</span><span class="n">context</span><span class="p">.</span><span class="n">CompilationProvider</span><span class="p">,</span> <span class="p">(</span><span class="n">ctx</span><span class="p">,</span> <span class="n">compilation</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">tree</span> <span class="k">in</span> <span class="n">compilation</span><span class="p">.</span><span class="n">SyntaxTrees</span><span class="p">)</span> <span class="c1">// Every file!</span>
    <span class="p">{</span>
        <span class="c1">// ...</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div></div>

<h3 id="dont-allocate-heavily">Don’t: Allocate Heavily</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// BAD - New StringBuilder per call</span>
<span class="kt">var</span> <span class="n">sb</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringBuilder</span><span class="p">();</span>

<span class="c1">// BETTER - Use pooled builder or pre-allocated</span>
<span class="kt">var</span> <span class="n">sb</span> <span class="p">=</span> <span class="n">StringBuilderPool</span><span class="p">.</span><span class="nf">Get</span><span class="p">();</span>
<span class="k">try</span> <span class="p">{</span> <span class="cm">/* ... */</span> <span class="p">}</span>
<span class="k">finally</span> <span class="p">{</span> <span class="n">StringBuilderPool</span><span class="p">.</span><span class="nf">Return</span><span class="p">(</span><span class="n">sb</span><span class="p">);</span> <span class="p">}</span>
</code></pre></div></div>

<h2 id="libraries-using-source-generators">Libraries Using Source Generators</h2>

<p>These production libraries prove the pattern works at scale:</p>

<table>
  <thead>
    <tr>
      <th>Library</th>
      <th>What It Generates</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><a href="https://docs.microsoft.com/en-us/dotnet/standard/serialization/system-text-json-source-generation">System.Text.Json</a></td>
      <td>JSON serializers (built-in)</td>
    </tr>
    <tr>
      <td><a href="https://github.com/riok/mapperly">Mapperly</a></td>
      <td>Object-to-object mappers</td>
    </tr>
    <tr>
      <td><a href="https://github.com/reactiveui/refit">Refit</a></td>
      <td>REST API clients</td>
    </tr>
    <tr>
      <td><a href="https://github.com/andrewlock/StronglyTypedId">StronglyTypedId</a></td>
      <td>Strongly-typed ID wrappers</td>
    </tr>
    <tr>
      <td><a href="https://github.com/domn1995/dunet">Dunet</a></td>
      <td>Discriminated unions</td>
    </tr>
    <tr>
      <td><a href="https://github.com/diegofrata/Generator.Equals">Generator.Equals</a></td>
      <td>Equality members</td>
    </tr>
    <tr>
      <td><a href="https://github.com/canton7/PropertyChanged.SourceGenerator">PropertyChanged.SourceGenerator</a></td>
      <td>INotifyPropertyChanged</td>
    </tr>
  </tbody>
</table>

<h2 id="common-pitfalls-and-solutions">Common Pitfalls and Solutions</h2>

<table>
  <thead>
    <tr>
      <th>Pitfall</th>
      <th>Symptom</th>
      <th>Solution</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Wrong target framework</td>
      <td>Generator silently doesn’t run</td>
      <td>Must be <code class="language-plaintext highlighter-rouge">netstandard2.0</code></td>
    </tr>
    <tr>
      <td>Missing analyzer reference</td>
      <td>Generated code not found</td>
      <td>Add <code class="language-plaintext highlighter-rouge">OutputItemType="Analyzer"</code></td>
    </tr>
    <tr>
      <td>Stale generated code</td>
      <td>Changes not reflected</td>
      <td>Restart IDE, clean rebuild</td>
    </tr>
    <tr>
      <td>Non-deterministic output</td>
      <td>Different output each build</td>
      <td>Don’t use timestamps, random values</td>
    </tr>
    <tr>
      <td>Slow IDE</td>
      <td>Typing lag, high CPU</td>
      <td>Use incremental generator, filter early</td>
    </tr>
    <tr>
      <td>Missing partial keyword</td>
      <td>Compiler error on generated code</td>
      <td>Consumer class must be <code class="language-plaintext highlighter-rouge">partial</code></td>
    </tr>
  </tbody>
</table>

<h2 id="what-id-actually-ship">What I’d Actually Ship</h2>

<p>Source generators aren’t just a cool compiler trick - they’re a practical tool for eliminating the repetitive code that makes codebases harder to maintain. The examples above show real patterns you can implement today:</p>

<ul>
  <li><strong>Configuration binding</strong> without magic strings</li>
  <li>Enum helpers that respect <code class="language-plaintext highlighter-rouge">DisplayAttribute</code> (no reflection on the hot path)</li>
  <li><strong>DTO mapping</strong> with compile-time safety</li>
  <li><code class="language-plaintext highlighter-rouge">ToString()</code> overrides without hand-written plumbing</li>
</ul>

<p>The investment in building a generator pays off every time it saves someone from writing (and debugging) ceremony. Start with a simple pattern, test thoroughly, and scale from there.</p>

<p>One long rant worth the pixels: if your team keeps copy-pasting the same mapper and the same <code class="language-plaintext highlighter-rouge">ToString()</code> for every new model, you are not “moving fast,” you are accruing diff noise that will explode the first time someone renames a property and forgets to update the third copy of the same method.</p>

<p><strong>Next steps:</strong></p>
<ol>
  <li>Clone the playground repo: https://github.com/animat089/playground/tree/main/SourceGenerators</li>
  <li>Run <code class="language-plaintext highlighter-rouge">dotnet test</code> and inspect generated <code class="language-plaintext highlighter-rouge">.g.cs</code> output under <code class="language-plaintext highlighter-rouge">obj/</code></li>
  <li>Compare source-generator output with the checked-in T4 output under <code class="language-plaintext highlighter-rouge">tools/</code></li>
</ol>

<hr />

<p><em>If you’ve built a generator for a different pattern, I’d be curious to see it.</em></p>

<hr />

<h2 id="related">Related</h2>

<ul>
  <li><a href="/technical/.net/.net-core/aspect-oriented-programming/">Aspect-oriented programming in C#</a></li>
  <li><a href="/technical/.net/.net-core/interesting-enhancements-cs12/">C# 12 highlights</a></li>
  <li><a href="/technical/.net/.net-core/better-result-handling-with-result-object/">Result pattern in C#</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term="Source Generators" /><category term="Code Generation" /><category term="Roslyn" /><category term="Performance" /><category term="Metaprogramming" /><summary type="html"><![CDATA[Stop writing repetitive code. C# source generators create it at compile time with zero runtime cost, full IntelliSense support, and complete debuggability.]]></summary></entry><entry><title type="html">WorkflowForge 2.0 Benchmarks: 511x Faster Than Workflow Core and Elsa in .NET</title><link href="https://animatlabs.com/technical/.net/workflow/workflow-forge-2-performance-unleashed/" rel="alternate" type="text/html" title="WorkflowForge 2.0 Benchmarks: 511x Faster Than Workflow Core and Elsa in .NET" /><published>2026-01-26T00:00:00+05:30</published><updated>2026-03-26T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/workflow/workflow-forge-2-performance-unleashed</id><content type="html" xml:base="https://animatlabs.com/technical/.net/workflow/workflow-forge-2-performance-unleashed/"><![CDATA[<h2 id="the-question-everyone-asked">The Question Everyone Asked</h2>

<p><em>“Your internal benchmarks look great, but how does WorkflowForge actually compare to other popular alternatives?”</em></p>

<p>Fair question. Internal metrics only tell half the story. So for version 2.0, I ran head-to-head benchmarks against the two most popular workflow frameworks in .NET, with BenchmarkDotNet, 50 iterations per scenario, and full transparency on methodology.</p>

<p>Honestly, I was curious whether the gap would shrink once I stopped cherry-picking scenarios.</p>

<p>The short answer: WorkflowForge operates in microseconds while the alternatives work in milliseconds. The gap widens as complexity increases. Not even close on the heavy cases.</p>

<p>Let me show you the numbers.</p>

<p><strong>Full codebase and benchmarks:</strong> <a href="https://github.com/animatlabs/workflow-forge" class="btn btn--primary">GitHub Repository</a></p>

<p><strong>Documentation:</strong> <a href="https://animatlabs.com/workflow-forge">animatlabs.com/workflow-forge</a></p>

<hr />

<h2 id="the-benchmarks">The Benchmarks</h2>

<p><strong>Test environment:</strong> Windows 11 (25H2), .NET 8.0.23, Intel i7-1185G7, BenchmarkDotNet v0.15.8</p>

<h3 id="execution-time">Execution Time</h3>

<table>
  <thead>
    <tr>
      <th>Scenario</th>
      <th>WorkflowForge</th>
      <th>Workflow Core</th>
      <th>Elsa</th>
      <th>Advantage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sequential (10 ops)</td>
      <td>247μs</td>
      <td>6,531μs</td>
      <td>17,617μs</td>
      <td><strong>26-71x</strong></td>
    </tr>
    <tr>
      <td>Data Passing (10 ops)</td>
      <td>262μs</td>
      <td>6,737μs</td>
      <td>18,222μs</td>
      <td><strong>26-70x</strong></td>
    </tr>
    <tr>
      <td>Conditional (10 ops)</td>
      <td>266μs</td>
      <td>8,543μs</td>
      <td>21,333μs</td>
      <td><strong>32-80x</strong></td>
    </tr>
    <tr>
      <td>Loop (50 items)</td>
      <td>497μs</td>
      <td>35,421μs</td>
      <td>64,171μs</td>
      <td><strong>71-129x</strong></td>
    </tr>
    <tr>
      <td>Concurrent (8 workers)</td>
      <td>356μs</td>
      <td>38,833μs</td>
      <td>94,018μs</td>
      <td><strong>109-264x</strong></td>
    </tr>
    <tr>
      <td>Error Handling</td>
      <td>111μs</td>
      <td>1,228μs</td>
      <td>7,150μs</td>
      <td><strong>11-64x</strong></td>
    </tr>
    <tr>
      <td>Creation Overhead</td>
      <td>13μs</td>
      <td>814μs</td>
      <td>2,107μs</td>
      <td><strong>63-162x</strong></td>
    </tr>
    <tr>
      <td>State Machine (25 transitions)</td>
      <td>68μs</td>
      <td>20,624μs</td>
      <td>36,695μs</td>
      <td><strong>303-511x</strong></td>
    </tr>
    <tr>
      <td>Parallel (16 ops)</td>
      <td>55μs</td>
      <td>2,437μs</td>
      <td>20,891μs</td>
      <td><strong>44-380x</strong></td>
    </tr>
  </tbody>
</table>

<p>The pattern is clear: simple workflows show 26-71x improvement, but state machines hit <strong>511x faster</strong>. Complexity amplifies the gap. Once you stack branching, persistence, and compensation on top of each other, the slower engines do not just get a little worse; they allocate and schedule their way into a completely different cost class than a library that keeps the hot path thin.</p>

<h3 id="memory-allocation">Memory Allocation</h3>

<table>
  <thead>
    <tr>
      <th>Scenario</th>
      <th>WorkflowForge</th>
      <th>Workflow Core</th>
      <th>Elsa</th>
      <th>Advantage</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Sequential (10 ops)</td>
      <td>16.31KB</td>
      <td>430KB</td>
      <td>2,984KB</td>
      <td><strong>26-183x</strong></td>
    </tr>
    <tr>
      <td>State Machine (25)</td>
      <td>20.92KB</td>
      <td>1,106KB</td>
      <td>5,949KB</td>
      <td><strong>53-284x</strong></td>
    </tr>
    <tr>
      <td>Concurrent (8 workers)</td>
      <td>121KB</td>
      <td>3,232KB</td>
      <td>19,139KB</td>
      <td><strong>27-158x</strong></td>
    </tr>
    <tr>
      <td>Parallel (16 ops)</td>
      <td>8.1KB</td>
      <td>122KB</td>
      <td>4,647KB</td>
      <td><strong>15-573x</strong></td>
    </tr>
    <tr>
      <td>Minimal Baseline</td>
      <td>3.49KB</td>
      <td>37KB</td>
      <td>1,032KB</td>
      <td><strong>11-296x</strong></td>
    </tr>
  </tbody>
</table>

<p>WorkflowForge stays in kilobytes. The competition allocates megabytes. Baseline overhead is just <strong>3.49 KB</strong>.</p>

<hr />

<h2 id="whats-new-in-20">What’s New in 2.0</h2>

<p>Beyond benchmarks, version 2.0 brings real improvements based on community feedback.</p>

<h3 id="13-packages-up-from-6">13 Packages (Up from 6)</h3>

<table>
  <thead>
    <tr>
      <th>Package</th>
      <th>What It Does</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>WorkflowForge</strong></td>
      <td>Zero-dependency core</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Testing</strong></td>
      <td>Unit testing with <code class="language-plaintext highlighter-rouge">FakeWorkflowFoundry</code></td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.DependencyInjection</strong></td>
      <td>ASP.NET Core integration</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.Validation</strong></td>
      <td>DataAnnotations validation</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.Audit</strong></td>
      <td>Compliance trails</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.Logging.Serilog</strong></td>
      <td>Structured logging</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.Resilience.Polly</strong></td>
      <td>Circuit breakers, retries</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.Persistence.Recovery</strong></td>
      <td>Resume interrupted workflows</td>
    </tr>
    <tr>
      <td><strong>WorkflowForge.Extensions.Observability.OpenTelemetry</strong></td>
      <td>Distributed tracing</td>
    </tr>
  </tbody>
</table>

<p>Plus 4 more for resilience, persistence, and health checks.</p>

<h3 id="lifecycle-hooks">Lifecycle Hooks</h3>

<p>The most requested feature: setup and teardown without middleware.</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">AuditedOperation</span> <span class="p">:</span> <span class="n">WorkflowOperationBase</span>
<span class="p">{</span>
    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">OnBeforeExecuteAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">foundry</span><span class="p">.</span><span class="nf">SetProperty</span><span class="p">(</span><span class="s">"StartTime"</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="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="nf">ProcessAsync</span><span class="p">(</span><span class="n">inputData</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">OnAfterExecuteAsync</span><span class="p">(</span>
        <span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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">duration</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">foundry</span><span class="p">.</span><span class="n">GetProperty</span><span class="p">&lt;</span><span class="n">DateTime</span><span class="p">&gt;(</span><span class="s">"StartTime"</span><span class="p">);</span>
        <span class="n">foundry</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">"Completed in {Duration}ms"</span><span class="p">,</span> <span class="n">duration</span><span class="p">.</span><span class="n">TotalMilliseconds</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="cleaner-apis">Cleaner APIs</h3>

<p>Build workflows faster with bulk operations:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"BatchProcess"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperations</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">ValidateOperation</span><span class="p">(),</span>
        <span class="k">new</span> <span class="nf">TransformOperation</span><span class="p">(),</span>
        <span class="k">new</span> <span class="nf">PersistOperation</span><span class="p">()</span>
    <span class="p">)</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// Or parallel execution</span>
<span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"ParallelFetch"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">InitOperation</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">AddParallelOperations</span><span class="p">(</span>
        <span class="k">new</span> <span class="nf">FetchFromApiA</span><span class="p">(),</span>
        <span class="k">new</span> <span class="nf">FetchFromApiB</span><span class="p">(),</span>
        <span class="k">new</span> <span class="nf">FetchFromApiC</span><span class="p">()</span>
    <span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="k">new</span> <span class="nf">AggregateResults</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>
</code></pre></div></div>

<h3 id="first-class-testing">First-Class Testing</h3>

<p>The new <code class="language-plaintext highlighter-rouge">WorkflowForge.Testing</code> package makes unit testing trivial:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><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">Operation_ProcessesData_Successfully</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FakeWorkflowFoundry</span><span class="p">();</span>
    <span class="kt">var</span> <span class="n">operation</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MyOperation</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">operation</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="k">null</span><span class="p">,</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">CancellationToken</span><span class="p">.</span><span class="n">None</span><span class="p">);</span>
    
    <span class="n">Assert</span><span class="p">.</span><span class="nf">True</span><span class="p">(</span><span class="n">foundry</span><span class="p">.</span><span class="n">ExecutedOperations</span><span class="p">.</span><span class="nf">Contains</span><span class="p">(</span><span class="s">"MyOperation"</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="zero-dependency-conflicts">Zero Dependency Conflicts</h3>

<p>Extensions now internalize third-party dependencies via ILRepack. Microsoft/System packages stay external. Result: no version conflicts with your existing projects.</p>

<hr />

<h2 id="breaking-changes">Breaking Changes</h2>

<p>Upgrading from 1.x? This changed:</p>

<p><strong>Event interfaces split</strong> (Single Responsibility Principle):</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">IWorkflowEvents</code> → <code class="language-plaintext highlighter-rouge">IWorkflowLifecycleEvents</code>, <code class="language-plaintext highlighter-rouge">IOperationLifecycleEvents</code>, <code class="language-plaintext highlighter-rouge">ICompensationLifecycleEvents</code></li>
</ul>

<p><strong>Base class method renamed</strong> (to support lifecycle hooks):</p>
<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 1.x</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="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsync</span><span class="p">(...)</span>

<span class="c1">// 2.0</span>
<span class="k">protected</span> <span class="k">override</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsyncCore</span><span class="p">(...)</span>
</code></pre></div></div>

<p><strong>ISystemTimeProvider</strong> now uses DI instead of static instance.</p>

<hr />

<h2 id="get-started">Get Started</h2>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package WorkflowForge
dotnet add package WorkflowForge.Testing  <span class="c"># Optional</span>
</code></pre></div></div>

<h3 id="hello-world">Hello World</h3>

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

<span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="s">"HelloWorld"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="s">"Greet"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span>
        <span class="n">foundry</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">"Hello from WorkflowForge 2.0!"</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateSmith</span><span class="p">();</span>
<span class="k">await</span> <span class="n">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="resources">Resources</h2>

<table>
  <thead>
    <tr>
      <th>What</th>
      <th>Where</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Documentation</td>
      <td><a href="https://animatlabs.com/workflow-forge">animatlabs.com/workflow-forge</a></td>
    </tr>
    <tr>
      <td>GitHub</td>
      <td><a href="https://github.com/animatlabs/workflow-forge">github.com/animatlabs/workflow-forge</a></td>
    </tr>
    <tr>
      <td>NuGet</td>
      <td><a href="https://www.nuget.org/packages/WorkflowForge">nuget.org/packages/WorkflowForge</a></td>
    </tr>
    <tr>
      <td>Benchmarks</td>
      <td><a href="https://animatlabs.com/workflow-forge/performance/competitive-analysis/">Full Methodology</a></td>
    </tr>
    <tr>
      <td>Samples</td>
      <td><a href="https://github.com/animatlabs/workflow-forge/tree/main/src/samples">33 Examples</a></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="the-bottom-line">The Bottom Line</h2>

<p>WorkflowForge 2.0 isn’t just about claiming performance. It’s about proving it. The benchmarks are reproducible, the methodology is documented, and the code is open source.</p>

<p>If workflow performance matters for your .NET application (high-throughput processing, real-time orchestration, microservice coordination), run your own benchmarks. The numbers speak for themselves. I think that’s the only way to trust a claim this loud.</p>

<p><strong>Questions?</strong> → <a href="https://github.com/animatlabs/workflow-forge/issues">Open an Issue</a></p>

<p>Happy forging.</p>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="more-on-this-topic">More on This Topic</h2>

<ul>
  <li><a href="/technical/.net/workflow/workflow-forge-introduction/">WorkflowForge introduction</a></li>
  <li><a href="/technical/.net/workflow/workflowforge-coravel-scheduled-workflows/">WorkflowForge with Coravel</a></li>
  <li><a href="/technical/.net/open-source/shipping-quality-dotnet-oss-release/">Shipping a quality .NET OSS release</a></li>
  <li><a href="/workflowforge-vs-elsa/">WorkflowForge vs Elsa</a></li>
  <li><a href="/workflowforge-vs-workflow-core/">WorkflowForge vs Workflow Core</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Workflow" /><category term=".NET" /><category term="Workflow" /><category term="WorkflowForge" /><category term="Performance" /><category term="Benchmarks" /><category term="Zero Dependencies" /><category term="Enterprise Architecture" /><summary type="html"><![CDATA[We benchmarked WorkflowForge 2.0 against Workflow Core and Elsa Workflows. Up to 511x faster execution and 573x less memory usage, with new extensions and a documentation site.]]></summary></entry><entry><title type="html">WorkflowForge: Lightweight .NET Workflow Engine with Zero Dependencies</title><link href="https://animatlabs.com/technical/.net/workflow/workflow-forge-introduction/" rel="alternate" type="text/html" title="WorkflowForge: Lightweight .NET Workflow Engine with Zero Dependencies" /><published>2025-06-03T00:00:00+05:30</published><updated>2025-06-03T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/workflow/workflow-forge-introduction</id><content type="html" xml:base="https://animatlabs.com/technical/.net/workflow/workflow-forge-introduction/"><![CDATA[<h2 id="tldr-workflowforge-in-30-seconds">TL;DR: WorkflowForge in 30 Seconds</h2>

<p><strong>Performance</strong>: Verified 4-56 μs operations with 15x concurrency scaling<br />
<strong>Zero Dependencies</strong>: Core package has 0 external dependencies, ~50KB footprint<br />
<strong>Developer Focused</strong>: Fluent API with industrial metaphor that maps to real workflows<br />
<strong>Production Ready</strong>: Available on NuGet with 6 specialized extension packages<br />
<strong>Built-in Compensation</strong>: Automatic saga pattern without manual orchestration</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package WorkflowForge
</code></pre></div></div>

<p>No configuration files. No heavy containers. Just workflows that work.</p>

<hr />

<h2 id="the-problem-with-existing-solutions">The Problem with Existing Solutions</h2>

<p>After working with various .NET workflow frameworks, the same issues kept surfacing:</p>

<ul>
  <li><strong>Performance bottlenecks</strong> - Operations taking milliseconds when they should take microseconds</li>
  <li><strong>Dependency bloat</strong> - Core packages requiring 20+ dependencies for basic functionality</li>
  <li><strong>Configuration complexity</strong> - Hours of setup for simple workflow scenarios</li>
  <li><strong>Memory inefficiency</strong> - Frameworks consuming megabytes for kilobytes of logic</li>
  <li><strong>Poor abstractions</strong> - APIs designed for visual designers, not developers</li>
</ul>

<p>Most enterprise workflow solutions optimize for drag-and-drop editors and XML configurations rather than code quality and runtime performance.</p>

<h2 id="workflowforge-built-for-developers">WorkflowForge: Built for Developers</h2>

<p>WorkflowForge takes a different approach with a developer-first philosophy:</p>

<ul>
  <li><strong>Start minimal, scale incrementally</strong> - Zero dependencies in core, optional extensions when needed</li>
  <li><strong>Performance verified</strong> - All claims backed by BenchmarkDotNet results</li>
  <li><strong>Intuitive design</strong> - Industrial metaphor that maps to how workflows actually operate</li>
  <li><strong>Production tested</strong> - Built-in compensation, observability, and resilience patterns</li>
</ul>

<h3 id="the-industrial-metaphor">The Industrial Metaphor</h3>

<p>Instead of abstract “engines” and “executors”, WorkflowForge uses intuitive industrial concepts:</p>

<ul>
  <li><strong>The Forge</strong> - Factory that creates workflows and components</li>
  <li><strong>Foundries</strong> - Execution environments where operations are performed</li>
  <li><strong>Smiths</strong> - Orchestration engines managing workflow execution</li>
  <li><strong>Operations</strong> - Individual tasks that transform data</li>
</ul>

<hr />

<h2 id="verified-performance-characteristics">Verified Performance Characteristics</h2>

<p>Every performance claim is backed by BenchmarkDotNet results with no marketing embellishment.</p>

<table>
  <thead>
    <tr>
      <th><strong>Metric</strong></th>
      <th><strong>Measured Result</strong></th>
      <th><strong>Context</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Operation Execution</strong></td>
      <td><strong>4-56 μs</strong></td>
      <td>Per operation overhead</td>
    </tr>
    <tr>
      <td><strong>Foundry Creation</strong></td>
      <td><strong>5-15 μs</strong></td>
      <td>Environment setup time</td>
    </tr>
    <tr>
      <td><strong>Concurrency Scaling</strong></td>
      <td><strong>15x improvement</strong></td>
      <td>16 workflows: 301ms concurrent vs 4,540ms sequential</td>
    </tr>
    <tr>
      <td><strong>Memory Footprint</strong></td>
      <td><strong>&lt;2KB per foundry</strong></td>
      <td>Runtime allocation</td>
    </tr>
    <tr>
      <td><strong>Package Size</strong></td>
      <td><strong>~50KB core</strong></td>
      <td>Zero external dependencies</td>
    </tr>
  </tbody>
</table>

<p><a href="https://github.com/animatlabs/workflow-forge/tree/main/src/benchmarks">Complete Benchmark Results</a></p>

<hr />

<h2 id="two-development-approaches">Two Development Approaches</h2>

<h3 id="rapid-prototyping">Rapid Prototyping</h3>

<p>Perfect for scripts, proof-of-concepts, or simple automation:</p>

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

<span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithName</span><span class="p">(</span><span class="s">"ProcessOrder"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="s">"ValidateOrder"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span> 
    <span class="p">{</span>
        <span class="n">foundry</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">"Validating order {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="k">return</span> <span class="k">await</span> <span class="nf">ValidateOrderAsync</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="s">"ProcessPayment"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span> 
    <span class="p">{</span>
        <span class="n">foundry</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">"Processing payment for order {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="k">return</span> <span class="k">await</span> <span class="nf">ProcessPaymentAsync</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">ct</span><span class="p">);</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span><span class="s">"OrderProcessing"</span><span class="p">);</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateSmith</span><span class="p">();</span>

<span class="k">await</span> <span class="n">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">foundry</span><span class="p">);</span>
</code></pre></div></div>

<h3 id="enterprise-grade-production">Enterprise-Grade Production</h3>

<p>Dependency injection with proper separation of concerns:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Registration</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">ValidateOrderOperation</span><span class="p">&gt;();</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddScoped</span><span class="p">&lt;</span><span class="n">ProcessPaymentOperation</span><span class="p">&gt;();</span>

<span class="c1">// Workflow Definition  </span>
<span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">(</span><span class="n">serviceProvider</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">WithName</span><span class="p">(</span><span class="s">"ProcessOrder"</span><span class="p">)</span>
    <span class="p">.</span><span class="n">AddOperation</span><span class="p">&lt;</span><span class="n">ValidateOrderOperation</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="n">AddOperation</span><span class="p">&lt;</span><span class="n">ProcessPaymentOperation</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="c1">// Execution</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span><span class="s">"OrderProcessing"</span><span class="p">);</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateSmith</span><span class="p">();</span>

<span class="k">await</span> <span class="n">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="n">order</span><span class="p">,</span> <span class="n">foundry</span><span class="p">);</span>
</code></pre></div></div>

<hr />

<h2 id="automatic-compensation-sagas-simplified">Automatic Compensation: Sagas Simplified</h2>

<p>Built-in compensation without complex orchestration:</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">ProcessPaymentOperation</span> <span class="p">:</span> <span class="n">IWorkflowOperation</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Name</span> <span class="p">=&gt;</span> <span class="s">"ProcessPayment"</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">bool</span> <span class="n">SupportsRestore</span> <span class="p">=&gt;</span> <span class="k">true</span><span class="p">;</span> <span class="c1">// Enables automatic compensation</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span><span class="p">&lt;</span><span class="kt">object</span><span class="p">?&gt;</span> <span class="nf">ForgeAsync</span><span class="p">(</span><span class="kt">object</span><span class="p">?</span> <span class="n">inputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="p">(</span><span class="n">Order</span><span class="p">)</span><span class="n">inputData</span><span class="p">!;</span>
        <span class="kt">var</span> <span class="n">paymentResult</span> <span class="p">=</span> <span class="k">await</span> <span class="n">_paymentService</span><span class="p">.</span><span class="nf">ProcessAsync</span><span class="p">(</span><span class="n">order</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
        
        <span class="c1">// Store compensation data</span>
        <span class="n">foundry</span><span class="p">.</span><span class="nf">SetProperty</span><span class="p">(</span><span class="s">"TransactionId"</span><span class="p">,</span> <span class="n">paymentResult</span><span class="p">.</span><span class="n">TransactionId</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">order</span><span class="p">;</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">async</span> <span class="n">Task</span> <span class="nf">RestoreAsync</span><span class="p">(</span><span class="kt">object</span><span class="p">?</span> <span class="n">outputData</span><span class="p">,</span> <span class="n">IWorkflowFoundry</span> <span class="n">foundry</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="c1">// Called automatically if downstream operations fail</span>
        <span class="kt">var</span> <span class="n">transactionId</span> <span class="p">=</span> <span class="n">foundry</span><span class="p">.</span><span class="n">GetProperty</span><span class="p">&lt;</span><span class="kt">string</span><span class="p">&gt;(</span><span class="s">"TransactionId"</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">transactionId</span><span class="p">))</span>
        <span class="p">{</span>
            <span class="k">await</span> <span class="n">_paymentService</span><span class="p">.</span><span class="nf">RefundAsync</span><span class="p">(</span><span class="n">transactionId</span><span class="p">,</span> <span class="n">cancellationToken</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When downstream operations fail, WorkflowForge automatically calls <code class="language-plaintext highlighter-rouge">RestoreAsync</code> on completed operations in reverse order. No saga coordinators, state machines, or XML configuration required.</p>

<hr />

<h2 id="production-extensions">Production Extensions</h2>

<p>Philosophy: Start with zero dependencies, add capabilities as requirements grow.</p>

<h3 id="available-on-nuget">Available on NuGet</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Core (zero dependencies)</span>
dotnet add package WorkflowForge

<span class="c"># Optional extensions</span>
dotnet add package WorkflowForge.Extensions.Logging.Serilog
dotnet add package WorkflowForge.Extensions.Resilience.Polly  
dotnet add package WorkflowForge.Extensions.Observability.Performance
dotnet add package WorkflowForge.Extensions.Observability.HealthChecks
dotnet add package WorkflowForge.Extensions.Observability.OpenTelemetry
</code></pre></div></div>

<h3 id="fluent-configuration">Fluent Configuration</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span><span class="s">"OrderProcessing"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">UseSerilog</span><span class="p">(</span><span class="n">Log</span><span class="p">.</span><span class="n">Logger</span><span class="p">)</span>                    <span class="c1">// Structured logging</span>
    <span class="p">.</span><span class="nf">UsePollyResilience</span><span class="p">()</span>                      <span class="c1">// Retry, circuit breaker, timeouts  </span>
    <span class="p">.</span><span class="nf">EnablePerformanceMonitoring</span><span class="p">()</span>             <span class="c1">// Real-time metrics</span>
    <span class="p">.</span><span class="nf">EnableHealthChecks</span><span class="p">()</span>                      <span class="c1">// System diagnostics</span>
    <span class="p">.</span><span class="nf">EnableOpenTelemetry</span><span class="p">(</span><span class="s">"OrderService"</span><span class="p">,</span> <span class="s">"1.0.0"</span><span class="p">);</span> <span class="c1">// Distributed tracing</span>
</code></pre></div></div>

<p><strong>Extension Packages:</strong></p>

<table>
  <thead>
    <tr>
      <th><strong>Extension</strong></th>
      <th><strong>Purpose</strong></th>
      <th><strong>Package</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><strong>Serilog Logging</strong></td>
      <td>Structured logging with context</td>
      <td><code class="language-plaintext highlighter-rouge">WorkflowForge.Extensions.Logging.Serilog</code></td>
    </tr>
    <tr>
      <td><strong>Polly Resilience</strong></td>
      <td>Circuit breakers, retries, timeouts</td>
      <td><code class="language-plaintext highlighter-rouge">WorkflowForge.Extensions.Resilience.Polly</code></td>
    </tr>
    <tr>
      <td><strong>Performance Monitoring</strong></td>
      <td>Metrics, profiling, statistics</td>
      <td><code class="language-plaintext highlighter-rouge">WorkflowForge.Extensions.Observability.Performance</code></td>
    </tr>
    <tr>
      <td><strong>Health Checks</strong></td>
      <td>Application health monitoring</td>
      <td><code class="language-plaintext highlighter-rouge">WorkflowForge.Extensions.Observability.HealthChecks</code></td>
    </tr>
    <tr>
      <td><strong>OpenTelemetry</strong></td>
      <td>Distributed tracing and observability</td>
      <td><code class="language-plaintext highlighter-rouge">WorkflowForge.Extensions.Observability.OpenTelemetry</code></td>
    </tr>
  </tbody>
</table>

<hr />

<h2 id="learn-through-examples">Learn Through Examples</h2>

<p>The most effective way to understand WorkflowForge is running the progressive examples:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>git clone https://github.com/animatlabs/workflow-forge.git
<span class="nb">cd </span>workflow-forge/src/samples/WorkflowForge.Samples.BasicConsole
dotnet run
</code></pre></div></div>

<p><strong>Learning Path:</strong></p>

<ul>
  <li><strong>Samples 1-4</strong>: Basic workflows and data flow patterns</li>
  <li><strong>Samples 5-8</strong>: Control flow and error handling strategies</li>
  <li><strong>Samples 9-12</strong>: Configuration management and middleware</li>
  <li><strong>Samples 13-17</strong>: Extension integration and observability</li>
  <li><strong>Sample 18</strong>: Full production scenario</li>
</ul>

<p>Each example builds progressively with clear explanations and real-time output.</p>

<hr />

<h2 id="production-readiness">Production Readiness</h2>

<h3 id="zero-dependencies-core">Zero Dependencies Core</h3>
<ul>
  <li>No dependency conflicts with existing applications</li>
  <li>Minimal security attack surface</li>
  <li>Simplified auditing and compliance processes</li>
</ul>

<h3 id="verified-performance">Verified Performance</h3>
<ul>
  <li>All performance claims backed by reproducible benchmarks</li>
  <li>Memory and CPU optimized for high-throughput scenarios</li>
  <li>Concurrent execution patterns validated under load</li>
</ul>

<h3 id="built-in-best-practices">Built-in Best Practices</h3>
<ul>
  <li>Automatic compensation using saga pattern</li>
  <li>Structured logging integration points</li>
  <li>Health checks and observability hooks</li>
  <li>Graceful error handling and recovery</li>
</ul>

<h3 id="developer-experience">Developer Experience</h3>
<ul>
  <li>Fluent API designed for code completion</li>
  <li>Full documentation with step-by-step guides</li>
  <li>18 progressive examples covering common scenarios</li>
  <li>Industrial metaphor that maps to real-world processes</li>
</ul>

<hr />

<h2 id="getting-started">Getting Started</h2>

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

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package WorkflowForge
</code></pre></div></div>

<h3 id="first-workflow">First Workflow</h3>

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

<span class="kt">var</span> <span class="n">workflow</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateWorkflow</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">WithName</span><span class="p">(</span><span class="s">"HelloWorld"</span><span class="p">)</span>
    <span class="p">.</span><span class="nf">AddOperation</span><span class="p">(</span><span class="s">"SayHello"</span><span class="p">,</span> <span class="k">async</span> <span class="p">(</span><span class="n">input</span><span class="p">,</span> <span class="n">foundry</span><span class="p">,</span> <span class="n">ct</span><span class="p">)</span> <span class="p">=&gt;</span> 
    <span class="p">{</span>
        <span class="n">foundry</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">"Hello from WorkflowForge!"</span><span class="p">);</span>
        <span class="k">return</span> <span class="s">"Hello World!"</span><span class="p">;</span>
    <span class="p">})</span>
    <span class="p">.</span><span class="nf">Build</span><span class="p">();</span>

<span class="k">using</span> <span class="nn">var</span> <span class="n">foundry</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateFoundry</span><span class="p">(</span><span class="s">"Demo"</span><span class="p">);</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">smith</span> <span class="p">=</span> <span class="n">WorkflowForge</span><span class="p">.</span><span class="nf">CreateSmith</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">smith</span><span class="p">.</span><span class="nf">ForgeAsync</span><span class="p">(</span><span class="n">workflow</span><span class="p">,</span> <span class="k">null</span><span class="p">,</span> <span class="n">foundry</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">result</span><span class="p">);</span> <span class="c1">// "Hello World!"</span>
</code></pre></div></div>

<h3 id="resources">Resources</h3>

<ul>
  <li><strong><a href="https://github.com/animatlabs/workflow-forge/tree/main/src/samples/WorkflowForge.Samples.BasicConsole">Interactive Samples</a></strong> - 18 hands-on examples</li>
  <li><strong><a href="https://github.com/animatlabs/workflow-forge/tree/main/docs">Documentation</a></strong> - Complete guides and API reference</li>
  <li><strong><a href="https://github.com/animatlabs/workflow-forge/tree/main/src/benchmarks">Performance Benchmarks</a></strong> - Detailed performance analysis</li>
  <li><strong><a href="https://github.com/animatlabs/workflow-forge/tree/main/docs/extensions.md">Extensions Guide</a></strong> - Production extension capabilities</li>
</ul>

<hr />

<h2 id="project-links">Project Links</h2>

<p><strong>Repository</strong>: <a href="https://github.com/animatlabs/workflow-forge">github.com/animatlabs/workflow-forge</a><br />
<strong>NuGet</strong>: <a href="https://www.nuget.org/packages/WorkflowForge">nuget.org/packages/WorkflowForge</a><br />
<strong>Benchmarks</strong>: <a href="https://github.com/animatlabs/workflow-forge/tree/main/src/benchmarks">Performance Analysis</a></p>

<p>WorkflowForge represents a philosophy: powerful capabilities shouldn’t require complex dependencies.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dotnet add package WorkflowForge
</code></pre></div></div>

<p><strong>Questions or contributions?</strong> → <a href="https://github.com/animatlabs/workflow-forge/issues">Open an Issue</a></p>

<p><strong>WorkflowForge</strong> - <em>Industrial strength workflows for modern .NET</em></p>

<div class="wf-cta">
  <div class="wf-cta__inner">
    <p class="wf-cta__message">
      If <strong>WorkflowForge</strong> has been useful to you, a &#11088; star on GitHub helps it reach more .NET developers.
      And if you'd like to support the work behind it, Ko-fi is always open!
    </p>
    <div class="wf-cta__buttons">
      <a class="wf-cta__btn wf-cta__btn--github" href="https://github.com/animatlabs/workflow-forge" target="_blank" rel="noopener noreferrer">
        <svg class="wf-cta__icon" viewBox="0 0 16 16" aria-hidden="true" fill="currentColor">
          <path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38
            0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13
            -.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66
            .07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15
            -.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0
            1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82
            1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01
            1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z" />
        </svg>
        &#11088; Star on GitHub
      </a>
      <a class="wf-cta__btn wf-cta__btn--kofi" href="https://ko-fi.com/animat089" target="_blank" rel="noopener noreferrer">
        <img class="wf-cta__kofi-icon" src="https://storage.ko-fi.com/cdn/cup-border.png" alt="Ko-fi icon" loading="lazy" />
        Support on Ko-fi
      </a>
    </div>
  </div>
</div>

<hr />

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/technical/.net/workflow/workflow-forge-2-performance-unleashed/">WorkflowForge 2.0 benchmarks</a></li>
  <li><a href="/technical/.net/workflow/workflowforge-coravel-scheduled-workflows/">WorkflowForge with Coravel</a></li>
  <li><a href="/technical/.net/workflow/masstransit-workflowforge-saga/">MassTransit saga with WorkflowForge</a></li>
  <li><a href="/technical/.net/workflow/htmx-dotnet/">HTMX dashboard in .NET</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term="Workflow" /><category term=".NET" /><category term="Workflow" /><category term="WorkflowForge" /><category term="Performance" /><category term="Zero Dependencies" /><category term="Microservices" /><category term="Enterprise Architecture" /><category term="Developer Experience" /><summary type="html"><![CDATA[WorkflowForge delivers microsecond workflow operations with zero core dependencies, proven concurrency scaling, and a code-first developer experience. Available on NuGet with production-ready extensions.]]></summary></entry><entry><title type="html">Aspect-Oriented Programming in C#: Centralize Logging, Caching, and Security</title><link href="https://animatlabs.com/technical/.net/.net-core/aspect-oriented-programming/" rel="alternate" type="text/html" title="Aspect-Oriented Programming in C#: Centralize Logging, Caching, and Security" /><published>2024-10-09T00:00:00+05:30</published><updated>2024-10-09T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/aspect-oriented-programming</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/aspect-oriented-programming/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>When building reliable software applications, logging becomes an essential part of the process. Logs provide invaluable insights into application behavior, making it easier to debug and monitor. However, manually adding logging code throughout a project is time-consuming and can clutter the codebase. What if you could automatically inject logging, security checks, and caching into your code without explicitly writing it everywhere?</p>

<p>This is where <strong>Aspect-Oriented Programming (AOP)</strong> shines. In this post, we’ll explore how AOP can simplify the logging, security, and caching processes in C#, making our code more maintainable and less repetitive.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/AspectOrientedProgramming/AspectOrientedProgrammingPostSharp" class="btn btn--primary">GitHub Repo</a></p>

<h3 id="what-is-aspect-oriented-programming-aop">What is Aspect-Oriented Programming (AOP)?</h3>

<p>Aspect-Oriented Programming (AOP) is a programming paradigm that helps manage cross-cutting concerns—functionality that spans multiple points of an application but is not related to the business logic itself. Examples of cross-cutting concerns include:</p>
<ul>
  <li>Logging</li>
  <li>Security</li>
  <li>Caching</li>
  <li>Exception handling</li>
</ul>

<p>AOP allows you to encapsulate these concerns in separate modules called <strong>aspects</strong>, so you don’t need to repeat them in every function or class. This separation makes code cleaner, as business logic remains unpolluted by repetitive code for logging, security, etc.</p>

<p>In C#, AOP is commonly implemented through frameworks like <strong>PostSharp</strong> or <strong>Castle DynamicProxy</strong>. These frameworks allow you to inject additional behavior (such as logging) before or after method execution, without modifying the method’s core logic.</p>

<h2 id="logging-cross-cutting-concern">Logging: Cross-Cutting Concern</h2>

<h3 id="general">General</h3>

<p>Logging is a textbook example of a cross-cutting concern. Whether it’s a small application or a large enterprise system, you’ll often find yourself adding logging statements across multiple methods. Manually adding <code class="language-plaintext highlighter-rouge">Console.WriteLine()</code> or calling logger objects throughout the codebase increases the risk of duplication and can become quite hard to maintain.</p>

<p>For instance, take the following code snippet, where logging is manually added:</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">PaymentService</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">ProcessPayment</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">"Processing payment..."</span><span class="p">);</span>
        <span class="c1">// Payment logic</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Payment processed successfully."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This approach works, but it gets cumbersome as the application grows. We need a better way—enter AOP.</p>

<h3 id="implementing-aop-in-c-for-logging">Implementing AOP in C# for Logging</h3>

<p>Let’s see how to implement AOP to handle logging in C# using <strong>PostSharp</strong>, a popular AOP library for .NET.</p>

<p>First, you’ll need to install the PostSharp NuGet package (I am using the free version for the demo):</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Install-Package PostSharp
</code></pre></div></div>

<p>Then, define a custom aspect for logging:</p>

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

<span class="p">[</span><span class="n">PSerializable</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">LogAspect</span> <span class="p">:</span> <span class="n">OnMethodBoundaryAspect</span>
<span class="p">{</span>
  <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnEntry</span><span class="p">(</span><span class="n">MethodExecutionArgs</span> <span class="n">args</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">$"Starting method </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s"> with arguments: </span><span class="p">{</span><span class="kt">string</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="s">", "</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">Arguments</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="k">void</span> <span class="nf">OnExit</span><span class="p">(</span><span class="n">MethodExecutionArgs</span> <span class="n">args</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">$"Completed method </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</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="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnException</span><span class="p">(</span><span class="n">MethodExecutionArgs</span> <span class="n">args</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">$"Exception in method </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</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">args</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="s">"</span><span class="p">);</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This <code class="language-plaintext highlighter-rouge">LogAspect</code> class uses the <code class="language-plaintext highlighter-rouge">OnMethodBoundaryAspect</code> from PostSharp, allowing us to inject code before (<code class="language-plaintext highlighter-rouge">OnEntry</code>) and after (<code class="language-plaintext highlighter-rouge">OnExit</code>) method execution. It also logs any exceptions encountered during execution.</p>

<p>Now that we have our logging aspect, let’s apply it to the <code class="language-plaintext highlighter-rouge">OrderService</code> and <code class="language-plaintext highlighter-rouge">PaymentService</code> methods:</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="p">[</span><span class="n">LogAspect</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">CreateOrder</span><span class="p">(</span><span class="kt">int</span> <span class="n">orderId</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Business logic for order creation</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">LogAspect</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">CancelOrder</span><span class="p">(</span><span class="kt">int</span> <span class="n">orderId</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Business logic for order cancellation</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="k">public</span> <span class="k">class</span> <span class="nc">PaymentService</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">LogAspect</span><span class="p">]</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">ProcessPayment</span><span class="p">(</span><span class="kt">int</span> <span class="n">paymentId</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Business logic for payment processing</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>When the <code class="language-plaintext highlighter-rouge">ProcessPayment</code> method is called, the logging behavior is automatically injected.</p>

<h4 id="example">Example</h4>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PaymentService</span> <span class="n">paymentService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">PaymentService</span><span class="p">();</span>
<span class="n">paymentService</span><span class="p">.</span><span class="nf">ProcessPayment</span><span class="p">();</span>
</code></pre></div></div>

<p>Output:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Starting method ProcessPayment with arguments: 501
Completed method ProcessPayment
</code></pre></div></div>

<p>The logging logic is now centralized in the <code class="language-plaintext highlighter-rouge">LogAspect</code>, keeping the actual business logic in <code class="language-plaintext highlighter-rouge">OrderService</code> and <code class="language-plaintext highlighter-rouge">PaymentService</code> clean and uncluttered.</p>

<h2 id="security-authentication-and-authorization">Security: Authentication and Authorization</h2>

<p>One of the most common uses for AOP, apart from logging, is security—specifically, adding authentication and authorization checks to methods. Instead of manually verifying whether a user has the right permissions to access each method, AOP can handle this automatically.</p>

<h3 id="implementing-aop-in-c-for-security">Implementing AOP in C# for Security</h3>

<p>Let’s define an aspect that checks if the user is authenticated and authorized to execute a method. We’ll simulate this with a <code class="language-plaintext highlighter-rouge">User</code> class and a <code class="language-plaintext highlighter-rouge">SecurityAspect</code>.</p>

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

<span class="k">public</span> <span class="k">class</span> <span class="nc">User</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Username</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">Role</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">static</span> <span class="k">class</span> <span class="nc">SecurityContext</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">User</span> <span class="n">CurrentUser</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">PSerializable</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">SecurityAspect</span> <span class="p">:</span> <span class="n">OnMethodBoundaryAspect</span>
<span class="p">{</span>
  <span class="p">[</span><span class="n">PNonSerialized</span><span class="p">]</span>
  <span class="k">private</span> <span class="k">readonly</span> <span class="kt">string</span> <span class="n">_requiredRole</span><span class="p">;</span>

  <span class="k">public</span> <span class="nf">SecurityAspect</span><span class="p">(</span><span class="kt">string</span> <span class="n">requiredRole</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="n">_requiredRole</span> <span class="p">=</span> <span class="n">requiredRole</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnEntry</span><span class="p">(</span><span class="n">MethodExecutionArgs</span> <span class="n">args</span><span class="p">)</span>
  <span class="p">{</span>
    <span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="n">SecurityContext</span><span class="p">.</span><span class="n">CurrentUser</span><span class="p">;</span>
    
    <span class="k">if</span> <span class="p">(</span><span class="n">user</span> <span class="p">==</span> <span class="k">null</span><span class="p">)</span>
    <span class="p">{</span>
      <span class="k">throw</span> <span class="k">new</span> <span class="nf">UnauthorizedAccessException</span><span class="p">(</span><span class="s">"User is not authenticated."</span><span class="p">);</span>
    <span class="p">}</span>

    <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">Role</span> <span class="p">!=</span> <span class="n">_requiredRole</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">$"User </span><span class="p">{</span><span class="n">user</span><span class="p">.</span><span class="n">Username</span><span class="p">}</span><span class="s"> does not have permission to access this method"</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">$"User </span><span class="p">{</span><span class="n">user</span><span class="p">.</span><span class="n">Username</span><span class="p">}</span><span class="s"> is authorized to access </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</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>

<p>This <code class="language-plaintext highlighter-rouge">SecurityAspect</code> will throw an exception if the user is not authenticated or does not have the required role. Now, let’s apply the above aspect to the <code class="language-plaintext highlighter-rouge">OrderService</code> and <code class="language-plaintext highlighter-rouge">CancelOrder</code> method in the following use case.</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="p">[</span><span class="nf">SecurityAspect</span><span class="p">(</span><span class="s">"Admin"</span><span class="p">)]</span>
    <span class="k">public</span> <span class="k">void</span> <span class="nf">CancelOrder</span><span class="p">(</span><span class="kt">int</span> <span class="n">orderId</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Business logic for order cancellation</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</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"> cancelled successfully."</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="example-1">Example</h4>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SecurityContext</span><span class="p">.</span><span class="n">CurrentUser</span> <span class="p">=</span> <span class="k">new</span> <span class="n">User</span> <span class="p">{</span> <span class="n">Username</span> <span class="p">=</span> <span class="s">"JohnDoe"</span><span class="p">,</span> <span class="n">Role</span> <span class="p">=</span> <span class="s">"User"</span> <span class="p">};</span>
<span class="n">OrderService</span> <span class="n">orderService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">OrderService</span><span class="p">();</span>

<span class="k">try</span>
<span class="p">{</span>
  <span class="n">orderService</span><span class="p">.</span><span class="nf">CancelOrder</span><span class="p">(</span><span class="m">101</span><span class="p">);</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">UnauthorizedAccessException</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="n">ex</span><span class="p">.</span><span class="n">Message</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Output:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User JohnDoe does not have permission to access this method.
</code></pre></div></div>

<p>Change the role to <code class="language-plaintext highlighter-rouge">Admin</code> and you’ll see the user is authorized:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>User JaneAdmin is authorized to access CancelOrder.
Order 101 cancelled successfully.
</code></pre></div></div>

<h2 id="caching-performance-optimization">Caching: Performance Optimization</h2>

<p>AOP can also be used for caching, improving performance by storing method results and returning them from a cache for subsequent requests with the same inputs.</p>

<h3 id="implementing-aop-in-c-for-caching">Implementing AOP in C# for Caching</h3>

<p>Let’s define an aspect that checks if the key is cached then return the valur from the cache else get the same.</p>

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

<span class="p">[</span><span class="n">PSerializable</span><span class="p">]</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">CachingAspect</span> <span class="p">:</span> <span class="n">MethodInterceptionAspect</span>
<span class="p">{</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="kt">object</span><span class="p">&gt;</span> <span class="n">Cache</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">object</span><span class="p">&gt;();</span>

  <span class="k">public</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnInvoke</span><span class="p">(</span><span class="n">MethodInterceptionArgs</span> <span class="n">args</span><span class="p">)</span>
  <span class="p">{</span>
      <span class="kt">string</span> <span class="n">key</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</span><span class="p">.</span><span class="n">Name</span><span class="p">}</span><span class="s">_</span><span class="p">{</span><span class="kt">string</span><span class="p">.</span><span class="nf">Join</span><span class="p">(</span><span class="s">"_"</span><span class="p">,</span> <span class="n">args</span><span class="p">.</span><span class="n">Arguments</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">ContainsKey</span><span class="p">(</span><span class="n">key</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">$"Returning cached result for </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</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">args</span><span class="p">.</span><span class="n">ReturnValue</span> <span class="p">=</span> <span class="n">Cache</span><span class="p">[</span><span class="n">key</span><span class="p">];</span>
      <span class="p">}</span>
      <span class="k">else</span>
      <span class="p">{</span>
          <span class="k">base</span><span class="p">.</span><span class="nf">OnInvoke</span><span class="p">(</span><span class="n">args</span><span class="p">);</span>
          <span class="n">Cache</span><span class="p">[</span><span class="n">key</span><span class="p">]</span> <span class="p">=</span> <span class="n">args</span><span class="p">.</span><span class="n">ReturnValue</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">$"Caching result for </span><span class="p">{</span><span class="n">args</span><span class="p">.</span><span class="n">Method</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>
<span class="p">}</span>
</code></pre></div></div>

<p>Further, let’s define a <code class="language-plaintext highlighter-rouge">ProductService</code> showing this cache in action:</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">ProductService</span>
<span class="p">{</span>
    <span class="p">[</span><span class="n">CachingAspect</span><span class="p">]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="nf">GetProductDetails</span><span class="p">(</span><span class="kt">int</span> <span class="n">productId</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="c1">// Simulate a slow operation</span>
        <span class="n">Console</span><span class="p">.</span><span class="nf">WriteLine</span><span class="p">(</span><span class="s">"Fetching product details from database..."</span><span class="p">);</span>
        <span class="n">System</span><span class="p">.</span><span class="n">Threading</span><span class="p">.</span><span class="n">Thread</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="m">2000</span><span class="p">);</span>
        <span class="k">return</span> <span class="s">$"Product </span><span class="p">{</span><span class="n">productId</span><span class="p">}</span><span class="s"> details"</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h4 id="example-2">Example</h4>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ProductService</span> <span class="n">productService</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">ProductService</span><span class="p">();</span>

<span class="kt">string</span> <span class="n">result1</span> <span class="p">=</span> <span class="n">productService</span><span class="p">.</span><span class="nf">GetProductDetails</span><span class="p">(</span><span class="m">101</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">result1</span><span class="p">);</span>

<span class="kt">string</span> <span class="n">result2</span> <span class="p">=</span> <span class="n">productService</span><span class="p">.</span><span class="nf">GetProductDetails</span><span class="p">(</span><span class="m">101</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">result2</span><span class="p">);</span>
</code></pre></div></div>

<p>Output:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Fetching product details from database...
Caching result <span class="k">for </span>GetProductDetails
Product 101 details
Returning cached result <span class="k">for </span>GetProductDetails
Product 101 details
</code></pre></div></div>

<h2 id="cross-cutting-done-right">Cross-Cutting Done Right</h2>

<p>Aspect-Oriented Programming is a powerful way to manage cross-cutting concerns like logging, security, and caching in C#. Using frameworks like PostSharp, developers can centralize and automate repetitive tasks, keeping business logic clean and easier to maintain. By implementing AOP for logging, security checks, and caching, you can improve your code’s maintainability, scalability, and performance.</p>

<hr />

<h2 id="see-also">See Also</h2>

<ul>
  <li><a href="/technical/.net/architecture/clean-architecture/">Clean Architecture with MediatR</a></li>
  <li><a href="/technical/.net/.net-core/di-multiple-implementations-of-same-interface/">Multiple implementations of the same interface</a></li>
  <li><a href="/technical/.net/.net-core/source-generators-csharp/">C# source generators</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term="Aspect-Oriented Programming" /><category term="Logging" /><category term="Security" /><category term="Caching" /><category term="PostSharp" /><category term="Cross-Cutting Concerns" /><category term="Software Development" /><category term="Code Optimization" /><category term="Best Practices" /><summary type="html"><![CDATA[Implement cross-cutting concerns in C# using Aspect-Oriented Programming with PostSharp to centralize logging, caching, and security without code duplication.]]></summary></entry><entry><title type="html">Result Pattern in C#: Replace Exceptions with Result for Cleaner Error Handling</title><link href="https://animatlabs.com/technical/.net/.net-core/better-result-handling-with-result-object/" rel="alternate" type="text/html" title="Result Pattern in C#: Replace Exceptions with Result for Cleaner Error Handling" /><published>2024-03-23T00:00:00+05:30</published><updated>2024-03-23T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/better-result-handling-with-result-object</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/better-result-handling-with-result-object/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Exceptions should be rare…<strong>Why?</strong>…Throwing and catching exceptions <strong>is slow</strong> relative to other code flow patterns. Because of this, exceptions shouldn’t be used to control normal program flow.<strong>Also</strong>…Code that relies heavily on exceptions for control flow can become <strong>difficult to read and maintain</strong> . It can be challenging to follow the logic of a program that jumps from one exception handler to another, as opposed to one that follows a simpler, linear flow.</p>

<p><strong>Also</strong>…Improperly handled exceptions can lead to resource leaks. Exceptions are designed to <strong>handle unexpected and rare events</strong> . Using them for regular control flow, like handling business logic or validations, is generally considered a bad practice because it misrepresents the intention of the exception mechanism.</p>

<p>Even Microsoft has a recommendation:</p>

<ul>
  <li>Do not use throwing or catching exceptions as a means of normal program flow, especially in hot code paths. </li>
  <li>Do include logic in the app to detect and handle conditions that would cause an exception. </li>
  <li>Do throw or catch exceptions for unusual or unexpected conditions.</li>
</ul>

<p>In this article, we explain how you can minimize using exceptions, and change them for the **Result<T>** object in the normal flow of the application.</T></p>

<h2 id="result-error-handling">Result Error Handling</h2>

<h3 id="handling-errors-with-regular-exceptions">Handling errors with regular Exceptions</h3>

<p>Let’s consider a common business logic scenario: validating user input for a registration form. Initially, let’s see how this might be done using exceptions:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">UserRegistration</span>
<span class="p">{</span>
<span class="err">    </span><span class="k">public</span> <span class="k">void</span> <span class="nf">RegisterUser</span><span class="p">(</span><span class="kt">string</span> <span class="n">username</span><span class="p">,</span> <span class="kt">string</span> <span class="n">password</span><span class="p">)</span>
<span class="err">    </span><span class="p">{</span>
<span class="err">        </span><span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">username</span><span class="p">)</span> <span class="p">||</span> <span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">password</span><span class="p">))</span>
<span class="err">        </span><span class="p">{</span>
<span class="err">            </span><span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentException</span><span class="p">(</span><span class="s">"Username and password are required."</span><span class="p">);</span>
<span class="err">        </span><span class="p">}</span>

<span class="err">        </span><span class="k">if</span> <span class="p">(</span><span class="n">password</span><span class="p">.</span><span class="n">Length</span> <span class="p">&amp;</span><span class="n">lt</span><span class="p">;</span> <span class="m">8</span><span class="p">)</span>
<span class="err">        </span><span class="p">{</span>
<span class="err">            </span><span class="k">throw</span> <span class="k">new</span> <span class="nf">ArgumentException</span><span class="p">(</span><span class="s">"Password must be at least 8 characters long."</span><span class="p">);</span>
<span class="err">        </span><span class="p">}</span>

<span class="err">        </span><span class="c1">// Proceed with registration</span>
<span class="err">    </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>
<p>In this approach, validation failures are treated as exceptions, which is not ideal for common scenarios like invalid input. They are best reserved for truly exceptional, unforeseen, and irregular situations.</p>

<p>For regular and predictable events like input validation, standard control flow mechanisms (like Result<T>) are more appropriate and efficient.</T></p>

<h3 id="handling-errors-with-result-object">Handling errors with Result<T> object</T></h3>

<p>By using Result, you’re ensuring that your code is handling expected scenarios (like invalid user input) in a more predictable and maintainable way, improving the overall quality and readability of your codebase.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">UserRegistration</span>
<span class="p">{</span>
<span class="err">    </span><span class="k">public</span> <span class="n">Result</span> <span class="nf">RegisterUser</span><span class="p">(</span><span class="kt">string</span> <span class="n">username</span><span class="p">,</span> <span class="kt">string</span> <span class="n">password</span><span class="p">)</span>
<span class="err">    </span><span class="p">{</span>
<span class="err">        </span><span class="k">if</span> <span class="p">(</span><span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">username</span><span class="p">)</span> <span class="p">||</span> <span class="kt">string</span><span class="p">.</span><span class="nf">IsNullOrWhiteSpace</span><span class="p">(</span><span class="n">password</span><span class="p">))</span>
<span class="err">        </span><span class="p">{</span>
<span class="err">            </span><span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="nf">Failure</span><span class="p">(</span><span class="s">"Username and password are required."</span><span class="p">);</span>
<span class="err">        </span><span class="p">}</span>

<span class="err">        </span><span class="k">if</span> <span class="p">(</span><span class="n">password</span><span class="p">.</span><span class="n">Length</span> <span class="p">&amp;</span><span class="n">lt</span><span class="p">;</span> <span class="m">8</span><span class="p">)</span>
<span class="err">        </span><span class="p">{</span>
<span class="err">            </span><span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="nf">Failure</span><span class="p">(</span><span class="s">"Password must be at least 8 characters long."</span><span class="p">);</span>
<span class="err">        </span><span class="p">}</span>

<span class="err">        </span><span class="c1">// Proceed with registration</span>
<span class="err">        </span><span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="nf">Success</span><span class="p">();</span>
<span class="err">    </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Here the flow is quite normal, we return values in relation to the code being executed instead of throwing an exception where it is not necessary. Result<T> does not come with any library (there are various libraries that already implement the Result object, like FluentResults) which means that we created it ourselves.</T></p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Result</span>
<span class="p">{</span>
<span class="err">    </span><span class="k">public</span> <span class="kt">bool</span> <span class="n">IsSuccess</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>
<span class="err">    </span><span class="k">public</span> <span class="kt">bool</span> <span class="n">IsFailure</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span> <span class="p">=&gt;</span> <span class="p">!</span><span class="n">IsSuccess</span>
<span class="err">    </span><span class="k">public</span> <span class="kt">string</span> <span class="n">ErrorMessage</span> <span class="p">{</span> <span class="k">get</span><span class="p">;</span> <span class="k">private</span> <span class="k">set</span><span class="p">;</span> <span class="p">}</span>

<span class="err">    </span><span class="k">protected</span> <span class="nf">Result</span><span class="p">(</span><span class="kt">bool</span> <span class="n">isSuccess</span><span class="p">,</span> <span class="kt">string</span> <span class="n">errorMessage</span><span class="p">)</span>
<span class="err">    </span><span class="p">{</span>
<span class="err">        </span><span class="n">IsSuccess</span> <span class="p">=</span> <span class="n">isSuccess</span><span class="p">;</span>
<span class="err">        </span><span class="n">ErrorMessage</span> <span class="p">=</span> <span class="n">errorMessage</span><span class="p">;</span>
<span class="err">    </span><span class="p">}</span>

<span class="err">    </span><span class="k">public</span> <span class="k">static</span> <span class="n">Result</span> <span class="nf">Success</span><span class="p">()</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="nf">Result</span><span class="p">(</span><span class="k">true</span><span class="p">,</span> <span class="k">null</span><span class="p">);</span>
<span class="err">    </span><span class="k">public</span> <span class="k">static</span> <span class="n">Result</span> <span class="nf">Failure</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">new</span> <span class="nf">Result</span><span class="p">(</span><span class="k">false</span><span class="p">,</span> <span class="n">message</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<ul>
  <li><strong>Success/Failure Indicator</strong> : At its core, a Result object contains a flag indicating whether the operation was successful. This is usually a boolean value.</li>
  <li><strong>Return Value</strong> : In the case of success, the Result object can hold the resulting value of the operation. For instance, if the operation was to process a file, the Result might contain the processed data.</li>
  <li><strong>Error Message or Error Object:</strong> In case of failure, the Result can hold an error message or an entire error object that provides more details about why the operation failed. This is more informative than a simple false or null return value.</li>
  <li><strong>Additional Metadata:</strong> Depending on the implementation, a Result object can also contain additional metadata about the operation, like error codes, timestamps, or diagnostic information.</li>
</ul>

<p>This is a better way to express the error. In this case, I don’t like the fact that the magic string is used for errors. Here we can create a class (or record) that will display the error as a combination of error type and error description.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="n">record</span> <span class="nf">Error</span><span class="p">(</span><span class="kt">string</span> <span class="n">Type</span><span class="p">,</span> <span class="kt">string</span> <span class="n">Description</span><span class="p">)</span>
<span class="p">{</span>
<span class="err">    </span><span class="k">public</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Error</span> <span class="n">None</span> <span class="p">=</span> <span class="k">new</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="kt">string</span><span class="p">.</span><span class="n">Empty</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>And now for each of the failed validations we can create a separate Error object that will represent a unique error for that type of validation:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">RegistrationErrors</span>
<span class="p">{</span>
<span class="err">    </span><span class="k">public</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Error</span> <span class="n">UsernameAndPasswordRequired</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Error</span><span class="p">(</span>
<span class="err">        </span><span class="s">"Registration.UsernameAndPasswordRequired"</span><span class="p">,</span> <span class="s">"Username and password are required."</span><span class="p">);</span>

<span class="err">    </span><span class="k">public</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Error</span> <span class="n">PasswordTooShort</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Error</span><span class="p">(</span>
<span class="err">        </span><span class="s">"Registration.PasswordTooShort"</span><span class="p">,</span> <span class="s">"Password must be at least 8 characters long."</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p>So, instead of an ordinary string, we can return a more structured value (this means that Error in the Result<T> object should be an Error type, and no longer a string):</T></p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>    <span class="k">return</span> <span class="n">Result</span><span class="p">.</span><span class="nf">Failure</span><span class="p">(</span><span class="n">RegistrationErrors</span><span class="p">.</span><span class="n">PasswordTooShort</span><span class="p">);</span>
</code></pre></div></div>

<p>The Result pattern clearly indicates that user input validation is a part of the normal flow and not an exceptional circumstance.</p>

<ul>
  <li><strong>Easier Error Handling:</strong> It guides the caller to handle both success and failure cases explicitly, making the code more reliable.</li>
  <li><strong>Enhanced Performance:</strong> Avoiding exceptions for regular control flow scenarios like input validation is more performance-efficient.</li>
  <li><strong>Flexibility and Extensibility:</strong> The Result pattern can easily be extended or modified to include additional details about the failure or even success scenarios, without changing the method signature.</li>
</ul>

<h2 id="exceptions-vs-results">Exceptions vs Results</h2>

<p>We should reserve exceptions for truly unforeseen events. They are best suited for situations where the error is beyond your immediate handling capabilities. For everything else, the clarity and structure offered by the Result pattern are far more beneficial.</p>

<p>Embracing the Result class in your code allows you to:</p>

<ul>
  <li>Clearly indicate that a method might not always succeed.</li>
  <li>Neatly wrap up an error occurring within your application.</li>
  <li>Offer a cleaner, functional approach to managing errors.</li>
</ul>

<p>What’s more, you can systematically catalog all the errors in your application using the Error class. This is incredibly useful, providing a clear guide on which errors to anticipate and handle.</p>

<hr />

<h2 id="related">Related</h2>

<ul>
  <li><a href="/technical/.net/architecture/clean-architecture/">Clean Architecture with MediatR</a></li>
  <li><a href="/technical/.net/.net-core/aspect-oriented-programming/">Aspect-oriented programming in C#</a></li>
  <li><a href="/technical/.net/.net-core/di-multiple-implementations-of-same-interface/">Multiple implementations of the same interface</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><summary type="html"><![CDATA[Stop using exceptions for control flow. The Result pattern in C# provides a cleaner, more maintainable approach to error handling in .NET applications.]]></summary></entry><entry><title type="html">3 C# String Operations Every .NET Developer Should Know for Better Performance</title><link href="https://animatlabs.com/technical/.net/.net-core/three-string-operations-should-know/" rel="alternate" type="text/html" title="3 C# String Operations Every .NET Developer Should Know for Better Performance" /><published>2024-02-08T00:00:00+05:30</published><updated>2024-02-08T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/three-string-operations-should-know</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/three-string-operations-should-know/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>We work with strings every day in our applications. We often don’t see the mistakes we’re making, or we don’t see ways to potentially optimize the code. And there are many of them. Today I’m going to show you 3 things you should know about working with strings as a .NET Developer. Those are:</p>

<ol>
  <li>Use <strong>StringBuilder</strong> for concatenation</li>
  <li>Use <strong>StringComparison</strong> for performance</li>
  <li>Use <strong>Span</strong> for memory efficiency</li>
</ol>

<h2 id="processing-strings">Processing Strings</h2>

<h3 id="use-stringbuilder-for-concatenation">Use StringBuilder for concatenation</h3>

<p>In .NET, strings are immutable. This means once a string object is created, it cannot be modified. If you need to change a string by appending another string to it, .NET doesn’t actually append the new string to the existing string. Instead, it creates a new string object that contains the combination of the two strings and then discards the old string. This behavior is efficient and safe for small or a few manipulations but becomes a performance and memory issue when done repeatedly, such as in a loop.</p>

<p><strong>StringBuilder</strong> is a dynamic object that allows you to expand the number of characters in the string it contains without creating a new object for every concatenation. Under the hood, StringBuilder maintains an array of characters. When you append a string to a StringBuilder instance, it simply copies the added characters to the end of the internal array. If the array runs out of space, StringBuilder automatically allocates a new, larger array and copies the characters into it.</p>

<p>This happens far less frequently than string immutability would force, making StringBuilder much more efficient for concatenation operations, particularly in loops.</p>

<pre><code class="language-C#">// Using string concatenation
string result = "";
for (int i = 0; i &lt; 1000; i++)
{
    result += "a"; // Creates a new string object in each iteration
}

// Using StringBuilder
var builder = new StringBuilder();
for (int i = 0; i &lt; 1000; i++)
{
    builder.Append("a"); // Appends to the existing character array
}
string result = builder.ToString(); // Converts to string once at the end
</code></pre>

<p>And if we check the performance, the performance improves by a stooping ~19X-20X both in terms of memory and ~40x-45X in terms or time. StringBuilder is essential for optimizing memory usage and improving performance in applications that perform extensive string manipulation.</p>

<h3 id="use-stringcomparison-for-performance">Use StringComparison for performance</h3>

<p>.NET provides several ways to compare strings, including simple equality checks (==), string.Equals, string.Compare, and methods like string.StartsWith or string.Contains. Each of these methods can optionally take a <strong>StringComparison</strong> enumeration as a parameter, which specifies <strong>how the comparison should be conducted</strong>. The StringComparison options include:</p>

<ul>
  <li><strong>Ordinal comparisons (Ordinal, OrdinalIgnoreCase):</strong> These comparisons are based on the binary values of the characters in the strings and are the fastest type of comparison. They are culture-insensitive, making them ideal for comparing strings for internal processing, file paths, machine-readable strings (like XML tags), and when performance is crucial.</li>
  <li><strong>Culture-sensitive comparisons (CurrentCulture, CurrentCultureIgnoreCase, InvariantCulture, InvariantCultureIgnoreCase):</strong> These comparisons consider the cultural context of the strings, which is essential when comparing strings that are displayed to the user or when the comparison results depend on specific cultural rules (like sorting in a user interface).</li>
  <li>Ordinal comparisons are faster than culture-sensitive comparisons because they directly compare the numeric Unicode value of each character in the strings.</li>
  <li>There’s no need to apply cultural rules, which can vary widely and involve complex logic like handling special characters, accent marks, or case conversions based on specific cultures.</li>
</ul>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">string</span> <span class="n">string1</span> <span class="p">=</span> <span class="s">"hello world"</span><span class="p">;</span>
<span class="kt">string</span> <span class="n">string2</span> <span class="p">=</span> <span class="s">"Hello World"</span><span class="p">;</span>

<span class="kt">bool</span> <span class="n">areEqual</span> <span class="p">=</span> <span class="kt">string</span><span class="p">.</span><span class="nf">Equals</span><span class="p">(</span><span class="n">string1</span><span class="p">,</span> <span class="n">string2</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="c1">// areEqual is true because the comparison is case-insensitive.</span>
</code></pre></div></div>

<h3 id="use-span-for-memory-efficiency">Use Span for memory efficiency</h3>

<p>Span is a stack-allocated type that can point to continuous memory regions representing slices of arrays, strings, or unmanaged memory. It provides the ability to work with a slice of data without allocating new memory for that slice.
This is particularly useful for strings because, as previously mentioned, strings are immutable in .NET. Key Advantages of Span:</p>

<ul>
  <li><strong>Reduced Allocations:</strong> Since Span can reference a portion of an array or string, it eliminates the need for creating new substrings or array segments when you only need to work with part of the data. This can significantly reduce the number of allocations, thereby reducing the Garbage Collector (GC) pressure and improving application performance.</li>
  <li><strong>Memory Efficiency:</strong> Span enables more efficient memory usage by allowing operations on slices of data without duplicating the underlying data structures. This is particularly beneficial in performance-critical applications, such as parsers or processing pipelines, where it’s common to only need to read or manipulate small portions of a larger data set at any one time.</li>
  <li><strong>Versatility:</strong> Span can be used with any type of contiguous memory, not just arrays or strings. This includes unmanaged memory, which opens up possibilities for high-performance scenarios that were previously more cumbersome or inefficient in .NET.</li>
</ul>

<p>Let’s compare it with a basic Substring mehod: This opens the JSON configuration where you can define your Rate Limiting Policy in detail.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">SpanVsSubstring</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">const</span> <span class="kt">string</span> <span class="n">testString</span> <span class="p">=</span> <span class="s">"This is a longer test string for demonstration."</span><span class="p">;</span>

    <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="nf">UseSubstring</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="n">testString</span><span class="p">.</span><span class="nf">Substring</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">5</span><span class="p">);</span> <span class="c1">// Extracts "longer"</span>
    <span class="p">}</span>

    <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
    <span class="k">public</span> <span class="n">ReadOnlySpan</span><span class="p">&lt;</span><span class="kt">char</span><span class="p">&gt;</span> <span class="nf">UseSpan</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">ReadOnlySpan</span><span class="p">&lt;</span><span class="kt">char</span><span class="p">&gt;</span> <span class="n">span</span> <span class="p">=</span> <span class="n">testString</span><span class="p">.</span><span class="nf">AsSpan</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">5</span><span class="p">);</span>
        <span class="k">return</span> <span class="n">span</span><span class="p">;</span> <span class="c1">// "longer"</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>The performance for both the versions varies by far using substring takes ~6ns whereas with span it takes ~0.01ns and there is no memory usage at all from ~32bytes used in substring.</p>

<h2 id="small-changes-big-impact">Small Changes, Big Impact</h2>

<p>Incorporating these techniques into your .NET applications can significantly improve string handling performance, both in terms of speed and memory efficiency. Always test these approaches in the context of your specific application to measure their impact.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://stefandjokic.tech/posts/3-things-you-should-know-about-strings">Stefan’s Blog talking about the same</a></li>
</ul>

<hr />

<h2 id="more-on-this-topic">More on This Topic</h2>

<ul>
  <li><a href="/technical/.net/.net-core/interesting-enhancements-cs12/">C# 12 highlights</a></li>
  <li><a href="/technical/.net/.net-core/improve-iteration-performance/">Collection iteration performance</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="Strings" /><summary type="html"><![CDATA[Three essential string handling techniques in C# that improve performance and avoid common mistakes in .NET applications.]]></summary></entry><entry><title type="html">C# 12 New Features: Primary Constructors, Collection Expressions, and More in .NET 8</title><link href="https://animatlabs.com/technical/.net/.net-core/interesting-enhancements-cs12/" rel="alternate" type="text/html" title="C# 12 New Features: Primary Constructors, Collection Expressions, and More in .NET 8" /><published>2024-01-12T00:00:00+05:30</published><updated>2024-01-12T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/interesting-enhancements-cs12</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/interesting-enhancements-cs12/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>In November 2023, C# 12 arrived with a bang, bundled with .NET 8, bringing with it a bunch of cool new features that have made developers sit up and take notice. I’ll be breaking down each feature with real coding examples to show how they can make your coding life better and more efficient.</p>

<h2 id="enhancements">Enhancements</h2>

<h3 id="collection-expressions">Collection Expressions</h3>

<p>C# 12 brings a fresh take on handling collections like arrays, lists, and spans, making the syntax cleaner and more intuitive. The introduction of collection expressions eliminates the need for the ‘new’ operator and specifying the type, allowing you to simply list your items within square brackets.</p>

<p>Even better, the new spread operator “..” makes combining collections smooth, enhancing code readability and reducing clutter.</p>

<pre><code class="language-C#">//Before
var integers = new int[] { 1,2,3,4,5 };
var list = new List&lt;int&gt;() { 1,2,3,4,5 };
var fruits = new List&lt;string&gt;() {"apple", "banana", "cherry"};

//After
int[] integers = [ 1,2,3,4,5 ];
List&lt;int&gt; list = [ 1,2,3,4,5 ];
List&lt;string&gt; fruits = ["apple", "banana", "cherry"];
</code></pre>

<p>This feature boosts coding efficiency by cutting down on boilerplate and potential errors, while also making your code easier to read and maintain. With these changes, working with collections in C# has become simpler, allowing for more expressive and flexible coding.</p>

<h3 id="primary-constructors">Primary Constructors</h3>

<p>C# 12 introduces a simpler approach to class and struct construction with the advent of primary constructors, significantly reducing the verbosity traditionally associated with object initialization.</p>

<p>This new feature <strong>allows constructors to be declared directly within the type’s declaration line</strong>, making it applicable to classes, structs, record classes, and record structs. It’s particularly effective for initializing fields or properties directly with constructor parameters, thereby making dependency injection simpler.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">User</span><span class="p">(</span><span class="kt">string</span> <span class="n">firstName</span><span class="p">,</span> <span class="kt">string</span> <span class="n">lastName</span><span class="p">,</span> <span class="kt">int</span> <span class="n">age</span><span class="p">,</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Role</span><span class="p">&gt;</span> <span class="n">roles</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">FirstName</span> <span class="p">=&gt;</span> <span class="n">firstName</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">LastName</span> <span class="p">=&gt;</span> <span class="n">lastName</span><span class="p">;</span>
    <span class="k">public</span> <span class="kt">int</span> <span class="n">Age</span> <span class="p">=&gt;</span> <span class="n">age</span><span class="p">;</span>
    <span class="k">public</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Role</span><span class="p">&gt;</span> <span class="p">=&gt;</span> <span class="n">roles</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>What makes primary constructors stand out:</p>

<ul>
  <li><strong>Conciseness:</strong> By integrating constructors into the type declaration, C# 12 eliminates the need for separate, often repetitive, constructor definitions. This not only simplifies the code but also enhances its readability.</li>
  <li><strong>Accessibility:</strong> Having the constructor logic within the class or struct definition itself makes it easier to understand and maintain the code, as it centralizes the logic for object creation and initialization.</li>
  <li><strong>Readability:</strong> The code more clearly communicates the structure of an object and its initialization needs, making it easier for developers to grasp the essentials at a glance.</li>
</ul>

<p>In essence, primary constructors cut down on the boilerplate code associated with setting up classes and structs, making code more concise, accessible, and readable, and thus simplifying the development process.</p>

<h3 id="inline-arrays">Inline Arrays</h3>

<p>C# 12 introduces inline arrays, a feature that enhances array usage by allowing fixed-size arrays to be declared within structs. This means arrays can now be allocated on the stack, boosting performance by reducing heap allocations and copying. Inline arrays can be initialized directly within expressions, making code more concise and memory-efficient.</p>

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

<span class="p">[</span><span class="nf">InlineArray</span><span class="p">(</span><span class="m">5</span><span class="p">)]</span>
<span class="k">public</span> <span class="k">struct</span> <span class="nc">FixedArray</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="kt">int</span> <span class="n">_element</span><span class="p">;</span>
    <span class="c1">// Usage</span>
    <span class="kt">var</span> <span class="n">buffer</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">FixedSizeBuffer</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="m">5</span><span class="p">;</span> <span class="n">i</span><span class="p">++)</span> <span class="n">buffer</span><span class="p">[</span><span class="n">i</span><span class="p">]</span> <span class="p">=</span> <span class="n">i</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This leads to:</p>

<ul>
  <li><strong>Memory Efficiency:</strong> Reduces overhead by avoiding unnecessary allocations.</li>
  <li><strong>Conciseness:</strong> Allows for direct array initialization in expressions, streamlining code.</li>
  <li><strong>Readability:</strong> Improves clarity by cutting out temporary variables.</li>
</ul>

<p>While inline arrays offer notable benefits like enhanced memory efficiency, their application may not always extend to replacing traditional arrays in everyday use.</p>

<h3 id="alias-any-type-with-using">Alias Any Type with ‘using’</h3>

<p>C# 12 introduces an enhancement to type aliasing, expanding its capabilities beyond named types like classes or structs, which were previously the only types that could be aliased. Now, it’s possible to alias any type, including tuples, arrays, and generics. This development simplifies code by reducing verbosity, thereby improving both readability and maintainability.</p>

<pre><code class="language-C#">// possible only with C# 12
using Point = (int x, int y);
Point origin = (0, 0);
    
Console.WriteLine(origin);
</code></pre>

<p>By broadening the scope of the using directive to encompass complex types, C# 12 makes it easier to work with intricate data structures while keeping the codebase clean and understandable.</p>

<h3 id="default-lambda-parameters">Default Lambda Parameters</h3>

<p>Prior to C# 12, incorporating default parameters into your code was restricted to traditional functions.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="nf">WelcomeUser</span><span class="p">(</span><span class="kt">string</span> <span class="n">username</span> <span class="p">=</span> <span class="s">"Guest"</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">$"Welcome, </span><span class="p">{</span><span class="n">username</span><span class="p">}</span><span class="s">!"</span><span class="p">);</span>
<span class="p">}</span>

<span class="nf">WelcomeUser</span><span class="p">();</span>
</code></pre></div></div>

<p>However, the advent of C# 12 revolutionizes this by extending the capability to lambda expressions. This new feature, known as Default Lambda Parameters, introduces the ability to specify default values for lambda parameters, thereby enhancing flexibility and streamlining execution.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">WelcomeUser</span> <span class="p">=</span> <span class="p">(</span><span class="kt">string</span> <span class="n">username</span> <span class="p">=</span> <span class="s">"Guest"</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">$"Welcome, </span><span class="p">{</span><span class="n">username</span><span class="p">}</span><span class="s">!"</span><span class="p">);</span>

<span class="nf">WelcomeUser</span><span class="p">();</span>
</code></pre></div></div>

<p>Now, lambda expressions can be executed without the mandatory need to specify every parameter, marking a significant leap in coding efficiency and expression conciseness.</p>

<h2 id="c-keeps-getting-better">C# Keeps Getting Better</h2>

<p>C# 12 brings a bunch of cool new updates that make coding easier, faster, and lets you try new ways of programming. But, not everything is ready for prime time right out of the gate. Some features are still being tested, so you might want to use them carefully.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://learn.microsoft.com/en-us/dotnet/csharp/whats-new/csharp-12">Official C# documentation</a></li>
  <li><a href="https://stefandjokic.tech/posts/5-new-cool-features-in-csharp?utm_source=emailoctopus&amp;utm_medium=email&amp;utm_campaign=%2361%20Stefan%27s%20Newsletter%20-%203%20things%20you%20should%20know%20about%20Strings">Stefan’s Blog talking about the same</a></li>
</ul>

<hr />

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/technical/.net/.net-core/three-string-operations-should-know/">Three string operations worth knowing</a></li>
  <li><a href="/technical/.net/.net-core/source-generators-csharp/">C# source generators</a></li>
  <li><a href="/technical/.net/.net-core/better-result-handling-with-result-object/">Result pattern in C#</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="C#12" /><summary type="html"><![CDATA[Explore the most impactful C# 12 features released with .NET 8 including primary constructors, collection expressions, alias any type, and default lambda parameters.]]></summary></entry><entry><title type="html">C# Object Mapping Techniques: AutoMapper vs Mapster vs Implicit Operators vs Manual</title><link href="https://animatlabs.com/technical/.net/.net-core/native-mapping-operations/" rel="alternate" type="text/html" title="C# Object Mapping Techniques: AutoMapper vs Mapster vs Implicit Operators vs Manual" /><published>2023-12-18T00:00:00+05:30</published><updated>2023-12-18T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/native-mapping-operations</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/native-mapping-operations/"><![CDATA[<h2 id="introduction">Introduction</h2>

<p>Object mapping in C# is a fundamental task in many applications, particularly when dealing with layered architectures or separate data models. While seemingly simple, the choice of mapping strategy can significantly impact the maintainability, performance, and complexity of your code. In this post, we’ll look at four popular mapping techniques: AutoMapper, Mapster, Implicit Operators, and Manual Mapping, comparing their features, use-cases, and providing examples.</p>

<h2 id="automapper-the-veteran-mapper">AutoMapper: The Veteran Mapper</h2>

<p>AutoMapper is a staple in the .NET world for object-to-object mapping. It relies on conventions to automatically map properties from one object to another.</p>

<h3 id="pros">Pros</h3>

<ul>
  <li><strong>Ease of Use:</strong> Simple setup and configuration process.</li>
  <li><strong>Flexibility:</strong> Handles complex nested mappings, custom mappings, and more.</li>
  <li><strong>Community Support:</strong> Extensive documentation and community support.</li>
</ul>

<h3 id="cons">Cons</h3>

<ul>
  <li><strong>Performance:</strong> Can be slower for complex mappings due to its overhead.</li>
  <li><strong>Overhead:</strong> May introduce unnecessary complexity for simple mappings.</li>
</ul>

<h3 id="usage-scenario">Usage Scenario</h3>

<p>Ideal for applications with numerous and complex DTOs, where writing mapping code manually would be impractical.</p>

<h3 id="example">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// AutoMapper Configuration</span>
<span class="kt">var</span> <span class="n">config</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MapperConfiguration</span><span class="p">(</span><span class="n">cfg</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="n">cfg</span><span class="p">.</span><span class="n">CreateMap</span><span class="p">&lt;</span><span class="n">Address</span><span class="p">,</span> <span class="n">AddressDto</span><span class="p">&gt;();</span>
    <span class="n">cfg</span><span class="p">.</span><span class="n">CreateMap</span><span class="p">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">UserDto</span><span class="p">&gt;();</span>
<span class="p">});</span>
<span class="kt">var</span> <span class="n">mapper</span> <span class="p">=</span> <span class="n">config</span><span class="p">.</span><span class="nf">CreateMapper</span><span class="p">();</span>

<span class="c1">// Mapping</span>
<span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="k">new</span> <span class="n">User</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">"John Doe"</span><span class="p">,</span> 
    <span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Address</span> <span class="p">{</span> <span class="n">Street</span> <span class="p">=</span> <span class="s">"123 Main St"</span><span class="p">,</span> <span class="n">City</span> <span class="p">=</span> <span class="s">"Anytown"</span> <span class="p">}</span>
<span class="p">};</span>
<span class="kt">var</span> <span class="n">userDto</span> <span class="p">=</span> <span class="n">mapper</span><span class="p">.</span><span class="n">Map</span><span class="p">&lt;</span><span class="n">UserDto</span><span class="p">&gt;(</span><span class="n">user</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="mapster-the-rising-star">Mapster: The Rising Star</h2>

<p>Mapster is a newer, performance-oriented mapping library. It’s gaining traction for its speed and simple approach.</p>

<h3 id="pros-1">Pros</h3>

<ul>
  <li><strong>Performance:</strong> Generally faster than AutoMapper, especially noticeable in high-load scenarios.</li>
  <li><strong>Ease of Learning:</strong> Simpler and more intuitive API.</li>
  <li><strong>Adaptability:</strong> Provides both dynamic and static mapping capabilities.</li>
</ul>

<h3 id="cons-1">Cons</h3>

<ul>
  <li><strong>Community and Resources:</strong> Smaller community and fewer resources compared to AutoMapper.</li>
  <li><strong>Maturity:</strong> Being relatively new, might lack some advanced features.</li>
</ul>

<h3 id="usage-scenario-1">Usage Scenario</h3>

<p>Perfect for projects where performance is a priority but still requires flexibility for complex mappings.</p>

<h3 id="example-1">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Mapster Configuration</span>
<span class="n">TypeAdapterConfig</span><span class="p">&lt;</span><span class="n">User</span><span class="p">,</span> <span class="n">UserDto</span><span class="p">&gt;.</span><span class="nf">NewConfig</span><span class="p">()</span>
    <span class="p">.</span><span class="nf">Map</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Address</span><span class="p">,</span> <span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">Address</span><span class="p">);</span>

<span class="c1">// Mapster Usage</span>
<span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="k">new</span> <span class="n">User</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">"John Doe"</span><span class="p">,</span> 
    <span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Address</span> <span class="p">{</span> <span class="n">Street</span> <span class="p">=</span> <span class="s">"123 Main St"</span><span class="p">,</span> <span class="n">City</span> <span class="p">=</span> <span class="s">"Anytown"</span> <span class="p">}</span>
<span class="p">};</span>
<span class="kt">var</span> <span class="n">userDto</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Adapt</span><span class="p">&lt;</span><span class="n">UserDto</span><span class="p">&gt;();</span>
</code></pre></div></div>

<h2 id="implicit-operators-the-c-native">Implicit Operators: The C# Native</h2>

<p>C#’s implicit operators allow custom type conversions, which can be used for mapping.</p>

<h3 id="pros-2">Pros</h3>

<ul>
  <li><strong>Performance:</strong> Excellent performance as it’s natively supported.</li>
  <li><strong>Control:</strong> Full control over the conversion logic.</li>
</ul>

<h3 id="cons-2">Cons</h3>

<ul>
  <li><strong>Complexity Management:</strong> Can get complex and hard to manage with large models.</li>
  <li><strong>Error Handling:</strong> Trickier error handling and validation.</li>
</ul>

<h3 id="usage-scenario-2">Usage Scenario</h3>

<p>Best for simple mappings or when you want zero dependencies on external libraries.</p>

<h3 id="example-2">Example</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">User</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="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="k">public</span> <span class="n">Address</span> <span class="n">Address</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="k">static</span> <span class="k">implicit</span> <span class="k">operator</span> <span class="nf">UserDto</span><span class="p">(</span><span class="n">User</span> <span class="n">user</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</span> <span class="k">new</span> <span class="n">UserDto</span>
        <span class="p">{</span>
            <span class="n">Id</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
            <span class="n">Name</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span>
            <span class="n">Address</span> <span class="p">=</span> <span class="s">$"</span><span class="p">{</span><span class="n">user</span><span class="p">.</span><span class="n">Address</span><span class="p">.</span><span class="n">Street</span><span class="p">}</span><span class="s">, </span><span class="p">{</span><span class="n">user</span><span class="p">.</span><span class="n">Address</span><span class="p">.</span><span class="n">City</span><span class="p">}</span><span class="s">"</span>
        <span class="p">};</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Implicit Conversion</span>
<span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="k">new</span> <span class="n">User</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">"John Doe"</span><span class="p">,</span> 
    <span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Address</span> <span class="p">{</span> <span class="n">Street</span> <span class="p">=</span> <span class="s">"123 Main St"</span><span class="p">,</span> <span class="n">City</span> <span class="p">=</span> <span class="s">"Anytown"</span> <span class="p">}</span>
<span class="p">};</span>
<span class="n">UserDto</span> <span class="n">userDto</span> <span class="p">=</span> <span class="n">user</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="manual-mapping-the-traditionalist">Manual Mapping: The Traditionalist</h2>

<p>Manual mapping involves writing custom code for each mapping. It’s the most basic form but offers complete control.</p>

<h3 id="pros-3">Pros</h3>

<ul>
  <li><strong>Control:</strong> Full control over how mapping is performed.</li>
  <li><strong>Performance:</strong> Can be optimized for specific scenarios.</li>
  <li><strong>Dependency-Free:</strong> No reliance on third-party libraries.</li>
</ul>

<h3 id="cons-3">Cons</h3>

<ul>
  <li><strong>Boilerplate:</strong> Can lead to repetitive and verbose code.</li>
  <li><strong>Maintenance:</strong> More challenging to maintain, especially in large projects.</li>
</ul>

<h3 id="usage-scenario-3">Usage Scenario</h3>

<p>Suitable for small projects or when you have very specific mapping needs that libraries can’t efficiently handle.</p>

<h3 id="example-3">Example</h3>

<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">class</span> <span class="nc">UserMapper</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="k">static</span> <span class="n">UserDto</span> <span class="nf">MapToDto</span><span class="p">(</span><span class="n">User</span> <span class="n">user</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="kt">var</span> <span class="n">userDto</span> <span class="p">=</span> <span class="k">new</span> <span class="n">UserDto</span>
        <span class="p">{</span>
            <span class="n">Id</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Id</span><span class="p">,</span>
            <span class="n">Name</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Name</span>
        <span class="p">};</span>

        <span class="k">if</span> <span class="p">(</span><span class="n">user</span><span class="p">.</span><span class="n">Address</span> <span class="p">!=</span> <span class="k">null</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="n">userDto</span><span class="p">.</span><span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="n">AddressDto</span>
            <span class="p">{</span>
                <span class="n">Street</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Address</span><span class="p">.</span><span class="n">Street</span><span class="p">,</span>
                <span class="n">City</span> <span class="p">=</span> <span class="n">user</span><span class="p">.</span><span class="n">Address</span><span class="p">.</span><span class="n">City</span>
            <span class="p">};</span>
        <span class="p">}</span>

        <span class="c1">// Additional complex mappings...</span>
        <span class="c1">// e.g., handling collections, conditional logic, etc.</span>

        <span class="k">return</span> <span class="n">userDto</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Manual Mapping</span>
<span class="kt">var</span> <span class="n">user</span> <span class="p">=</span> <span class="k">new</span> <span class="n">User</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">"John Doe"</span><span class="p">,</span> 
    <span class="n">Address</span> <span class="p">=</span> <span class="k">new</span> <span class="n">Address</span> <span class="p">{</span> <span class="n">Street</span> <span class="p">=</span> <span class="s">"123 Main St"</span><span class="p">,</span> <span class="n">City</span> <span class="p">=</span> <span class="s">"Anytown"</span> <span class="p">}</span>
<span class="p">};</span>
<span class="kt">var</span> <span class="n">userDto</span> <span class="p">=</span> <span class="n">UserMapper</span><span class="p">.</span><span class="nf">MapToDto</span><span class="p">(</span><span class="n">user</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="pick-what-fits-your-case">Pick What Fits Your Case</h2>

<p>The choice of mapping strategy in C# should be guided by your project’s specific needs. AutoMapper shines in scenarios with complex object graphs and saves time by reducing boilerplate code. Mapster is a great middle ground, offering both performance and ease of use. Implicit operators and manual mapping provide the highest level of control and are suitable for performance-critical or simpler applications. Each method has its trade-offs, and the best choice depends on factors like project size, performance requirements, and team familiarity with the tools.</p>

<hr />

<h2 id="see-also">See Also</h2>

<ul>
  <li><a href="/technical/.net/.net-core/mapping-performance/">Object mapping performance</a></li>
  <li><a href="/technical/.net/.net-core/improve-iteration-performance/">Collection iteration performance</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="object Mapping" /><category term="AutoMapper" /><category term="Mapster" /><category term="implicit Operation" /><category term="DTOs" /><summary type="html"><![CDATA[Comparing four C# object mapping strategies: AutoMapper for complex mappings, Mapster for performance, implicit operators for native support, and manual mapping for full control.]]></summary></entry><entry><title type="html">EF Core Performance Optimization: AsNoTracking, Compiled Queries, and Concurrency in .NET</title><link href="https://animatlabs.com/technical/.net/.net-core/ef-core-performance/" rel="alternate" type="text/html" title="EF Core Performance Optimization: AsNoTracking, Compiled Queries, and Concurrency in .NET" /><published>2023-11-11T00:00:00+05:30</published><updated>2023-11-11T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/ef-core-performance</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/ef-core-performance/"><![CDATA[<p>Entity Framework Core (EF Core) is a powerful and flexible ORM widely used in .NET applications for data access. However, as with any technology, understanding how to optimize its use is crucial for building high-performance applications. This blog post covers practical strategies for enhancing EF Core’s performance, complete with examples for each technique.</p>

<h2 id="using-asnotracking-for-read-only-scenarios">Using AsNoTracking for Read-Only Scenarios</h2>

<h3 id="explanation">Explanation</h3>

<p>When EF Core tracks changes in entities, it adds overhead. For read-only operations, disabling this tracking can improve performance.</p>

<h3 id="example">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">customers</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Customers</span>
                       <span class="p">.</span><span class="nf">AsNoTracking</span><span class="p">()</span>
                       <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="projecting-only-required-data">Projecting Only Required Data</h2>

<h3 id="explanation-1">Explanation</h3>

<p>Fetching only the necessary fields from the database reduces data transfer and processing overhead.</p>

<h3 id="example-1">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">productInfo</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Products</span>
                         <span class="p">.</span><span class="nf">Select</span><span class="p">(</span><span class="n">p</span> <span class="p">=&gt;</span> <span class="k">new</span> <span class="p">{</span> <span class="n">p</span><span class="p">.</span><span class="n">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">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="writing-efficient-linq-queries">Writing Efficient LINQ Queries</h2>

<h3 id="explanation-2">Explanation</h3>

<p>The way LINQ queries are composed impacts the SQL generated. Avoid inefficient patterns that can lead to poor performance.</p>

<h3 id="example-2">Example</h3>

<p>Avoid:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">list</span> <span class="p">=</span> <span class="n">context</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="kt">var</span> <span class="n">filteredList</span> <span class="p">=</span> <span class="n">list</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">Price</span> <span class="p">&gt;</span> <span class="m">100</span><span class="p">).</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<p>Prefer:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">filteredList</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Products</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">Price</span> <span class="p">&gt;</span> <span class="m">100</span><span class="p">).</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="using-raw-sql-for-complex-queries">Using Raw SQL for Complex Queries</h2>

<h3 id="explanation-3">Explanation</h3>

<p>In scenarios where LINQ may not generate optimal SQL, using raw SQL queries can be more efficient.</p>

<h3 id="example-3">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">users</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Users</span>
                   <span class="p">.</span><span class="nf">FromSqlRaw</span><span class="p">(</span><span class="s">"SELECT * FROM Users WHERE Name = 'John'"</span><span class="p">)</span>
                   <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="database-level-filtering-and-sorting">Database-Level Filtering and Sorting</h2>

<h3 id="explanation-4">Explanation</h3>

<p>Applying filters and sorting in the database query itself is more efficient than processing data in memory.</p>

<h3 id="example-4">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">filteredUsers</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Users</span>
                           <span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">u</span> <span class="p">=&gt;</span> <span class="n">u</span><span class="p">.</span><span class="n">IsActive</span><span class="p">)</span>
                           <span class="p">.</span><span class="nf">OrderBy</span><span class="p">(</span><span class="n">u</span> <span class="p">=&gt;</span> <span class="n">u</span><span class="p">.</span><span class="n">LastName</span><span class="p">)</span>
                           <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="employing-eager-loading-for-related-data">Employing Eager Loading for Related Data</h2>

<h3 id="explanation-5">Explanation</h3>

<p>Eager loading fetches related entities in a single query, reducing the number of database calls.</p>

<h3 id="example-5">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">orders</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Orders</span>
                    <span class="p">.</span><span class="nf">Include</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="n">Customer</span><span class="p">)</span>
                    <span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="exploring-explicit-and-lazy-loading">Exploring Explicit and Lazy Loading</h2>

<h3 id="explanation-6">Explanation</h3>

<p>Explicit and lazy loading can be alternatives to eager loading, especially when dealing with complex data structures.</p>

<h3 id="example-6">Example</h3>

<p>Lazy Loading:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">order</span> <span class="p">=</span> <span class="n">context</span><span class="p">.</span><span class="n">Orders</span><span class="p">.</span><span class="nf">Find</span><span class="p">(</span><span class="m">1</span><span class="p">);</span>
<span class="c1">// Customer is loaded when accessed</span>
<span class="kt">var</span> <span class="n">customerName</span> <span class="p">=</span> <span class="n">order</span><span class="p">.</span><span class="n">Customer</span><span class="p">.</span><span class="n">Name</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="model-optimization">Model Optimization</h2>

<h3 id="explanation-7">Explanation</h3>

<p>Properly configuring indexes, relationships, and query filters in the EF Core model can lead to significant performance gains.</p>

<h3 id="example-7">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">modelBuilder</span><span class="p">.</span><span class="n">Entity</span><span class="p">&lt;</span><span class="n">Order</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">HasIndex</span><span class="p">(</span><span class="n">o</span> <span class="p">=&gt;</span> <span class="n">o</span><span class="p">.</span><span class="n">OrderDate</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="batching-operations">Batching Operations</h2>

<h3 id="explanation-8">Explanation</h3>

<p>Batching multiple updates or inserts into a single operation reduces the number of database round trips.</p>

<h3 id="example-8">Example</h3>

<p>Using third-party libraries like EF Core Plus:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">context</span><span class="p">.</span><span class="nf">BulkInsert</span><span class="p">(</span><span class="n">listOfEntities</span><span class="p">);</span>
</code></pre></div></div>

<h2 id="implementing-caching-strategies">Implementing Caching Strategies</h2>

<h3 id="explanation-9">Explanation</h3>

<p>Caching frequently accessed data reduces database load but requires careful management to avoid stale data.</p>

<h3 id="example-9">Example</h3>

<p>Implementing memory caching:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">var</span> <span class="n">categories</span> <span class="p">=</span> <span class="n">memoryCache</span><span class="p">.</span><span class="nf">GetOrCreate</span><span class="p">(</span><span class="s">"categories"</span><span class="p">,</span> <span class="n">entry</span> <span class="p">=&gt;</span> <span class="p">{</span>
    <span class="k">return</span> <span class="n">context</span><span class="p">.</span><span class="n">Categories</span><span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="using-compiled-queries">Using Compiled Queries</h2>

<h3 id="explanation-10">Explanation</h3>

<p>Compiled queries in EF Core are beneficial for queries executed repeatedly, as they are compiled once and reused.</p>

<h3 id="example-10">Example</h3>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Func</span><span class="p">&lt;</span><span class="n">MyDbContext</span><span class="p">,</span> <span class="kt">int</span><span class="p">,</span> <span class="n">List</span><span class="p">&lt;</span><span class="n">Customer</span><span class="p">&gt;&gt;</span> <span class="n">_compiledQuery</span> <span class="p">=</span>
    <span class="n">EF</span><span class="p">.</span><span class="nf">CompileQuery</span><span class="p">((</span><span class="n">MyDbContext</span> <span class="n">context</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">context</span><span class="p">.</span><span class="n">Customers</span><span class="p">.</span><span class="nf">Where</span><span class="p">(</span><span class="n">c</span> <span class="p">=&gt;</span> <span class="n">c</span><span class="p">.</span><span class="n">Id</span> <span class="p">==</span> <span class="n">id</span><span class="p">).</span><span class="nf">ToList</span><span class="p">());</span>
</code></pre></div></div>

<h2 id="profiling-and-monitoring-sql-queries">Profiling and Monitoring SQL Queries</h2>

<h3 id="explanation-11">Explanation</h3>

<p>Using tools like SQL Server Profiler helps understand the SQL generated by EF Core, aiding in identifying and optimizing inefficient queries.</p>

<h3 id="example-11">Example</h3>

<p>Analyzing the SQL output in the profiler to identify bottlenecks.</p>

<h2 id="keeping-ef-core-updated">Keeping EF Core Updated</h2>

<h3 id="explanation-12">Explanation</h3>

<p>Each new version of EF Core brings enhancements and performance improvements.</p>

<h3 id="example-12">Example</h3>

<p>Regularly updating the EF Core NuGet package to the latest version.</p>

<h2 id="database-optimization">Database Optimization</h2>

<h3 id="explanation-13">Explanation</h3>

<p>Optimizing the underlying database, such as creating appropriate indexes, is crucial for overall performance.</p>

<h3 id="example-13">Example</h3>

<p>Creating indexes based on query analysis in the database.</p>

<h2 id="managing-concurrency-efficiently">Managing Concurrency Efficiently</h2>

<h3 id="explanation-14">Explanation</h3>

<p>Concurrency control is vital in applications where multiple users or processes might attempt to modify the same data concurrently. Without proper handling, this can lead to data conflicts or loss. Entity Framework Core supports Optimistic Concurrency Control, which assumes that multiple transactions can complete without affecting each other and checks for conflicts only when data is saved.</p>

<h3 id="example-14">Example</h3>

<p>Suppose you have a <code class="language-plaintext highlighter-rouge">Product</code> entity with a <code class="language-plaintext highlighter-rouge">Price</code> field. You want to ensure that when two users attempt to update the price of the same product concurrently, the application detects and handles the conflict.</p>

<p>First, decorate the <code class="language-plaintext highlighter-rouge">Price</code> property with the <code class="language-plaintext highlighter-rouge">[ConcurrencyCheck]</code> attribute in your entity class:</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">Product</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="p">[</span><span class="n">ConcurrencyCheck</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="c1">// Other properties...</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Then, in your update logic, handle <code class="language-plaintext highlighter-rouge">DbUpdateConcurrencyException</code> to decide what to do when a concurrency conflict occurs:</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="n">context</span><span class="p">.</span><span class="nf">SaveChanges</span><span class="p">();</span>
<span class="p">}</span>
<span class="k">catch</span> <span class="p">(</span><span class="n">DbUpdateConcurrencyException</span> <span class="n">ex</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Handle the concurrency exception</span>
    <span class="c1">// For example, you can reload the entity, log the conflict, or inform the user</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="reducing-savechanges-calls">Reducing SaveChanges() Calls</h2>

<h3 id="explanation-15">Explanation</h3>

<p>Minimizing the number of <code class="language-plaintext highlighter-rouge">SaveChanges()</code> calls in a transaction can improve performance.</p>

<h3 id="example-15">Example</h3>

<p>Batching multiple changes and calling <code class="language-plaintext highlighter-rouge">SaveChanges()</code> once.</p>

<h2 id="careful-use-of-linq-methods">Careful Use of LINQ Methods</h2>

<h3 id="explanation-16">Explanation</h3>

<p>Certain LINQ methods, if used improperly, can lead to performance issues.</p>

<h3 id="example-16">Example</h3>

<p>Using <code class="language-plaintext highlighter-rouge">Any()</code> instead of <code class="language-plaintext highlighter-rouge">Count()</code> to check for existence.</p>

<h2 id="effective-connection-management">Effective Connection Management</h2>

<h3 id="explanation-17">Explanation</h3>

<p>Effective management of database connections is crucial for performance, especially in web applications that handle numerous concurrent requests. Connection pooling is a technique used to maintain a cache of database connections that can be reused, rather than opening a new connection for each request.</p>

<h3 id="example-17">Example</h3>

<p>In .NET Core and EF Core, connection pooling is handled by the database provider, such as SQL Server, and is enabled by default. However, it’s essential to ensure that your connection strings are consistent, as connection pools are segregated based on the connection string.</p>

<p>An example of how you might configure a SQL Server connection in your <code class="language-plaintext highlighter-rouge">DbContext</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">protected</span> <span class="k">override</span> <span class="k">void</span> <span class="nf">OnConfiguring</span><span class="p">(</span><span class="n">DbContextOptionsBuilder</span> <span class="n">optionsBuilder</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(!</span><span class="n">optionsBuilder</span><span class="p">.</span><span class="n">IsConfigured</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="n">optionsBuilder</span><span class="p">.</span><span class="nf">UseSqlServer</span><span class="p">(</span><span class="s">"Server=myServerAddress;Database=myDataBase;User Id=myUsername;Password=myPassword;"</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>With this configuration, EF Core and the SQL Server provider manage the connection pooling. Just remember that every unique connection string gets its own pool, so avoid dynamically generating connection strings.</p>

<p>Additionally, it’s crucial to properly manage the lifecycle of your DbContext instances. In web applications, it’s generally recommended to have a DbContext instance per request, which you can configure via dependency injection in your <code class="language-plaintext highlighter-rouge">Startup.cs</code>:</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">void</span> <span class="nf">ConfigureServices</span><span class="p">(</span><span class="n">IServiceCollection</span> <span class="n">services</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">services</span><span class="p">.</span><span class="n">AddDbContext</span><span class="p">&lt;</span><span class="n">MyDbContext</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">Configuration</span><span class="p">.</span><span class="nf">GetConnectionString</span><span class="p">(</span><span class="s">"MyDatabase"</span><span class="p">)));</span>
<span class="p">}</span>
</code></pre></div></div>

<p>This setup ensures that each web request gets a fresh DbContext instance, which aligns well with the connection pooling mechanism, leading to efficient use of database connections.</p>

<h2 id="performance-wins-add-up">Performance Wins Add Up</h2>

<p>In conclusion, optimizing EF Core involves a combination of efficient coding practices, strategic query writing, and database-level optimizations. By applying these strategies, developers can ensure their applications run efficiently, making the most out of the capabilities of Entity Framework Core.</p>

<hr />

<h2 id="related">Related</h2>

<ul>
  <li><a href="/technical/.net/ef-core/ef-core-managing-configurations-migrations/">EF Core configurations and migrations</a></li>
  <li><a href="/technical/.net/ef-core/ef-core-managing-sp-udf-custom-sql/">EF Core: custom SQL, SPs, and UDFs</a></li>
  <li><a href="/technical/.net/.net-core/improve-iteration-performance/">Collection iteration performance</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="EntityFrameworkCore" /><category term="EFCorePerformance" /><summary type="html"><![CDATA[Practical strategies for optimizing Entity Framework Core performance including AsNoTracking, compiled queries, connection pooling, and concurrency management in .NET.]]></summary></entry><entry><title type="html">ASP.NET Core Security Guide: Authentication, Encryption, and Secure Coding</title><link href="https://animatlabs.com/technical/.net/.net-core/security-dotnet/" rel="alternate" type="text/html" title="ASP.NET Core Security Guide: Authentication, Encryption, and Secure Coding" /><published>2023-10-13T00:00:00+05:30</published><updated>2023-10-13T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/security-dotnet</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/security-dotnet/"><![CDATA[<p>Security is one of those topics that nobody prioritizes until something breaks. I’ve seen production systems with plain-text connection strings in config files and admin endpoints behind nothing but a query parameter. This guide walks through the areas that matter most in ASP.NET Core, with code you can actually drop into a project.</p>

<h2 id="authentication-and-authorization">Authentication and Authorization</h2>

<p>Authentication answers “who are you?” and authorization answers “what are you allowed to do?” ASP.NET Core Identity handles both. You wire it up in <code class="language-plaintext highlighter-rouge">Program.cs</code> (or <code class="language-plaintext highlighter-rouge">Startup.cs</code> if you’re on the older hosting model), and from there you can layer in role checks or policy-based authorization depending on how granular you need to get.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Setting up Identity in ASP.NET Core</span>
<span class="n">services</span><span class="p">.</span><span class="n">AddIdentity</span><span class="p">&lt;</span><span class="n">ApplicationUser</span><span class="p">,</span> <span class="n">IdentityRole</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="n">AddEntityFrameworkStores</span><span class="p">&lt;</span><span class="n">ApplicationDbContext</span><span class="p">&gt;()</span>
    <span class="p">.</span><span class="nf">AddDefaultTokenProviders</span><span class="p">();</span>

<span class="c1">// Custom claims-based authorization</span>
<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="k">public</span> <span class="n">IActionResult</span> <span class="nf">AdminDashboard</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Access restricted to users with the "AdminOnly" policy</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="input-validation">Input Validation</h2>

<p>Every external input is suspect. Form fields, query strings, headers, uploaded file names. Trusting any of them without validation is how SQL injection and XSS happen. Data annotations handle the simple cases; FluentValidation gives you more control when rules get complex.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Using data annotations for input validation</span>
<span class="p">[</span><span class="n">Required</span><span class="p">]</span>
<span class="p">[</span><span class="nf">RegularExpression</span><span class="p">(</span><span class="s">@"^\d{5}(-\d{4})?$"</span><span class="p">)]</span>
<span class="k">public</span> <span class="kt">string</span> <span class="n">PostalCode</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="c1">// Using FluentValidation</span>
<span class="k">public</span> <span class="k">class</span> <span class="nc">ProductValidator</span> <span class="p">:</span> <span class="n">AbstractValidator</span><span class="p">&lt;</span><span class="n">Product</span><span class="p">&gt;</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="nf">ProductValidator</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="nf">RuleFor</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="nf">NotEmpty</span><span class="p">();</span>
        <span class="nf">RuleFor</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">Price</span><span class="p">).</span><span class="nf">GreaterThan</span><span class="p">(</span><span class="m">0</span><span class="p">);</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="cross-site-request-forgery-csrf-protection">Cross-Site Request Forgery (CSRF) Protection</h2>

<p>CSRF tricks a logged-in user’s browser into making requests they didn’t intend. The classic example: a hidden form on an attacker’s page that submits a POST to your app while the victim’s session cookie is still valid. ASP.NET Core has built-in anti-forgery token support. You generate the token in the form and validate it on the server side.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Generating anti-forgery tokens</span>
<span class="n">@Html</span><span class="p">.</span><span class="nf">AntiForgeryToken</span><span class="p">()</span>

<span class="c1">// Validating anti-forgery tokens in a POST action</span>
<span class="p">[</span><span class="n">HttpPost</span><span class="p">]</span>
<span class="p">[</span><span class="n">ValidateAntiForgeryToken</span><span class="p">]</span>
<span class="k">public</span> <span class="n">IActionResult</span> <span class="nf">SubmitOrder</span><span class="p">(</span><span class="n">Order</span> <span class="n">order</span><span class="p">)</span>
<span class="p">{</span>
    <span class="c1">// Token is automatically validated by ASP.NET Core</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="sql-injection-prevention">SQL Injection Prevention</h2>

<p>SQL injection has been in the OWASP Top 10 for over two decades and it still shows up in production code. The fix is simple: never concatenate user input into a query string. Use parameterized queries or let EF Core handle it. Both approaches ensure the database treats input as data, not executable SQL.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Using parameterized queries</span>
<span class="kt">var</span> <span class="n">query</span> <span class="p">=</span> <span class="s">"SELECT * FROM Users WHERE Username = @Username"</span><span class="p">;</span>
<span class="k">using</span> <span class="nn">var</span> <span class="n">cmd</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">SqlCommand</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">connection</span><span class="p">);</span>
<span class="n">cmd</span><span class="p">.</span><span class="n">Parameters</span><span class="p">.</span><span class="nf">AddWithValue</span><span class="p">(</span><span class="s">"@Username"</span><span class="p">,</span> <span class="n">inputUsername</span><span class="p">);</span>

<span class="c1">// Using Entity Framework Core to avoid SQL injection</span>
<span class="kt">var</span> <span class="n">users</span> <span class="p">=</span> <span class="n">dbContext</span><span class="p">.</span><span class="n">Users</span><span class="p">.</span><span class="nf">FromSqlRaw</span><span class="p">(</span><span class="s">"SELECT * FROM Users WHERE Username = {0}"</span><span class="p">,</span> <span class="n">inputUsername</span><span class="p">).</span><span class="nf">ToList</span><span class="p">();</span>
</code></pre></div></div>

<h2 id="cross-site-scripting-xss-mitigation">Cross-Site Scripting (XSS) Mitigation</h2>

<p>XSS is the mirror image of SQL injection, but for HTML. An attacker injects a script that runs in another user’s browser. The defense has two layers: encode all dynamic output so the browser treats it as text, and add a Content Security Policy to limit where scripts can load from.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Output encoding in Razor Views</span>
<span class="n">@Html</span><span class="p">.</span><span class="nf">Raw</span><span class="p">(</span><span class="n">Model</span><span class="p">.</span><span class="n">Description</span><span class="p">)</span>
<span class="n">@Html</span><span class="p">.</span><span class="nf">Encode</span><span class="p">(</span><span class="n">Model</span><span class="p">.</span><span class="n">Description</span><span class="p">)</span>

<span class="c1">// Content Security Policy (CSP) configuration in ASP.NET Core</span>
<span class="n">app</span><span class="p">.</span><span class="nf">UseCsp</span><span class="p">(</span><span class="n">options</span> <span class="p">=&gt;</span> <span class="n">options</span>
    <span class="p">.</span><span class="nf">DefaultSources</span><span class="p">(</span><span class="n">s</span> <span class="p">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="nf">Self</span><span class="p">())</span>
    <span class="p">.</span><span class="nf">ScriptSources</span><span class="p">(</span><span class="n">s</span> <span class="p">=&gt;</span> <span class="n">s</span><span class="p">.</span><span class="nf">Self</span><span class="p">().</span><span class="nf">CustomSources</span><span class="p">(</span><span class="s">"https://cdn.example.com"</span><span class="p">)));</span>
</code></pre></div></div>

<h2 id="secure-password-storage">Secure Password Storage</h2>

<p>Storing passwords in plain text is an obvious no. But MD5 or SHA-256 without salting isn’t much better. BCrypt (or Argon2, if you want the newer option) handles hashing and salting in one call. On top of that, ASP.NET Core Identity lets you enforce password policies so users can’t set “password123” and call it a day.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Hashing and salting passwords using BCrypt</span>
<span class="kt">string</span> <span class="n">hashedPassword</span> <span class="p">=</span> <span class="n">BCrypt</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">BCrypt</span><span class="p">.</span><span class="nf">HashPassword</span><span class="p">(</span><span class="n">password</span><span class="p">,</span> <span class="n">BCrypt</span><span class="p">.</span><span class="n">Net</span><span class="p">.</span><span class="n">BCrypt</span><span class="p">.</span><span class="nf">GenerateSalt</span><span class="p">());</span>

<span class="c1">// Enforcing password policies in ASP.NET Core Identity</span>
<span class="n">services</span><span class="p">.</span><span class="n">Configure</span><span class="p">&lt;</span><span class="n">IdentityOptions</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">Password</span><span class="p">.</span><span class="n">RequireUppercase</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">Password</span><span class="p">.</span><span class="n">RequiredLength</span> <span class="p">=</span> <span class="m">8</span><span class="p">;</span>
<span class="p">});</span>
</code></pre></div></div>

<h2 id="https-and-data-encryption">HTTPS and Data Encryption</h2>

<p>Data in transit needs TLS. Data at rest needs encryption. ASP.NET Core pushes you toward HTTPS by default with HSTS (HTTP Strict Transport Security) in production. For the database side, most engines support transparent data encryption. Both together mean even if someone intercepts traffic or copies disk files, the data is useless without the keys.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Configuring HTTPS in ASP.NET Core</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">Configure</span><span class="p">(</span><span class="n">IApplicationBuilder</span> <span class="n">app</span><span class="p">,</span> <span class="n">IWebHostEnvironment</span> <span class="n">env</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">env</span><span class="p">.</span><span class="nf">IsDevelopment</span><span class="p">())</span>
    <span class="p">{</span>
        <span class="n">app</span><span class="p">.</span><span class="nf">UseDeveloperExceptionPage</span><span class="p">();</span>
    <span class="p">}</span>
    <span class="k">else</span>
    <span class="p">{</span>
        <span class="n">app</span><span class="p">.</span><span class="nf">UseHsts</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Database-level encryption with SQL Server Transparent Data Encryption</span>
<span class="n">ALTER</span> <span class="n">DATABASE</span> <span class="n">YourDatabaseName</span> <span class="n">SET</span> <span class="n">ENCRYPTION</span> <span class="n">ON</span><span class="p">;</span>
</code></pre></div></div>

<h2 id="api-security">API Security</h2>

<p>APIs need their own security layer since there’s no browser session or cookie jar to fall back on. JWT (JSON Web Tokens) is the standard approach. The client sends a token in the <code class="language-plaintext highlighter-rouge">Authorization</code> header, and the server validates it on every request. You configure the expected issuer, audience, and signing key; anything that doesn’t match gets rejected.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Token-based authentication with JWT in ASP.NET Core</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="s">"yourissuer"</span><span class="p">,</span>
            <span class="n">ValidAudience</span> <span class="p">=</span> <span class="s">"youraudience"</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="s">"yoursecretkey"</span><span class="p">))</span>
        <span class="p">};</span>
    <span class="p">});</span>
</code></pre></div></div>

<h2 id="logging-and-monitoring">Logging and Monitoring</h2>

<p>You can have perfect input validation and still get compromised if nobody is watching the logs. Serilog gives you structured logging that’s easy to query, and Application Insights (or any APM tool) gives you the dashboards and alerts. The key rule: log the event, not the sensitive data. No passwords, tokens, or PII in log output.</p>

<div class="language-csharp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Setting up Serilog in ASP.NET Core</span>
<span class="k">public</span> <span class="k">static</span> <span class="n">IHostBuilder</span> <span class="nf">CreateHostBuilder</span><span class="p">(</span><span class="kt">string</span><span class="p">[]</span> <span class="n">args</span><span class="p">)</span> <span class="p">=&gt;</span>
    <span class="n">Host</span><span class="p">.</span><span class="nf">CreateDefaultBuilder</span><span class="p">(</span><span class="n">args</span><span class="p">)</span>
        <span class="p">.</span><span class="nf">ConfigureWebHostDefaults</span><span class="p">(</span><span class="n">webBuilder</span> <span class="p">=&gt;</span>
        <span class="p">{</span>
            <span class="n">webBuilder</span><span class="p">.</span><span class="n">UseStartup</span><span class="p">&lt;</span><span class="n">Startup</span><span class="p">&gt;()</span>
                <span class="p">.</span><span class="nf">UseSerilog</span><span class="p">();</span>
        <span class="p">});</span>

<span class="c1">// Integrating Application Insights for monitoring</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">ConfigureServices</span><span class="p">(</span><span class="n">IServiceCollection</span> <span class="n">services</span><span class="p">)</span>
<span class="p">{</span>
    <span class="n">services</span><span class="p">.</span><span class="nf">AddApplicationInsightsTelemetry</span><span class="p">(</span><span class="n">Configuration</span><span class="p">[</span><span class="s">"ApplicationInsights:InstrumentationKey"</span><span class="p">]);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="security-is-never-done">Security Is Never Done</h2>

<p>None of this is “set and forget.” Packages get CVEs, authentication standards evolve, and attackers find new angles. Run <code class="language-plaintext highlighter-rouge">dotnet list package --vulnerable</code> regularly. Review OWASP updates. Audit your dependencies. The patterns in this guide cover the fundamentals, but the real work is keeping them current as your project grows.</p>

<hr />

<h2 id="more-on-this-topic">More on This Topic</h2>

<ul>
  <li><a href="/technical/.net/.net-core/data-protection-apis/">Data Protection APIs</a></li>
  <li><a href="/technical/.net/.net-core/secure-alternate-to-exposing-identifiers/">Hashing internal IDs</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="Security" /><summary type="html"><![CDATA[A practical guide to .NET security covering authentication, authorization, encryption, input validation, and secure coding with code samples.]]></summary></entry><entry><title type="html">Object Mapping in .NET: Comparing AutoMapper, Mapster, XSLT, and Manual Approaches</title><link href="https://animatlabs.com/technical/.net/.net-core/mapping-performance/" rel="alternate" type="text/html" title="Object Mapping in .NET: Comparing AutoMapper, Mapster, XSLT, and Manual Approaches" /><published>2023-08-29T00:00:00+05:30</published><updated>2023-08-29T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/mapping-performance</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/mapping-performance/"><![CDATA[<p>Based on the requirements of the organization/project, we may end up choosing the tech on how do we wish to map objects in a commercial project. Now to a many, this might seem to be a thing like what are you saying. How, can this we such a big thing?…It can be, just explore with me the possibilities I present here and lets take it from there.</p>

<p>There are many object to object mapping libraries available in the world as of right now, One such example is <code class="language-plaintext highlighter-rouge">AutoMapper</code>; there are other libraries in the community like <code class="language-plaintext highlighter-rouge">Mapster</code> to do the same. On the other hand, there are other parsing and transforming solutions like <code class="language-plaintext highlighter-rouge">XSLT</code> (Extensible Stylesheet Language Transformations).</p>

<blockquote>
  <p>In the world of .NET the inbuilt library only supports <code class="language-plaintext highlighter-rouge">XSL v1.0</code> with <code class="language-plaintext highlighter-rouge">XSLTCompiledTransform</code>, there are other paid libraries like that from Saxon that work with the latest and the greatest and provide other functionalities like <code class="language-plaintext highlighter-rouge">XSpec</code> (unit-testing for XSLT) testing smoothly.</p>
</blockquote>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/Benchmarking/Mapping" class="btn btn--primary">GitHub Repo</a></p>

<h2 id="results-first---lets-blow-your-mind">Results First - Let’s blow your mind…</h2>

<p>To set the context, let’s take a case of an API that needs to read an XML and then map it to another object and then revert return a string response downstream. Here, we would be looking at the conventional ways to solve the solution like general object-object mapping and then look at other patterns as well. Now, looking for the strategies to be followed given we have an XML file that we need to map the object from there are only given set of ways we do the transformations:</p>

<ol>
  <li>XMLDocument - Extract the required props using XPath for the values required</li>
  <li>ModelToModel - Deserialize input to C# classes and map those classes</li>
  <li>AutoMapper - Use AutoMapper to perform the mapping instead explicit manual mapping</li>
  <li>XSLT -&gt; XML - Use XSLT to convert the input to XML and then to Json</li>
  <li>XSLT -&gt; JSON - Use XSLT to convert the input to Json</li>
  <li>XSLT -&gt; JSON - Use XSLT to convert the input to Text like Json</li>
</ol>

<table>
  <thead>
    <tr>
      <th>Method</th>
      <th style="text-align: right">Mean</th>
      <th style="text-align: right">Error</th>
      <th style="text-align: right">StdDev</th>
      <th style="text-align: right">Allocated</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>XmlDocToModelMapping</td>
      <td style="text-align: right">10.373 us</td>
      <td style="text-align: right">0.1684 us</td>
      <td style="text-align: right">0.2949 us</td>
      <td style="text-align: right">15.84 KB</td>
    </tr>
    <tr>
      <td>ModelToModelMapping</td>
      <td style="text-align: right">2.260 us</td>
      <td style="text-align: right">0.0441 us</td>
      <td style="text-align: right">0.0573 us</td>
      <td style="text-align: right">2.49 KB</td>
    </tr>
    <tr>
      <td>AutoMapperMapping</td>
      <td style="text-align: right">2.583 us</td>
      <td style="text-align: right">0.0439 us</td>
      <td style="text-align: right">0.0925 us</td>
      <td style="text-align: right">2.68 KB</td>
    </tr>
    <tr>
      <td>XsltXMLMapping</td>
      <td style="text-align: right">18.056 us</td>
      <td style="text-align: right">0.3144 us</td>
      <td style="text-align: right">0.2940 us</td>
      <td style="text-align: right">41.54 KB</td>
    </tr>
    <tr>
      <td>XsltJsonMapping</td>
      <td style="text-align: right">8.359 us</td>
      <td style="text-align: right">0.1631 us</td>
      <td style="text-align: right">0.2062 us</td>
      <td style="text-align: right">37.46 KB</td>
    </tr>
    <tr>
      <td>XsltTextMapping</td>
      <td style="text-align: right">3.338 us</td>
      <td style="text-align: right">0.0383 us</td>
      <td style="text-align: right">0.0320 us</td>
      <td style="text-align: right">17.66 KB</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p>What we observe here is that both speed and memory wise, direct model to model mapping beats everyone to the game. The methods had been setup to not read the required files but just to map and generate a string result!</p>
</blockquote>

<h2 id="looking-into-the-implementations">Looking into the implementations</h2>

<p>So, to begin it off lets look into sample set data and what is the expectation that we are trying to achieve from all the variants.</p>

<h3 id="global-setup">Global Setup</h3>

<p>So, let’s talk about the sample xml, that we are looking into as of right now:</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span>
<span class="nt">&lt;root&gt;</span>
	<span class="nt">&lt;person&gt;</span>
		<span class="nt">&lt;name&gt;</span>John Doe<span class="nt">&lt;/name&gt;</span>
		<span class="nt">&lt;position&gt;</span>Manager<span class="nt">&lt;/position&gt;</span>
		<span class="nt">&lt;age&gt;</span>30<span class="nt">&lt;/age&gt;</span>
	<span class="nt">&lt;/person&gt;</span>
	<span class="nt">&lt;book&gt;</span>
		<span class="nt">&lt;title&gt;</span>XML Essentials<span class="nt">&lt;/title&gt;</span>
		<span class="nt">&lt;author&gt;</span>Alice Johnson<span class="nt">&lt;/author&gt;</span>
		<span class="nt">&lt;publicationYear&gt;</span>2022<span class="nt">&lt;/publicationYear&gt;</span>
	<span class="nt">&lt;/book&gt;</span>
	<span class="nt">&lt;company&gt;</span>
		<span class="nt">&lt;name&gt;</span>ABC Corporation<span class="nt">&lt;/name&gt;</span>
		<span class="nt">&lt;city&gt;</span>Chicago<span class="nt">&lt;/city&gt;</span>
		<span class="nt">&lt;state&gt;</span>IL<span class="nt">&lt;/state&gt;</span>
	<span class="nt">&lt;/company&gt;</span>
<span class="nt">&lt;/root&gt;</span>
</code></pre></div></div>

<p>As we can observe above, the xml has person, book and company at the root level and the expectation in the output is to have a model that encompasses company as response with has list of employees as persons inside them and then list of books associated to the employees. Therefore setting up the target classes:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">public</span> <span class="k">class</span> <span class="nc">Response</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="n">Company</span> <span class="n">Company</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">Company</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="k">public</span> <span class="kt">string</span> <span class="n">City</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">State</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">IEnumerable</span><span class="p">&lt;</span><span class="n">Person</span><span class="p">&gt;</span> <span class="n">Employees</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">Book</span>
<span class="p">{</span>
    <span class="k">public</span> <span class="kt">string</span> <span class="n">Title</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">Author</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">PublicationYear</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">Person</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="k">public</span> <span class="kt">int</span> <span class="n">Age</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">Position</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">IEnumerable</span><span class="p">&lt;</span><span class="n">Book</span><span class="p">&gt;</span> <span class="n">Books</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>Now, loading the required details into the required classes, as follows:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="k">static</span> <span class="n">XmlDocument</span> <span class="n">doc</span> <span class="p">=</span> <span class="k">new</span><span class="p">();</span>
<span class="k">private</span> <span class="k">static</span> <span class="n">root</span> <span class="n">root</span><span class="p">;</span>
<span class="k">private</span> <span class="k">static</span> <span class="n">IMapper</span> <span class="n">map</span><span class="p">;</span>
<span class="k">private</span> <span class="k">static</span> <span class="n">XslCompiledTransform</span> <span class="n">xsltXml</span> <span class="p">=</span> <span class="k">new</span><span class="p">();</span>
<span class="k">private</span> <span class="k">static</span> <span class="n">XslCompiledTransform</span> <span class="n">xsltText</span> <span class="p">=</span> <span class="k">new</span><span class="p">();</span>
<span class="k">private</span> <span class="k">static</span> <span class="n">XslCompiledTransform</span> <span class="n">xsltJson</span> <span class="p">=</span> <span class="k">new</span><span class="p">(</span><span class="k">true</span><span class="p">);</span>

<span class="p">[</span><span class="n">GlobalSetup</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">SetupData</span><span class="p">()</span>
<span class="p">{</span>
    <span class="c1">// Load the original document</span>
    <span class="kt">var</span> <span class="n">xmlReader</span> <span class="p">=</span> <span class="n">XmlReader</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="k">new</span> <span class="nf">StringReader</span><span class="p">(</span><span class="n">File</span><span class="p">.</span><span class="nf">ReadAllText</span><span class="p">(</span><span class="s">"BaseFiles/Sample.xml"</span><span class="p">)));</span>
    <span class="n">doc</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="n">xmlReader</span><span class="p">);</span>

    <span class="c1">// Setting up Automapper Configuration</span>
    <span class="kt">var</span> <span class="n">config</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">MapperConfiguration</span><span class="p">(</span><span class="n">cfg</span> <span class="p">=&gt;</span>
    <span class="p">{</span>
        <span class="n">cfg</span><span class="p">.</span><span class="n">CreateMap</span><span class="p">&lt;</span><span class="n">root</span><span class="p">,</span> <span class="n">Book</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Title</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">book</span><span class="p">.</span><span class="n">title</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">PublicationYear</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">book</span><span class="p">.</span><span class="n">publicationYear</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Author</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">book</span><span class="p">.</span><span class="n">author</span><span class="p">));</span>

        <span class="n">cfg</span><span class="p">.</span><span class="n">CreateMap</span><span class="p">&lt;</span><span class="n">root</span><span class="p">,</span> <span class="n">Person</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Position</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">person</span><span class="p">.</span><span class="n">position</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Age</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">person</span><span class="p">.</span><span class="n">age</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Books</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">src</span> <span class="p">}));</span>

        <span class="n">cfg</span><span class="p">.</span><span class="n">CreateMap</span><span class="p">&lt;</span><span class="n">root</span><span class="p">,</span> <span class="n">Company</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Name</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">company</span><span class="p">.</span><span class="n">name</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">City</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">company</span><span class="p">.</span><span class="n">city</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">State</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">.</span><span class="n">company</span><span class="p">.</span><span class="n">state</span><span class="p">))</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Employees</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">src</span> <span class="p">}));</span>

        <span class="n">cfg</span><span class="p">.</span><span class="n">CreateMap</span><span class="p">&lt;</span><span class="n">root</span><span class="p">,</span> <span class="n">Response</span><span class="p">&gt;()</span>
            <span class="p">.</span><span class="nf">ForMember</span><span class="p">(</span><span class="n">dest</span> <span class="p">=&gt;</span> <span class="n">dest</span><span class="p">.</span><span class="n">Company</span><span class="p">,</span> <span class="n">opt</span> <span class="p">=&gt;</span> <span class="n">opt</span><span class="p">.</span><span class="nf">MapFrom</span><span class="p">(</span><span class="n">src</span> <span class="p">=&gt;</span> <span class="n">src</span><span class="p">));</span>

    <span class="p">});</span>
    <span class="n">map</span> <span class="p">=</span> <span class="n">config</span><span class="p">.</span><span class="nf">CreateMapper</span><span class="p">();</span>

    <span class="c1">// Since we cannot reset an xmlreader and we need to load the target</span>
    <span class="n">xmlReader</span> <span class="p">=</span> <span class="n">XmlReader</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="k">new</span> <span class="nf">StringReader</span><span class="p">(</span><span class="n">File</span><span class="p">.</span><span class="nf">ReadAllText</span><span class="p">(</span><span class="s">"BaseFiles/Sample.xml"</span><span class="p">)));</span>
    <span class="n">root</span> <span class="p">=</span> <span class="p">(</span><span class="n">root</span><span class="p">)</span><span class="k">new</span> <span class="nf">XmlSerializer</span><span class="p">(</span><span class="k">typeof</span><span class="p">(</span><span class="n">root</span><span class="p">)).</span><span class="nf">Deserialize</span><span class="p">(</span><span class="n">xmlReader</span><span class="p">);</span>

    <span class="n">xsltXml</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="s">"BaseFiles/ConvertXML.xslt"</span><span class="p">);</span>
    <span class="n">xsltText</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="s">"BaseFiles/ConvertText.xslt"</span><span class="p">);</span>
    <span class="n">xsltJson</span><span class="p">.</span><span class="nf">Load</span><span class="p">(</span><span class="s">"BaseFiles/ConvertJson.xslt"</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="xmldoctomodel-mapping">XMLDocToModel Mapping</h3>

<p>In this case, we look forward to directly parse the xml document in to the model via XPath, what we observe here is that using the actual path of the elements in the file, we are mapping each and every property one by one.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">XmlDocToModelMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">book</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Book</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Author</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/book/author"</span><span class="p">),</span>
        <span class="n">PublicationYear</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/book/publicationYear"</span><span class="p">),</span>
        <span class="n">Title</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/book/title"</span><span class="p">),</span>
    <span class="p">};</span>
    <span class="kt">var</span> <span class="n">person</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Person</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Age</span> <span class="p">=</span> <span class="n">Convert</span><span class="p">.</span><span class="nf">ToInt32</span><span class="p">(</span><span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/person/age"</span><span class="p">)),</span>
        <span class="n">Name</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/person/name"</span><span class="p">),</span>
        <span class="n">Position</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/person/position"</span><span class="p">),</span>
        <span class="n">Books</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">book</span> <span class="p">}</span>
    <span class="p">};</span>
    <span class="kt">var</span> <span class="n">company</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Company</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">City</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/company/city"</span><span class="p">),</span>
        <span class="n">Name</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/company/name"</span><span class="p">),</span>
        <span class="n">State</span> <span class="p">=</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="s">"/root/company/state"</span><span class="p">),</span>
        <span class="n">Employees</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">person</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">Response</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Company</span> <span class="p">=</span> <span class="n">company</span>
    <span class="p">};</span>

    <span class="kt">var</span> <span class="n">output</span> <span class="p">=</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">response</span><span class="p">);</span>

    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>

<span class="k">private</span> <span class="kt">string</span> <span class="nf">GetValue</span><span class="p">(</span><span class="n">XmlDocument</span> <span class="n">xmlDocument</span><span class="p">,</span> <span class="kt">string</span> <span class="n">path</span><span class="p">)</span>
<span class="p">{</span>
    <span class="k">return</span> <span class="n">xmlDocument</span><span class="p">.</span><span class="nf">SelectSingleNode</span><span class="p">(</span><span class="n">path</span><span class="p">)?.</span><span class="n">InnerText</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="modeltomodel-mapping">ModelToModel Mapping</h3>

<p>In this case, we look forward to parse the xml document into a source C# model and then map it to the target model, what we observe here is that using the we are mapping each and every property one by one from model to model.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">ModelToModelMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">book</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Book</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Author</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">book</span><span class="p">.</span><span class="n">author</span><span class="p">,</span>
        <span class="n">PublicationYear</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">book</span><span class="p">.</span><span class="n">publicationYear</span><span class="p">.</span><span class="nf">ToString</span><span class="p">(),</span>
        <span class="n">Title</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">book</span><span class="p">.</span><span class="n">title</span>
    <span class="p">};</span>
    <span class="kt">var</span> <span class="n">person</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Person</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Age</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">person</span><span class="p">.</span><span class="n">age</span><span class="p">,</span>
        <span class="n">Name</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">person</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
        <span class="n">Position</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">person</span><span class="p">.</span><span class="n">position</span><span class="p">,</span>
        <span class="n">Books</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">book</span> <span class="p">}</span>
    <span class="p">};</span>
    <span class="kt">var</span> <span class="n">company</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Company</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">City</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">company</span><span class="p">.</span><span class="n">city</span><span class="p">,</span>
        <span class="n">Name</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">company</span><span class="p">.</span><span class="n">name</span><span class="p">,</span>
        <span class="n">State</span> <span class="p">=</span> <span class="n">root</span><span class="p">.</span><span class="n">company</span><span class="p">.</span><span class="n">state</span><span class="p">,</span>
        <span class="n">Employees</span> <span class="p">=</span> <span class="k">new</span><span class="p">[]</span> <span class="p">{</span> <span class="n">person</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">Response</span><span class="p">()</span>
    <span class="p">{</span>
        <span class="n">Company</span> <span class="p">=</span> <span class="n">company</span>
    <span class="p">};</span>

    <span class="kt">var</span> <span class="n">output</span> <span class="p">=</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">response</span><span class="p">);</span>

    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="automappermodel-mapping">AutoMapperModel Mapping</h3>

<p>In this case, we look forward to directly parse the xml document in to the model an then use Automapper to map it into the target model, with the configuration that was defined in the global setup.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">AutoMapperMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">output</span> <span class="p">=</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeObject</span><span class="p">(</span><span class="n">map</span><span class="p">.</span><span class="n">Map</span><span class="p">&lt;</span><span class="n">Response</span><span class="p">&gt;(</span><span class="n">root</span><span class="p">));</span>

    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="xslt---xml-mapping">XSLT -&gt; XML Mapping</h3>

<p>In this case, we look forward to directly transform the xml document into target structure without use of any classes. Although XSLT load configuration was done as part of the global setup, we will work towards setting those up here. What we will observe here is that there is an additional step that converts the xml output to json and therefore it is expected to take more time, as that was the expected outcome!</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;xsl:stylesheet</span> <span class="na">version=</span><span class="s">"1.0"</span> <span class="na">xmlns:xsl=</span><span class="s">"http://www.w3.org/1999/XSL/Transform"</span><span class="nt">&gt;</span>
	<span class="nt">&lt;xsl:output</span> <span class="na">method=</span><span class="s">"xml"</span> <span class="na">indent=</span><span class="s">"yes"</span> <span class="na">omit-xml-declaration=</span><span class="s">"yes"</span><span class="nt">/&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"/"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;company&gt;</span>
			<span class="nt">&lt;xsl:copy-of</span> <span class="na">select=</span><span class="s">"root/company/*"</span><span class="nt">/&gt;</span>
			<span class="nt">&lt;employees&gt;</span>
				<span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"root/person"</span><span class="nt">/&gt;</span>
			<span class="nt">&lt;/employees&gt;</span>
		<span class="nt">&lt;/company&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"person"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;xsl:copy-of</span> <span class="na">select=</span><span class="s">"name|position|age"</span><span class="nt">/&gt;</span>
		<span class="nt">&lt;books&gt;</span>
			<span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/book"</span><span class="nt">/&gt;</span>
		<span class="nt">&lt;/books&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"book"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;xsl:copy-of</span> <span class="na">select=</span><span class="s">"title|author|publicationYear"</span><span class="nt">/&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>
<span class="nt">&lt;/xsl:stylesheet&gt;</span>
</code></pre></div></div>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">XsltXMLMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">output</span> <span class="p">=</span> <span class="n">String</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">StringWriter</span> <span class="n">sw</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringWriter</span><span class="p">())</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">XmlWriter</span> <span class="n">xwo</span> <span class="p">=</span> <span class="n">XmlWriter</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="n">sw</span><span class="p">,</span> <span class="n">xsltXml</span><span class="p">.</span><span class="n">OutputSettings</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="n">xsltXml</span><span class="p">.</span><span class="nf">Transform</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">xwo</span><span class="p">);</span>
        <span class="n">output</span> <span class="p">=</span> <span class="n">JsonConvert</span><span class="p">.</span><span class="nf">SerializeXNode</span><span class="p">(</span><span class="n">XDocument</span><span class="p">.</span><span class="nf">Parse</span><span class="p">(</span><span class="n">sw</span><span class="p">.</span><span class="nf">ToString</span><span class="p">()));</span>
    <span class="p">}</span>
    
    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="xslt---json-mapping">XSLT -&gt; Json Mapping</h3>

<p>In this case, we look forward to directly transform the xml document into target structure without use of any classes but this time directly into JSON. Although, the XSLCompile Transform only supports XSL v1.0 (which does not have json rendering) but just making the version as 2.0 in the XSL let’s us build and run the code, else it would not build itself.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;xsl:stylesheet</span> <span class="na">version=</span><span class="s">"2.0"</span> <span class="na">xmlns:xsl=</span><span class="s">"http://www.w3.org/1999/XSL/Transform"</span><span class="nt">&gt;</span>
	<span class="nt">&lt;xsl:output</span> <span class="na">method=</span><span class="s">"json"</span> <span class="na">indent=</span><span class="s">"yes"</span> <span class="na">omit-xml-declaration=</span><span class="s">"yes"</span><span class="nt">/&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"/"</span><span class="nt">&gt;</span>
		{
			"company": <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"root/company"</span><span class="nt">/&gt;</span>
		}
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"company"</span><span class="nt">&gt;</span>
		{
			"name": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"name"</span><span class="nt">/&gt;</span>",
			"city": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"city"</span><span class="nt">/&gt;</span>",
			"state": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"state"</span><span class="nt">/&gt;</span>",
			"employees": [<span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/person"</span><span class="nt">/&gt;</span>
			]
		}
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"person"</span><span class="nt">&gt;</span>
		{
			"name": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"name"</span><span class="nt">/&gt;</span>",
			"position": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"position"</span><span class="nt">/&gt;</span>",
			"age": <span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"age"</span><span class="nt">/&gt;</span>,
			"books": [<span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/book"</span><span class="nt">/&gt;</span>
			]
		}
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"book"</span><span class="nt">&gt;</span>
		{
			"title": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"title"</span><span class="nt">/&gt;</span>",
			"author": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"author"</span><span class="nt">/&gt;</span>",
			"publicationYear": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"publicationYear"</span><span class="nt">/&gt;</span>",
		}
	<span class="nt">&lt;/xsl:template&gt;</span>
<span class="nt">&lt;/xsl:stylesheet&gt;</span>
</code></pre></div></div>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">XsltJsonMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">output</span> <span class="p">=</span> <span class="n">String</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">StringWriter</span> <span class="n">sw</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringWriter</span><span class="p">())</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">XmlWriter</span> <span class="n">xwo</span> <span class="p">=</span> <span class="n">XmlWriter</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="n">sw</span><span class="p">,</span> <span class="n">xsltJson</span><span class="p">.</span><span class="n">OutputSettings</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="n">xsltJson</span><span class="p">.</span><span class="nf">Transform</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">xwo</span><span class="p">);</span>
        <span class="n">output</span> <span class="p">=</span> <span class="n">sw</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="xslt---json-mapping-1">XSLT -&gt; Json Mapping</h3>

<p>In this case, we look forward to directly transform the xml document into target structure without use of any classes but this time directly into JSON. Although, the XSLCompile Transform only supports XSL v1.0 (which does not have json rendering) but just making the version as 2.0 in the XSL let’s us build and run the code, else it would not build itself.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;xsl:stylesheet</span> <span class="na">version=</span><span class="s">"2.0"</span> <span class="na">xmlns:xsl=</span><span class="s">"http://www.w3.org/1999/XSL/Transform"</span><span class="nt">&gt;</span>
	<span class="nt">&lt;xsl:output</span> <span class="na">method=</span><span class="s">"json"</span> <span class="na">indent=</span><span class="s">"yes"</span> <span class="na">omit-xml-declaration=</span><span class="s">"yes"</span><span class="nt">/&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"/"</span><span class="nt">&gt;</span>
		{
			"company": <span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"root/company"</span><span class="nt">/&gt;</span>
		}
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"company"</span><span class="nt">&gt;</span>
		{
			"name": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"name"</span><span class="nt">/&gt;</span>",
			"city": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"city"</span><span class="nt">/&gt;</span>",
			"state": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"state"</span><span class="nt">/&gt;</span>",
			"employees": [<span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/person"</span><span class="nt">/&gt;</span>
			]
		}
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"person"</span><span class="nt">&gt;</span>
		{
			"name": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"name"</span><span class="nt">/&gt;</span>",
			"position": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"position"</span><span class="nt">/&gt;</span>",
			"age": <span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"age"</span><span class="nt">/&gt;</span>,
			"books": [<span class="nt">&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/book"</span><span class="nt">/&gt;</span>
			]
		}
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"book"</span><span class="nt">&gt;</span>
		{
			"title": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"title"</span><span class="nt">/&gt;</span>",
			"author": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"author"</span><span class="nt">/&gt;</span>",
			"publicationYear": "<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"publicationYear"</span><span class="nt">/&gt;</span>",
		}
	<span class="nt">&lt;/xsl:template&gt;</span>
<span class="nt">&lt;/xsl:stylesheet&gt;</span>
</code></pre></div></div>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">XsltJsonMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">output</span> <span class="p">=</span> <span class="n">String</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">StringWriter</span> <span class="n">sw</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringWriter</span><span class="p">())</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">XmlWriter</span> <span class="n">xwo</span> <span class="p">=</span> <span class="n">XmlWriter</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="n">sw</span><span class="p">,</span> <span class="n">xsltJson</span><span class="p">.</span><span class="n">OutputSettings</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="n">xsltJson</span><span class="p">.</span><span class="nf">Transform</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">xwo</span><span class="p">);</span>
        <span class="n">output</span> <span class="p">=</span> <span class="n">sw</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="xslt---text-mapping">XSLT -&gt; Text Mapping</h3>

<p>In this case, we look forward to directly transform the xml document into target structure without use of any classes but this time directly into JSON but as text.</p>

<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;xsl:stylesheet</span> <span class="na">version=</span><span class="s">"1.0"</span> <span class="na">xmlns:xsl=</span><span class="s">"http://www.w3.org/1999/XSL/Transform"</span><span class="nt">&gt;</span>
	<span class="nt">&lt;xsl:output</span> <span class="na">method=</span><span class="s">"text"</span> <span class="na">omit-xml-declaration=</span><span class="s">"yes"</span><span class="nt">/&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"/"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;xsl:text&gt;</span> {
			"company": <span class="nt">&lt;/xsl:text&gt;&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"root/company"</span><span class="nt">/&gt;&lt;xsl:text&gt;</span>
		}<span class="nt">&lt;/xsl:text&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"company"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;xsl:text&gt;</span>{
			"name": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"name"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"city": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"city"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"state": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"state"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"employees": [<span class="nt">&lt;/xsl:text&gt;&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/person"</span><span class="nt">/&gt;&lt;xsl:text&gt;</span>
			]
		}<span class="nt">&lt;/xsl:text&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"person"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;xsl:text&gt;</span>{
			"name": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"name"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"position": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"position"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"age": <span class="nt">&lt;/xsl:text&gt;&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"age"</span><span class="nt">/&gt;&lt;xsl:text&gt;</span>,
			"books": [<span class="nt">&lt;/xsl:text&gt;&lt;xsl:apply-templates</span> <span class="na">select=</span><span class="s">"/root/book"</span><span class="nt">/&gt;&lt;xsl:text&gt;</span>
			]
		}<span class="nt">&lt;/xsl:text&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>

	<span class="nt">&lt;xsl:template</span> <span class="na">match=</span><span class="s">"book"</span><span class="nt">&gt;</span>
		<span class="nt">&lt;xsl:text&gt;</span>{
			"title": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"title"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"author": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"author"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>,
			"publicationYear": <span class="nt">&lt;/xsl:text&gt;</span>"<span class="nt">&lt;xsl:value-of</span> <span class="na">select=</span><span class="s">"publicationYear"</span><span class="nt">/&gt;</span>"<span class="nt">&lt;xsl:text&gt;</span>
		}<span class="nt">&lt;/xsl:text&gt;</span>
	<span class="nt">&lt;/xsl:template&gt;</span>
<span class="nt">&lt;/xsl:stylesheet&gt;</span>
</code></pre></div></div>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">XsltTextMapping</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">string</span> <span class="n">output</span> <span class="p">=</span> <span class="n">String</span><span class="p">.</span><span class="n">Empty</span><span class="p">;</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">StringWriter</span> <span class="n">sw</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">StringWriter</span><span class="p">())</span>
    <span class="k">using</span> <span class="p">(</span><span class="n">XmlWriter</span> <span class="n">xwo</span> <span class="p">=</span> <span class="n">XmlWriter</span><span class="p">.</span><span class="nf">Create</span><span class="p">(</span><span class="n">sw</span><span class="p">,</span> <span class="n">xsltText</span><span class="p">.</span><span class="n">OutputSettings</span><span class="p">))</span>
    <span class="p">{</span>
        <span class="n">xsltText</span><span class="p">.</span><span class="nf">Transform</span><span class="p">(</span><span class="n">doc</span><span class="p">,</span> <span class="n">xwo</span><span class="p">);</span>
        <span class="n">output</span> <span class="p">=</span> <span class="n">sw</span><span class="p">.</span><span class="nf">ToString</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="c1">// PrintJson(output);</span>
<span class="p">}</span>
</code></pre></div></div>

<h2 id="choosing-your-mapper">Choosing Your Mapper</h2>

<p>This article provides with the basic details on how we can map and takes into account the performance only at the mapping level and not the setup part of it. This is intended to explain and explore the available ways to map and how they might perform, but overall I am trying to encourage to explore the how and why to choose a tech stack to be dependent on a data-based decision rather than just like that! Happy and performant coding!</p>

<hr />

<h2 id="related-reading">Related Reading</h2>

<ul>
  <li><a href="/technical/.net/.net-core/improve-iteration-performance/">Collection iteration performance</a></li>
  <li><a href="/technical/.net/.net-core/native-mapping-operations/">Native mapping techniques</a></li>
  <li><a href="/technical/.net/.net-core/ef-core-performance/">EF Core performance optimization</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="XSLT" /><category term="Mapping" /><category term="Performance" /><summary type="html"><![CDATA[Exploring and benchmarking various object transformation and mapping strategies in .NET including AutoMapper, Mapster, XSLT, and manual mapping.]]></summary></entry><entry><title type="html">C# Collection Iteration Performance: Benchmarking For, Foreach, LINQ, and Span</title><link href="https://animatlabs.com/technical/.net/.net-core/improve-iteration-performance/" rel="alternate" type="text/html" title="C# Collection Iteration Performance: Benchmarking For, Foreach, LINQ, and Span" /><published>2023-07-11T00:00:00+05:30</published><updated>2023-07-11T00:00:00+05:30</updated><id>https://animatlabs.com/technical/.net/.net-core/improve-iteration-performance</id><content type="html" xml:base="https://animatlabs.com/technical/.net/.net-core/improve-iteration-performance/"><![CDATA[<p>When we discuss about any programming language, perhaps the first thing we look into after variables is iterations. Although there are many types of iterations available over several types, lets explore a few available with .NET 6 and also explore the code for the same. For benchmarking, we will be using a third-party library called <code class="language-plaintext highlighter-rouge">BenchmarkDotNet</code>.</p>

<p><strong>You can access the entire code from my</strong> <a href="https://github.com/animat089/playground/tree/main/Benchmarking/Iterations" class="btn btn--primary">GitHub Repo</a></p>

<h2 id="exploring-various-types">Exploring Various Types</h2>

<p>When we discuss about collections, we generally think about quite a few type of collections and sometimes even implement those without even the proper understanding on why to use a specific one… In the study here, we are going to explore the performance in .NET 6.0 for the types as follows:</p>

<ol>
  <li>Array</li>
  <li>Enumerable</li>
  <li>List</li>
</ol>

<h2 id="exploring-various-looping-mechanisms">Exploring Various Looping Mechanisms</h2>

<p>In terms of loops that we are going to use, we are going to be fairly simple:</p>

<ol>
  <li>For</li>
  <li>ForEach</li>
  <li>ForEach over <a href="https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay">Span</a> (think of pointers references in C/C++)</li>
  <li>Linq based Foreach</li>
  <li>For over reference via MemoryMarshal (think of pointers references in C/C++)</li>
  <li>Parallel.ForAll()</li>
  <li>Parallel.ForEach()</li>
</ol>

<h2 id="results-first---lets-blow-your-mind">Results First - Let’s blow your mind…</h2>

<p>To set the context, we have performed the said benchmarking over the collections of 10, 1_000, and 100_000 size of elements all generated in random and some methods are specific to a certain type therefore missing items in the block!</p>

<figure class=""><img src="/assets/images/posts/2023-07-11/PerformanceResults.JPG" alt="Performance Results" /><figcaption>
      Iteration Performance Looping over different types and methods

    </figcaption></figure>

<blockquote>
  <p>The performance is computed as a whole for looping and accessing the item in the collection and the caveat that spans to not have an extension on top of IEnumerable to be able to do any operations with the same.</p>
</blockquote>

<p>As per the computations above, the best ways of iterations over different types of records are as follows:</p>

<ol>
  <li>Array - ForEach (ForEach over .AsSpan() - Marginally different)</li>
  <li>Enumerable - ForEach</li>
  <li>List - Foreach over Span</li>
</ol>

<p>In any of the comparison block, we can clearly see that iterations over an array or even a list is much faster than that over an Enumeration.</p>

<h2 id="looking-into-the-implementations">Looking into the implementations</h2>

<p>So, to begin it off lets look into how we generated a sample set data, we used a random value generator and generated an enumerable that gets the defines size of enumerable returned form the method. Apart from that, there is an extension for the ForEach on the Enumerable itself for uses later.</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">internal</span> <span class="k">static</span> <span class="k">class</span> <span class="nc">ListGenerator</span>
<span class="p">{</span>
    <span class="k">private</span> <span class="k">static</span> <span class="k">readonly</span> <span class="n">Random</span> <span class="n">random</span> <span class="p">=</span> <span class="k">new</span> <span class="nf">Random</span><span class="p">(</span><span class="m">10</span><span class="n">_00_000</span><span class="p">);</span>

    <span class="k">public</span> <span class="k">static</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="nf">GenerateList</span><span class="p">(</span><span class="kt">int</span> <span class="n">size</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">return</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="n">size</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="n">random</span><span class="p">.</span><span class="nf">Next</span><span class="p">()).</span><span class="nf">AsEnumerable</span><span class="p">();</span>
    <span class="p">}</span>

    <span class="k">public</span> <span class="k">static</span> <span class="k">void</span> <span class="n">ForEach</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;(</span><span class="k">this</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">@this</span><span class="p">,</span> <span class="n">Action</span><span class="p">&lt;</span><span class="n">T</span><span class="p">&gt;</span> <span class="n">action</span><span class="p">)</span>
    <span class="p">{</span>
        <span class="k">foreach</span> <span class="p">(</span><span class="n">T</span> <span class="n">item</span> <span class="k">in</span> <span class="n">@this</span><span class="p">)</span>
        <span class="p">{</span>
            <span class="nf">action</span><span class="p">(</span><span class="n">item</span><span class="p">);</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Now, given we have setup the sample set generator, let’s look into setting up the collections for iterations:</p>

<h3 id="global-setup">Global Setup</h3>

<p>The <code class="language-plaintext highlighter-rouge">GlobalSetupAttribute</code> is used over the method that sets up the data for the performance benchmarking in the library that we are referencing to. Along with the same, we use the <code class="language-plaintext highlighter-rouge">ParamsAttribute</code> for defining the set of values for which the globalSetup and the test cases need to run for:</p>

<div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">private</span> <span class="kt">int</span><span class="p">[]</span> <span class="n">sampleSetArray</span><span class="p">;</span>
<span class="k">private</span> <span class="n">List</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="n">sampleSetList</span><span class="p">;</span>
<span class="k">private</span> <span class="n">IEnumerable</span><span class="p">&lt;</span><span class="kt">int</span><span class="p">&gt;</span> <span class="n">sampleSetEnumerable</span><span class="p">;</span>

<span class="p">[</span><span class="nf">Params</span><span class="p">(</span><span class="m">10</span><span class="p">,</span> <span class="m">1</span><span class="n">_000</span><span class="p">,</span> <span class="m">1</span><span class="n">_00_000</span><span class="p">)]</span>
<span class="k">public</span> <span class="kt">int</span> <span class="n">Size</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="n">GlobalSetup</span><span class="p">]</span>
<span class="k">public</span> <span class="k">void</span> <span class="nf">SetupData</span><span class="p">()</span>
<span class="p">{</span>
    <span class="kt">var</span> <span class="n">sampleSet</span> <span class="p">=</span> <span class="n">ListGenerator</span><span class="p">.</span><span class="nf">GenerateList</span><span class="p">(</span><span class="n">Size</span><span class="p">);</span>

    <span class="n">sampleSetArray</span> <span class="p">=</span> <span class="n">sampleSet</span><span class="p">.</span><span class="nf">ToArray</span><span class="p">();</span>
    <span class="n">sampleSetList</span> <span class="p">=</span> <span class="n">sampleSet</span><span class="p">.</span><span class="nf">ToList</span><span class="p">();</span>
    <span class="n">sampleSetEnumerable</span> <span class="p">=</span> <span class="n">sampleSet</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="array">Array</h3>

<p>Now, let’s look into all the code written for the loops with Arrays</p>

<ul>
  <li>
    <p><strong>For</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_For</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">iterator</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">iterator</span> <span class="p">&lt;</span> <span class="n">sampleSetArray</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">iterator</span><span class="p">++)</span>
      <span class="p">{</span>
          <span class="kt">var</span> <span class="n">item</span> <span class="p">=</span> <span class="n">sampleSetArray</span><span class="p">[</span><span class="n">iterator</span><span class="p">];</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEach</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_ForEach</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">item</span> <span class="k">in</span> <span class="n">sampleSetArray</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEachLinq</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_ForEachLinq</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">Array</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">(</span><span class="n">sampleSetArray</span><span class="p">,</span> <span class="p">(</span><span class="n">item</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ParallelForEach</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_ParallelForEach</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">Parallel</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">(</span><span class="n">sampleSetArray</span><span class="p">,</span> <span class="n">item</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ParallelForAll</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_ParallelForAll</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">sampleSetArray</span><span class="p">.</span><span class="nf">AsParallel</span><span class="p">().</span><span class="nf">ForAll</span><span class="p">(</span><span class="n">item</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEachAsSpan</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_ForEachAsSpan</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">item</span> <span class="k">in</span> <span class="n">sampleSetArray</span><span class="p">.</span><span class="nf">AsSpan</span><span class="p">())</span>
      <span class="p">{</span>
          <span class="c1">//Perform Something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForMemoryMarshalSpanUnsafe</strong></p>
    <blockquote>
      <p>Note: This method only exists for arrays and nothing else</p>
    </blockquote>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Array_ForMemoryMarshalSpanUnsafe</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="c1">// Get Reference of the first item in the collection</span>
      <span class="k">ref</span> <span class="kt">var</span> <span class="n">itemRef</span> <span class="p">=</span> <span class="k">ref</span> <span class="n">MemoryMarshal</span><span class="p">.</span><span class="nf">GetArrayDataReference</span><span class="p">(</span><span class="n">sampleSetArray</span><span class="p">);</span>
      <span class="k">for</span> <span class="p">(</span><span class="kt">int</span> <span class="n">iterator</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">iterator</span> <span class="p">&lt;</span> <span class="n">sampleSetArray</span><span class="p">.</span><span class="n">Length</span><span class="p">;</span> <span class="n">iterator</span><span class="p">++)</span>
      <span class="p">{</span>
          <span class="kt">var</span> <span class="n">item</span> <span class="p">=</span> <span class="n">Unsafe</span><span class="p">.</span><span class="nf">Add</span><span class="p">(</span><span class="k">ref</span> <span class="n">itemRef</span><span class="p">,</span> <span class="n">iterator</span><span class="p">);</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="list">List</h3>

<p>Now, let’s look into all the code written for the loops with Lists</p>

<ul>
  <li>
    <p><strong>For</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">List_For</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">iterator</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">iterator</span> <span class="p">&lt;</span> <span class="n">sampleSetList</span><span class="p">.</span><span class="nf">Count</span><span class="p">();</span> <span class="n">iterator</span><span class="p">++)</span>
      <span class="p">{</span>
          <span class="kt">var</span> <span class="n">item</span> <span class="p">=</span> <span class="n">sampleSetList</span><span class="p">[</span><span class="n">iterator</span><span class="p">];</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEach</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">List_Foreach</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">item</span> <span class="k">in</span> <span class="n">sampleSetList</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEachLinq</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">List_ForEachLinq</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">sampleSetList</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">((</span><span class="n">item</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ParallelForEach</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">List_ParallelForEach</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">Parallel</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">(</span><span class="n">sampleSetList</span><span class="p">,</span> <span class="n">item</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ParallelForAll</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">List_ParallelForAll</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">sampleSetList</span><span class="p">.</span><span class="nf">AsParallel</span><span class="p">().</span><span class="nf">ForAll</span><span class="p">(</span><span class="n">item</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEachAsSpanUnsafe</strong></p>
    <blockquote>
      <p>Point to note here, unlike in the case fo arrays, this charters into the unsafe code territory!</p>
    </blockquote>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">List_ForEachAsSpanUnsafe</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">item</span> <span class="k">in</span> <span class="n">CollectionsMarshal</span><span class="p">.</span><span class="nf">AsSpan</span><span class="p">(</span><span class="n">sampleSetList</span><span class="p">))</span>
      <span class="p">{</span>
          <span class="c1">//Perform Something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<h3 id="enumerable">Enumerable</h3>

<p>Now, let’s look into all the code written for the loops with Enumerables</p>

<ul>
  <li>
    <p><strong>For</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Enumerable_For</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">iterator</span> <span class="p">=</span> <span class="m">0</span><span class="p">;</span> <span class="n">iterator</span> <span class="p">&lt;</span> <span class="n">sampleSetEnumerable</span><span class="p">.</span><span class="nf">Count</span><span class="p">();</span> <span class="n">iterator</span><span class="p">++)</span>
      <span class="p">{</span>
          <span class="kt">var</span> <span class="n">item</span> <span class="p">=</span> <span class="n">sampleSetEnumerable</span><span class="p">.</span><span class="nf">ElementAt</span><span class="p">(</span><span class="n">iterator</span><span class="p">);</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEach</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Enumerable_Foreach</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="k">foreach</span> <span class="p">(</span><span class="kt">int</span> <span class="n">item</span> <span class="k">in</span> <span class="n">sampleSetEnumerable</span><span class="p">)</span>
      <span class="p">{</span>
          <span class="c1">//Perform something</span>
      <span class="p">}</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ForEachLinq</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Enumerable_ForEachLinq</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">sampleSetEnumerable</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">((</span><span class="n">item</span><span class="p">)</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ParallelForEach</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Enumerable_ParallelForEach</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">Parallel</span><span class="p">.</span><span class="nf">ForEach</span><span class="p">(</span><span class="n">sampleSetEnumerable</span><span class="p">,</span> <span class="n">item</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
  <li>
    <p><strong>ParallelForAll</strong></p>

    <div class="language-c# highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="p">[</span><span class="n">Benchmark</span><span class="p">]</span>
  <span class="k">public</span> <span class="k">void</span> <span class="nf">Enumerable_ParallelForAll</span><span class="p">()</span>
  <span class="p">{</span>
      <span class="n">sampleSetEnumerable</span><span class="p">.</span><span class="nf">AsParallel</span><span class="p">().</span><span class="nf">ForAll</span><span class="p">(</span><span class="n">item</span> <span class="p">=&gt;</span> <span class="p">{</span> <span class="p">});</span>
  <span class="p">}</span>
</code></pre></div>    </div>
  </li>
</ul>

<h2 id="what-the-numbers-tell-us">What the Numbers Tell Us</h2>

<p>Given, now we have seen the perspective of the that it is not just the looping mechanism but also the collection type that plays a vital role in the process. Assuming that this will help you in understanding the process better and using the information to write better code!</p>

<hr />

<h2 id="see-also">See Also</h2>

<ul>
  <li><a href="/technical/.net/.net-core/mapping-performance/">Object mapping performance</a></li>
  <li><a href="/technical/.net/.net-core/native-mapping-operations/">Native mapping techniques</a></li>
  <li><a href="/technical/.net/.net-core/three-string-operations-should-know/">Three string operations worth knowing</a></li>
</ul>]]></content><author><name>Animesh Agarwal</name><email>animesh@animatlabs.com</email></author><category term="Technical" /><category term=".NET" /><category term=".NET-Core" /><category term="C#" /><category term=".NET" /><category term=".NET-Core" /><category term="Iteration" /><category term="Performance" /><summary type="html"><![CDATA[Benchmarking different collection iteration methods in .NET including for, foreach, LINQ, and Span to find the fastest approach.]]></summary></entry></feed>