<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Correlate on Mohamad Dbouk</title>
    <link>https://mdbouk.com/tags/correlate/</link>
    <description>Recent content in Correlate on Mohamad Dbouk</description>
    <language>en-us</language>
    <lastBuildDate>Mon, 11 May 2026 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://mdbouk.com/tags/correlate/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Correlate Everything in .NET</title>
      <link>https://mdbouk.com/correlate-everything-in-dotnet/</link>
      <pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate>
      <guid>https://mdbouk.com/correlate-everything-in-dotnet/</guid>
      <description>Add correlation IDs to ASP.NET Core with Correlate, propagate them across HttpClient and Azure Service Bus, and never lose a request again. Tested companion repo included.</description>
      <content:encoded><![CDATA[<p>Imagine you run a handful of microservices in an event-driven architecture where every request flows asynchronously through queues and background workers. Structured logging is in place and life is good.</p>
<p>Until a customer reports a failure. You open your log viewer (Seq, Datadog, Application Insights, take your pick) and you do see an error, but the breadcrumb trail stops there. Which request triggered it? Did it originate from another service, or is it part of a larger saga? Without more context you have little hope of stitching the story together.</p>
<p>What is missing is correlation. When you link the actions and log entries that belong to the same request with a shared correlation identifier, you can follow a request across services, topics, and threads.</p>
<p>The idea is simple. Create or forward a correlation ID at the edge of your system, hold onto it for the duration of the request, and pass it along to any downstream call. Every log line written within that flow gets stamped with the same identifier, so the full journey becomes searchable.</p>
<p>Rather than wiring this plumbing by hand, you can hand it off to <a href="https://github.com/skwasjer/Correlate">Correlate</a>. It is a small library that manages the lifecycle of the correlation ID, exposes it to your code, and propagates it for you.</p>
<blockquote>
<p>All the code in this post is in a runnable companion repo: <a href="https://github.com/mhdbouk/correlate-everything-in-dotnet">mhdbouk/correlate-everything-in-dotnet</a>. Two web services, one Azure Service Bus consumer, and ten passing tests that prove every claim.</p></blockquote>
<p><strong>TL;DR</strong></p>
<ul>
<li>Install <code>Correlate.AspNetCore</code> and call <code>AddCorrelate()</code> + <code>UseCorrelate()</code> in <code>Program.cs</code>.</li>
<li>Inject <code>ICorrelationContextAccessor</code> to read the current correlation ID from any service.</li>
<li>Chain <code>.CorrelateRequests(&quot;X-Correlation-ID&quot;)</code> on every <code>HttpClient</code> to propagate the ID outbound.</li>
<li>For Azure Service Bus, write the ID into <code>ServiceBusMessage.CorrelationId</code> on publish, then wrap consumers in <code>IAsyncCorrelationManager.CorrelateAsync(message.CorrelationId, ...)</code> to restore it.</li>
<li>Add <code>Enrich.FromLogContext()</code> to Serilog so every log line carries the correlation ID as a structured property.</li>
</ul>
<h2 id="install-the-package">Install the package</h2>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>dotnet add package Correlate.AspNetCore
</span></span></code></pre></div><p>That single package pulls in the core library and the dependency injection extensions.</p>
<h2 id="register-and-add-the-middleware">Register and add the middleware</h2>
<p>Register the services in <code>Program.cs</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">var</span> builder = WebApplication.CreateBuilder(args);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>builder.Services.AddCorrelate(options =&gt;
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    options.RequestHeaders = [<span style="color:#e6db74">&#34;X-Correlation-ID&#34;</span>];
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">var</span> app = builder.Build();
</span></span></code></pre></div><p>The <code>RequestHeaders</code> list controls which inbound headers Correlate inspects. If the request already carries one of those headers, that value is used. If not, a new ID is generated. You can list more than one entry to support older clients that send a different header name.</p>
<p>Add the middleware before any handler you want correlated:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>app.UseCorrelate();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> app.RunAsync();
</span></span></code></pre></div><p>From this point on, every request lives inside a request-scoped correlation context.</p>
<h2 id="read-the-id-inside-your-code">Read the ID inside your code</h2>
<p>Anywhere you need the current ID, inject <code>ICorrelationContextAccessor</code>:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">internal</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderService</span>
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ICorrelationContextAccessor _accessor;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ILogger&lt;OrderService&gt; _logger;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> OrderService(ICorrelationContextAccessor accessor, ILogger&lt;OrderService&gt; logger)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _accessor = accessor;
</span></span><span style="display:flex;"><span>        _logger = logger;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Task PlaceOrderAsync(OrderRequest request, CancellationToken ct)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">var</span> id = _accessor.CorrelationContext?.CorrelationId ?? <span style="color:#e6db74">&#34;none&#34;</span>;
</span></span><span style="display:flex;"><span>        _logger.LogInformation(<span style="color:#e6db74">&#34;Placing order under correlation {CorrelationId}&#34;</span>, id);
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> Task.CompletedTask;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>The context is <code>AsyncLocal</code> under the hood, so it survives across <code>await</code> boundaries within the same logical request.</p>
<h2 id="propagate-it-to-outbound-http-calls">Propagate it to outbound HTTP calls</h2>
<p>This is where most setups quietly break. You correlate the inbound side, then forget the outbound side, and the chain ends at the first downstream call.</p>
<p>Correlate ships a <code>DelegatingHandler</code> that attaches the current ID to outbound requests. Wire it onto any <code>HttpClient</code> you register:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>builder.Services
</span></span><span style="display:flex;"><span>    .AddHttpClient&lt;IPaymentClient, PaymentClient&gt;()
</span></span><span style="display:flex;"><span>    .CorrelateRequests(<span style="color:#e6db74">&#34;X-Correlation-ID&#34;</span>);
</span></span></code></pre></div><p>Every outbound request sent through that client now carries the correlation ID in the configured header. The downstream service, also running Correlate, picks it up at its own edge, and the chain continues.</p>
<h2 id="propagate-the-correlation-id-across-azure-service-bus">Propagate the correlation ID across Azure Service Bus</h2>
<p>HTTP is the easy hop. Message buses are where correlation usually gets dropped. The fix is the same idea: read the ID before you publish, restore it after you consume.</p>
<p>Azure Service Bus makes this clean because every <code>ServiceBusMessage</code> has a built-in <code>CorrelationId</code> property on the envelope. You do not need a custom header. Read from Correlate on publish, write into the envelope, restore on consume.</p>
<p>On the publish side, stamp the current correlation ID onto the message:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">static</span> ServiceBusMessage Create&lt;T&gt;
</span></span><span style="display:flex;"><span>(
</span></span><span style="display:flex;"><span>    T payload,
</span></span><span style="display:flex;"><span>    ICorrelationContextAccessor accessor,
</span></span><span style="display:flex;"><span>    JsonSerializerOptions? serializerOptions = <span style="color:#66d9ef">null</span>
</span></span><span style="display:flex;"><span>)
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">var</span> body = JsonSerializer.Serialize(payload, serializerOptions);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">new</span> ServiceBusMessage(body)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        ContentType = <span style="color:#e6db74">&#34;application/json&#34;</span>,
</span></span><span style="display:flex;"><span>        Subject = <span style="color:#66d9ef">typeof</span>(T).Name,
</span></span><span style="display:flex;"><span>        MessageId = Guid.NewGuid().ToString(),
</span></span><span style="display:flex;"><span>        CorrelationId = accessor.CorrelationContext?.CorrelationId ?? Guid.NewGuid().ToString()
</span></span><span style="display:flex;"><span>    };
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>On the consume side, wrap the handler in <code>IAsyncCorrelationManager.CorrelateAsync</code>, using the value off the envelope:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span><span style="color:#66d9ef">public</span> <span style="color:#66d9ef">sealed</span> <span style="color:#66d9ef">class</span> <span style="color:#a6e22e">OrderPlacedProcessor</span> : IHostedService
</span></span><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> IAsyncCorrelationManager _correlationManager;
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> <span style="color:#66d9ef">readonly</span> ServiceBusProcessor _processor;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> OrderPlacedProcessor
</span></span><span style="display:flex;"><span>    (
</span></span><span style="display:flex;"><span>        ServiceBusClient client,
</span></span><span style="display:flex;"><span>        IAsyncCorrelationManager correlationManager
</span></span><span style="display:flex;"><span>    )
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        _correlationManager = correlationManager;
</span></span><span style="display:flex;"><span>        _processor = client.CreateProcessor(<span style="color:#e6db74">&#34;orders&#34;</span>);
</span></span><span style="display:flex;"><span>        _processor.ProcessMessageAsync += OnMessageReceivedAsync;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">private</span> Task OnMessageReceivedAsync(ProcessMessageEventArgs args)
</span></span><span style="display:flex;"><span>    {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> _correlationManager.CorrelateAsync
</span></span><span style="display:flex;"><span>        (
</span></span><span style="display:flex;"><span>            args.Message.CorrelationId,
</span></span><span style="display:flex;"><span>            <span style="color:#66d9ef">async</span> () =&gt;
</span></span><span style="display:flex;"><span>            {
</span></span><span style="display:flex;"><span>                <span style="color:#75715e">// your handler runs here under the same correlation context the publisher was in</span>
</span></span><span style="display:flex;"><span>                <span style="color:#66d9ef">await</span> args.CompleteMessageAsync(args.Message, args.CancellationToken);
</span></span><span style="display:flex;"><span>            }
</span></span><span style="display:flex;"><span>        );
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Task StartAsync(CancellationToken ct) =&gt; _processor.StartProcessingAsync(ct);
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">public</span> Task StopAsync(CancellationToken ct) =&gt; _processor.StopProcessingAsync(ct);
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p>That is the whole pattern. Publisher writes the ID into a property the SDK already has, consumer pulls it back out and restores the ambient context. Every log line on the worker now carries the same correlation ID as the request that produced the message.</p>
<h2 id="wire-it-into-your-logger">Wire it into your logger</h2>
<p>Correlate adds the correlation ID to the active log scope. With Serilog, one line activates it:</p>
<div class="highlight"><pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-csharp" data-lang="csharp"><span style="display:flex;"><span>Log.Logger = <span style="color:#66d9ef">new</span> LoggerConfiguration()
</span></span><span style="display:flex;"><span>    .Enrich.FromLogContext()
</span></span><span style="display:flex;"><span>    .WriteTo.Console()
</span></span><span style="display:flex;"><span>    .CreateLogger();
</span></span></code></pre></div><p>Every entry inside a request now includes <code>CorrelationId</code> as a structured property. With <code>Microsoft.Extensions.Logging</code> and a sink that respects scopes (Datadog, Seq, Application Insights), you get the same behaviour without extra code.</p>
<h2 id="how-is-this-different-from-distributed-tracing">How is this different from distributed tracing?</h2>
<p>The two often get conflated. They solve overlapping problems but they are not the same thing.</p>
<p>W3C TraceContext (<code>traceparent</code>, <code>tracestate</code>) is the protocol used by OpenTelemetry, Datadog APM, and Application Insights to stitch spans into a trace. It is built for tooling, not for humans.</p>
<p>A correlation ID is a single value you control. It survives in places traces do not. A customer-facing error reference. A manual log query. A SQL audit column. A support ticket. Tracing pipelines are opaque to most readers. A correlation ID is the string you paste into chat when you say &ldquo;look at this one&rdquo;.</p>
<p>Use both. Let your APM handle traces. Let Correlate handle the ID that humans search for.</p>
<h2 id="what-you-get-back">What you get back</h2>
<p>Here is the same correlation ID riding through every hop in a real run from the <a href="https://github.com/mhdbouk/correlate-everything-in-dotnet">companion repo</a>:</p>
<table>
  <thead>
      <tr>
          <th>Hop</th>
          <th>Log line</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Caller sets header</td>
          <td><code>X-Correlation-ID: ride-the-id-1778521287</code></td>
      </tr>
      <tr>
          <td>ServiceA receives</td>
          <td><code>[ride-the-id-1778521287] Service A handling order 11111111-...</code></td>
      </tr>
      <tr>
          <td>ServiceA outbound HTTP</td>
          <td><code>[ride-the-id-1778521287] Sending HTTP request GET http://localhost:5002/downstream</code></td>
      </tr>
      <tr>
          <td>ServiceB receives</td>
          <td><code>[ride-the-id-1778521287] Service B handled downstream call</code></td>
      </tr>
      <tr>
          <td>ServiceA publishes to bus</td>
          <td><code>[ride-the-id-1778521287] Service A published OrderPlaced 22222222-... as 2cafa8c9-...</code></td>
      </tr>
      <tr>
          <td>Worker consumes</td>
          <td><code>[ride-the-id-1778521287] Worker handled OrderPlaced 22222222-... under correlation ride-the-id-1778521287</code></td>
      </tr>
  </tbody>
</table>
<p>Six log lines, three processes, one ID. The bracketed prefix is Serilog enrichment kicking in via <code>Enrich.FromLogContext()</code>. The trailing value on the Worker line is <code>ICorrelationContextAccessor.CorrelationContext.CorrelationId</code> read inside the handler, after the context was restored from <code>ServiceBusMessage.CorrelationId</code>. Both resolve to the same string.</p>
<p>With this wired up, a customer-reported failure goes from a hunt to a lookup. Take the correlation ID off the response, paste it into your log viewer, and read the trail across every service that touched the request. Without it, you are stitching timestamps. With it, you are reading a story.</p>
<h2 id="faq">FAQ</h2>
<h4 id="what-is-a-correlation-id-in-net">What is a correlation ID in .NET?</h4>
<p>A correlation ID is a single value attached to every log line, metric, and outbound call that belongs to the same logical request. When you search a log viewer for that value, you see the full journey of the request across services, threads, and queues. In .NET the <a href="https://github.com/skwasjer/Correlate">Correlate</a> library is the most common way to manage one.</p>
<h4 id="do-i-still-need-a-correlation-id-if-i-use-opentelemetry">Do I still need a correlation ID if I use OpenTelemetry?</h4>
<p>Yes. OpenTelemetry trace IDs are designed for tooling pipelines like Datadog APM, Application Insights, and Honeycomb. A correlation ID is the value humans use: customer-facing error references, manual log queries, SQL audit columns, support tickets. They coexist. Let the APM handle traces, let Correlate handle the ID humans search for.</p>
<h4 id="which-http-header-should-i-use-for-a-correlation-id">Which HTTP header should I use for a correlation ID?</h4>
<p><code>X-Correlation-ID</code> is the most common convention in .NET. Correlate inspects it by default and echoes it back on the response. You can configure additional headers via <code>options.RequestHeaders</code> to support older clients that send a different name.</p>
<h4 id="how-do-i-propagate-a-correlation-id-across-an-httpclient">How do I propagate a correlation ID across an HttpClient?</h4>
<p>Chain <code>.CorrelateRequests(&quot;X-Correlation-ID&quot;)</code> on the <code>IHttpClientBuilder</code> returned by <code>AddHttpClient</code>. Correlate registers a <code>DelegatingHandler</code> that stamps the current correlation ID onto every outbound request.</p>
<h4 id="how-do-i-propagate-a-correlation-id-across-azure-service-bus">How do I propagate a correlation ID across Azure Service Bus?</h4>
<p>Azure Service Bus messages have a built-in <code>CorrelationId</code> property on the envelope. Write <code>ICorrelationContextAccessor.CorrelationContext?.CorrelationId</code> into it on publish. On consume, wrap the handler in <code>IAsyncCorrelationManager.CorrelateAsync(message.CorrelationId, ...)</code> to restore the context for every log line and downstream call inside that handler.</p>
<h4 id="is-correlate-still-maintained">Is Correlate still maintained?</h4>
<p>Yes. As of May 2026 the package targets <code>net8.0</code>, <code>net9.0</code>, and <code>net10.0</code>. The active version is <code>Correlate.AspNetCore</code> 6.x on NuGet.</p>
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "FAQPage",
  "mainEntity": [
    {
      "@type": "Question",
      "name": "What is a correlation ID in .NET?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "A correlation ID is a single value attached to every log line, metric, and outbound call that belongs to the same logical request. When you search a log viewer for that value, you see the full journey of the request across services, threads, and queues. In .NET the Correlate library by skwasjer is the most common way to manage one."
      }
    },
    {
      "@type": "Question",
      "name": "Do I still need a correlation ID if I use OpenTelemetry?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. OpenTelemetry trace IDs are designed for tooling pipelines like Datadog APM, Application Insights, and Honeycomb. A correlation ID is the value humans use: customer-facing error references, manual log queries, SQL audit columns, support tickets. They coexist. Let the APM handle traces, let Correlate handle the ID humans search for."
      }
    },
    {
      "@type": "Question",
      "name": "Which HTTP header should I use for a correlation ID?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "X-Correlation-ID is the most common convention in .NET. The Correlate library inspects it by default and echoes it back on the response. You can configure additional headers via options.RequestHeaders to support older clients that send a different name."
      }
    },
    {
      "@type": "Question",
      "name": "How do I propagate a correlation ID across an HttpClient?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Chain .CorrelateRequests(\"X-Correlation-ID\") on the IHttpClientBuilder returned by AddHttpClient. Correlate registers a DelegatingHandler that stamps the current correlation ID onto every outbound request."
      }
    },
    {
      "@type": "Question",
      "name": "How do I propagate a correlation ID across Azure Service Bus?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Azure Service Bus messages have a built-in CorrelationId property on the envelope. Write ICorrelationContextAccessor.CorrelationContext.CorrelationId into it on publish. On consume, wrap the handler in IAsyncCorrelationManager.CorrelateAsync(message.CorrelationId, ...) to restore the context for every log line and downstream call inside that handler."
      }
    },
    {
      "@type": "Question",
      "name": "Is Correlate still maintained?",
      "acceptedAnswer": {
        "@type": "Answer",
        "text": "Yes. As of May 2026 the package targets net8.0, net9.0, and net10.0. The active version is Correlate.AspNetCore 6.x on NuGet."
      }
    }
  ]
}
</script>
]]></content:encoded>
    </item>
  </channel>
</rss>
