<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luciano Azevedo</title>
    <description>The latest articles on DEV Community by Luciano Azevedo (@oluciaano).</description>
    <link>https://dev.to/oluciaano</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2578506%2F87fc6ae6-24ea-4381-9b8a-c4cc17457c5f.jpg</url>
      <title>DEV Community: Luciano Azevedo</title>
      <link>https://dev.to/oluciaano</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/oluciaano"/>
    <language>en</language>
    <item>
      <title>Consuming RabbitMQ Messages as Background Jobs in .NET with NexJob</title>
      <dc:creator>Luciano Azevedo</dc:creator>
      <pubDate>Wed, 28 Oct 2026 14:39:15 +0000</pubDate>
      <link>https://dev.to/oluciaano/consuming-rabbitmq-messages-as-background-jobs-in-net-with-nexjob-om6</link>
      <guid>https://dev.to/oluciaano/consuming-rabbitmq-messages-as-background-jobs-in-net-with-nexjob-om6</guid>
      <description>&lt;p&gt;A couple of weeks ago, I posted about &lt;a href="https://dev.to/oluciaano/consuming-kafka-events-as-background-jobs-in-net-with-nexjob-21j6"&gt;consuming Kafka events as background jobs in .NET&lt;/a&gt;. Today, let's look at RabbitMQ.&lt;/p&gt;

&lt;p&gt;Writing a basic RabbitMQ consumer in C# looks easy on tutorials: you create a connection, open a channel, attach an &lt;code&gt;EventingBasicConsumer&lt;/code&gt; or &lt;code&gt;AsyncDefaultBasicConsumer&lt;/code&gt;, and handle messages.&lt;/p&gt;

&lt;p&gt;Then you run it in production, and edge cases start piling up:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Long-running processing blocks the consumer.&lt;/strong&gt; If a message takes 30 seconds to process (like calling a slow third-party API or generating a PDF), RabbitMQ's &lt;code&gt;consumer_timeout&lt;/code&gt; (default 30 minutes in newer versions, or much less if configured) can fire and drop your channel. Worse, prefetching messages while one thread is stuck can starve other consumers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Ack dilemma.&lt;/strong&gt; If you ack the message as soon as it arrives, you lose data when the worker crashes halfway through. If you hold the ack until everything finishes, an unexpected crash re-delivers the message, causing duplicate side effects unless you hand-roll deduplication everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage/database blips crash the channel.&lt;/strong&gt; If your database is restarting, your handler throws an unhandled exception. In RabbitMQ, an unhandled exception inside the consumer callback often closes the AMQP channel. You have to write reconnection and channel recreation logic yourself.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken trace context.&lt;/strong&gt; Your incoming API request had a W3C &lt;code&gt;traceparent&lt;/code&gt;. Once it went through RabbitMQ, your background logs and spans in OpenTelemetry / Jaeger are completely detached.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Instead of keeping connections open while doing heavy processing, a much cleaner pattern is to &lt;strong&gt;treat RabbitMQ as an ingress trigger&lt;/strong&gt;: pull the message, persist it into durable job storage (Postgres, SQL Server, Redis, Mongo), ack it immediately to the broker, and let a dedicated background job executor handle the heavy lifting, retries, and dead-lettering.&lt;/p&gt;

&lt;p&gt;Here is how we do that with &lt;code&gt;NexJob.RabbitMQ&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Setting Up the Trigger
&lt;/h2&gt;

&lt;p&gt;Install the package:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package NexJob.RabbitMQ
dotnet add package NexJob.PostgreSQL &lt;span class="c"&gt;# or Redis, SqlServer, Mongo, InMemory&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In your &lt;code&gt;Program.cs&lt;/code&gt;, wire up NexJob with your preferred storage, and register the RabbitMQ trigger bound to a job type:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob.RabbitMQ&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WebApplication&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Storage provider (Postgres here, but any NexJob provider works)&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJobPostgres&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;GetConnectionString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"NexJob"&lt;/span&gt;&lt;span class="p"&gt;)!);&lt;/span&gt;

&lt;span class="c1"&gt;// Register NexJob core and bind the RabbitMQ trigger to our job handler&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJob&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddRabbitMqTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HostName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMQ:Host"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Port&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5672&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMQ:User"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Password&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMQ:Pass"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"guest"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;QueueName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"incoming-orders"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TargetQueue&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// internal NexJob queue priority/partition&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PrefetchCount&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  2. Writing the Job Handler
&lt;/h2&gt;

&lt;p&gt;The job handler implements &lt;code&gt;IJob&amp;lt;string&amp;gt;&lt;/code&gt;. The incoming RabbitMQ payload is passed directly as the input string (typically JSON):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IPaymentGateway&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
    &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJob&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IJobContext&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)!;&lt;/span&gt;

        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order {OrderId} (Attempt {Attempt})"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; 
            &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Attempt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Run your business logic safely.&lt;/span&gt;
        &lt;span class="c1"&gt;// If this throws or crashes, RabbitMQ is already freed up.&lt;/span&gt;
        &lt;span class="c1"&gt;// NexJob manages retry delays, backoff, and dead-lettering.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;paymentGateway&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ChargeAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderPayload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;CustomerId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  3. What Happens Under the Hood
&lt;/h2&gt;

&lt;p&gt;When a message hits &lt;code&gt;incoming-orders&lt;/code&gt;, here is the exact sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[ RabbitMQ Queue ]
       │
       ▼
[ NexJob RabbitMQ Trigger ]
       │  1. Reads W3C 'traceparent' header -&amp;gt; injects into JobRecord.TraceParent
       │  2. Uses broker's MessageId as IdempotencyKey (duplicate delivery protection)
       │  3. Calls IScheduler.EnqueueAsync(job)
       ▼
[ Durable Storage (Postgres/Redis/etc.) ]
       │
       ├─ If Commit OK: Trigger immediately sends BasicAck to RabbitMQ.
       │  RabbitMQ's job is done. The broker channel stays clean and free.
       │
       └─ If Storage Down: Trigger Nacks with requeue after 1s backoff.
          If message has no valid JobType: Trigger Nacks with requeue=false (moves to DLX).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The 4 Guarantees You Get for Free:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Broker Ack only after durable commit:&lt;/strong&gt; The message is only acknowledged to RabbitMQ after the job record is safely written to the database. If your app dies before the commit, RabbitMQ still has the message.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native Idempotency:&lt;/strong&gt; The trigger uses RabbitMQ's native &lt;code&gt;BasicProperties.MessageId&lt;/code&gt; as the job's &lt;code&gt;IdempotencyKey&lt;/code&gt;. If a network hiccup causes RabbitMQ to redeliver the message, NexJob detects the duplicate and ignores it instead of double-charging your customer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trace Propagation:&lt;/strong&gt; If the message producer passed a &lt;code&gt;traceparent&lt;/code&gt; header, NexJob parses it and links the background execution to that exact distributed trace. In your dashboard or telemetry logs, you see the entire lifecycle end-to-end.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Channel Isolation:&lt;/strong&gt; If &lt;code&gt;ProcessOrderJob&lt;/code&gt; throws an exception, fails payment, or times out, it does &lt;strong&gt;not&lt;/strong&gt; close your RabbitMQ channel or cause connection thrashing. It follows your NexJob retry policy (e.g., 3 retries with exponential backoff) and lands in the dead-letter queue in your dashboard if all retries fail.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  4. What About Publishing? (The Outbox Producer)
&lt;/h2&gt;

&lt;p&gt;Handling inbound messages is half the story. The other common failure point is publishing to RabbitMQ from your API handlers.&lt;/p&gt;

&lt;p&gt;If you publish directly to RabbitMQ inside an HTTP endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What if the broker is restarting or temporarily unreachable?&lt;/li&gt;
&lt;li&gt;What if RabbitMQ accepts the TCP connection but fails to persist the message to disk before acknowledging?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;code&gt;NexJob.RabbitMQ&lt;/code&gt; includes a resilient &lt;strong&gt;Outbox Producer&lt;/strong&gt; with RabbitMQ &lt;strong&gt;Publisher Confirms&lt;/strong&gt; built-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Registering the Producer:
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJob&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddRabbitMqProducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HostName&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Configuration&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"RabbitMQ:Host"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;??&lt;/span&gt; &lt;span class="s"&gt;"localhost"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DefaultExchange&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders.exchange"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConfirmTimeout&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TimeSpan&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;FromSeconds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Enqueuing Messages via &lt;code&gt;IScheduler&lt;/code&gt;:
&lt;/h3&gt;

&lt;p&gt;Instead of dealing with &lt;code&gt;IConnection&lt;/code&gt; or &lt;code&gt;IChannel&lt;/code&gt; pools in your controllers or services, inject &lt;code&gt;IScheduler&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ApiController&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OrdersController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IScheduler&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ControllerBase&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;HttpPost&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IActionResult&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;CreateOrder&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;FromBody&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="n"&gt;CreateOrderRequest&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;orderEvent&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;OrderCreatedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;NewGuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Enqueues as a reliable background job. &lt;/span&gt;
        &lt;span class="c1"&gt;// Persisted to database first, then delivered to RabbitMQ with Publisher Confirms.&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnqueueRabbitMqAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;exchange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"orders.exchange"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;routingKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"order.created"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;orderEvent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;correlationId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;orderEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;cancellationToken&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;Accepted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;orderEvent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How this works:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;scheduler.EnqueueRabbitMqAsync&lt;/code&gt; saves a small job record in your database.&lt;/li&gt;
&lt;li&gt;NexJob's background dispatcher picks it up, opens a channel with &lt;code&gt;ConfirmSelect()&lt;/code&gt; enabled, and publishes the message.&lt;/li&gt;
&lt;li&gt;The job succeeds &lt;strong&gt;only after RabbitMQ responds with an ACK&lt;/strong&gt; (confirming it saved the message).&lt;/li&gt;
&lt;li&gt;If RabbitMQ responds with a NACK or times out, the job fails, backs off, and retries automatically. You never drop messages due to broker blips.&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  5. Monitoring in the Dashboard
&lt;/h2&gt;

&lt;p&gt;If you use &lt;code&gt;NexJob.Dashboard&lt;/code&gt; or &lt;code&gt;NexJob.Dashboard.Standalone&lt;/code&gt;, you don't need to guess what is happening:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can see incoming trigger jobs, their retry counts, and execution latency.&lt;/li&gt;
&lt;li&gt;The W3C &lt;code&gt;traceparent&lt;/code&gt; and &lt;code&gt;MessageId&lt;/code&gt; are visible and copyable right from the job details page.&lt;/li&gt;
&lt;li&gt;Outbox publishing jobs show up with their delivery attempts and publisher confirm status.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Handling messaging in production is rarely about the protocol; it's about handling partial failures cleanly.&lt;/p&gt;

&lt;p&gt;By using NexJob as the intermediary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;RabbitMQ handles queue distribution and routing.&lt;/li&gt;
&lt;li&gt;NexJob handles persistence, retries, dead-lettering, and distributed tracing.&lt;/li&gt;
&lt;li&gt;Your application code stays clean: just an &lt;code&gt;IJob&amp;lt;string&amp;gt;&lt;/code&gt; implementation and normal &lt;code&gt;IScheduler&lt;/code&gt; calls.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to check out the project or see more samples, here is the repo:&lt;br&gt;
👉 &lt;strong&gt;&lt;a href="https://github.com/oluciano/NexJob" rel="noopener noreferrer"&gt;GitHub: oluciano/NexJob&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Have you hit issues with long-running consumers in RabbitMQ before? How do you currently decouple queue consumption from job execution in your .NET services?&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>architecture</category>
      <category>eventdriven</category>
    </item>
    <item>
      <title>Consuming Kafka events as background jobs in .NET with NexJob</title>
      <dc:creator>Luciano Azevedo</dc:creator>
      <pubDate>Sun, 18 Oct 2026 18:54:02 +0000</pubDate>
      <link>https://dev.to/oluciaano/consuming-kafka-events-as-background-jobs-in-net-with-nexjob-21j6</link>
      <guid>https://dev.to/oluciaano/consuming-kafka-events-as-background-jobs-in-net-with-nexjob-21j6</guid>
      <description>&lt;p&gt;When a message arrives on a Kafka topic, processing it directly inside your consumer loop has a well-known problem: if the handler is slow or makes external network calls, you risk exceeding &lt;code&gt;max.poll.interval.ms&lt;/code&gt; and causing a partition rebalance. If the handler fails halfway through, you either commit the offset and lose the work, or crash the consumer loop.&lt;/p&gt;

&lt;p&gt;A standard pattern to solve this is to decouple ingestion from execution: read the message, persist it immediately as a background job, commit the Kafka offset, and let worker slots process it with retries and dead-lettering.&lt;/p&gt;

&lt;p&gt;This post sets up a Kafka consumer in .NET that turns incoming topic messages into NexJob background jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://dotnet.microsoft.com/download" rel="noopener noreferrer"&gt;.NET 8 or 10 SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A running Kafka broker (e.g., &lt;code&gt;localhost:9092&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. A project and packages
&lt;/h2&gt;

&lt;p&gt;Create a worker service or API project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new worker &lt;span class="nt"&gt;-n&lt;/span&gt; KafkaWorker
&lt;span class="nb"&gt;cd &lt;/span&gt;KafkaWorker
dotnet add package NexJob
dotnet add package NexJob.Kafka
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NexJob.Kafka&lt;/code&gt; bundles both the consumer trigger and the outbox producer in a single library.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Write the job
&lt;/h2&gt;

&lt;p&gt;A consumer trigger hands the raw message payload to a job implementing &lt;code&gt;IJob&amp;lt;string&amp;gt;&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;System.Text.Json&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;OrderPlacedEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Guid&lt;/span&gt; &lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;decimal&lt;/span&gt; &lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;CustomerEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ProcessOrderJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJob&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ILogger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_logger&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;JsonSerializer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Deserialize&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;OrderPlacedEvent&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;InvalidOperationException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Failed to deserialize order payload."&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="n"&gt;_logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;LogInformation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Processing order {OrderId} for {Amount:C}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

        &lt;span class="c1"&gt;// Your business logic here: charge card, update database, call API&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The job is resolved through dependency injection, so you can inject your DbContext, HTTP clients, or repositories into the constructor.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Register the Kafka trigger
&lt;/h2&gt;

&lt;p&gt;In &lt;code&gt;Program.cs&lt;/code&gt;, bind the topic to your job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob.Kafka&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateApplicationBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// Register NexJob engine and the Kafka trigger&lt;/span&gt;
&lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Workers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// Concurrent worker threads processing jobs&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AddKafkaTrigger&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;ProcessOrderJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;&lt;span class="n"&gt;opt&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BootstrapServers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"localhost:9092"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Topic&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="n"&gt;opt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GroupId&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"orders-processor-group"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJobJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;RunAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;AddKafkaTrigger&amp;lt;TJob&amp;gt;&lt;/code&gt; registers the consumer as a background service and automatically registers &lt;code&gt;ProcessOrderJob&lt;/code&gt; as transient in your service container.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. How offsets and retries work
&lt;/h2&gt;

&lt;p&gt;A trigger does not execute the job directly inside the Kafka loop. The lifecycle works as follows:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Consume:&lt;/strong&gt; The consumer reads a message batch from Kafka.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Store:&lt;/strong&gt; Each message is enqueued into NexJob storage. The Kafka message key (or partition + offset) is used as the idempotency key, preventing duplicate jobs if a network glitch occurs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Commit:&lt;/strong&gt; The offset is committed to Kafka &lt;strong&gt;only after&lt;/strong&gt; the job is safely written to storage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Execute:&lt;/strong&gt; A NexJob worker picks up the job from storage and runs &lt;code&gt;ExecuteAsync&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If your business logic throws an exception inside &lt;code&gt;ExecuteAsync&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The Kafka partition is &lt;strong&gt;not&lt;/strong&gt; blocked; other messages continue to be consumed.&lt;/li&gt;
&lt;li&gt;The job follows your NexJob retry policy (exponential backoff by default).&lt;/li&gt;
&lt;li&gt;If retries are exhausted, the job moves to the dead-letter queue.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5. Publishing messages back to Kafka (Outbox pattern)
&lt;/h2&gt;

&lt;p&gt;If your app also needs to produce messages reliably, &lt;code&gt;NexJob.Kafka&lt;/code&gt; includes an outbox producer. Instead of making an in-flight network call to Kafka inside an HTTP request or database transaction, enqueue it through &lt;code&gt;IScheduler&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;MapPost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"/orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OrderPlacedEvent&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;IScheduler&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Save to local database first, then enqueue Kafka message&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;EnqueueKafkaAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;topic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"orders"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OrderId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ToString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="k"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Accepted&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;NexJob persists the message locally and delivers it to Kafka in the background with retry and guaranteed ordering per key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Using a database provider (&lt;code&gt;NexJob.Postgres&lt;/code&gt; or &lt;code&gt;NexJob.SqlServer&lt;/code&gt;) instead of in-memory storage for persistent guarantees across restarts.&lt;/li&gt;
&lt;li&gt;Setting dead-letter alert topics with &lt;code&gt;options.ExhaustedJobsTopic&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Monitoring consumers and worker slots using OpenTelemetry.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source and samples: &lt;a href="https://github.com/oluciano/NexJob" rel="noopener noreferrer"&gt;https://github.com/oluciano/NexJob&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://oluciano.github.io/NexJob/" rel="noopener noreferrer"&gt;https://oluciano.github.io/NexJob/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>kafka</category>
      <category>backgroundjobs</category>
    </item>
    <item>
      <title>Your first background job in .NET with NexJob</title>
      <dc:creator>Luciano Azevedo</dc:creator>
      <pubDate>Tue, 13 Oct 2026 12:57:29 +0000</pubDate>
      <link>https://dev.to/oluciaano/your-first-background-job-in-net-with-nexjob-1c17</link>
      <guid>https://dev.to/oluciaano/your-first-background-job-in-net-with-nexjob-1c17</guid>
      <description>&lt;h2&gt;
  
  
  Your first background job in .NET with NexJob
&lt;/h2&gt;

&lt;p&gt;Sooner or later a web app needs to do something that should not block the request: send an e-mail, resize an image, call a slow API. This post builds the smallest possible background job in .NET 8 and runs it. No database, no extra services, about five minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;a href="https://dotnet.microsoft.com/download" rel="noopener noreferrer"&gt;.NET 8 SDK&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;A terminal&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  1. A project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new worker &lt;span class="nt"&gt;-n&lt;/span&gt; MyJobs
&lt;span class="nb"&gt;cd &lt;/span&gt;MyJobs
dotnet add package NexJob
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A worker service is the simplest host. Everything below works the same inside an ASP.NET Core app.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Register NexJob
&lt;/h2&gt;

&lt;p&gt;Two lines in &lt;code&gt;Program.cs&lt;/code&gt;: one to add NexJob, one to let it find your job classes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;using&lt;/span&gt; &lt;span class="nn"&gt;NexJob&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;CreateDefaultBuilder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ConfigureServices&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJob&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJobJobs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Program&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Assembly&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;Build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;AddNexJob()&lt;/code&gt; once. Calling it twice registers the background services twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Write the job
&lt;/h2&gt;

&lt;p&gt;A job is a class with one method. Implement &lt;code&gt;IJob&lt;/code&gt; when the work needs no input.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;HelloJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJob&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;$"Hello at &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;DateTimeOffset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UtcNow&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;O&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CompletedTask&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things worth knowing right away:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NexJob builds the job from your dependency injection container, so constructor injection works like anywhere else in .NET.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CancellationToken&lt;/code&gt; is yours to honor. When the host shuts down, it is how the job finds out.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Enqueue it
&lt;/h2&gt;

&lt;p&gt;Ask the container for &lt;code&gt;IScheduler&lt;/code&gt; and enqueue the job.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;StartAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="kt"&gt;var&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GetRequiredService&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;IScheduler&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnqueueAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;HelloJob&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;();&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;WaitForShutdownAsync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello at 2026-10-13T12:33:27.9831829+00:00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the whole loop. The job was stored, picked up by a worker running inside your process, and executed. There is no separate worker process to deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  A job that takes input
&lt;/h2&gt;

&lt;p&gt;Most real jobs need data. Implement &lt;code&gt;IJob&amp;lt;T&amp;gt;&lt;/code&gt; and use a &lt;code&gt;record&lt;/code&gt; for the input: it serializes cleanly and cannot change after it is enqueued.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;record&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmailInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;sealed&lt;/span&gt; &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SendWelcomeEmailJob&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;IJob&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SendWelcomeEmailInput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;private&lt;/span&gt; &lt;span class="k"&gt;readonly&lt;/span&gt; &lt;span class="n"&gt;IEmailService&lt;/span&gt; &lt;span class="n"&gt;_email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailJob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;IEmailService&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;_email&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;public&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="n"&gt;Task&lt;/span&gt; &lt;span class="nf"&gt;ExecuteAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SendWelcomeEmailInput&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CancellationToken&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;_email&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;SendAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;$"Welcome, &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;UserName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s"&gt;!"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ct&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EnqueueAsync&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;SendWelcomeEmailJob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SendWelcomeEmailInput&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;(&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nf"&gt;SendWelcomeEmailInput&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"user@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Jane"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What happens if the process stops?
&lt;/h2&gt;

&lt;p&gt;With the default storage, jobs live in memory and are gone when the process exits. That is fine for trying things out and for tests. For anything real, add a database provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet add package NexJob.Postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight csharp"&gt;&lt;code&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJobPostgres&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Host=localhost;Database=nexjob;Username=postgres;Password=secret"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;AddNexJob&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the provider before &lt;code&gt;AddNexJob()&lt;/code&gt;, so it replaces the in-memory default. SQL Server, Redis and MongoDB providers exist too.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Retries and what happens when a job keeps failing&lt;/li&gt;
&lt;li&gt;Delays, recurring jobs and deadlines&lt;/li&gt;
&lt;li&gt;Seeing what your jobs are doing (the next posts in this series)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Source and samples: &lt;a href="https://github.com/oluciano/NexJob" rel="noopener noreferrer"&gt;https://github.com/oluciano/NexJob&lt;/a&gt;&lt;br&gt;
Documentation: &lt;a href="https://oluciano.github.io/NexJob/" rel="noopener noreferrer"&gt;https://oluciano.github.io/NexJob/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dotnet</category>
      <category>csharp</category>
      <category>backgroundjobs</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>🏗️ Aplicando Adapter e Decorator Patterns com Injeção de Dependência no C#</title>
      <dc:creator>Luciano Azevedo</dc:creator>
      <pubDate>Sun, 02 Feb 2025 22:00:25 +0000</pubDate>
      <link>https://dev.to/oluciaano/aplicando-adapter-e-decorator-patterns-com-injecao-de-dependencia-no-c-2cb4</link>
      <guid>https://dev.to/oluciaano/aplicando-adapter-e-decorator-patterns-com-injecao-de-dependencia-no-c-2cb4</guid>
      <description>&lt;p&gt;Ao desenvolver software, muitas vezes precisamos integrar sistemas legados e adicionar funcionalidades sem modificar diretamente o código existente. Para isso, podemos usar padrões de design como Adapter e Decorator, que nos ajudam a estruturar o código de forma modular e reutilizável.&lt;/p&gt;

&lt;p&gt;Neste artigo, vamos explorar como usar esses padrões no C# com injeção de dependência (DI) e a biblioteca Scrutor para simplificar o registro dos decorators.&lt;/p&gt;

&lt;p&gt;🎯 Cenário do Problema&lt;/p&gt;

&lt;p&gt;Imagine que temos um serviço legado que processa pagamentos, mas ele não segue a interface moderna que queremos usar em nosso sistema. Além disso, queremos adicionar logs ao processamento de pagamentos sem modificar diretamente o serviço existente.&lt;/p&gt;

&lt;p&gt;Vamos resolver isso combinando Adapter e Decorator!&lt;/p&gt;

&lt;p&gt;📌 Passo 1: Definindo a Interface Padrão&lt;/p&gt;

&lt;p&gt;Criamos a interface IPaymentProcessor para definir um contrato padrão para processadores de pagamento:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Essa interface será usada tanto pelo Adapter quanto pelo Decorator.&lt;/p&gt;

&lt;p&gt;📌 Passo 2: Criando o Serviço Legado&lt;/p&gt;

&lt;p&gt;Nosso serviço legado (LegacyPaymentService) não segue a interface moderna. Ele espera um string em vez de um decimal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public sealed class LegacyPaymentService
{
    public void MakePayment(string amount)
    {
        Console.WriteLine($"Processing payment of {amount} via legacy system.");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Para integrá-lo ao sistema moderno, usaremos o Adapter Pattern.&lt;/p&gt;

&lt;p&gt;📌 Passo 3: Criando o Adapter&lt;/p&gt;

&lt;p&gt;O Adapter faz a conversão entre IPaymentProcessor e LegacyPaymentService:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public sealed class PaymentAdapter : IPaymentProcessor
{
    private readonly LegacyPaymentService _legacyPaymentService;

    public PaymentAdapter(LegacyPaymentService legacyPaymentService)
    {
        _legacyPaymentService = legacyPaymentService;
    }

    public void ProcessPayment(decimal amount)
    {
        string amountString = amount.ToString("F2");
        _legacyPaymentService.MakePayment(amountString);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora podemos usar PaymentAdapter em qualquer lugar que espera IPaymentProcessor.&lt;/p&gt;

&lt;p&gt;📌 Passo 4: Criando o Decorator para Logging&lt;/p&gt;

&lt;p&gt;Agora adicionamos um Decorator (LoggingDecorator) para registrar logs sempre que um pagamento for processado:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.Extensions.Logging;

public class LoggingDecorator : IPaymentProcessor
{
    private readonly IPaymentProcessor _innerProcessor;
    private readonly ILogger&amp;lt;LoggingDecorator&amp;gt; _logger;

    public LoggingDecorator(IPaymentProcessor innerProcessor, ILogger&amp;lt;LoggingDecorator&amp;gt; logger)
    {
        _innerProcessor = innerProcessor;
        _logger = logger;
    }

    public void ProcessPayment(decimal amount)
    {
        _logger.LogInformation($"Iniciando processamento do pagamento de {amount:C}");
        _innerProcessor.ProcessPayment(amount);
        _logger.LogInformation($"Pagamento de {amount:C} processado com sucesso.");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Esse Decorator adiciona logs antes e depois da chamada real ao PaymentProcessor, sem modificar sua implementação.&lt;/p&gt;

&lt;p&gt;📌 Passo 5: Criando o Worker Service&lt;/p&gt;

&lt;p&gt;Para simular um sistema em execução contínua, criamos um Worker Service que chama IPaymentProcessor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

public class PaymentWorker : BackgroundService
{
    private readonly ILogger&amp;lt;PaymentWorker&amp;gt; _logger;
    private readonly IPaymentProcessor _paymentProcessor;

    public PaymentWorker(ILogger&amp;lt;PaymentWorker&amp;gt; logger, IPaymentProcessor paymentProcessor)
    {
        _logger = logger;
        _paymentProcessor = paymentProcessor;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Iniciando ciclo do Worker...");

            _paymentProcessor.ProcessPayment(100.50m); // Simulando pagamento

            await Task.Delay(5000, stoppingToken); // Executa a cada 5 segundos
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 Passo 6: Configurando a Injeção de Dependência (DI) com Scrutor&lt;/p&gt;

&lt;p&gt;Podemos usar a biblioteca Scrutor para simplificar o registro do Decorator.&lt;br&gt;
Primeiro, instalamos a dependência via NuGet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;dotnet add package Scrutor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agora, configuramos os serviços no Program.cs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Scrutor;

class Program
{
    static void Main(string[] args)
    {
        var host = Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =&amp;gt;
            {
                services.AddSingleton&amp;lt;LegacyPaymentService&amp;gt;(); // Serviço legado

                // Registra o Adapter
                services.AddSingleton&amp;lt;PaymentAdapter&amp;gt;(); 
                services.AddSingleton&amp;lt;IPaymentProcessor&amp;gt;(provider =&amp;gt; 
                    provider.GetRequiredService&amp;lt;PaymentAdapter&amp;gt;());

                // Adiciona o LoggingDecorator automaticamente com Scrutor
                services.Decorate&amp;lt;IPaymentProcessor, LoggingDecorator&amp;gt;();

                // Registra o Worker Service
                services.AddHostedService&amp;lt;PaymentWorker&amp;gt;(); 
                services.AddLogging(); 
            })
            .Build();

        host.Run();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;📌 Estrutura de Pastas&lt;/p&gt;

&lt;p&gt;Aqui está a estrutura do projeto para manter tudo organizado:&lt;/p&gt;

&lt;p&gt;📂 PaymentProcessorApp&lt;br&gt;
│── 📂 Adapters&lt;br&gt;
│   └── PaymentAdapter.cs&lt;br&gt;
│── 📂 Decorators&lt;br&gt;
│   └── LoggingDecorator.cs&lt;br&gt;
│── 📂 Interfaces&lt;br&gt;
│   └── IPaymentProcessor.cs&lt;br&gt;
│── 📂 Services&lt;br&gt;
│   └── LegacyPaymentService.cs&lt;br&gt;
│── 📂 Workers&lt;br&gt;
│   └── PaymentWorker.cs&lt;br&gt;
│── Program.cs&lt;br&gt;
│── PaymentProcessorApp.csproj&lt;/p&gt;

&lt;p&gt;🚀 Benefícios dessa Abordagem&lt;/p&gt;

&lt;p&gt;✅ Código modular e reutilizável → Podemos adicionar novos IPaymentProcessor ou decorators sem alterar a implementação principal.&lt;br&gt;
✅ Extensibilidade → Podemos criar mais decorators, como um validador ou auditoria.&lt;br&gt;
✅ Baixo acoplamento → Usando DI e Scrutor, evitamos dependências rígidas.&lt;br&gt;
✅ Facilidade de Testes → Podemos testar LoggingDecorator separadamente.&lt;/p&gt;

&lt;p&gt;Essa abordagem combina Adapter e Decorator Patterns com Injeção de Dependência, mantendo o código limpo, flexível e escalável.&lt;/p&gt;

&lt;p&gt;Se você gostou, deixe um comentário ou compartilhe! 🚀🔥&lt;/p&gt;




</description>
      <category>csharp</category>
      <category>designpatterns</category>
      <category>dotnet</category>
    </item>
    <item>
      <title>Adapter Pattern em C# com Injeção de Dependência e Worker Service</title>
      <dc:creator>Luciano Azevedo</dc:creator>
      <pubDate>Sun, 02 Feb 2025 20:36:38 +0000</pubDate>
      <link>https://dev.to/oluciaano/adapter-pattern-em-c-com-injecao-de-dependencia-e-worker-service-4f61</link>
      <guid>https://dev.to/oluciaano/adapter-pattern-em-c-com-injecao-de-dependencia-e-worker-service-4f61</guid>
      <description>&lt;p&gt;🚀 Aplicando o Adapter Pattern em C# com Injeção de Dependência e Worker Service&lt;/p&gt;

&lt;p&gt;📌 Introdução&lt;/p&gt;

&lt;p&gt;Ao trabalhar com sistemas legados, é comum encontrar APIs ou serviços que não seguem as interfaces modernas do nosso código. Em vez de modificar diretamente esses sistemas, podemos usar o Adapter Pattern para fazer a integração de forma elegante.&lt;/p&gt;

&lt;p&gt;Neste artigo, vou mostrar como implementar o Adapter Pattern em C#, usando Injeção de Dependência (DI) e um Worker Service para simular um processo contínuo.&lt;/p&gt;




&lt;p&gt;🎯 O que é o Adapter Pattern?&lt;/p&gt;

&lt;p&gt;O Adapter Pattern é um padrão estrutural que permite que objetos com interfaces incompatíveis trabalhem juntos. Ele age como um intermediário, convertendo a interface de uma classe para outra esperada pelo sistema.&lt;/p&gt;

&lt;p&gt;📌 Quando usar?&lt;/p&gt;

&lt;p&gt;✅ Quando precisamos integrar APIs ou bibliotecas legadas que não podem ser alteradas.&lt;br&gt;
✅ Quando queremos manter um código desacoplado e flexível.&lt;br&gt;
✅ Quando temos diferentes implementações que precisam seguir um mesmo contrato.&lt;/p&gt;



&lt;p&gt;🛠️ Implementação&lt;/p&gt;

&lt;p&gt;Vamos construir um exemplo onde temos um sistema de pagamentos legados e queremos integrá-lo a um Worker Service que processa pagamentos periodicamente.&lt;/p&gt;

&lt;p&gt;🏗️ 1️⃣ Criamos a interface moderna IPaymentProcessor&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public interface IPaymentProcessor
{
    void ProcessPayment(decimal amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🏗️ 2️⃣ Criamos o serviço legado LegacyPaymentService&lt;/p&gt;

&lt;p&gt;Este é um serviço que já existe e que queremos reutilizar, mas ele usa uma interface diferente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public sealed class LegacyPaymentService
{
    public void MakePayment(string amount)
    {
        Console.WriteLine($"Processing payment of {amount} via legacy system.");
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🏗️ 3️⃣ Criamos o Adapter PaymentAdapter&lt;/p&gt;

&lt;p&gt;Aqui adaptamos LegacyPaymentService para seguir a interface IPaymentProcessor.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;public sealed class PaymentAdapter : IPaymentProcessor
{
    private readonly LegacyPaymentService _legacyPaymentService;

    public PaymentAdapter(LegacyPaymentService legacyPaymentService)
    {
        _legacyPaymentService = legacyPaymentService;
    }

    public void ProcessPayment(decimal amount)
    {
        string amountString = amount.ToString("F2");
        _legacyPaymentService.MakePayment(amountString);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🏗️ 4️⃣ Criamos o Worker Service PaymentWorker&lt;/p&gt;

&lt;p&gt;Nosso Worker será responsável por chamar o IPaymentProcessor periodicamente.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using System;
using System.Threading;
using System.Threading.Tasks;

public class PaymentWorker : BackgroundService
{
    private readonly ILogger&amp;lt;PaymentWorker&amp;gt; _logger;
    private readonly IPaymentProcessor _paymentProcessor;

    public PaymentWorker(ILogger&amp;lt;PaymentWorker&amp;gt; logger, IPaymentProcessor paymentProcessor)
    {
        _logger = logger;
        _paymentProcessor = paymentProcessor;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _logger.LogInformation("Iniciando processamento de pagamento...");

            _paymentProcessor.ProcessPayment(100.50m); // Simulando pagamento de R$100,50

            _logger.LogInformation("Pagamento processado com sucesso!");

            await Task.Delay(5000, stoppingToken); // Executa a cada 5 segundos
        }
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🏗️ 5️⃣ Configuramos a Injeção de Dependência no Host&lt;/p&gt;

&lt;p&gt;Agora precisamos configurar os serviços no Host do Worker Service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

class Program
{
    static void Main(string[] args)
    {
        var host = Host.CreateDefaultBuilder(args)
            .ConfigureServices((hostContext, services) =&amp;gt;
            {
                services.AddSingleton&amp;lt;LegacyPaymentService&amp;gt;(); // Serviço legado
                services.AddSingleton&amp;lt;IPaymentProcessor, PaymentAdapter&amp;gt;(); // Adapter
                services.AddHostedService&amp;lt;PaymentWorker&amp;gt;(); // Worker Service
                services.AddLogging(); // Logs
            })
            .Build();

        host.Run();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;✅ O que aprendemos?&lt;/p&gt;

&lt;p&gt;✔ Como usar o Adapter Pattern para conectar serviços legados a um código moderno.&lt;br&gt;
✔ Como estruturar Injeção de Dependência (DI) para desacoplar o código.&lt;br&gt;
✔ Como criar um Worker Service que processa tarefas periodicamente.&lt;/p&gt;




&lt;p&gt;🚀 Conclusão&lt;/p&gt;

&lt;p&gt;O Adapter Pattern é uma solução poderosa para integrar serviços sem alterar o código legado. Combinado com Injeção de Dependência e Worker Services, ele nos permite construir sistemas escaláveis e bem estruturados.&lt;/p&gt;

&lt;p&gt;E você? Já usou esse padrão em algum projeto? Deixe um comentário! 🚀&lt;/p&gt;

</description>
      <category>csharp</category>
      <category>designpatterns</category>
      <category>dotnet</category>
    </item>
  </channel>
</rss>
